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 }