• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.Manifest.permission;
19 import android.app.Activity;
20 import android.app.WallpaperManager;
21 import android.content.ActivityNotFoundException;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.net.Uri;
25 import android.os.Build.VERSION;
26 import android.os.Build.VERSION_CODES;
27 import android.service.wallpaper.WallpaperService;
28 import android.util.Log;
29 
30 import androidx.activity.result.ActivityResultLauncher;
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.fragment.app.FragmentActivity;
34 
35 import com.android.wallpaper.R;
36 import com.android.wallpaper.config.BaseFlags;
37 import com.android.wallpaper.model.Category;
38 import com.android.wallpaper.model.CategoryProvider;
39 import com.android.wallpaper.model.CategoryReceiver;
40 import com.android.wallpaper.model.ImageWallpaperInfo;
41 import com.android.wallpaper.model.WallpaperInfo;
42 import com.android.wallpaper.module.Injector;
43 import com.android.wallpaper.module.InjectorProvider;
44 import com.android.wallpaper.module.PackageStatusNotifier;
45 import com.android.wallpaper.module.PackageStatusNotifier.PackageStatus;
46 import com.android.wallpaper.module.WallpaperPersister;
47 import com.android.wallpaper.module.WallpaperPreferences;
48 import com.android.wallpaper.picker.WallpaperDisabledFragment.WallpaperSupportLevel;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 
53 /**
54  * Implements all the logic for handling a WallpaperPicker container Activity.
55  * @see CustomizationPickerActivity for usage details.
56  */
57 public class WallpaperPickerDelegate implements MyPhotosStarter {
58 
59     private static final String TAG = "WallpaperPickerDelegate";
60     private final FragmentActivity mActivity;
61     private final WallpapersUiContainer mContainer;
62     public static boolean DISABLE_MY_PHOTOS_BLOCK_PREVIEW = false;
63     public static final int SHOW_CATEGORY_REQUEST_CODE = 0;
64     public static final int PREVIEW_WALLPAPER_REQUEST_CODE = 1;
65     public static final int VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE = 2;
66     public static final int READ_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 3;
67     public static final int PREVIEW_LIVE_WALLPAPER_REQUEST_CODE = 4;
68     public static final String IS_LIVE_WALLPAPER = "isLiveWallpaper";
69     private final MyPhotosIntentProvider mMyPhotosIntentProvider;
70     private WallpaperPreferences mPreferences;
71     private PackageStatusNotifier mPackageStatusNotifier;
72     private BaseFlags mFlags;
73 
74     private List<PermissionChangedListener> mPermissionChangedListeners;
75     private PackageStatusNotifier.Listener mLiveWallpaperStatusListener;
76     private PackageStatusNotifier.Listener mThirdPartyStatusListener;
77     private PackageStatusNotifier.Listener mDownloadableWallpaperStatusListener;
78     private String mDownloadableIntentAction;
79     private CategoryProvider mCategoryProvider;
80     private WallpaperPersister mWallpaperPersister;
81     private static final String READ_IMAGE_PERMISSION = permission.READ_MEDIA_IMAGES;
82 
WallpaperPickerDelegate(WallpapersUiContainer container, FragmentActivity activity, Injector injector)83     public WallpaperPickerDelegate(WallpapersUiContainer container, FragmentActivity activity,
84             Injector injector) {
85         mContainer = container;
86         mActivity = activity;
87         mFlags = injector.getFlags();
88         mCategoryProvider = injector.getCategoryProvider(activity);
89         mPreferences = injector.getPreferences(activity);
90 
91         mPackageStatusNotifier = injector.getPackageStatusNotifier(activity);
92         mWallpaperPersister = injector.getWallpaperPersister(activity);
93 
94         mPermissionChangedListeners = new ArrayList<>();
95         mDownloadableIntentAction = injector.getDownloadableIntentAction();
96         mMyPhotosIntentProvider = injector.getMyPhotosIntentProvider();
97     }
98 
initialize(boolean forceCategoryRefresh)99     public void initialize(boolean forceCategoryRefresh) {
100         if (!mFlags.isWallpaperCategoryRefactoringEnabled()) {
101             populateCategories(forceCategoryRefresh);
102             mLiveWallpaperStatusListener = this::updateLiveWallpapersCategories;
103             mThirdPartyStatusListener = this::updateThirdPartyCategories;
104             mPackageStatusNotifier.addListener(
105                     mLiveWallpaperStatusListener,
106                     WallpaperService.SERVICE_INTERFACE);
107             mPackageStatusNotifier.addListener(mThirdPartyStatusListener,
108                     Intent.ACTION_SET_WALLPAPER);
109             if (mDownloadableIntentAction != null) {
110                 mDownloadableWallpaperStatusListener = (packageName, status) -> {
111                     if (status != PackageStatusNotifier.PackageStatus.REMOVED) {
112                         populateCategories(/* forceRefresh= */ true);
113                     }
114                 };
115                 mPackageStatusNotifier.addListener(
116                         mDownloadableWallpaperStatusListener, mDownloadableIntentAction);
117             }
118         }
119     }
120 
121     @Override
requestCustomPhotoPicker(PermissionChangedListener listener)122     public void requestCustomPhotoPicker(PermissionChangedListener listener) {
123         //TODO (b/282073506): Figure out a better way to have better photos experience
124         if (mFlags.isWallpaperCategoryRefactoringEnabled()) {
125             if (!isReadExternalStoragePermissionGranted()) {
126                 PermissionChangedListener wrappedListener = new PermissionChangedListener() {
127                     @Override
128                     public void onPermissionsGranted() {
129                         listener.onPermissionsGranted();
130                         showCustomPhotoPicker();
131                     }
132 
133                     @Override
134                     public void onPermissionsDenied(boolean dontAskAgain) {
135                         listener.onPermissionsDenied(dontAskAgain);
136                     }
137                 };
138                 requestExternalStoragePermission(wrappedListener);
139 
140                 return;
141             }
142         }
143 
144         showCustomPhotoPicker();
145     }
146 
147     @Override
requestCustomPhotoPicker(PermissionChangedListener listener, Activity activity, ActivityResultLauncher<Intent> photoPickerLauncher)148     public void requestCustomPhotoPicker(PermissionChangedListener listener, Activity activity,
149             ActivityResultLauncher<Intent> photoPickerLauncher) {
150         requestCustomPhotoPicker(listener);
151     }
152 
153     /**
154      * Requests to show the Android custom photo picker for the sake of picking a
155      * photo to set as the device's wallpaper.
156      */
requestExternalStoragePermission(PermissionChangedListener listener)157     public void requestExternalStoragePermission(PermissionChangedListener listener) {
158         mPermissionChangedListeners.add(listener);
159         mActivity.requestPermissions(
160                 new String[]{READ_IMAGE_PERMISSION},
161                 READ_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE);
162     }
163 
164     /**
165      * Returns whether READ_MEDIA_IMAGES has been granted for the application.
166      */
isReadExternalStoragePermissionGranted()167     public boolean isReadExternalStoragePermissionGranted() {
168         return mActivity.getPackageManager().checkPermission(
169                 permission.READ_MEDIA_IMAGES,
170                 mActivity.getPackageName()) == PackageManager.PERMISSION_GRANTED;
171     }
172 
showCustomPhotoPicker()173     private void showCustomPhotoPicker() {
174         try {
175             Intent intent = mMyPhotosIntentProvider.getMyPhotosIntent();
176             mActivity.startActivityForResult(intent, SHOW_CATEGORY_REQUEST_CODE);
177         } catch (ActivityNotFoundException e) {
178             Intent fallback = mMyPhotosIntentProvider.getFallbackIntent();
179             if (fallback != null) {
180                 Log.i(TAG, "Couldn't launch photo picker with main intent, trying with fallback");
181                 mActivity.startActivityForResult(fallback, SHOW_CATEGORY_REQUEST_CODE);
182             } else {
183                 Log.e(TAG,
184                         "Couldn't launch photo picker with main intent and no fallback is "
185                                 + "available");
186                 throw e;
187             }
188         }
189     }
190 
updateThirdPartyCategories(String packageName, @PackageStatus int status)191     private void updateThirdPartyCategories(String packageName, @PackageStatus int status) {
192         if (status == PackageStatus.ADDED) {
193             mCategoryProvider.fetchCategories(new CategoryReceiver() {
194                 @Override
195                 public void onCategoryReceived(Category category) {
196                     if (category.supportsThirdParty() && category.containsThirdParty(packageName)) {
197                         addCategory(category, false);
198                     }
199                 }
200 
201                 @Override
202                 public void doneFetchingCategories() {
203                     // Do nothing here.
204                 }
205             }, true);
206         } else if (status == PackageStatus.REMOVED) {
207             Category oldCategory = findThirdPartyCategory(packageName);
208             if (oldCategory != null) {
209                 mCategoryProvider.fetchCategories(new CategoryReceiver() {
210                     @Override
211                     public void onCategoryReceived(Category category) {
212                         // Do nothing here
213                     }
214 
215                     @Override
216                     public void doneFetchingCategories() {
217                         removeCategory(oldCategory);
218                     }
219                 }, true);
220             }
221         } else {
222             // CHANGED package, let's reload all categories as we could have more or fewer now
223             populateCategories(/* forceRefresh= */ true);
224         }
225     }
226 
findThirdPartyCategory(String packageName)227     private Category findThirdPartyCategory(String packageName) {
228         int size = mCategoryProvider.getSize();
229         for (int i = 0; i < size; i++) {
230             Category category = mCategoryProvider.getCategory(i);
231             if (category.supportsThirdParty() && category.containsThirdParty(packageName)) {
232                 return category;
233             }
234         }
235         return null;
236     }
237 
updateLiveWallpapersCategories(String packageName, @PackageStatus int status)238     private void updateLiveWallpapersCategories(String packageName,
239             @PackageStatus int status) {
240         String liveWallpaperCollectionId = mActivity.getString(
241                 R.string.live_wallpaper_collection_id);
242         Category oldLiveWallpapersCategory = mCategoryProvider.getCategory(
243                 liveWallpaperCollectionId);
244         if (status == PackageStatus.REMOVED
245                 && (oldLiveWallpapersCategory == null
246                 || !oldLiveWallpapersCategory.containsThirdParty(packageName))) {
247             // If we're removing a wallpaper and the live category didn't contain it already,
248             // there's nothing to do.
249             return;
250         }
251         mCategoryProvider.fetchCategories(new CategoryReceiver() {
252             @Override
253             public void onCategoryReceived(Category category) {
254                 // Do nothing here
255             }
256 
257             @Override
258             public void doneFetchingCategories() {
259                 Category liveWallpapersCategory =
260                         mCategoryProvider.getCategory(liveWallpaperCollectionId);
261                 if (liveWallpapersCategory == null) {
262                     // There are no more 3rd party live wallpapers, so the Category is gone.
263                     removeCategory(oldLiveWallpapersCategory);
264                 } else {
265                     if (oldLiveWallpapersCategory != null) {
266                         updateCategory(liveWallpapersCategory);
267                     } else {
268                         addCategory(liveWallpapersCategory, false);
269                     }
270                 }
271             }
272         }, true);
273     }
274 
275     /**
276      * Fetch the wallpaper categories but don't call any callbacks on the result, just so that
277      * they're cached when loading later.
278      */
prefetchCategories()279     public void prefetchCategories() {
280         boolean forceRefresh = mCategoryProvider.resetIfNeeded();
281         mCategoryProvider.fetchCategories(new CategoryReceiver() {
282             @Override
283             public void onCategoryReceived(Category category) {
284                 // Do nothing
285             }
286 
287             @Override
288             public void doneFetchingCategories() {
289                 // Do nothing
290             }
291         }, forceRefresh);
292     }
293 
294     /**
295      * Populates the categories appropriately.
296      *
297      * @param forceRefresh        Whether to force a refresh of categories from the
298      *                            CategoryProvider. True if
299      *                            on first launch.
300      */
populateCategories(boolean forceRefresh)301     public void populateCategories(boolean forceRefresh) {
302 
303         final CategorySelectorFragment categorySelectorFragment = getCategorySelectorFragment();
304 
305         if (forceRefresh && categorySelectorFragment != null) {
306             categorySelectorFragment.clearCategories();
307         }
308 
309         mCategoryProvider.fetchCategories(new CategoryReceiver() {
310             @Override
311             public void onCategoryReceived(Category category) {
312                 addCategory(category, true);
313             }
314 
315             @Override
316             public void doneFetchingCategories() {
317                 notifyDoneFetchingCategories();
318             }
319         }, forceRefresh);
320     }
321 
notifyDoneFetchingCategories()322     private void notifyDoneFetchingCategories() {
323         CategorySelectorFragment categorySelectorFragment = getCategorySelectorFragment();
324         if (categorySelectorFragment != null) {
325             categorySelectorFragment.doneFetchingCategories();
326         }
327     }
328 
addCategory(Category category, boolean fetchingAll)329     public void addCategory(Category category, boolean fetchingAll) {
330         CategorySelectorFragment categorySelectorFragment = getCategorySelectorFragment();
331         if (categorySelectorFragment != null) {
332             categorySelectorFragment.addCategory(category, fetchingAll);
333         }
334     }
335 
removeCategory(Category category)336     public void removeCategory(Category category) {
337         CategorySelectorFragment categorySelectorFragment = getCategorySelectorFragment();
338         if (categorySelectorFragment != null) {
339             categorySelectorFragment.removeCategory(category);
340         }
341     }
342 
updateCategory(Category category)343     public void updateCategory(Category category) {
344         CategorySelectorFragment categorySelectorFragment = getCategorySelectorFragment();
345         if (categorySelectorFragment != null) {
346             categorySelectorFragment.updateCategory(category);
347         }
348     }
349 
350     @Nullable
getCategorySelectorFragment()351     private CategorySelectorFragment getCategorySelectorFragment() {
352         return mContainer.getCategorySelectorFragment();
353     }
354 
355     /**
356      * Shows the view-only preview activity for the given wallpaper.
357      */
showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isAssetIdPresent)358     public void showViewOnlyPreview(WallpaperInfo wallpaperInfo, boolean isAssetIdPresent) {
359         wallpaperInfo.showPreview(
360                 mActivity, InjectorProvider.getInjector().getViewOnlyPreviewActivityIntentFactory(),
361                 VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE, isAssetIdPresent);
362     }
363 
364     /**
365      * Shows the picker activity for the given category.
366      */
show(String collectionId)367     public void show(String collectionId) {
368         Category category = findCategoryForCollectionId(collectionId);
369         if (category == null) {
370             return;
371         }
372         category.show(mActivity, SHOW_CATEGORY_REQUEST_CODE);
373     }
374 
375     @Nullable
findCategoryForCollectionId(String collectionId)376     public Category findCategoryForCollectionId(String collectionId) {
377         return mCategoryProvider.getCategory(collectionId);
378     }
379 
380     @WallpaperSupportLevel
getWallpaperSupportLevel()381     public int getWallpaperSupportLevel() {
382         WallpaperManager wallpaperManager = WallpaperManager.getInstance(mActivity);
383 
384         if (VERSION.SDK_INT >= VERSION_CODES.N) {
385             if (wallpaperManager.isWallpaperSupported()) {
386                 return wallpaperManager.isSetWallpaperAllowed()
387                         ? WallpaperDisabledFragment.SUPPORTED_CAN_SET
388                         : WallpaperDisabledFragment.NOT_SUPPORTED_BLOCKED_BY_ADMIN;
389             }
390             return WallpaperDisabledFragment.NOT_SUPPORTED_BY_DEVICE;
391         } else if (VERSION.SDK_INT >= VERSION_CODES.M) {
392             return wallpaperManager.isWallpaperSupported()
393                     ? WallpaperDisabledFragment.SUPPORTED_CAN_SET
394                     : WallpaperDisabledFragment.NOT_SUPPORTED_BY_DEVICE;
395         } else {
396             boolean isSupported = WallpaperManager.getInstance(mActivity.getApplicationContext())
397                     .getDrawable() != null;
398             wallpaperManager.forgetLoadedWallpaper();
399             return isSupported ? WallpaperDisabledFragment.SUPPORTED_CAN_SET
400                     : WallpaperDisabledFragment.NOT_SUPPORTED_BY_DEVICE;
401         }
402     }
403 
getPreferences()404     public WallpaperPreferences getPreferences() {
405         return mPreferences;
406     }
407 
getPermissionChangedListeners()408     public List<PermissionChangedListener> getPermissionChangedListeners() {
409         return mPermissionChangedListeners;
410     }
411 
getCategoryProvider()412     public CategoryProvider getCategoryProvider() {
413         return mCategoryProvider;
414     }
415 
416     /**
417      * Call when the owner activity is destroyed to clean up listeners.
418      */
cleanUp()419     public void cleanUp() {
420         if (mPackageStatusNotifier != null) {
421             mPackageStatusNotifier.removeListener(mLiveWallpaperStatusListener);
422             mPackageStatusNotifier.removeListener(mThirdPartyStatusListener);
423             mPackageStatusNotifier.removeListener(mDownloadableWallpaperStatusListener);
424         }
425     }
426 
427     /**
428      * Call from the Activity's onRequestPermissionsResult callback to handle permission request
429      * relevant to wallpapers (ie, READ_MEDIA_IMAGES)
430      * @see androidx.fragment.app.FragmentActivity#onRequestPermissionsResult(int, String[], int[])
431      */
onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)432     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
433             @NonNull int[] grantResults) {
434         if (requestCode == WallpaperPickerDelegate.READ_EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE
435                 && permissions.length > 0
436                 && permissions[0].equals(READ_IMAGE_PERMISSION)
437                 && grantResults.length > 0) {
438             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
439                 for (PermissionChangedListener listener : getPermissionChangedListeners()) {
440                     listener.onPermissionsGranted();
441                 }
442             } else if (!mActivity.shouldShowRequestPermissionRationale(READ_IMAGE_PERMISSION)) {
443                 for (PermissionChangedListener listener : getPermissionChangedListeners()) {
444                     listener.onPermissionsDenied(true /* dontAskAgain */);
445                 }
446             } else {
447                 for (PermissionChangedListener listener :getPermissionChangedListeners()) {
448                     listener.onPermissionsDenied(false /* dontAskAgain */);
449                 }
450             }
451         }
452        getPermissionChangedListeners().clear();
453     }
454 
455     /**
456      * To be called from an Activity's onActivityResult method.
457      * Checks the result for ones that are handled by this delegate
458      * @return true if the intent was handled and calling Activity needs to finish with result
459      * OK, false otherwise.
460      */
handleActivityResult(int requestCode, int resultCode, Intent data)461     public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
462         if (resultCode != Activity.RESULT_OK) {
463             return false;
464         }
465 
466         switch (requestCode) {
467             case SHOW_CATEGORY_REQUEST_CODE:
468                 Uri imageUri = (data == null) ? null : data.getData();
469                 if (imageUri == null) {
470                     // User finished viewing a category without any data, which implies that the
471                     // user previewed and selected a wallpaper in-app, so finish this activity.
472                     return true;
473                 }
474 
475                 // User selected an image from the system picker, so launch the preview for that
476                 // image.
477                 ImageWallpaperInfo imageWallpaper = new ImageWallpaperInfo(imageUri);
478 
479                 mWallpaperPersister.setWallpaperInfoInPreview(imageWallpaper);
480                 imageWallpaper.showPreview(mActivity,
481                         InjectorProvider.getInjector().getPreviewActivityIntentFactory(),
482                         PREVIEW_WALLPAPER_REQUEST_CODE, true);
483                 return false;
484             case PREVIEW_LIVE_WALLPAPER_REQUEST_CODE:
485                 populateCategories(/* forceRefresh= */ true);
486                 return true;
487             case VIEW_ONLY_PREVIEW_WALLPAPER_REQUEST_CODE:
488                 return true;
489             case PREVIEW_WALLPAPER_REQUEST_CODE:
490                 // User previewed and selected a wallpaper, so finish this activity.
491                 if (data != null && data.getBooleanExtra(IS_LIVE_WALLPAPER, false)) {
492                     populateCategories(/* forceRefresh= */ true);
493                 }
494                 return true;
495             default:
496                 return false;
497         }
498     }
499 }
500