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