1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wallpaper.picker; 17 18 import android.annotation.SuppressLint; 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.WallpaperColors; 22 import android.app.WallpaperInfo; 23 import android.app.WallpaperManager; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.ServiceConnection; 28 import android.content.pm.ActivityInfo; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ServiceInfo; 32 import android.graphics.Rect; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.IBinder; 36 import android.os.ParcelFileDescriptor; 37 import android.os.RemoteException; 38 import android.service.wallpaper.IWallpaperConnection; 39 import android.service.wallpaper.IWallpaperEngine; 40 import android.service.wallpaper.IWallpaperService; 41 import android.service.wallpaper.WallpaperService; 42 import android.service.wallpaper.WallpaperSettingsActivity; 43 import android.text.TextUtils; 44 import android.util.Log; 45 import android.util.Pair; 46 import android.view.ContextThemeWrapper; 47 import android.view.LayoutInflater; 48 import android.view.Menu; 49 import android.view.MenuInflater; 50 import android.view.MenuItem; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.WindowManager.LayoutParams; 54 import android.view.animation.AnimationUtils; 55 56 import androidx.annotation.NonNull; 57 import androidx.annotation.Nullable; 58 import androidx.lifecycle.LiveData; 59 import androidx.slice.Slice; 60 import androidx.slice.widget.SliceLiveData; 61 import androidx.slice.widget.SliceView; 62 import androidx.viewpager.widget.PagerAdapter; 63 import androidx.viewpager.widget.ViewPager; 64 65 import com.android.wallpaper.R; 66 import com.android.wallpaper.compat.BuildCompat; 67 import com.android.wallpaper.model.LiveWallpaperInfo; 68 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback; 69 70 import com.google.android.material.tabs.TabLayout; 71 72 import java.util.ArrayList; 73 import java.util.List; 74 75 /** 76 * Fragment which displays the UI for previewing an individual live wallpaper, its attribution 77 * information and settings slices if available. 78 */ 79 public class LivePreviewFragment extends PreviewFragment { 80 81 private static final String TAG = "LivePreviewFragment"; 82 83 private static final String KEY_ACTION_DELETE_LIVE_WALLPAPER = "action_delete_live_wallpaper"; 84 private static final String EXTRA_LIVE_WALLPAPER_INFO = "android.live_wallpaper.info"; 85 86 /** 87 * Instance of {@link WallpaperConnection} used to bind to the live wallpaper service to show 88 * it in this preview fragment. 89 * @see IWallpaperConnection 90 */ 91 protected WallpaperConnection mWallpaperConnection; 92 93 private Intent mWallpaperIntent; 94 private Intent mDeleteIntent; 95 private Intent mSettingsIntent; 96 97 private List<Pair<String, View>> mPages; 98 private ViewPager mViewPager; 99 private TabLayout mTabLayout; 100 private SliceView mSettingsSliceView; 101 private LiveData<Slice> mSettingsLiveData; 102 private View mLoadingScrim; 103 private InfoPageController mInfoPageController; 104 105 @Override onCreate(Bundle savedInstanceState)106 public void onCreate(Bundle savedInstanceState) { 107 super.onCreate(savedInstanceState); 108 android.app.WallpaperInfo info = mWallpaper.getWallpaperComponent(); 109 mWallpaperIntent = getWallpaperIntent(info); 110 setUpExploreIntent(null); 111 112 android.app.WallpaperInfo currentWallpaper = 113 WallpaperManager.getInstance(requireContext()).getWallpaperInfo(); 114 String deleteAction = getDeleteAction(info, currentWallpaper); 115 116 if (!TextUtils.isEmpty(deleteAction)) { 117 mDeleteIntent = new Intent(deleteAction); 118 mDeleteIntent.setPackage(info.getPackageName()); 119 mDeleteIntent.putExtra(EXTRA_LIVE_WALLPAPER_INFO, info); 120 } 121 122 String settingsActivity = getSettingsActivity(info); 123 if (settingsActivity != null) { 124 mSettingsIntent = new Intent(); 125 mSettingsIntent.setComponent(new ComponentName(info.getPackageName(), 126 settingsActivity)); 127 mSettingsIntent.putExtra(WallpaperSettingsActivity.EXTRA_PREVIEW_MODE, true); 128 PackageManager pm = requireContext().getPackageManager(); 129 ActivityInfo activityInfo = mSettingsIntent.resolveActivityInfo(pm, 0); 130 if (activityInfo == null) { 131 Log.i(TAG, "Couldn't find wallpaper settings activity: " + settingsActivity); 132 mSettingsIntent = null; 133 } 134 } 135 } 136 137 @Nullable getSettingsActivity(WallpaperInfo info)138 protected String getSettingsActivity(WallpaperInfo info) { 139 return info.getSettingsActivity(); 140 } 141 getWallpaperIntent(WallpaperInfo info)142 protected Intent getWallpaperIntent(WallpaperInfo info) { 143 return new Intent(WallpaperService.SERVICE_INTERFACE) 144 .setClassName(info.getPackageName(), info.getServiceName()); 145 } 146 147 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)148 public View onCreateView(LayoutInflater inflater, ViewGroup container, 149 Bundle savedInstanceState) { 150 mPages = new ArrayList<>(); 151 View view = super.onCreateView(inflater, container, savedInstanceState); 152 if (view == null) { 153 return null; 154 } 155 156 Activity activity = requireActivity(); 157 158 mLoadingScrim = view.findViewById(R.id.loading); 159 setUpLoadingIndicator(); 160 161 mWallpaperConnection = new WallpaperConnection(mWallpaperIntent, activity, 162 getWallpaperConnectionListener()); 163 container.post(() -> { 164 if (!mWallpaperConnection.connect()) { 165 mWallpaperConnection = null; 166 } 167 }); 168 169 return view; 170 } 171 172 @Override onDestroyView()173 public void onDestroyView() { 174 super.onDestroyView(); 175 if (mSettingsLiveData != null && mSettingsLiveData.hasObservers()) { 176 mSettingsLiveData.removeObserver(mSettingsSliceView); 177 mSettingsLiveData = null; 178 } 179 if (mWallpaperConnection != null) { 180 mWallpaperConnection.disconnect(); 181 } 182 mWallpaperConnection = null; 183 super.onDestroy(); 184 } 185 186 @Override setUpBottomSheetView(ViewGroup bottomSheet)187 protected void setUpBottomSheetView(ViewGroup bottomSheet) { 188 189 initInfoPage(); 190 initSettingsPage(); 191 192 mViewPager = bottomSheet.findViewById(R.id.viewpager); 193 mTabLayout = bottomSheet.findViewById(R.id.tablayout); 194 195 // Create PagerAdapter 196 final PagerAdapter pagerAdapter = new PagerAdapter() { 197 @Override 198 public Object instantiateItem(ViewGroup container, int position) { 199 final View page = mPages.get(position).second; 200 container.addView(page); 201 return page; 202 } 203 204 @Override 205 public void destroyItem(@NonNull ViewGroup container, int position, 206 @NonNull Object object) { 207 if (object instanceof View) { 208 container.removeView((View) object); 209 } 210 } 211 212 @Override 213 public int getCount() { 214 return mPages.size(); 215 } 216 217 @Override 218 public CharSequence getPageTitle(int position) { 219 try { 220 return mPages.get(position).first; 221 } catch (IndexOutOfBoundsException e) { 222 return null; 223 } 224 } 225 226 @Override 227 public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { 228 return (view == object); 229 } 230 }; 231 232 // Add OnPageChangeListener to re-measure ViewPager's height 233 mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { 234 @Override 235 public void onPageSelected(int position) { 236 mViewPager.requestLayout(); 237 } 238 }); 239 240 // Set PagerAdapter 241 mViewPager.setAdapter(pagerAdapter); 242 243 // Make TabLayout visible if there are more than one page 244 if (mPages.size() > 1) { 245 mTabLayout.setVisibility(View.VISIBLE); 246 mTabLayout.setupWithViewPager(mViewPager); 247 } 248 mViewPager.setCurrentItem(0); 249 } 250 getWallpaperConnectionListener()251 protected WallpaperConnectionListener getWallpaperConnectionListener() { 252 return null; 253 } 254 255 @Override isLoaded()256 protected boolean isLoaded() { 257 return mWallpaperConnection != null && mWallpaperConnection.isEngineReady(); 258 } 259 initInfoPage()260 private void initInfoPage() { 261 View pageInfo = InfoPageController.createView(getLayoutInflater()); 262 mInfoPageController = new InfoPageController(pageInfo, mPreviewMode); 263 mPages.add(Pair.create(getString(R.string.tab_info), pageInfo)); 264 } 265 initSettingsPage()266 private void initSettingsPage() { 267 final Uri uriSettingsSlice = getSettingsSliceUri(mWallpaper.getWallpaperComponent()); 268 if (uriSettingsSlice == null) { 269 return; 270 } 271 272 final View pageSettings = getLayoutInflater().inflate(R.layout.preview_page_settings, 273 null /* root */); 274 275 mSettingsSliceView = pageSettings.findViewById(R.id.settings_slice); 276 mSettingsSliceView.setMode(SliceView.MODE_LARGE); 277 mSettingsSliceView.setScrollable(false); 278 279 // Set LiveData for SliceView 280 mSettingsLiveData = SliceLiveData.fromUri(requireContext() /* context */, uriSettingsSlice); 281 mSettingsLiveData.observeForever(mSettingsSliceView); 282 283 pageSettings.findViewById(R.id.preview_settings_pane_set_wallpaper_button) 284 .setOnClickListener(this::onSetWallpaperClicked); 285 286 mPages.add(Pair.create(getResources().getString(R.string.tab_customize), pageSettings)); 287 } 288 289 @Override getExploreButtonLabel(Context context)290 protected CharSequence getExploreButtonLabel(Context context) { 291 CharSequence exploreLabel = ((LiveWallpaperInfo) mWallpaper).getActionDescription(context); 292 if (TextUtils.isEmpty(exploreLabel)) { 293 exploreLabel = context.getString(mWallpaper.getActionLabelRes(context)); 294 } 295 return exploreLabel; 296 } 297 298 @SuppressLint("NewApi") //Already checking with isAtLeastQ getSettingsSliceUri(android.app.WallpaperInfo info)299 protected Uri getSettingsSliceUri(android.app.WallpaperInfo info) { 300 if (BuildCompat.isAtLeastQ()) { 301 return info.getSettingsSliceUri(); 302 } 303 return null; 304 } 305 306 @Override getLayoutResId()307 protected int getLayoutResId() { 308 return R.layout.fragment_live_preview; 309 } 310 311 @Override getBottomSheetResId()312 protected int getBottomSheetResId() { 313 return R.id.bottom_sheet; 314 } 315 316 @Override getLoadingIndicatorResId()317 protected int getLoadingIndicatorResId() { 318 return R.id.loading_indicator; 319 } 320 321 @Override setCurrentWallpaper(int destination)322 protected void setCurrentWallpaper(int destination) { 323 mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, null, 324 destination, 0, null, new SetWallpaperCallback() { 325 @Override 326 public void onSuccess() { 327 finishActivityWithResultOk(); 328 } 329 330 @Override 331 public void onError(@Nullable Throwable throwable) { 332 showSetWallpaperErrorDialog(destination); 333 } 334 }); 335 } 336 337 @Override setBottomSheetContentAlpha(float alpha)338 protected void setBottomSheetContentAlpha(float alpha) { 339 mInfoPageController.setContentAlpha(alpha); 340 } 341 342 343 @Nullable getDeleteAction(android.app.WallpaperInfo wallpaperInfo, @Nullable android.app.WallpaperInfo currentInfo)344 protected String getDeleteAction(android.app.WallpaperInfo wallpaperInfo, 345 @Nullable android.app.WallpaperInfo currentInfo) { 346 ServiceInfo serviceInfo = wallpaperInfo.getServiceInfo(); 347 if (!isPackagePreInstalled(serviceInfo.applicationInfo)) { 348 Log.d(TAG, "This wallpaper is not pre-installed: " + serviceInfo.name); 349 return null; 350 } 351 352 ServiceInfo currentService = currentInfo == null ? null : currentInfo.getServiceInfo(); 353 // A currently set Live wallpaper should not be deleted. 354 if (currentService != null && TextUtils.equals(serviceInfo.name, currentService.name)) { 355 return null; 356 } 357 358 final Bundle metaData = serviceInfo.metaData; 359 if (metaData != null) { 360 return metaData.getString(KEY_ACTION_DELETE_LIVE_WALLPAPER); 361 } 362 return null; 363 } 364 365 @Override onResume()366 public void onResume() { 367 super.onResume(); 368 if (mWallpaperConnection != null) { 369 mWallpaperConnection.setVisibility(true); 370 } 371 } 372 373 @Override onPause()374 public void onPause() { 375 super.onPause(); 376 if (mWallpaperConnection != null) { 377 mWallpaperConnection.setVisibility(false); 378 } 379 } 380 381 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)382 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 383 super.onCreateOptionsMenu(menu, inflater); 384 menu.findItem(R.id.configure).setVisible(mSettingsIntent != null); 385 menu.findItem(R.id.delete_wallpaper).setVisible(mDeleteIntent != null); 386 } 387 388 @Override onOptionsItemSelected(MenuItem item)389 public boolean onOptionsItemSelected(MenuItem item) { 390 int id = item.getItemId(); 391 if (id == R.id.configure) { 392 if (getActivity() != null) { 393 startActivity(mSettingsIntent); 394 return true; 395 } 396 } else if (id == R.id.delete_wallpaper) { 397 showDeleteConfirmDialog(); 398 return true; 399 } 400 return super.onOptionsItemSelected(item); 401 } 402 showDeleteConfirmDialog()403 private void showDeleteConfirmDialog() { 404 final AlertDialog alertDialog = new AlertDialog.Builder( 405 new ContextThemeWrapper(getContext(), getDeviceDefaultTheme())) 406 .setMessage(R.string.delete_wallpaper_confirmation) 407 .setPositiveButton(R.string.delete_live_wallpaper, 408 (dialog, which) -> deleteLiveWallpaper()) 409 .setNegativeButton(android.R.string.cancel, null /* listener */) 410 .create(); 411 alertDialog.show(); 412 } 413 deleteLiveWallpaper()414 private void deleteLiveWallpaper() { 415 if (mDeleteIntent != null) { 416 requireContext().startService(mDeleteIntent); 417 finishActivityWithResultOk(); 418 } 419 } 420 isPackagePreInstalled(ApplicationInfo info)421 private boolean isPackagePreInstalled(ApplicationInfo info) { 422 if (info != null && (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 423 return true; 424 } 425 return false; 426 } 427 428 /** 429 * Interface to be notified of connect/disconnect events from {@link WallpaperConnection} 430 */ 431 public interface WallpaperConnectionListener { 432 /** 433 * Called after the Wallpaper service has been bound. 434 */ onConnected()435 void onConnected(); 436 437 /** 438 * Called after the Wallpaper engine has been terminated and the service has been unbound. 439 */ onDisconnected()440 void onDisconnected(); 441 } 442 443 protected class WallpaperConnection extends IWallpaperConnection.Stub 444 implements ServiceConnection { 445 446 private final Activity mActivity; 447 private final Intent mIntent; 448 private final WallpaperConnectionListener mListener; 449 private IWallpaperService mService; 450 private IWallpaperEngine mEngine; 451 private boolean mConnected; 452 private boolean mIsVisible; 453 private boolean mIsEngineVisible; 454 private boolean mEngineReady; 455 WallpaperConnection(Intent intent, Activity activity, @Nullable WallpaperConnectionListener listener)456 WallpaperConnection(Intent intent, Activity activity, 457 @Nullable WallpaperConnectionListener listener) { 458 mActivity = activity; 459 mIntent = intent; 460 mListener = listener; 461 } 462 connect()463 public boolean connect() { 464 synchronized (this) { 465 if (!mActivity.bindService(mIntent, this, 466 Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT)) { 467 return false; 468 } 469 470 mConnected = true; 471 } 472 if (mListener != null) { 473 mListener.onConnected(); 474 } 475 return true; 476 } 477 disconnect()478 public void disconnect() { 479 synchronized (this) { 480 mConnected = false; 481 if (mEngine != null) { 482 try { 483 mEngine.destroy(); 484 } catch (RemoteException e) { 485 // Ignore 486 } 487 mEngine = null; 488 } 489 try { 490 mActivity.unbindService(this); 491 } catch (IllegalArgumentException e) { 492 Log.w(TAG, "Can't unbind wallpaper service. " 493 + "It might have crashed, just ignoring.", e); 494 } 495 mService = null; 496 } 497 if (mListener != null) { 498 mListener.onDisconnected(); 499 } 500 } 501 onServiceConnected(ComponentName name, IBinder service)502 public void onServiceConnected(ComponentName name, IBinder service) { 503 if (mWallpaperConnection == this) { 504 mService = IWallpaperService.Stub.asInterface(service); 505 try { 506 View root = mActivity.getWindow().getDecorView(); 507 int displayId = root.getDisplay().getDisplayId(); 508 mService.attach(this, root.getWindowToken(), 509 LayoutParams.TYPE_APPLICATION_MEDIA, 510 true, root.getWidth(), root.getHeight(), 511 new Rect(0, 0, 0, 0), displayId); 512 } catch (RemoteException e) { 513 Log.w(TAG, "Failed attaching wallpaper; clearing", e); 514 } 515 } 516 } 517 onServiceDisconnected(ComponentName name)518 public void onServiceDisconnected(ComponentName name) { 519 mService = null; 520 mEngine = null; 521 if (mWallpaperConnection == this) { 522 Log.w(TAG, "Wallpaper service gone: " + name); 523 } 524 } 525 attachEngine(IWallpaperEngine engine, int displayId)526 public void attachEngine(IWallpaperEngine engine, int displayId) { 527 synchronized (this) { 528 if (mConnected) { 529 mEngine = engine; 530 if (mIsVisible) { 531 setEngineVisibility(true); 532 } 533 } else { 534 try { 535 engine.destroy(); 536 } catch (RemoteException e) { 537 // Ignore 538 } 539 } 540 } 541 } 542 getEngine()543 public IWallpaperEngine getEngine() { 544 return mEngine; 545 } 546 setWallpaper(String name)547 public ParcelFileDescriptor setWallpaper(String name) { 548 return null; 549 } 550 551 @Override onWallpaperColorsChanged(WallpaperColors colors, int displayId)552 public void onWallpaperColorsChanged(WallpaperColors colors, int displayId) 553 throws RemoteException { 554 555 } 556 557 @Override engineShown(IWallpaperEngine engine)558 public void engineShown(IWallpaperEngine engine) { 559 mLoadingScrim.post(() -> { 560 mLoadingScrim.animate() 561 .alpha(0f) 562 .setDuration(220) 563 .setStartDelay(300) 564 .setInterpolator(AnimationUtils.loadInterpolator(mActivity, 565 android.R.interpolator.fast_out_linear_in)) 566 .withEndAction(() -> { 567 if (mLoadingProgressBar != null) { 568 mLoadingProgressBar.hide(); 569 } 570 mLoadingScrim.setVisibility(View.INVISIBLE); 571 populateInfoPage(mInfoPageController); 572 }); 573 }); 574 mEngineReady = true; 575 } 576 isEngineReady()577 public boolean isEngineReady() { 578 return mEngineReady; 579 } 580 setVisibility(boolean visible)581 public void setVisibility(boolean visible) { 582 mIsVisible = visible; 583 setEngineVisibility(visible); 584 } 585 setEngineVisibility(boolean visible)586 private void setEngineVisibility(boolean visible) { 587 if (mEngine != null && visible != mIsEngineVisible) { 588 try { 589 mEngine.setVisibility(visible); 590 mIsEngineVisible = visible; 591 } catch (RemoteException e) { 592 Log.w(TAG, "Failure setting wallpaper visibility ", e); 593 } 594 } 595 } 596 } 597 } 598