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