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