• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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