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