1 /* 2 * Copyright (C) 2021 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 17 package com.android.providers.media.photopicker.viewmodel; 18 19 import static android.content.Intent.ACTION_GET_CONTENT; 20 import static android.content.Intent.EXTRA_LOCAL_ONLY; 21 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA; 22 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS; 23 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES; 24 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS; 25 import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS; 26 27 import static com.android.providers.media.PickerUriResolver.INIT_PATH; 28 import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI; 29 import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN; 30 import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY; 31 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST; 32 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_GRID; 33 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_DEFAULT; 34 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE; 35 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS; 36 import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED; 37 38 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; 39 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; 40 41 import android.annotation.SuppressLint; 42 import android.app.ActivityManager; 43 import android.app.Application; 44 import android.content.ContentResolver; 45 import android.content.Context; 46 import android.content.Intent; 47 import android.content.pm.PackageManager; 48 import android.content.pm.ProviderInfo; 49 import android.database.ContentObserver; 50 import android.database.Cursor; 51 import android.net.Uri; 52 import android.os.Build; 53 import android.os.Bundle; 54 import android.os.CancellationSignal; 55 import android.os.Handler; 56 import android.os.Looper; 57 import android.provider.MediaStore; 58 import android.text.TextUtils; 59 import android.util.Log; 60 61 import androidx.annotation.MainThread; 62 import androidx.annotation.NonNull; 63 import androidx.annotation.Nullable; 64 import androidx.annotation.UiThread; 65 import androidx.annotation.VisibleForTesting; 66 import androidx.lifecycle.AndroidViewModel; 67 import androidx.lifecycle.LiveData; 68 import androidx.lifecycle.MutableLiveData; 69 import androidx.lifecycle.Observer; 70 71 import com.android.internal.logging.InstanceId; 72 import com.android.internal.logging.InstanceIdSequence; 73 import com.android.modules.utils.BackgroundThread; 74 import com.android.modules.utils.build.SdkLevel; 75 import com.android.providers.media.ConfigStore; 76 import com.android.providers.media.MediaApplication; 77 import com.android.providers.media.photopicker.DataLoaderThread; 78 import com.android.providers.media.photopicker.NotificationContentObserver; 79 import com.android.providers.media.photopicker.PickerAccentColorParameters; 80 import com.android.providers.media.photopicker.data.ItemsProvider; 81 import com.android.providers.media.photopicker.data.MuteStatus; 82 import com.android.providers.media.photopicker.data.PaginationParameters; 83 import com.android.providers.media.photopicker.data.PickerResult; 84 import com.android.providers.media.photopicker.data.Selection; 85 import com.android.providers.media.photopicker.data.UserIdManager; 86 import com.android.providers.media.photopicker.data.UserManagerState; 87 import com.android.providers.media.photopicker.data.model.Category; 88 import com.android.providers.media.photopicker.data.model.Item; 89 import com.android.providers.media.photopicker.data.model.RefreshRequest; 90 import com.android.providers.media.photopicker.data.model.UserId; 91 import com.android.providers.media.photopicker.metrics.NonUiEventLogger; 92 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger; 93 import com.android.providers.media.photopicker.ui.ItemsAction; 94 import com.android.providers.media.photopicker.util.CategoryOrganiserUtils; 95 import com.android.providers.media.photopicker.util.MimeFilterUtils; 96 import com.android.providers.media.photopicker.util.ThreadUtils; 97 import com.android.providers.media.util.MimeUtils; 98 99 import java.util.ArrayList; 100 import java.util.Arrays; 101 import java.util.HashSet; 102 import java.util.List; 103 import java.util.Map; 104 import java.util.Objects; 105 import java.util.Set; 106 import java.util.stream.Collectors; 107 import java.util.stream.IntStream; 108 109 /** 110 * PickerViewModel to store and handle data for PhotoPickerActivity. 111 */ 112 public class PickerViewModel extends AndroidViewModel { 113 public static final String TAG = "PhotoPicker"; 114 private static final int INSTANCE_ID_MAX = 1 << 15; 115 private static final int DELAY_MILLIS = 0; 116 117 // Token for the tasks to load the category items in the data loader thread's queue 118 private final Object mLoadCategoryItemsThreadToken = new Object(); 119 120 @NonNull 121 @SuppressLint("StaticFieldLeak") 122 private final Context mAppContext; 123 124 private final Selection mSelection; 125 126 private int mPackageUid = -1; 127 128 private final MuteStatus mMuteStatus; 129 public boolean mEmptyPageDisplayed = false; 130 131 private int mCallingPackageUid = -1; 132 @MediaStore.PickImagesTab 133 private int mPickerLaunchTab = MediaStore.PICK_IMAGES_TAB_IMAGES; 134 135 // TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the 136 // data set to reduce memories. 137 // The list of Items with all photos and videos 138 private MutableLiveData<PaginatedItemsResult> mItemsResult; 139 private int mItemsPageSize = -1; 140 141 // The list of Items with all photos and videos in category 142 private MutableLiveData<PaginatedItemsResult> mCategoryItemsResult; 143 144 private int mCategoryItemsPageSize = -1; 145 146 // The list of categories. 147 private MutableLiveData<List<Category>> mCategoryList; 148 149 private MutableLiveData<Boolean> mIsAllPreGrantedMediaLoaded = new MutableLiveData<>(false); 150 private final MutableLiveData<RefreshRequest> mRefreshUiLiveData = 151 new MutableLiveData<>(RefreshRequest.DEFAULT); 152 private final ContentObserver mRefreshUiNotificationObserver = new ContentObserver(null) { 153 @Override 154 public void onChange(boolean selfChange, Uri uri) { 155 boolean shouldInit = uri.getLastPathSegment().equals(INIT_PATH); 156 mRefreshUiLiveData.postValue(new RefreshRequest(true, shouldInit)); 157 } 158 }; 159 160 private MutableLiveData<Boolean> mIsSyncInProgress = new MutableLiveData<>(false); 161 162 private ItemsProvider mItemsProvider; 163 private UserIdManager mUserIdManager; 164 private UserManagerState mUserManagerState; 165 private BannerManager mBannerManager; 166 167 private InstanceId mInstanceId; 168 private PhotoPickerUiEventLogger mLogger; 169 private ConfigStore mConfigStore; 170 171 private String[] mMimeTypeFilters = null; 172 private int mBottomSheetState; 173 174 private Category mCurrentCategory; 175 176 // Content resolver for the currently selected user 177 private ContentResolver mContentResolver; 178 179 // Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates 180 private boolean mIsUserSelectForApp; 181 182 private boolean mIsPickImagesAction; 183 184 private boolean mIsPreSelectionInPickImagesEnabled; 185 186 private boolean mIsManagedSelectionEnabled; 187 private boolean mIsLocalOnly; 188 private boolean mIsAllCategoryItemsLoaded = false; 189 private boolean mIsNotificationForUpdateReceived = false; 190 private CancellationSignal mCancellationSignal = new CancellationSignal(); 191 private Application mApplication; 192 private PickerAccentColorParameters mPickerAccentColorParameters = 193 new PickerAccentColorParameters(); 194 195 // This boolean remembers that the data has been initialized so that if Picker Activity gets 196 // re-created, we don't re-send a data initialization request. 197 private boolean mIsPhotoPickerDataInitialized = false; 198 PickerViewModel(@onNull Application application)199 public PickerViewModel(@NonNull Application application) { 200 super(application); 201 mApplication = application; 202 mAppContext = application.getApplicationContext(); 203 mItemsProvider = new ItemsProvider(mAppContext); 204 mSelection = new Selection(); 205 mMuteStatus = new MuteStatus(); 206 mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId(); 207 mLogger = new PhotoPickerUiEventLogger(); 208 mIsUserSelectForApp = false; 209 mIsManagedSelectionEnabled = false; 210 mIsLocalOnly = false; 211 212 initConfigStore(); 213 214 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 215 mUserManagerState = UserManagerState.create(mAppContext); 216 mUserIdManager = null; 217 } else { 218 mUserIdManager = UserIdManager.create(mAppContext); 219 mUserManagerState = null; 220 } 221 222 registerRefreshUiNotificationObserver(); 223 // Add notification content observer for any notifications received for changes in media. 224 NotificationContentObserver contentObserver = new NotificationContentObserver(null); 225 contentObserver.registerKeysToObserverCallback( 226 Arrays.asList(NotificationContentObserver.MEDIA), 227 (dateTakenMs, albumId) -> { 228 onNotificationReceived(); 229 }); 230 contentObserver.register(mAppContext.getContentResolver()); 231 } 232 233 @Override onCleared()234 protected void onCleared() { 235 unregisterRefreshUiNotificationObserver(); 236 237 // Signal ContentProvider to cancel currently running task. 238 mCancellationSignal.cancel(); 239 240 clearQueuedTasksInDataLoaderThread(); 241 } 242 onNotificationReceived()243 private void onNotificationReceived() { 244 Log.d(TAG, "Notification for media update has been received"); 245 mIsNotificationForUpdateReceived = true; 246 if (mEmptyPageDisplayed && mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 247 (new Handler(Looper.getMainLooper())).post(() -> { 248 Log.d(TAG, "Refreshing UI to display new items."); 249 mEmptyPageDisplayed = false; 250 getPaginatedItemsForAction(ACTION_REFRESH_ITEMS, 251 new PaginationParameters(mItemsPageSize, -1, -1)); 252 }); 253 } 254 } 255 setCallingPackageUid(int callingPackageUid)256 public void setCallingPackageUid(int callingPackageUid) { 257 mCallingPackageUid = callingPackageUid; 258 } 259 getCallingPackageUid()260 private int getCallingPackageUid() { 261 return mCallingPackageUid; 262 } 263 getPickerLaunchTab()264 public int getPickerLaunchTab() { 265 return mPickerLaunchTab; 266 } 267 setPickerLaunchTab(int launchTab)268 public void setPickerLaunchTab(int launchTab) { 269 mPickerLaunchTab = launchTab; 270 } 271 272 @VisibleForTesting initConfigStore()273 protected void initConfigStore() { 274 mConfigStore = MediaApplication.getConfigStore(); 275 } 276 277 @VisibleForTesting setItemsProvider(@onNull ItemsProvider itemsProvider)278 public void setItemsProvider(@NonNull ItemsProvider itemsProvider) { 279 mItemsProvider = itemsProvider; 280 } 281 282 @VisibleForTesting setUserIdManager(@onNull UserIdManager userIdManager)283 public void setUserIdManager(@NonNull UserIdManager userIdManager) { 284 if (userIdManager == null) { 285 throw new IllegalArgumentException("Given UserIdManager object can not be null"); 286 } 287 mUserIdManager = userIdManager; 288 } 289 290 /** 291 * Injects given {@link UserManagerState} object into {@link #mUserManagerState} 292 */ 293 @VisibleForTesting setUserManagerState(@onNull UserManagerState userManagerState)294 public void setUserManagerState(@NonNull UserManagerState userManagerState) { 295 if (userManagerState == null) { 296 throw new IllegalArgumentException("Given UserManagerState object can not be null"); 297 } 298 mUserManagerState = userManagerState; 299 } 300 301 @VisibleForTesting setBannerManager(@onNull BannerManager bannerManager)302 public void setBannerManager(@NonNull BannerManager bannerManager) { 303 mBannerManager = bannerManager; 304 } 305 306 @VisibleForTesting setNotificationForUpdateReceived(boolean notificationForUpdateReceived)307 public void setNotificationForUpdateReceived(boolean notificationForUpdateReceived) { 308 mIsNotificationForUpdateReceived = notificationForUpdateReceived; 309 } 310 311 @VisibleForTesting setLogger(@onNull PhotoPickerUiEventLogger logger)312 public void setLogger(@NonNull PhotoPickerUiEventLogger logger) { 313 mLogger = logger; 314 } 315 316 @VisibleForTesting setConfigStore(@onNull ConfigStore configStore)317 public void setConfigStore(@NonNull ConfigStore configStore) { 318 mConfigStore = configStore; 319 } 320 setEmptyPageDisplayed(boolean emptyPageDisplayed)321 public void setEmptyPageDisplayed(boolean emptyPageDisplayed) { 322 mEmptyPageDisplayed = emptyPageDisplayed; 323 } 324 325 /** 326 * @return the {@link ConfigStore} for this context. 327 */ getConfigStore()328 public ConfigStore getConfigStore() { 329 return mConfigStore; 330 } 331 332 /** 333 * @return {@link UserIdManager} for this context. 334 */ getUserIdManager()335 public UserIdManager getUserIdManager() { 336 return mUserIdManager; 337 } 338 339 /** 340 * @return {@link UserManagerState} for this context. 341 */ getUserManagerState()342 public UserManagerState getUserManagerState() { 343 return mUserManagerState; 344 } 345 346 /** 347 * @return {@code mSelection} that manages the selection 348 */ getSelection()349 public Selection getSelection() { 350 return mSelection; 351 } 352 353 /** 354 * @return {@code mMuteStatus} that tracks the volume mute status of the video preview 355 */ getMuteStatus()356 public MuteStatus getMuteStatus() { 357 return mMuteStatus; 358 } 359 360 /** 361 * @return {@code mIsUserSelectForApp} if the picker is currently being used 362 * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action. 363 */ isUserSelectForApp()364 public boolean isUserSelectForApp() { 365 return mIsUserSelectForApp; 366 } 367 368 /** 369 * @return {@code mIsPickImagesAction} if the picker is currently being used 370 * for the {@link MediaStore#ACTION_PICK_IMAGES} action. 371 */ isPickImagesAction()372 public boolean isPickImagesAction() { 373 return mIsPickImagesAction; 374 } 375 376 /** 377 * @return {@code mIsManagedSelectionEnabled} if the picker is currently being used 378 * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and flag 379 * pickerChoiceManagedSelection is enabled.. 380 */ isManagedSelectionEnabled()381 public boolean isManagedSelectionEnabled() { 382 return mIsManagedSelectionEnabled; 383 } 384 385 /** 386 * @return true if the picker is currently being used 387 * for the {@link MediaStore#ACTION_PICK_IMAGES} action and pre-selection is required or if the 388 * picker is being used in {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action and 389 * managed selection is enabled; 390 */ isPreSelectionEnabled()391 public boolean isPreSelectionEnabled() { 392 return mIsPreSelectionInPickImagesEnabled || mIsManagedSelectionEnabled; 393 } 394 395 396 /** 397 * @return a {@link LiveData} that holds the value (once it's fetched) of the 398 * {@link android.content.ContentProvider#mAuthority authority} of the current 399 * {@link android.provider.CloudMediaProvider}. 400 */ 401 @NonNull getCloudMediaProviderAuthorityLiveData()402 public LiveData<String> getCloudMediaProviderAuthorityLiveData() { 403 return mBannerManager.getCloudMediaProviderAuthorityLiveData(); 404 } 405 406 /** 407 * @return a {@link LiveData} that holds the value (once it's fetched) of the label 408 * of the current {@link android.provider.CloudMediaProvider}. 409 */ 410 @NonNull getCloudMediaProviderAppTitleLiveData()411 public LiveData<String> getCloudMediaProviderAppTitleLiveData() { 412 return mBannerManager.getCloudMediaProviderAppTitleLiveData(); 413 } 414 415 /** 416 * @return a {@link LiveData} that holds the value (once it's fetched) of the account name 417 * of the current {@link android.provider.CloudMediaProvider}. 418 */ 419 @NonNull getCloudMediaAccountNameLiveData()420 public LiveData<String> getCloudMediaAccountNameLiveData() { 421 return mBannerManager.getCloudMediaAccountNameLiveData(); 422 } 423 424 /** 425 * @return the account selection activity {@link Intent} of the current 426 * {@link android.provider.CloudMediaProvider}. 427 */ 428 @Nullable getChooseCloudMediaAccountActivityIntent()429 public Intent getChooseCloudMediaAccountActivityIntent() { 430 return mBannerManager.getChooseCloudMediaAccountActivityIntent(); 431 } 432 433 /** 434 * Reset to personal profile mode. 435 */ 436 @UiThread resetToPersonalProfile()437 public void resetToPersonalProfile() { 438 mUserIdManager.setPersonalAsCurrentUserProfile(); 439 onSwitchedProfile(); 440 } 441 442 /** 443 * Reset to a given profile 444 * @param userId : the profile where photopicker want switch to 445 */ 446 @UiThread resetToGivenUserProfile(@onNull UserId userId)447 public void resetToGivenUserProfile(@NonNull UserId userId) { 448 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 449 if (userId == null) { 450 throw new IllegalArgumentException("Given userId can not be null"); 451 } 452 mUserManagerState.setUserAsCurrentUserProfile(userId); 453 onSwitchedProfile(); 454 } 455 } 456 457 /** 458 * Reset to a user profile that starts photopicker activity 459 */ 460 @UiThread resetToCurrentUserProfile()461 public void resetToCurrentUserProfile() { 462 resetToGivenUserProfile(UserId.CURRENT_USER); 463 } 464 465 /** 466 * Reset the content observer & all the content on profile switched. 467 */ 468 @UiThread onSwitchedProfile()469 public void onSwitchedProfile() { 470 resetRefreshUiNotificationObserver(); 471 resetAllContentInCurrentProfile(/* shouldSendInitRequest */ true); 472 } 473 474 /** 475 * Reset all the content (items, categories & banners) in the current profile. 476 */ 477 @UiThread resetAllContentInCurrentProfile(boolean shouldSendInitRequest)478 public void resetAllContentInCurrentProfile(boolean shouldSendInitRequest) { 479 Log.d(TAG, "Reset all content in current profile"); 480 481 // Post 'should refresh UI live data' value as false to avoid unnecessary repetitive resets 482 mRefreshUiLiveData.postValue(RefreshRequest.DEFAULT); 483 484 clearQueuedTasksInDataLoaderThread(); 485 486 if (shouldSendInitRequest) { 487 initPhotoPickerData(); 488 } 489 490 // Clear the existing content - selection, photos grid, albums grid, banners 491 mSelection.clearSelectedItems(); 492 493 final List<Item> itemsList = new ArrayList<>(); 494 itemsList.add(Item.EMPTY_VIEW); 495 if (mItemsResult != null) { 496 DataLoaderThread.getHandler().postDelayed(() -> 497 mItemsResult.postValue(new PaginatedItemsResult(itemsList, ACTION_CLEAR_GRID)), 498 TOKEN, 499 DELAY_MILLIS 500 ); 501 } 502 503 final List<Category> categoryList = new ArrayList<>(); 504 categoryList.add(Category.EMPTY_VIEW); 505 if (mCategoryList != null) { 506 DataLoaderThread.getHandler().postDelayed(() -> 507 mCategoryList.postValue(categoryList), 508 TOKEN, 509 DELAY_MILLIS 510 ); 511 } 512 513 mBannerManager.hideAllBanners(); 514 515 // Update items, categories & banners 516 getPaginatedItemsForAction(ACTION_CLEAR_AND_UPDATE_LIST, null); 517 updateCategories(); 518 mBannerManager.reset(); 519 } 520 521 /** 522 * Loads list of pre granted items for the current package and userID. 523 */ initialisePreGrantsIfNecessary(Selection selection, Bundle intentExtras, String[] mimeTypeFilters)524 public void initialisePreGrantsIfNecessary(Selection selection, Bundle intentExtras, 525 String[] mimeTypeFilters) { 526 if (isManagedSelectionEnabled() && selection.getPreGrantedUris() == null) { 527 DataLoaderThread.getHandler().postDelayed(() -> { 528 List<Uri> preGrantedUris = mItemsProvider.fetchReadGrantedItemsUrisForPackage( 529 intentExtras.getInt(Intent.EXTRA_UID), mimeTypeFilters); 530 selection.setPreGrantedItems(preGrantedUris); 531 logPickerChoiceInitGrantsCount(preGrantedUris.size(), intentExtras); 532 }, TOKEN, DELAY_MILLIS); 533 } else if (isPickImagesAction() && mSelection.canSelectMultiple()) { 534 initialisePreSelectionItems(intentExtras); 535 } 536 } 537 538 /** 539 * Performs required modification to the item list and returns the live data for it. 540 */ getPaginatedItemsForAction( @temsAction.Type int action, @Nullable PaginationParameters paginationParameters)541 public LiveData<PaginatedItemsResult> getPaginatedItemsForAction( 542 @ItemsAction.Type int action, 543 @Nullable PaginationParameters paginationParameters) { 544 switch (action) { 545 case ACTION_VIEW_CREATED: { 546 // Use this when a fresh view is created. If the current list is empty, it will 547 // load the first page and return the list, else it will return previously 548 // existing values. 549 mItemsPageSize = paginationParameters.getPageSize(); 550 if (mItemsResult == null) { 551 updatePaginatedItems(paginationParameters, true, action); 552 } 553 break; 554 } 555 case ACTION_LOAD_NEXT_PAGE: { 556 // Loads next page of the list, using the previously loaded list. 557 // If the current list is empty then it will not perform any actions. 558 if (mItemsResult != null && mItemsResult.getValue() != null) { 559 List<Item> currentItemList = mItemsResult.getValue().getItems(); 560 // If the list is already empty that would mean that the first page was not 561 // loaded since there were no items to be loaded. 562 if (currentItemList != null && !currentItemList.isEmpty()) { 563 // get the last item of the existing list. 564 Item item = currentItemList.get(currentItemList.size() - 1); 565 updatePaginatedItems( 566 new PaginationParameters(mItemsPageSize, item.getDateTaken(), 567 item.getRowId()), false, action); 568 } 569 } 570 break; 571 } 572 case ACTION_CLEAR_AND_UPDATE_LIST: { 573 // Clears the existing list and loads the list with for mItemsPageSize 574 // number of items. This will be equal to page size for pagination if cloud 575 // picker feature flag is enabled, else it will be -1 implying that the complete 576 // list should be loaded. 577 updatePaginatedItems(new PaginationParameters(mItemsPageSize, 578 /*dateBeforeMs*/ Long.MIN_VALUE, /*rowId*/ -1), /* isReset */ true, action); 579 break; 580 } 581 case ACTION_REFRESH_ITEMS: { 582 if (mIsNotificationForUpdateReceived 583 && mItemsResult != null 584 && mItemsResult.getValue() != null) { 585 updatePaginatedItems(paginationParameters, true, action); 586 mIsNotificationForUpdateReceived = false; 587 } 588 break; 589 } 590 default: 591 Log.w(TAG, "Invalid action passed to fetch items"); 592 } 593 return mItemsResult; 594 } 595 596 /** 597 * Update the item List {@link #mItemsResult}. Loads the page requested represented by the 598 * pagination parameters and replaces/appends it to the existing list of items based on the 599 * reset value. 600 */ updatePaginatedItems(PaginationParameters pagingParameters, boolean isReset, @ItemsAction.Type int action)601 private void updatePaginatedItems(PaginationParameters pagingParameters, boolean isReset, 602 @ItemsAction.Type int action) { 603 if (mItemsResult == null) { 604 mItemsResult = new MutableLiveData<>(); 605 } 606 loadItemsAsync(pagingParameters, /* isReset */ isReset, action); 607 } 608 getCurrentUserProfileId()609 private UserId getCurrentUserProfileId() { 610 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 611 return mUserManagerState.getCurrentUserProfileId(); 612 } 613 return mUserIdManager.getCurrentUserProfileId(); 614 } 615 616 /** 617 * Loads required items and sets it to the {@link PickerViewModel#mItemsResult} while 618 * considering the isReset value. 619 * 620 * @param pagingParameters parameters representing the items that needs to be loaded next. 621 * @param isReset If this is true, clear the pre-existing list and add the newly loaded 622 * items. 623 * @param action This is used while posting the result of the operation. 624 */ loadItemsAsync(@onNull PaginationParameters pagingParameters, boolean isReset, @ItemsAction.Type int action)625 private void loadItemsAsync(@NonNull PaginationParameters pagingParameters, boolean isReset, 626 @ItemsAction.Type int action) { 627 final UserId userId = getCurrentUserProfileId(); 628 DataLoaderThread.getHandler().postDelayed(() -> { 629 // Load the items as per the pagination parameters passed as params to this method. 630 List<Item> newPageItemList = loadItems(Category.DEFAULT, userId, pagingParameters); 631 632 // Based on if it is a reset case or not, create an updated list. 633 // If it is a reset case, assign an empty list else use the contents of the pre-existing 634 // list. Then add the newly loaded items. 635 List<Item> updatedList = 636 mItemsResult.getValue() == null || isReset ? new ArrayList<>() 637 : mItemsResult.getValue().getItems(); 638 updatedList.addAll(newPageItemList); 639 Log.d(TAG, "Next page for photos items have been loaded."); 640 if (newPageItemList.isEmpty()) { 641 Log.d(TAG, "All photos items have been loaded."); 642 } 643 644 // post the result with the action. 645 mItemsResult.postValue(new PaginatedItemsResult(updatedList, action)); 646 mIsSyncInProgress.postValue(false); 647 }, TOKEN, DELAY_MILLIS); 648 } 649 loadItems(Category category, UserId userId, PaginationParameters pagingParameters)650 private List<Item> loadItems(Category category, UserId userId, 651 PaginationParameters pagingParameters) { 652 final List<Item> items = new ArrayList<>(); 653 String cloudProviderAuthority = null; // NULL if fetched items have NO cloud only media item 654 655 try (Cursor cursor = fetchItems(category, userId, pagingParameters)) { 656 if (cursor == null || cursor.getCount() == 0) { 657 Log.d(TAG, "Didn't receive any items for " + category 658 + ", either cursor is null or cursor count is zero"); 659 return items; 660 } 661 662 Set<Uri> preGrantedUris = new HashSet<>(0); 663 Set<Uri> deSelectedPreGrantedUris = new HashSet<>(0); 664 Set<Uri> currentSelection = mSelection.getSelectedItemsUris(); 665 if (isPreSelectionEnabled() && mSelection.getPreGrantedUris() != null) { 666 preGrantedUris = mSelection.getPreGrantedUris(); 667 deSelectedPreGrantedUris = mSelection.getDeselectedUrisToBeRevoked(); 668 Log.d(TAG, "pre granted items : " + preGrantedUris); 669 } 670 671 while (cursor.moveToNext()) { 672 final Item item = Item.fromCursor(cursor, userId); 673 if (preGrantedUris.contains(item.getContentUri())) { 674 item.setPreGranted(); 675 if (!deSelectedPreGrantedUris.contains(item.getContentUri()) 676 && !currentSelection.contains(item.getContentUri())) { 677 // if the item has been de-selected or is already present in the current 678 // selection set, then it should not be added again. 679 mSelection.addSelectedItem(item); 680 } 681 } 682 String authority = item.getContentUri().getAuthority(); 683 684 if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) { 685 cloudProviderAuthority = authority; 686 } 687 items.add(item); 688 } 689 690 Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user " 691 + userId.toString()); 692 return items; 693 } finally { 694 int count = items.size(); 695 if (category.isDefault()) { 696 mLogger.logLoadedMainGridMediaItems(cloudProviderAuthority, mInstanceId, count); 697 } else { 698 mLogger.logLoadedAlbumGridMediaItems(cloudProviderAuthority, mInstanceId, count); 699 } 700 } 701 } 702 703 /** 704 * @return true when all pre-granted items data has been loaded for this session. 705 */ 706 @NonNull getIsAllPreGrantedMediaLoaded()707 public MutableLiveData<Boolean> getIsAllPreGrantedMediaLoaded() { 708 return mIsAllPreGrantedMediaLoaded; 709 } 710 711 /** 712 * Gets item data for Uris which have not yet been loaded to the UI. This is important when the 713 * preview fragment is created and hence should be called only before creation. 714 * 715 * <p>This is used during pagination. All the items are not loaded at once and hence the 716 * preGranted item which is on a page that is yet to be loaded will would not be part of the 717 * mSelected list and hence will not show up in the preview fragment. This method fixes this 718 * issue by selectively loading those items and adding them to the selection list.</p> 719 */ getRemainingPreGrantedItems()720 public void getRemainingPreGrantedItems() { 721 if (!isManagedSelectionEnabled() || mSelection.getPreGrantedUris() == null) return; 722 723 List<Uri> urisForItemsToBeFetched = 724 new ArrayList<>(mSelection.getPreGrantedUris()); 725 urisForItemsToBeFetched.removeAll(mSelection.getSelectedItems().stream().map( 726 Item::getContentUri).collect(Collectors.toSet())); 727 urisForItemsToBeFetched.removeAll(mSelection.getDeselectedUrisToBeRevoked()); 728 729 if (!urisForItemsToBeFetched.isEmpty()) { 730 getItemDataForUris(urisForItemsToBeFetched, /* callingPackageUid */ -1, 731 /* shouldScreenSelectionUris */ false); 732 } 733 } 734 initialisePreSelectionItems(Bundle intentExtras)735 private void initialisePreSelectionItems(Bundle intentExtras) { 736 if (Boolean.TRUE.equals(mIsAllPreGrantedMediaLoaded.getValue())) { 737 return; 738 } 739 List<Uri> preSelectedUris; 740 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 741 // type safe getParcelableArrayList was introduced in Build.VERSION_CODES.TIRAMISU 742 preSelectedUris = intentExtras.getParcelableArrayList( 743 MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS, Uri.class); 744 } else { 745 preSelectedUris = intentExtras.getParcelableArrayList( 746 MediaStore.EXTRA_PICKER_PRE_SELECTION_URIS); 747 } 748 if (preSelectedUris != null) { 749 // If more than 100 URIs are passed in as intent extras then this is not supported. 750 if (preSelectedUris.size() > mSelection.getMaxSelectionLimit()) { 751 throw new IllegalArgumentException( 752 "The number of URIs exceed the maximum allowed limit: " 753 + mSelection.getMaxSelectionLimit()); 754 } 755 getItemDataForUris(preSelectedUris, getCallingPackageUid(), 756 /* isFilterUrisForSelection */ true); 757 } else { 758 Log.d(TAG, "No pre-selection URIs to be loaded"); 759 mIsAllPreGrantedMediaLoaded.postValue(true); 760 } 761 } 762 getItemDataForUris(List<Uri> urisForItemsToBeFetched, int callingPackageUid, boolean shouldScreenSelectionUris)763 private void getItemDataForUris(List<Uri> urisForItemsToBeFetched, int callingPackageUid, 764 boolean shouldScreenSelectionUris) { 765 if (!urisForItemsToBeFetched.isEmpty()) { 766 UserId userId = getCurrentUserProfileId(); 767 DataLoaderThread.getHandler().postDelayed(() -> { 768 loadItemsDataForPreSelection(Category.DEFAULT, userId, 769 urisForItemsToBeFetched, callingPackageUid, shouldScreenSelectionUris); 770 // If new data has loaded then post value representing a successful operation. 771 mIsAllPreGrantedMediaLoaded.postValue(true); 772 }, TOKEN, 0); 773 } 774 } 775 loadItemsDataForPreSelection(Category category, UserId userId, List<Uri> selectionArg, int callingPackageUid, boolean shouldScreenSelectionUris)776 private void loadItemsDataForPreSelection(Category category, UserId userId, 777 List<Uri> selectionArg, int callingPackageUid, boolean shouldScreenSelectionUris) { 778 try (Cursor cursor = mItemsProvider.getItemsForPreselectedMedia(category, selectionArg, 779 mMimeTypeFilters, userId, shouldShowOnlyLocalFeatures(), callingPackageUid, 780 shouldScreenSelectionUris, mCancellationSignal)) { 781 if (cursor == null || cursor.getCount() == 0) { 782 Log.d(TAG, "Didn't receive any items for pre granted URIs" + category 783 + ", either cursor is null or cursor count is zero"); 784 return; 785 } 786 Set<Uri> selectedUrisSet = mSelection.getSelectedItemsUris(); 787 // Add all loaded items to selection after marking them as pre granted. 788 List<Item> preSelectedItems = new ArrayList<>(); 789 while (cursor.moveToNext()) { 790 final Item item = Item.fromCursor(cursor, userId); 791 item.setPreGranted(); 792 if (!selectedUrisSet.contains(item.getContentUri())) { 793 preSelectedItems.add(item); 794 } 795 } 796 797 if (isPickImagesAction()) { 798 // If the code has reached this point it implies that valid items are present for 799 // pre-selection. 800 mIsPreSelectionInPickImagesEnabled = true; 801 802 List<Uri> preSelectedPickerUris = PickerResult.getPickerUrisForItems( 803 MediaStore.ACTION_PICK_IMAGES, preSelectedItems); 804 805 Map<Uri, Item> preGrantedUriToItemMap = IntStream.range(0, 806 preSelectedPickerUris.size()) 807 .boxed() 808 .collect(Collectors.toMap(preSelectedPickerUris::get, 809 preSelectedItems::get)); 810 811 // Now add loaded items to selection in the same order as they were received in the 812 // input list. This is done to maintain order in case 813 // MediaStore.EXTRA_PICK_IMAGES_IN_ORDER is also enabled. 814 for (Uri uri : selectionArg) { 815 if (preGrantedUriToItemMap.containsKey(uri)) { 816 mSelection.addSelectedItem(preGrantedUriToItemMap.get(uri)); 817 } 818 } 819 } else if (isManagedSelectionEnabled()) { 820 for (Item item : preSelectedItems) { 821 mSelection.addSelectedItem(item); 822 } 823 } 824 } 825 } 826 fetchItems(Category category, UserId userId, PaginationParameters pagingParameters)827 private Cursor fetchItems(Category category, UserId userId, 828 PaginationParameters pagingParameters) { 829 try { 830 if (shouldShowOnlyLocalFeatures()) { 831 return mItemsProvider.getLocalItems(category, pagingParameters, 832 mMimeTypeFilters, userId, mCancellationSignal); 833 } else { 834 return mItemsProvider.getAllItems(category, pagingParameters, 835 mMimeTypeFilters, userId, mCancellationSignal); 836 } 837 } catch (RuntimeException ignored) { 838 // Catch OperationCanceledException. 839 Log.e(TAG, "Failed to fetch items due to a runtime exception", ignored); 840 return null; 841 } 842 } 843 844 /** 845 * Modifies and returns the live data for category items. 846 */ getPaginatedCategoryItemsForAction( @onNull Category category, @ItemsAction.Type int action, @Nullable PaginationParameters paginationParameters)847 public LiveData<PaginatedItemsResult> getPaginatedCategoryItemsForAction( 848 @NonNull Category category, 849 @ItemsAction.Type int action, @Nullable PaginationParameters paginationParameters) { 850 switch (action) { 851 case ACTION_VIEW_CREATED: { 852 // This call is made only for loading the first page of album media, 853 // so the existing data loader thread tasks for updating the category items should 854 // be cleared and the category and category item list should be refreshed each time. 855 DataLoaderThread.getHandler().removeCallbacksAndMessages( 856 mLoadCategoryItemsThreadToken); 857 mCategoryItemsResult = new MutableLiveData<>(); 858 mCurrentCategory = category; 859 assert paginationParameters != null; 860 mCategoryItemsPageSize = paginationParameters.getPageSize(); 861 updateCategoryItems(paginationParameters, action); 862 break; 863 } 864 case ACTION_LOAD_NEXT_PAGE: { 865 // Loads next page of the list, using the previously loaded list. 866 // If the current list is empty then it will not perform any actions. 867 if (mCategoryItemsResult == null || mCategoryItemsResult.getValue() == null 868 || !TextUtils.equals(mCurrentCategory.getId(), 869 category.getId())) { 870 break; 871 } 872 List<Item> currentItemList = mCategoryItemsResult.getValue().getItems(); 873 // If the categoryItemList does not contain any items, it would mean that the first 874 // page was empty. 875 if (currentItemList != null && !currentItemList.isEmpty()) { 876 Item item = currentItemList.get(currentItemList.size() - 1); 877 PaginationParameters pagingParams = new PaginationParameters( 878 mCategoryItemsPageSize, 879 item.getDateTaken(), 880 item.getRowId()); 881 updateCategoryItems(pagingParams, action); 882 } 883 break; 884 } 885 default: 886 Log.w(TAG, "Invalid action passed to fetch category items"); 887 } 888 return mCategoryItemsResult; 889 } 890 891 /** 892 * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemsResult} 893 * 894 * @throws IllegalStateException category and category items is not initiated before calling 895 * this method 896 */ 897 @VisibleForTesting updateCategoryItems(PaginationParameters pagingParameters, @ItemsAction.Type int action)898 public void updateCategoryItems(PaginationParameters pagingParameters, 899 @ItemsAction.Type int action) { 900 if (mCategoryItemsResult == null || mCurrentCategory == null) { 901 throw new IllegalStateException("mCurrentCategory and mCategoryItemsResult are not" 902 + " initiated. Please call getCategoryItems before calling this method"); 903 } 904 loadCategoryItemsAsync(pagingParameters, action != ACTION_LOAD_NEXT_PAGE, action); 905 } 906 907 /** 908 * Loads required category items and sets it to the {@link PickerViewModel#mCategoryItemsResult} 909 * while considering the isReset value. 910 * 911 * @param pagingParameters parameters representing the items that needs to be loaded next. 912 * @param isReset If this is true, clear the pre-existing list and add the newly loaded 913 * items. 914 * @param action This is used while posting the result of the operation. 915 */ loadCategoryItemsAsync(PaginationParameters pagingParameters, boolean isReset, @ItemsAction.Type int action)916 private void loadCategoryItemsAsync(PaginationParameters pagingParameters, boolean isReset, 917 @ItemsAction.Type int action) { 918 final UserId userId = getCurrentUserProfileId(); 919 final Category category = mCurrentCategory; 920 921 DataLoaderThread.getHandler().postDelayed(() -> { 922 if (action == ACTION_LOAD_NEXT_PAGE && mIsAllCategoryItemsLoaded) { 923 return; 924 } 925 // Load the items as per the pagination parameters passed as params to this method. 926 List<Item> newPageItemList = loadItems(category, userId, pagingParameters); 927 928 // Based on if it is a reset case or not, create an updated list. 929 // If it is a reset case, assign an empty list else use the contents of the pre-existing 930 // list. Then add the newly loaded items. 931 List<Item> updatedList = mCategoryItemsResult.getValue() == null || isReset 932 ? new ArrayList<>() : mCategoryItemsResult.getValue().getItems(); 933 updatedList.addAll(newPageItemList); 934 935 if (isReset) { 936 mIsAllCategoryItemsLoaded = false; 937 } 938 Log.d(TAG, "Next page for category items have been loaded. Category: " 939 + category + " " + updatedList.size()); 940 if (newPageItemList.isEmpty()) { 941 mIsAllCategoryItemsLoaded = true; 942 Log.d(TAG, "All items have been loaded for category: " + mCurrentCategory); 943 } 944 if (Objects.equals(category, mCurrentCategory)) { 945 mCategoryItemsResult.postValue(new PaginatedItemsResult(updatedList, action)); 946 } 947 }, mLoadCategoryItemsThreadToken, DELAY_MILLIS); 948 } 949 950 /** 951 * Used only for testing, clears out any data in item list and category item list. 952 */ 953 @VisibleForTesting clearItemsAndCategoryItemsList()954 public void clearItemsAndCategoryItemsList() { 955 mItemsResult = null; 956 mCategoryItemsResult = null; 957 } 958 959 /** 960 * @return the list of Categories {@link #mCategoryList} 961 */ getCategories()962 public LiveData<List<Category>> getCategories() { 963 if (mCategoryList == null) { 964 updateCategories(); 965 } 966 return mCategoryList; 967 } 968 loadCategories(UserId userId)969 private List<Category> loadCategories(UserId userId) { 970 final List<Category> categoryList = new ArrayList<>(); 971 String cloudProviderAuthority = null; // NULL if fetched albums have NO cloud album 972 try (Cursor cursor = fetchCategories(userId)) { 973 if (cursor == null || cursor.getCount() == 0) { 974 Log.d(TAG, "Didn't receive any categories, either cursor is null or" 975 + " cursor count is zero"); 976 return categoryList; 977 } 978 979 while (cursor.moveToNext()) { 980 final Category category = Category.fromCursor(cursor, userId); 981 String authority = category.getAuthority(); 982 983 if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) { 984 cloudProviderAuthority = authority; 985 } 986 categoryList.add(category); 987 } 988 989 Log.d(TAG, 990 "Loaded " + categoryList.size() + " categories for user " + userId.toString()); 991 CategoryOrganiserUtils.getReorganisedCategoryList(categoryList); 992 return categoryList; 993 } finally { 994 mLogger.logLoadedAlbums(cloudProviderAuthority, mInstanceId, categoryList.size()); 995 } 996 } 997 fetchCategories(UserId userId)998 private Cursor fetchCategories(UserId userId) { 999 try { 1000 if (shouldShowOnlyLocalFeatures()) { 1001 return mItemsProvider 1002 .getLocalCategories(mMimeTypeFilters, userId, mCancellationSignal); 1003 } else { 1004 return mItemsProvider 1005 .getAllCategories(mMimeTypeFilters, userId, mCancellationSignal); 1006 } 1007 } catch (RuntimeException ignored) { 1008 // Catch OperationCanceledException. 1009 Log.e(TAG, "Failed to fetch categories due to a runtime exception", ignored); 1010 return null; 1011 } 1012 } 1013 loadCategoriesAsync()1014 private void loadCategoriesAsync() { 1015 final UserId userId = getCurrentUserProfileId(); 1016 DataLoaderThread.getHandler().postDelayed(() -> { 1017 mCategoryList.postValue(loadCategories(userId)); 1018 }, TOKEN, DELAY_MILLIS); 1019 } 1020 1021 /** 1022 * Update the category List {@link #mCategoryList} 1023 */ updateCategories()1024 public void updateCategories() { 1025 if (mCategoryList == null) { 1026 mCategoryList = new MutableLiveData<>(); 1027 } 1028 loadCategoriesAsync(); 1029 } 1030 1031 /** 1032 * Return whether the {@link #mMimeTypeFilters} is {@code null} or not 1033 */ hasMimeTypeFilters()1034 public boolean hasMimeTypeFilters() { 1035 return mMimeTypeFilters != null && mMimeTypeFilters.length > 0; 1036 } 1037 isAllImagesFilter()1038 private boolean isAllImagesFilter() { 1039 return mMimeTypeFilters != null && mMimeTypeFilters.length == 1 1040 && MimeUtils.isAllImagesMimeType(mMimeTypeFilters[0]); 1041 } 1042 isAllVideosFilter()1043 private boolean isAllVideosFilter() { 1044 return mMimeTypeFilters != null && mMimeTypeFilters.length == 1 1045 && MimeUtils.isAllVideosMimeType(mMimeTypeFilters[0]); 1046 } 1047 1048 /** 1049 * Parse values from {@code intent} and set corresponding fields 1050 */ parseValuesFromIntent(Intent intent)1051 public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException { 1052 mIsPickImagesAction = MediaStore.ACTION_PICK_IMAGES.equals(intent.getAction()); 1053 final Bundle extras = intent.getExtras(); 1054 if (extras != null) { 1055 // Get the tab with which the picker needs to be launched 1056 if (extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB)) { 1057 if (intent.getAction().equals(ACTION_GET_CONTENT)) { 1058 Log.e(TAG, "EXTRA_PICKER_LAUNCH_TAB cannot be passed as an extra in " 1059 + "ACTION_GET_CONTENT"); 1060 } else if (intent.getAction().equals( 1061 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) { 1062 throw new IllegalArgumentException("EXTRA_PICKER_LAUNCH_TAB cannot be passed " 1063 + "as an extra in ACTION_USER_SELECT_IMAGES_FOR_APP"); 1064 } else { 1065 mPickerLaunchTab = extras.getInt(MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB); 1066 if (!checkPickerLaunchOptionValidity(mPickerLaunchTab)) { 1067 throw new IllegalArgumentException("Incorrect value " + mPickerLaunchTab 1068 + " received for the intent extra: " 1069 + MediaStore.EXTRA_PICK_IMAGES_LAUNCH_TAB); 1070 } 1071 } 1072 } 1073 // Get the picker accent color 1074 if (extras.containsKey(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR)) { 1075 if (intent.getAction().equals(ACTION_GET_CONTENT)) { 1076 Log.w(TAG, "EXTRA_PICK_IMAGES_ACCENT_COLOR cannot be passed as an " 1077 + "extra in ACTION_GET_CONTENT"); 1078 } else if (intent.getAction().equals( 1079 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP)) { 1080 throw new IllegalArgumentException( 1081 "EXTRA_PICK_IMAGES_ACCENT_COLOR cannot be passed " 1082 + "as an extra in ACTION_USER_SELECT_IMAGES_FOR_APP"); 1083 } else if (intent.getAction().equals(MediaStore.ACTION_PICK_IMAGES)) { 1084 try { 1085 long inputColor = extras.getLong(MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR); 1086 int validatedColor = 1087 PickerAccentColorParameters.checkColorValidityAndGetColor( 1088 inputColor); 1089 if (validatedColor != -1) { 1090 mPickerAccentColorParameters = new PickerAccentColorParameters( 1091 validatedColor, mApplication); 1092 } 1093 } catch (Exception exception) { 1094 throw new IllegalArgumentException("The Accent colour provided in " 1095 + MediaStore.EXTRA_PICK_IMAGES_ACCENT_COLOR 1096 + " fails validation. Please refer to the javadocs on what " 1097 + "is acceptable."); 1098 } 1099 } 1100 } 1101 } 1102 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1103 mUserManagerState.setIntentAndCheckRestrictions(intent); 1104 } else { 1105 mUserIdManager.setIntentAndCheckRestrictions(intent); 1106 } 1107 1108 mMimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(intent); 1109 1110 mSelection.parseSelectionValuesFromIntent(intent); 1111 1112 mIsLocalOnly = intent.getBooleanExtra(EXTRA_LOCAL_ONLY, false); 1113 1114 mIsUserSelectForApp = 1115 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intent.getAction()); 1116 mIsManagedSelectionEnabled = mIsUserSelectForApp 1117 && getConfigStore().isPickerChoiceManagedSelectionEnabled(); 1118 if (!SdkLevel.isAtLeastU() && mIsUserSelectForApp) { 1119 throw new IllegalArgumentException("ACTION_USER_SELECT_IMAGES_FOR_APP is not enabled " 1120 + " for this OS version"); 1121 } 1122 1123 // Ensure that if Photopicker is being used for permissions the target app UID is present 1124 // in the extras. 1125 if (mIsUserSelectForApp 1126 && (intent.getExtras() == null 1127 || !intent.getExtras() 1128 .containsKey(Intent.EXTRA_UID))) { 1129 throw new IllegalArgumentException( 1130 "EXTRA_UID is required for" + " ACTION_USER_SELECT_IMAGES_FOR_APP"); 1131 } 1132 1133 if (mIsUserSelectForApp) { 1134 mPackageUid = intent.getExtras().getInt(Intent.EXTRA_UID); 1135 } 1136 // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates 1137 if (mBannerManager == null) { 1138 initBannerManager(); 1139 } 1140 } 1141 1142 /** 1143 * Returns the PickerAccentColorParameters object to access accent color parameters 1144 */ getPickerAccentColorParameters()1145 public PickerAccentColorParameters getPickerAccentColorParameters() { 1146 return mPickerAccentColorParameters; 1147 } 1148 checkPickerLaunchOptionValidity(int launchOption)1149 private boolean checkPickerLaunchOptionValidity(int launchOption) { 1150 return launchOption == MediaStore.PICK_IMAGES_TAB_IMAGES 1151 || launchOption == MediaStore.PICK_IMAGES_TAB_ALBUMS; 1152 } 1153 initBannerManager()1154 private void initBannerManager() { 1155 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1156 mBannerManager = shouldShowOnlyLocalFeatures() 1157 ? new BannerManager(mAppContext, mUserManagerState, mConfigStore) 1158 : new BannerManager.CloudBannerManager( 1159 mAppContext, mUserManagerState, mConfigStore); 1160 } else { 1161 mBannerManager = shouldShowOnlyLocalFeatures() 1162 ? new BannerManager(mAppContext, mUserIdManager, mConfigStore) 1163 : new BannerManager.CloudBannerManager( 1164 mAppContext, mUserIdManager, mConfigStore); 1165 } 1166 } 1167 1168 /** 1169 * Set BottomSheet state 1170 */ setBottomSheetState(int state)1171 public void setBottomSheetState(int state) { 1172 mBottomSheetState = state; 1173 } 1174 1175 /** 1176 * @return BottomSheet state 1177 */ getBottomSheetState()1178 public int getBottomSheetState() { 1179 return mBottomSheetState; 1180 } 1181 1182 /** 1183 * Log picker opened metrics 1184 */ logPickerOpened(int callingUid, String callingPackage, String intentAction)1185 public void logPickerOpened(int callingUid, String callingPackage, String intentAction) { 1186 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1187 UserManagerState userManagerState = getUserManagerState(); 1188 if (userManagerState.getCurrentUserProfileId().getIdentifier() 1189 == ActivityManager.getCurrentUser()) { 1190 mLogger.logPickerOpenPersonal(mInstanceId, callingUid, callingPackage); 1191 } else if (userManagerState.isManagedUserProfile( 1192 userManagerState.getCurrentUserProfileId())) { 1193 mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage); 1194 } else { 1195 mLogger.logPickerOpenUnknown(mInstanceId, callingUid, callingPackage); 1196 } 1197 } else { 1198 if (getUserIdManager().isManagedUserSelected()) { 1199 mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage); 1200 } else { 1201 mLogger.logPickerOpenPersonal(mInstanceId, callingUid, callingPackage); 1202 } 1203 } 1204 1205 // TODO(b/235326735): Optimise logging multiple times on picker opened 1206 // TODO(b/235326736): Check if we should add a metric for PICK_IMAGES intent to simplify 1207 // metrics reading 1208 if (ACTION_GET_CONTENT.equals(intentAction)) { 1209 mLogger.logPickerOpenViaGetContent(mInstanceId, callingUid, callingPackage); 1210 } 1211 1212 if (mBottomSheetState == STATE_COLLAPSED) { 1213 mLogger.logPickerOpenInHalfScreen(mInstanceId, callingUid, callingPackage); 1214 } else if (mBottomSheetState == STATE_EXPANDED) { 1215 mLogger.logPickerOpenInFullScreen(mInstanceId, callingUid, callingPackage); 1216 } 1217 1218 if (mSelection != null && mSelection.canSelectMultiple()) { 1219 mLogger.logPickerOpenInMultiSelect(mInstanceId, callingUid, callingPackage); 1220 } else { 1221 mLogger.logPickerOpenInSingleSelect(mInstanceId, callingUid, callingPackage); 1222 } 1223 1224 if (isAllImagesFilter()) { 1225 mLogger.logPickerOpenWithFilterAllImages(mInstanceId, callingUid, callingPackage); 1226 } else if (isAllVideosFilter()) { 1227 mLogger.logPickerOpenWithFilterAllVideos(mInstanceId, callingUid, callingPackage); 1228 } else if (hasMimeTypeFilters()) { 1229 mLogger.logPickerOpenWithAnyOtherFilter(mInstanceId, callingUid, callingPackage); 1230 } 1231 1232 maybeLogPickerOpenedWithCloudProvider(); 1233 } 1234 maybeLogPickerOpenedWithCloudProvider()1235 private void maybeLogPickerOpenedWithCloudProvider() { 1236 if (shouldShowOnlyLocalFeatures()) { 1237 return; 1238 } 1239 1240 final LiveData<String> cloudMediaProviderAuthorityLiveData = 1241 getCloudMediaProviderAuthorityLiveData(); 1242 cloudMediaProviderAuthorityLiveData.observeForever(new Observer<String>() { 1243 @Override 1244 public void onChanged(@Nullable String providerAuthority) { 1245 Log.d(TAG, "logPickerOpenedWithCloudProvider() provider=" + providerAuthority 1246 + ", log=" + (providerAuthority != null)); 1247 1248 if (providerAuthority != null) { 1249 BackgroundThread.getExecutor().execute(() -> 1250 logPickerOpenedWithCloudProvider(providerAuthority)); 1251 } 1252 // We only need to get the value once. 1253 cloudMediaProviderAuthorityLiveData.removeObserver(this); 1254 } 1255 }); 1256 } 1257 logPickerOpenedWithCloudProvider(@onNull String providerAuthority)1258 private void logPickerOpenedWithCloudProvider(@NonNull String providerAuthority) { 1259 String cloudProviderPackage = providerAuthority; 1260 int cloudProviderUid = -1; 1261 1262 try { 1263 final PackageManager packageManager = 1264 UserId.CURRENT_USER.getPackageManager(mAppContext); 1265 final ProviderInfo providerInfo = packageManager.resolveContentProvider( 1266 providerAuthority, /* flags= */ 0); 1267 1268 if (providerInfo != null && providerInfo.applicationInfo != null) { 1269 cloudProviderPackage = providerInfo.applicationInfo.packageName; 1270 cloudProviderUid = providerInfo.applicationInfo.uid; 1271 } 1272 } catch (PackageManager.NameNotFoundException e) { 1273 Log.d(TAG, "Logging the ui event 'picker open with an active cloud provider' with its " 1274 + "authority in place of the package name and a default uid.", e); 1275 } 1276 1277 mLogger.logPickerOpenWithActiveCloudProvider( 1278 mInstanceId, cloudProviderUid, cloudProviderPackage); 1279 } 1280 1281 /** 1282 * Log metrics to notify that the user has clicked Browse to open DocumentsUi 1283 */ logBrowseToDocumentsUi(int callingUid, String callingPackage)1284 public void logBrowseToDocumentsUi(int callingUid, String callingPackage) { 1285 mLogger.logBrowseToDocumentsUi(mInstanceId, callingUid, callingPackage); 1286 } 1287 1288 /** 1289 * Log metrics to notify that the user has confirmed selection 1290 */ logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed)1291 public void logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed) { 1292 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1293 UserManagerState userManagerState = getUserManagerState(); 1294 if (userManagerState.getCurrentUserProfileId().getIdentifier() 1295 == ActivityManager.getCurrentUser()) { 1296 mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage, 1297 countOfItemsConfirmed); 1298 } else if (userManagerState.isManagedUserProfile( 1299 userManagerState.getCurrentUserProfileId())) { 1300 mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage, 1301 countOfItemsConfirmed); 1302 } else { 1303 mLogger.logPickerConfirmUnknown( 1304 mInstanceId, callingUid, callingPackage, countOfItemsConfirmed); 1305 } 1306 } else { 1307 if (getUserIdManager().isManagedUserSelected()) { 1308 mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage, 1309 countOfItemsConfirmed); 1310 } else { 1311 mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage, 1312 countOfItemsConfirmed); 1313 } 1314 } 1315 } 1316 1317 /** 1318 * Log metrics to notify that the user has exited Picker without any selection 1319 */ logPickerCancel(int callingUid, String callingPackage)1320 public void logPickerCancel(int callingUid, String callingPackage) { 1321 if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) { 1322 UserManagerState userManagerState = getUserManagerState(); 1323 if (userManagerState.getCurrentUserProfileId().getIdentifier() 1324 == ActivityManager.getCurrentUser()) { 1325 mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage); 1326 } else if (userManagerState.isManagedUserProfile( 1327 userManagerState.getCurrentUserProfileId())) { 1328 mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage); 1329 } else { 1330 mLogger.logPickerCancelUnknown(mInstanceId, callingUid, callingPackage); 1331 } 1332 } else { 1333 if (getUserIdManager().isManagedUserSelected()) { 1334 mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage); 1335 } else { 1336 mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage); 1337 } 1338 } 1339 } 1340 1341 /** 1342 * Log metrics to notify that the user has clicked the mute / unmute button in a video preview 1343 */ logVideoPreviewMuteButtonClick()1344 public void logVideoPreviewMuteButtonClick() { 1345 mLogger.logVideoPreviewMuteButtonClick(mInstanceId); 1346 } 1347 1348 /** 1349 * Log metrics to notify that the user has clicked the 'view selected' button 1350 * 1351 * @param selectedItemCount the number of items selected for preview all 1352 */ logPreviewAllSelected(int selectedItemCount)1353 public void logPreviewAllSelected(int selectedItemCount) { 1354 mLogger.logPreviewAllSelected(mInstanceId, selectedItemCount); 1355 } 1356 1357 /** 1358 * Log metrics to notify that the 'switch profile' button is visible & enabled 1359 */ logProfileSwitchButtonEnabled()1360 public void logProfileSwitchButtonEnabled() { 1361 mLogger.logProfileSwitchButtonEnabled(mInstanceId); 1362 } 1363 1364 /** 1365 * Log metrics to notify that the 'switch profile' button is visible but disabled 1366 */ logProfileSwitchButtonDisabled()1367 public void logProfileSwitchButtonDisabled() { 1368 mLogger.logProfileSwitchButtonDisabled(mInstanceId); 1369 } 1370 1371 /** 1372 * Log metrics to notify that the 'switch profile menu' button is visible 1373 */ logProfileSwitchMenuButtonVisible()1374 public void logProfileSwitchMenuButtonVisible() { 1375 mLogger.logProfileSwitchMenuButtonVisible(mInstanceId); 1376 } 1377 1378 /** 1379 * Log metrics to notify that the user has clicked the 'switch profile' button 1380 */ logProfileSwitchButtonClick()1381 public void logProfileSwitchButtonClick() { 1382 mLogger.logProfileSwitchButtonClick(mInstanceId); 1383 } 1384 1385 /** 1386 * Log metrics to notify that the user has clicked the 'switch profile menu ' button 1387 */ logProfileSwitchMenuButtonClick()1388 public void logProfileSwitchMenuButtonClick() { 1389 mLogger.logProfileSwitchMenuButtonClick(mInstanceId); 1390 } 1391 1392 /** 1393 * Log metrics to notify that the user has cancelled the current session by swiping down 1394 */ logSwipeDownExit()1395 public void logSwipeDownExit() { 1396 mLogger.logSwipeDownExit(mInstanceId); 1397 } 1398 1399 /** 1400 * Log metrics to notify that the user has made a back gesture 1401 * @param backStackEntryCount the number of fragment entries currently in the back stack 1402 */ logBackGestureWithStackCount(int backStackEntryCount)1403 public void logBackGestureWithStackCount(int backStackEntryCount) { 1404 mLogger.logBackGestureWithStackCount(mInstanceId, backStackEntryCount); 1405 } 1406 1407 /** 1408 * Log metrics to notify that the user has clicked the action bar home button 1409 * @param backStackEntryCount the number of fragment entries currently in the back stack 1410 */ logActionBarHomeButtonClick(int backStackEntryCount)1411 public void logActionBarHomeButtonClick(int backStackEntryCount) { 1412 mLogger.logActionBarHomeButtonClick(mInstanceId, backStackEntryCount); 1413 } 1414 1415 /** 1416 * Log metrics to notify that the user has expanded from half screen to full 1417 */ logExpandToFullScreen()1418 public void logExpandToFullScreen() { 1419 mLogger.logExpandToFullScreen(mInstanceId); 1420 } 1421 1422 /** 1423 * Log metrics to notify that the user has opened the photo picker menu 1424 */ logMenuOpened()1425 public void logMenuOpened() { 1426 mLogger.logMenuOpened(mInstanceId); 1427 } 1428 1429 /** 1430 * Log metrics to notify that the user has switched to the photos tab 1431 */ logSwitchToPhotosTab()1432 public void logSwitchToPhotosTab() { 1433 mLogger.logSwitchToPhotosTab(mInstanceId); 1434 } 1435 1436 /** 1437 * Log metrics to notify that the user has switched to the albums tab 1438 */ logSwitchToAlbumsTab()1439 public void logSwitchToAlbumsTab() { 1440 mLogger.logSwitchToAlbumsTab(mInstanceId); 1441 } 1442 1443 /** 1444 * Log metrics to notify that the user has opened an album 1445 * 1446 * @param category the opened album metadata 1447 * @param position the position of the album in the recycler view 1448 */ logAlbumOpened(@onNull Category category, int position)1449 public void logAlbumOpened(@NonNull Category category, int position) { 1450 final String albumId = category.getId(); 1451 if (ALBUM_ID_FAVORITES.equals(albumId)) { 1452 mLogger.logFavoritesAlbumOpened(mInstanceId); 1453 } else if (ALBUM_ID_CAMERA.equals(albumId)) { 1454 mLogger.logCameraAlbumOpened(mInstanceId); 1455 } else if (ALBUM_ID_DOWNLOADS.equals(albumId)) { 1456 mLogger.logDownloadsAlbumOpened(mInstanceId); 1457 } else if (ALBUM_ID_SCREENSHOTS.equals(albumId)) { 1458 mLogger.logScreenshotsAlbumOpened(mInstanceId); 1459 } else if (ALBUM_ID_VIDEOS.equals(albumId)) { 1460 mLogger.logVideosAlbumOpened(mInstanceId); 1461 } else if (!category.isLocal()) { 1462 mLogger.logCloudAlbumOpened(mInstanceId, position); 1463 } 1464 } 1465 1466 /** 1467 * Log metrics to notify that the user has selected a media item 1468 * 1469 * @param item the selected item metadata 1470 * @param category the category of the item selected, {@link Category#DEFAULT} for main grid 1471 * @param position the position of the album in the recycler view 1472 */ logMediaItemSelected(@onNull Item item, @NonNull Category category, int position)1473 public void logMediaItemSelected(@NonNull Item item, @NonNull Category category, int position) { 1474 if (category.isDefault()) { 1475 mLogger.logSelectedMainGridItem(mInstanceId, position); 1476 } else { 1477 mLogger.logSelectedAlbumItem(mInstanceId, position); 1478 } 1479 1480 if (!item.isLocal()) { 1481 mLogger.logSelectedCloudOnlyItem(mInstanceId, position); 1482 } 1483 } 1484 1485 /** 1486 * Log metrics to notify that the user has previewed a media item 1487 * 1488 * @param item the previewed item metadata 1489 * @param category the category of the item previewed, {@link Category#DEFAULT} for main grid 1490 * @param position the position of the album in the recycler view 1491 */ logMediaItemPreviewed( @onNull Item item, @NonNull Category category, int position)1492 public void logMediaItemPreviewed( 1493 @NonNull Item item, @NonNull Category category, int position) { 1494 if (category.isDefault()) { 1495 mLogger.logPreviewedMainGridItem( 1496 item.getSpecialFormat(), item.getMimeType(), mInstanceId, position); 1497 } 1498 } 1499 1500 /** 1501 * Log metrics to notify create surface controller triggered 1502 * @param authority the authority of the provider 1503 */ logCreateSurfaceControllerStart(String authority)1504 public void logCreateSurfaceControllerStart(String authority) { 1505 mLogger.logPickerCreateSurfaceControllerStart(mInstanceId, authority); 1506 } 1507 1508 /** 1509 * Log metrics to notify create surface controller ended 1510 * @param authority the authority of the provider 1511 */ logCreateSurfaceControllerEnd(String authority)1512 public void logCreateSurfaceControllerEnd(String authority) { 1513 mLogger.logPickerCreateSurfaceControllerEnd(mInstanceId, authority); 1514 } 1515 1516 /** 1517 * Log metrics to notify that the selected media preloading started 1518 * @param count the number of items to preload 1519 */ logPreloadingStarted(int count)1520 public void logPreloadingStarted(int count) { 1521 mLogger.logPreloadingStarted(mInstanceId, count); 1522 } 1523 1524 /** 1525 * Log metrics to notify that the selected media preloading finished 1526 */ logPreloadingFinished()1527 public void logPreloadingFinished() { 1528 mLogger.logPreloadingFinished(mInstanceId); 1529 } 1530 1531 /** 1532 * Log metrics to notify that the user cancelled the selected media preloading 1533 * @param count the number of items pending to preload 1534 */ logPreloadingCancelled(int count)1535 public void logPreloadingCancelled(int count) { 1536 mLogger.logPreloadingCancelled(mInstanceId, count); 1537 } 1538 1539 /** 1540 * Log metrics to notify that the selected media preloading failed for some items 1541 * @param count the number of items pending / failed to preload 1542 */ logPreloadingFailed(int count)1543 public void logPreloadingFailed(int count) { 1544 mLogger.logPreloadingFailed(mInstanceId, count); 1545 } 1546 1547 /** 1548 * Logs metrics for count of grants initialised for a package. 1549 */ logPickerChoiceInitGrantsCount(int numberOfGrants, Bundle intentExtras)1550 public void logPickerChoiceInitGrantsCount(int numberOfGrants, Bundle intentExtras) { 1551 NonUiEventLogger.logPickerChoiceInitGrantsCount(mInstanceId, android.os.Process.myUid(), 1552 getPackageNameForUid(intentExtras), numberOfGrants); 1553 1554 } 1555 1556 /** 1557 * Logs metrics for count of grants added for a package. 1558 */ logPickerChoiceAddedGrantsCount(int numberOfGrants, Bundle intentExtras)1559 public void logPickerChoiceAddedGrantsCount(int numberOfGrants, Bundle intentExtras) { 1560 NonUiEventLogger.logPickerChoiceGrantsAdditionCount(mInstanceId, android.os.Process.myUid(), 1561 getPackageNameForUid(intentExtras), numberOfGrants); 1562 } 1563 1564 /** 1565 * Logs metrics for count of grants removed for a package. 1566 */ logPickerChoiceRevokedGrantsCount(int numberOfGrants, Bundle intentExtras)1567 public void logPickerChoiceRevokedGrantsCount(int numberOfGrants, Bundle intentExtras) { 1568 NonUiEventLogger.logPickerChoiceGrantsRemovedCount(mInstanceId, android.os.Process.myUid(), 1569 getPackageNameForUid(intentExtras), numberOfGrants); 1570 } 1571 1572 /** 1573 * Log metrics to notify that the banner is added to display in the recycler view grids 1574 * @param bannerName the name of the banner added, 1575 * refer {@link com.android.providers.media.photopicker.ui.TabAdapter.Banner} 1576 */ logBannerAdded(@onNull String bannerName)1577 public void logBannerAdded(@NonNull String bannerName) { 1578 mLogger.logBannerAdded(mInstanceId, bannerName); 1579 } 1580 1581 /** 1582 * Log metrics to notify that the banner is dismissed by the user 1583 */ logBannerDismissed()1584 public void logBannerDismissed() { 1585 mLogger.logBannerDismissed(mInstanceId); 1586 } 1587 1588 /** 1589 * Log metrics to notify that the user clicked the banner action button 1590 */ logBannerActionButtonClicked()1591 public void logBannerActionButtonClicked() { 1592 mLogger.logBannerActionButtonClicked(mInstanceId); 1593 } 1594 1595 /** 1596 * Log metrics to notify that the user clicked on the remaining part of the banner 1597 */ logBannerClicked()1598 public void logBannerClicked() { 1599 mLogger.logBannerClicked(mInstanceId); 1600 } 1601 1602 @NonNull getPackageNameForUid(Bundle extras)1603 private String getPackageNameForUid(Bundle extras) { 1604 final int uid = extras.getInt(Intent.EXTRA_UID); 1605 final PackageManager pm = mAppContext.getPackageManager(); 1606 String[] packageNames = pm.getPackagesForUid(uid); 1607 if (packageNames.length != 0) { 1608 return packageNames[0]; 1609 } 1610 return new String(); 1611 } 1612 getInstanceId()1613 public InstanceId getInstanceId() { 1614 return mInstanceId; 1615 } 1616 setInstanceId(InstanceId parcelable)1617 public void setInstanceId(InstanceId parcelable) { 1618 mInstanceId = parcelable; 1619 } 1620 1621 // Return whether hotopicker's launch intent has extra {@link EXTRA_LOCAL_ONLY} set to true 1622 // or not. 1623 @VisibleForTesting isLocalOnly()1624 boolean isLocalOnly() { 1625 return mIsLocalOnly; 1626 } 1627 1628 /** 1629 * Return whether only the local features should be shown (the cloud features should be hidden). 1630 * 1631 * Show only the local features in the following cases - 1632 * 1. Photo Picker is launched by the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} 1633 * action for the permission flow. 1634 * 2. Photo Picker is launched with the {@link Intent#EXTRA_LOCAL_ONLY} as {@code true} in the 1635 * {@link Intent#ACTION_GET_CONTENT} or {@link MediaStore#ACTION_PICK_IMAGES} action. 1636 * 3. Cloud Media in Photo picker is disabled, i.e., 1637 * {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}. 1638 * 1639 * @return {@code true} iff either {@link #isUserSelectForApp()} or {@link #isLocalOnly()} is 1640 * {@code true}, OR if {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}. 1641 */ shouldShowOnlyLocalFeatures()1642 public boolean shouldShowOnlyLocalFeatures() { 1643 return isUserSelectForApp() || isLocalOnly() 1644 || !mConfigStore.isCloudMediaInPhotoPickerEnabled(); 1645 } 1646 1647 /** 1648 * @return the {@link LiveData} of the 'Choose App' banner visibility. 1649 */ 1650 @NonNull shouldShowChooseAppBannerLiveData()1651 public LiveData<Boolean> shouldShowChooseAppBannerLiveData() { 1652 return mBannerManager.shouldShowChooseAppBannerLiveData(); 1653 } 1654 1655 /** 1656 * @return the {@link LiveData} of the 'Cloud Media Available' banner visibility. 1657 */ 1658 @NonNull shouldShowCloudMediaAvailableBannerLiveData()1659 public LiveData<Boolean> shouldShowCloudMediaAvailableBannerLiveData() { 1660 return mBannerManager.shouldShowCloudMediaAvailableBannerLiveData(); 1661 } 1662 1663 /** 1664 * @return the {@link LiveData} of the 'Account Updated' banner visibility. 1665 */ 1666 @NonNull shouldShowAccountUpdatedBannerLiveData()1667 public LiveData<Boolean> shouldShowAccountUpdatedBannerLiveData() { 1668 return mBannerManager.shouldShowAccountUpdatedBannerLiveData(); 1669 } 1670 1671 /** 1672 * @return the {@link LiveData} of the 'Choose Account' banner visibility. 1673 */ 1674 @NonNull shouldShowChooseAccountBannerLiveData()1675 public LiveData<Boolean> shouldShowChooseAccountBannerLiveData() { 1676 return mBannerManager.shouldShowChooseAccountBannerLiveData(); 1677 } 1678 1679 /** 1680 * Dismiss (hide) the 'Choose App' banner for the current user. 1681 */ 1682 @MainThread onUserDismissedChooseAppBanner()1683 public void onUserDismissedChooseAppBanner() { 1684 ThreadUtils.assertMainThread(); 1685 mBannerManager.onUserDismissedChooseAppBanner(); 1686 } 1687 1688 /** 1689 * Dismiss (hide) the 'Cloud Media Available' banner for the current user. 1690 */ 1691 @MainThread onUserDismissedCloudMediaAvailableBanner()1692 public void onUserDismissedCloudMediaAvailableBanner() { 1693 ThreadUtils.assertMainThread(); 1694 mBannerManager.onUserDismissedCloudMediaAvailableBanner(); 1695 } 1696 1697 /** 1698 * Dismiss (hide) the 'Account Updated' banner for the current user. 1699 */ 1700 @MainThread onUserDismissedAccountUpdatedBanner()1701 public void onUserDismissedAccountUpdatedBanner() { 1702 ThreadUtils.assertMainThread(); 1703 mBannerManager.onUserDismissedAccountUpdatedBanner(); 1704 } 1705 1706 /** 1707 * Dismiss (hide) the 'Choose Account' banner for the current user. 1708 */ 1709 @MainThread onUserDismissedChooseAccountBanner()1710 public void onUserDismissedChooseAccountBanner() { 1711 ThreadUtils.assertMainThread(); 1712 mBannerManager.onUserDismissedChooseAccountBanner(); 1713 } 1714 1715 /** 1716 * @return a {@link LiveData} that posts Should Refresh Picker UI as {@code true} when notified. 1717 */ 1718 @NonNull refreshUiLiveData()1719 public LiveData<RefreshRequest> refreshUiLiveData() { 1720 return mRefreshUiLiveData; 1721 } 1722 registerRefreshUiNotificationObserver()1723 private void registerRefreshUiNotificationObserver() { 1724 mContentResolver = getContentResolverForSelectedUser(); 1725 mContentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, 1726 /* notifyForDescendants */ true, mRefreshUiNotificationObserver); 1727 } 1728 unregisterRefreshUiNotificationObserver()1729 private void unregisterRefreshUiNotificationObserver() { 1730 if (mContentResolver != null) { 1731 mContentResolver.unregisterContentObserver(mRefreshUiNotificationObserver); 1732 mContentResolver = null; 1733 } 1734 } 1735 resetRefreshUiNotificationObserver()1736 private void resetRefreshUiNotificationObserver() { 1737 unregisterRefreshUiNotificationObserver(); 1738 registerRefreshUiNotificationObserver(); 1739 } 1740 getContentResolverForSelectedUser()1741 private ContentResolver getContentResolverForSelectedUser() { 1742 final UserId selectedUserId = getCurrentUserProfileId(); 1743 if (selectedUserId == null) { 1744 Log.d(TAG, "Selected user id is NULL; returning the default content resolver."); 1745 return mAppContext.getContentResolver(); 1746 } 1747 try { 1748 return selectedUserId.getContentResolver(mAppContext); 1749 } catch (PackageManager.NameNotFoundException e) { 1750 Log.d(TAG, "Failed to get the content resolver for the selected user id " 1751 + selectedUserId + "; returning the default content resolver.", e); 1752 return mAppContext.getContentResolver(); 1753 } 1754 } 1755 isSyncInProgress()1756 public LiveData<Boolean> isSyncInProgress() { 1757 return mIsSyncInProgress; 1758 } 1759 1760 /** 1761 * Class used to store the result of the item modification operations. 1762 */ 1763 public class PaginatedItemsResult { 1764 private List<Item> mItems = new ArrayList<>(); 1765 1766 private int mAction = ACTION_DEFAULT; 1767 PaginatedItemsResult(@onNull List<Item> itemList, @ItemsAction.Type int action)1768 public PaginatedItemsResult(@NonNull List<Item> itemList, 1769 @ItemsAction.Type int action) { 1770 mItems = itemList; 1771 mAction = action; 1772 } 1773 getItems()1774 public List<Item> getItems() { 1775 return mItems; 1776 } 1777 1778 @ItemsAction.Type getAction()1779 public int getAction() { 1780 return mAction; 1781 } 1782 } 1783 1784 /** 1785 * Sends an init notification to the Media Provider process if it hasn't already been sent yet. 1786 */ maybeInitPhotoPickerData()1787 public void maybeInitPhotoPickerData() { 1788 if (!mIsPhotoPickerDataInitialized) { 1789 initPhotoPickerData(); 1790 mIsPhotoPickerDataInitialized = true; 1791 } else { 1792 Log.d(TAG, "Main grid is already initialized."); 1793 } 1794 } 1795 1796 /** 1797 * Sends an init notification to the Media Provider process. 1798 */ initPhotoPickerData()1799 private void initPhotoPickerData() { 1800 initPhotoPickerData(Category.DEFAULT); 1801 } 1802 1803 /** 1804 * This will inform the media Provider process that the UI is preparing to load data for main 1805 * photos grid or album contents grid. 1806 */ initPhotoPickerData(@onNull Category category)1807 public void initPhotoPickerData(@NonNull Category category) { 1808 if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) { 1809 final UserId userId = getCurrentUserProfileId(); 1810 DataLoaderThread.getHandler().postDelayed(() -> { 1811 if (category == Category.DEFAULT) { 1812 mIsSyncInProgress.postValue(true); 1813 } 1814 mItemsProvider.initPhotoPickerData(category.getId(), 1815 category.getAuthority(), 1816 shouldShowOnlyLocalFeatures(), 1817 userId); 1818 }, TOKEN, DELAY_MILLIS); 1819 } 1820 } 1821 clearQueuedTasksInDataLoaderThread()1822 private void clearQueuedTasksInDataLoaderThread() { 1823 DataLoaderThread.getHandler().removeCallbacksAndMessages(TOKEN); 1824 DataLoaderThread.getHandler().removeCallbacksAndMessages(mLoadCategoryItemsThreadToken); 1825 } 1826 } 1827