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