1 package com.android.wallpaper.module; 2 3 import static android.app.WallpaperManager.FLAG_LOCK; 4 5 import android.app.Activity; 6 import android.app.ProgressDialog; 7 import android.app.WallpaperColors; 8 import android.app.WallpaperManager; 9 import android.content.ComponentName; 10 import android.content.Context; 11 import android.content.pm.ActivityInfo; 12 import android.graphics.Point; 13 import android.graphics.Rect; 14 import android.os.Build.VERSION; 15 import android.os.Build.VERSION_CODES; 16 import android.util.Log; 17 import android.view.Display; 18 19 import androidx.annotation.NonNull; 20 import androidx.annotation.Nullable; 21 import androidx.annotation.StringRes; 22 import androidx.fragment.app.FragmentManager; 23 import androidx.lifecycle.Lifecycle.Event; 24 import androidx.lifecycle.LifecycleEventObserver; 25 import androidx.lifecycle.LifecycleOwner; 26 27 import com.android.wallpaper.R; 28 import com.android.wallpaper.asset.Asset; 29 import com.android.wallpaper.model.LiveWallpaperInfo; 30 import com.android.wallpaper.model.WallpaperInfo; 31 import com.android.wallpaper.module.UserEventLogger.WallpaperSetFailureReason; 32 import com.android.wallpaper.module.WallpaperPersister.Destination; 33 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback; 34 import com.android.wallpaper.picker.SetWallpaperDialogFragment; 35 import com.android.wallpaper.picker.SetWallpaperDialogFragment.Listener; 36 import com.android.wallpaper.util.ScreenSizeCalculator; 37 import com.android.wallpaper.util.ThrowableAnalyzer; 38 import com.android.wallpaper.util.WallpaperCropUtils; 39 40 import com.bumptech.glide.Glide; 41 42 import java.io.IOException; 43 import java.lang.reflect.Method; 44 import java.util.Optional; 45 46 /** 47 * Helper class used to set the current wallpaper. It handles showing the destination request dialog 48 * and actually setting the wallpaper on a given destination. 49 * It is expected to be instantiated within a Fragment or Activity, and {@link #cleanUp()} should 50 * be called from its owner's onDestroy method (or equivalent). 51 */ 52 public class WallpaperSetter { 53 54 private static final String TAG = "WallpaperSetter"; 55 private static final String PROGRESS_DIALOG_NO_TITLE = null; 56 private static final boolean PROGRESS_DIALOG_INDETERMINATE = true; 57 58 private static final int UNUSED_REQUEST_CODE = 1; 59 private static final String TAG_SET_WALLPAPER_DIALOG_FRAGMENT = "set_wallpaper_dialog"; 60 61 private final WallpaperPersister mWallpaperPersister; 62 private final WallpaperPreferences mPreferences; 63 private final boolean mTestingModeEnabled; 64 private final UserEventLogger mUserEventLogger; 65 private final CurrentWallpaperInfoFactory mCurrentWallpaperInfoFactory; 66 private ProgressDialog mProgressDialog; 67 private Optional<Integer> mCurrentScreenOrientation = Optional.empty(); 68 WallpaperSetter(WallpaperPersister wallpaperPersister, WallpaperPreferences preferences, UserEventLogger userEventLogger, CurrentWallpaperInfoFactory currentWallpaperInfoFactory, boolean isTestingModeEnabled)69 public WallpaperSetter(WallpaperPersister wallpaperPersister, 70 WallpaperPreferences preferences, UserEventLogger userEventLogger, 71 CurrentWallpaperInfoFactory currentWallpaperInfoFactory, 72 boolean isTestingModeEnabled) { 73 mTestingModeEnabled = isTestingModeEnabled; 74 mWallpaperPersister = wallpaperPersister; 75 mPreferences = preferences; 76 mUserEventLogger = userEventLogger; 77 mCurrentWallpaperInfoFactory = currentWallpaperInfoFactory; 78 } 79 80 /** 81 * Sets current wallpaper to the device with the minimum scale to fit the screen size. 82 * 83 * @param containerActivity main Activity that owns the current fragment 84 * @param wallpaper info for the actual wallpaper to set 85 * @param destination the wallpaper destination i.e. home vs. lockscreen vs. both. 86 * @param callback optional callback to be notified when the wallpaper is set. 87 */ setCurrentWallpaper(Activity containerActivity, WallpaperInfo wallpaper, @Destination final int destination, @Nullable SetWallpaperCallback callback)88 public void setCurrentWallpaper(Activity containerActivity, WallpaperInfo wallpaper, 89 @Destination final int destination, 90 @Nullable SetWallpaperCallback callback) { 91 Asset wallpaperAsset = wallpaper.getAsset(containerActivity.getApplicationContext()); 92 wallpaperAsset.decodeRawDimensions(containerActivity, dimensions -> { 93 if (dimensions == null) { 94 Log.e(TAG, "Raw wallpaper's dimensions are null"); 95 return; 96 } 97 98 Display defaultDisplay = containerActivity.getWindowManager().getDefaultDisplay(); 99 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay); 100 Rect visibleRawWallpaperRect = 101 WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize); 102 float wallpaperScale = WallpaperCropUtils.calculateMinZoom(dimensions, screenSize); 103 Rect cropRect = WallpaperCropUtils.calculateCropRect( 104 containerActivity.getApplicationContext(), defaultDisplay, 105 dimensions, visibleRawWallpaperRect, wallpaperScale); 106 107 setCurrentWallpaper(containerActivity, wallpaper, wallpaperAsset, destination, 108 wallpaperScale, cropRect, null, callback); 109 }); 110 } 111 112 /** 113 * Sets current wallpaper to the device based on current zoom and scroll state. 114 * 115 * @param containerActivity main Activity that owns the current fragment 116 * @param wallpaper Info for the actual wallpaper to set 117 * @param wallpaperAsset Wallpaper asset from which to retrieve image data. 118 * @param destination The wallpaper destination i.e. home vs. lockscreen vs. both. 119 * @param wallpaperScale Scaling factor applied to the source image before setting the 120 * wallpaper to the device. 121 * @param cropRect Desired crop area of the wallpaper in post-scale units. If null, 122 * then the 123 * wallpaper image will be set without any scaling or cropping. 124 * @param callback Optional callback to be notified when the wallpaper is set. 125 */ setCurrentWallpaper(Activity containerActivity, WallpaperInfo wallpaper, @Nullable Asset wallpaperAsset, @Destination final int destination, float wallpaperScale, @Nullable Rect cropRect, WallpaperColors wallpaperColors, @Nullable SetWallpaperCallback callback)126 public void setCurrentWallpaper(Activity containerActivity, WallpaperInfo wallpaper, 127 @Nullable Asset wallpaperAsset, @Destination final int destination, 128 float wallpaperScale, @Nullable Rect cropRect, WallpaperColors wallpaperColors, 129 @Nullable SetWallpaperCallback callback) { 130 if (wallpaper instanceof LiveWallpaperInfo) { 131 setCurrentLiveWallpaper(containerActivity, (LiveWallpaperInfo) wallpaper, destination, 132 wallpaperColors, callback); 133 return; 134 } 135 mPreferences.setPendingWallpaperSetStatus( 136 WallpaperPreferences.WALLPAPER_SET_PENDING); 137 138 // Save current screen rotation so we can temporarily disable rotation while setting the 139 // wallpaper and restore after setting the wallpaper finishes. 140 saveAndLockScreenOrientationIfNeeded(containerActivity); 141 142 // Clear MosaicView tiles and Glide's cache and pools to reclaim memory for final cropped 143 // bitmap. 144 Glide.get(containerActivity).clearMemory(); 145 146 // ProgressDialog endlessly updates the UI thread, keeping it from going idle which 147 // therefore causes Espresso to hang once the dialog is shown. 148 if (!mTestingModeEnabled && !containerActivity.isFinishing()) { 149 int themeResId = (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) 150 ? R.style.ProgressDialogThemePreL : R.style.LightDialogTheme; 151 mProgressDialog = new ProgressDialog(containerActivity, themeResId); 152 153 mProgressDialog.setTitle(PROGRESS_DIALOG_NO_TITLE); 154 mProgressDialog.setMessage(containerActivity.getString( 155 R.string.set_wallpaper_progress_message)); 156 mProgressDialog.setIndeterminate(PROGRESS_DIALOG_INDETERMINATE); 157 if (containerActivity instanceof LifecycleOwner) { 158 ((LifecycleOwner) containerActivity).getLifecycle().addObserver( 159 new LifecycleEventObserver() { 160 @Override 161 public void onStateChanged(@NonNull LifecycleOwner source, 162 @NonNull Event event) { 163 if (event == Event.ON_DESTROY) { 164 if (mProgressDialog != null) { 165 mProgressDialog.dismiss(); 166 mProgressDialog = null; 167 } 168 } 169 } 170 }); 171 } 172 mProgressDialog.show(); 173 } 174 175 mWallpaperPersister.setIndividualWallpaper( 176 wallpaper, wallpaperAsset, cropRect, 177 wallpaperScale, destination, new SetWallpaperCallback() { 178 @Override 179 public void onSuccess(WallpaperInfo wallpaperInfo, 180 @Destination int destination) { 181 onWallpaperApplied(wallpaper, containerActivity); 182 if (callback != null) { 183 callback.onSuccess(wallpaper, destination); 184 } 185 } 186 187 @Override 188 public void onError(Throwable throwable) { 189 onWallpaperApplyError(throwable, containerActivity); 190 if (callback != null) { 191 callback.onError(throwable); 192 } 193 } 194 }); 195 mCurrentWallpaperInfoFactory.clearCurrentWallpaperInfos(); 196 } 197 setCurrentLiveWallpaper(Activity activity, LiveWallpaperInfo wallpaper, @Destination final int destination, WallpaperColors colors, @Nullable SetWallpaperCallback callback)198 private void setCurrentLiveWallpaper(Activity activity, LiveWallpaperInfo wallpaper, 199 @Destination final int destination, WallpaperColors colors, 200 @Nullable SetWallpaperCallback callback) { 201 try { 202 // Save current screen rotation so we can temporarily disable rotation while setting the 203 // wallpaper and restore after setting the wallpaper finishes. 204 saveAndLockScreenOrientationIfNeeded(activity); 205 206 WallpaperManager wallpaperManager = WallpaperManager.getInstance(activity); 207 if (destination == WallpaperPersister.DEST_LOCK_SCREEN 208 && !wallpaperManager.isLockscreenLiveWallpaperEnabled()) { 209 throw new IllegalArgumentException( 210 "Live wallpaper cannot be applied on lock screen only"); 211 } 212 213 LiveWallpaperInfo updatedWallpaperInfo = wallpaper.saveWallpaper( 214 activity.getApplicationContext(), destination); 215 if (updatedWallpaperInfo != null) { 216 wallpaper = updatedWallpaperInfo; 217 } 218 setWallpaperComponent(wallpaperManager, wallpaper, destination); 219 wallpaperManager.setWallpaperOffsetSteps(0.5f /* xStep */, 0.0f /* yStep */); 220 wallpaperManager.setWallpaperOffsets( 221 activity.getWindow().getDecorView().getRootView().getWindowToken(), 222 0.5f /* xOffset */, 0.0f /* yOffset */); 223 mPreferences.storeLatestWallpaper(WallpaperPersister.destinationToFlags(destination), 224 wallpaper.getWallpaperId(), wallpaper, colors); 225 mCurrentWallpaperInfoFactory.clearCurrentWallpaperInfos(); 226 onWallpaperApplied(wallpaper, activity); 227 if (callback != null) { 228 callback.onSuccess(wallpaper, destination); 229 } 230 mWallpaperPersister.onLiveWallpaperSet(destination); 231 } catch (RuntimeException | IOException e) { 232 onWallpaperApplyError(e, activity); 233 if (callback != null) { 234 callback.onError(e); 235 } 236 } 237 } 238 setWallpaperComponent(WallpaperManager wallpaperManager, LiveWallpaperInfo wallpaper, int destination)239 private void setWallpaperComponent(WallpaperManager wallpaperManager, 240 LiveWallpaperInfo wallpaper, int destination) throws IOException { 241 try { 242 Method m = wallpaperManager.getClass().getMethod("setWallpaperComponentWithFlags", 243 ComponentName.class, int.class); 244 wallpaperManager.setWallpaperComponentWithFlags( 245 wallpaper.getWallpaperComponent().getComponent(), 246 WallpaperPersister.destinationToFlags(destination)); 247 } catch (NoSuchMethodException e) { 248 Log.d(TAG, "setWallpaperComponentWithFlags not available, using setWallpaperComponent"); 249 wallpaperManager.setWallpaperComponent( 250 wallpaper.getWallpaperComponent().getComponent()); 251 } 252 if (!wallpaperManager.isLockscreenLiveWallpaperEnabled() 253 && destination == WallpaperPersister.DEST_BOTH) { 254 wallpaperManager.clear(FLAG_LOCK); 255 } 256 } 257 258 /** 259 * Sets current live wallpaper to the device (restore case) 260 * 261 * @param context The context for initiating wallpaper manager 262 * @param wallpaper Information for the actual wallpaper to set 263 * @param destination The wallpaper destination i.e. home vs. lockscreen vs. both 264 * @param colors The {@link WallpaperColors} for placeholder of quickswitching 265 * @param callback Optional callback to be notified when the wallpaper is set. 266 */ setCurrentLiveWallpaper(Context context, LiveWallpaperInfo wallpaper, @Destination final int destination, @Nullable WallpaperColors colors, @Nullable SetWallpaperCallback callback)267 public void setCurrentLiveWallpaper(Context context, LiveWallpaperInfo wallpaper, 268 @Destination final int destination, @Nullable WallpaperColors colors, 269 @Nullable SetWallpaperCallback callback) { 270 try { 271 WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); 272 if (destination == WallpaperPersister.DEST_LOCK_SCREEN 273 && !wallpaperManager.isLockscreenLiveWallpaperEnabled()) { 274 throw new IllegalArgumentException( 275 "Live wallpaper cannot be applied on lock screen only"); 276 } 277 setWallpaperComponent(wallpaperManager, wallpaper, destination); 278 mPreferences.storeLatestWallpaper(WallpaperPersister.destinationToFlags(destination), 279 wallpaper.getWallpaperId(), 280 wallpaper, colors != null ? colors : 281 WallpaperColors.fromBitmap(wallpaper.getThumbAsset(context) 282 .getLowResBitmap(context))); 283 mCurrentWallpaperInfoFactory.clearCurrentWallpaperInfos(); 284 // Not call onWallpaperApplied() as no UI is presented. 285 if (callback != null) { 286 callback.onSuccess(wallpaper, destination); 287 } 288 mWallpaperPersister.onLiveWallpaperSet(destination); 289 } catch (RuntimeException | IOException e) { 290 // Not call onWallpaperApplyError() as no UI is presented. 291 if (callback != null) { 292 callback.onError(e); 293 } 294 } 295 } 296 onWallpaperApplied(WallpaperInfo wallpaper, Activity containerActivity)297 private void onWallpaperApplied(WallpaperInfo wallpaper, Activity containerActivity) { 298 mUserEventLogger.logWallpaperSet( 299 wallpaper.getCollectionId(containerActivity), 300 wallpaper.getWallpaperId(), wallpaper.getEffectNames()); 301 mPreferences.setPendingWallpaperSetStatus( 302 WallpaperPreferences.WALLPAPER_SET_NOT_PENDING); 303 mUserEventLogger.logWallpaperSetResult( 304 UserEventLogger.WALLPAPER_SET_RESULT_SUCCESS); 305 cleanUp(); 306 restoreScreenOrientationIfNeeded(containerActivity); 307 } 308 onWallpaperApplyError(Throwable throwable, Activity containerActivity)309 private void onWallpaperApplyError(Throwable throwable, Activity containerActivity) { 310 mPreferences.setPendingWallpaperSetStatus( 311 WallpaperPreferences.WALLPAPER_SET_NOT_PENDING); 312 mUserEventLogger.logWallpaperSetResult( 313 UserEventLogger.WALLPAPER_SET_RESULT_FAILURE); 314 @WallpaperSetFailureReason int failureReason = ThrowableAnalyzer.isOOM( 315 throwable) 316 ? UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OOM 317 : UserEventLogger.WALLPAPER_SET_FAILURE_REASON_OTHER; 318 mUserEventLogger.logWallpaperSetFailureReason(failureReason); 319 320 cleanUp(); 321 restoreScreenOrientationIfNeeded(containerActivity); 322 } 323 324 /** 325 * Call this method to clean up this instance's state. 326 */ cleanUp()327 public void cleanUp() { 328 if (mProgressDialog != null) { 329 mProgressDialog.dismiss(); 330 mProgressDialog = null; 331 } 332 } 333 334 /** 335 * Show a dialog asking the user for the Wallpaper's destination 336 * (eg, "Home screen", "Lock Screen") 337 * 338 * @param isLiveWallpaper whether the wallpaper that we want to set is a live wallpaper. 339 * @param listener {@link SetWallpaperDialogFragment.Listener} that will receive the 340 * response. 341 * @param isLockOptionAllowed whether the wallpaper we want to set can be set on lockscreen 342 * @param isHomeOptionAllowed whether the wallpaper we want to set can be set on homescreen 343 * @see Destination 344 */ requestDestination(Activity activity, FragmentManager fragmentManager, Listener listener, boolean isLiveWallpaper, boolean isHomeOptionAllowed, boolean isLockOptionAllowed)345 public void requestDestination(Activity activity, FragmentManager fragmentManager, 346 Listener listener, boolean isLiveWallpaper, boolean isHomeOptionAllowed, 347 boolean isLockOptionAllowed) { 348 requestDestination(activity, fragmentManager, R.string.set_wallpaper_dialog_message, 349 listener, isLiveWallpaper, isHomeOptionAllowed, isLockOptionAllowed); 350 } 351 352 /** 353 * Show a dialog asking the user for the Wallpaper's destination 354 * (eg, "Home screen", "Lock Screen") 355 * 356 * @param isLiveWallpaper whether the wallpaper that we want to set is a live wallpaper. 357 * @param listener {@link SetWallpaperDialogFragment.Listener} that will receive the 358 * response. 359 * @param titleResId title for the dialog 360 * @param isHomeOption whether the wallpaper we want to set can be set on homescreen 361 * @param isLockOption whether the wallpaper we want to set can be set on lockscreen 362 * @see Destination 363 */ requestDestination(Activity activity, FragmentManager fragmentManager, @StringRes int titleResId, Listener listener, boolean isLiveWallpaper, boolean isHomeOption, boolean isLockOption)364 public void requestDestination(Activity activity, FragmentManager fragmentManager, 365 @StringRes int titleResId, Listener listener, boolean isLiveWallpaper, 366 boolean isHomeOption, boolean isLockOption) { 367 saveAndLockScreenOrientationIfNeeded(activity); 368 Listener listenerWrapper = new Listener() { 369 @Override 370 public void onSet(int destination) { 371 if (listener != null) { 372 listener.onSet(destination); 373 } 374 } 375 376 @Override 377 public void onDialogDismissed(boolean withItemSelected) { 378 if (!withItemSelected) { 379 restoreScreenOrientationIfNeeded(activity); 380 } 381 if (listener != null) { 382 listener.onDialogDismissed(withItemSelected); 383 } 384 } 385 }; 386 387 WallpaperManager wallpaperManager = WallpaperManager.getInstance(activity); 388 SetWallpaperDialogFragment setWallpaperDialog = new SetWallpaperDialogFragment(); 389 setWallpaperDialog.setTitleResId(titleResId); 390 setWallpaperDialog.setListener(listenerWrapper); 391 if (isLiveWallpaper) { 392 setWallpaperDialog.setHomeOptionAvailable(isHomeOption); 393 setWallpaperDialog.setLockOptionAvailable(isLockOption); 394 } 395 if (wallpaperManager.isLockscreenLiveWallpaperEnabled()) { 396 setWallpaperDialog.show(fragmentManager, TAG_SET_WALLPAPER_DIALOG_FRAGMENT); 397 return; 398 } 399 400 WallpaperStatusChecker wallpaperStatusChecker = InjectorProvider.getInjector() 401 .getWallpaperStatusChecker(activity.getApplicationContext()); 402 boolean isLiveWallpaperSet = 403 WallpaperManager.getInstance(activity).getWallpaperInfo() != null; 404 // Alternative of ag/15567276 405 boolean isBuiltIn = !isLiveWallpaperSet 406 && !wallpaperStatusChecker.isHomeStaticWallpaperSet(); 407 408 if ((isLiveWallpaperSet || isBuiltIn) 409 && !wallpaperStatusChecker.isLockWallpaperSet()) { 410 if (isLiveWallpaper) { 411 // If lock wallpaper is live and we're setting a live wallpaper, we can only 412 // set it to both, so bypass the dialog. 413 listener.onSet(WallpaperPersister.DEST_BOTH); 414 restoreScreenOrientationIfNeeded(activity); 415 return; 416 } 417 // if the lock wallpaper is a live wallpaper, we cannot set a home-only static one 418 setWallpaperDialog.setHomeOptionAvailable(false); 419 } 420 if (isLiveWallpaper) { 421 setWallpaperDialog.setLockOptionAvailable(false); 422 } 423 setWallpaperDialog.show(fragmentManager, TAG_SET_WALLPAPER_DIALOG_FRAGMENT); 424 } 425 saveAndLockScreenOrientationIfNeeded(Activity activity)426 private void saveAndLockScreenOrientationIfNeeded(Activity activity) { 427 if (!mCurrentScreenOrientation.isPresent()) { 428 mCurrentScreenOrientation = Optional.of(activity.getRequestedOrientation()); 429 activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED); 430 } 431 } 432 restoreScreenOrientationIfNeeded(Activity activity)433 private void restoreScreenOrientationIfNeeded(Activity activity) { 434 mCurrentScreenOrientation.ifPresent(orientation -> { 435 if (activity.getRequestedOrientation() != orientation) { 436 activity.setRequestedOrientation(orientation); 437 } 438 mCurrentScreenOrientation = Optional.empty(); 439 }); 440 } 441 }