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