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