| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.providers.media.photopicker.viewmodel; |
| |
| import static android.content.Intent.ACTION_GET_CONTENT; |
| import static android.content.Intent.EXTRA_LOCAL_ONLY; |
| import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_CAMERA; |
| import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_DOWNLOADS; |
| import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_FAVORITES; |
| import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_SCREENSHOTS; |
| import static android.provider.CloudMediaProviderContract.AlbumColumns.ALBUM_ID_VIDEOS; |
| |
| import static com.android.providers.media.PickerUriResolver.REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI; |
| import static com.android.providers.media.photopicker.DataLoaderThread.TOKEN; |
| import static com.android.providers.media.photopicker.PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_AND_UPDATE_LIST; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_CLEAR_GRID; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_DEFAULT; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_LOAD_NEXT_PAGE; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_REFRESH_ITEMS; |
| import static com.android.providers.media.photopicker.ui.ItemsAction.ACTION_VIEW_CREATED; |
| |
| import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED; |
| import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED; |
| |
| import android.annotation.SuppressLint; |
| import android.app.Application; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ProviderInfo; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.os.CancellationSignal; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.provider.MediaStore; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.UiThread; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.lifecycle.AndroidViewModel; |
| import androidx.lifecycle.LiveData; |
| import androidx.lifecycle.MutableLiveData; |
| import androidx.lifecycle.Observer; |
| |
| import com.android.internal.logging.InstanceId; |
| import com.android.internal.logging.InstanceIdSequence; |
| import com.android.modules.utils.BackgroundThread; |
| import com.android.modules.utils.build.SdkLevel; |
| import com.android.providers.media.ConfigStore; |
| import com.android.providers.media.MediaApplication; |
| import com.android.providers.media.photopicker.DataLoaderThread; |
| import com.android.providers.media.photopicker.NotificationContentObserver; |
| import com.android.providers.media.photopicker.data.ItemsProvider; |
| import com.android.providers.media.photopicker.data.MuteStatus; |
| import com.android.providers.media.photopicker.data.PaginationParameters; |
| import com.android.providers.media.photopicker.data.Selection; |
| import com.android.providers.media.photopicker.data.UserIdManager; |
| import com.android.providers.media.photopicker.data.model.Category; |
| import com.android.providers.media.photopicker.data.model.Item; |
| import com.android.providers.media.photopicker.data.model.UserId; |
| import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger; |
| import com.android.providers.media.photopicker.ui.ItemsAction; |
| import com.android.providers.media.photopicker.util.CategoryOrganiserUtils; |
| import com.android.providers.media.photopicker.util.MimeFilterUtils; |
| import com.android.providers.media.util.MimeUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| |
| /** |
| * PickerViewModel to store and handle data for PhotoPickerActivity. |
| */ |
| public class PickerViewModel extends AndroidViewModel { |
| public static final String TAG = "PhotoPicker"; |
| |
| private static final int RECENT_MINIMUM_COUNT = 12; |
| |
| private static final int INSTANCE_ID_MAX = 1 << 15; |
| private static final int DELAY_MILLIS = 0; |
| |
| // Token for the tasks to load the category items in the data loader thread's queue |
| private final Object mLoadCategoryItemsThreadToken = new Object(); |
| |
| @NonNull |
| @SuppressLint("StaticFieldLeak") |
| private final Context mAppContext; |
| |
| private final Selection mSelection; |
| private final MuteStatus mMuteStatus; |
| public boolean mEmptyPageDisplayed = false; |
| |
| // TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the |
| // data set to reduce memories. |
| // The list of Items with all photos and videos |
| private MutableLiveData<PaginatedItemsResult> mItemsResult; |
| private int mItemsPageSize = -1; |
| |
| // The list of Items with all photos and videos in category |
| private MutableLiveData<PaginatedItemsResult> mCategoryItemsResult; |
| |
| private int mCategoryItemsPageSize = -1; |
| |
| // The list of categories. |
| private MutableLiveData<List<Category>> mCategoryList; |
| |
| private final MutableLiveData<Boolean> mShouldRefreshUiLiveData = new MutableLiveData<>(false); |
| private final ContentObserver mRefreshUiNotificationObserver = new ContentObserver(null) { |
| @Override |
| public void onChange(boolean selfChange) { |
| mShouldRefreshUiLiveData.postValue(true); |
| } |
| }; |
| |
| private MutableLiveData<Boolean> mIsSyncInProgress = new MutableLiveData<>(false); |
| |
| private ItemsProvider mItemsProvider; |
| private UserIdManager mUserIdManager; |
| private BannerManager mBannerManager; |
| |
| private InstanceId mInstanceId; |
| private PhotoPickerUiEventLogger mLogger; |
| private ConfigStore mConfigStore; |
| |
| private String[] mMimeTypeFilters = null; |
| private int mBottomSheetState; |
| |
| private Category mCurrentCategory; |
| |
| // Content resolver for the currently selected user |
| private ContentResolver mContentResolver; |
| |
| // Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates |
| private boolean mIsUserSelectForApp; |
| private boolean mIsLocalOnly; |
| private boolean mIsAllCategoryItemsLoaded = false; |
| private boolean mIsNotificationForUpdateReceived = false; |
| private CancellationSignal mCancellationSignal = new CancellationSignal(); |
| |
| public PickerViewModel(@NonNull Application application) { |
| super(application); |
| mAppContext = application.getApplicationContext(); |
| mItemsProvider = new ItemsProvider(mAppContext); |
| mSelection = new Selection(); |
| mUserIdManager = UserIdManager.create(mAppContext); |
| mMuteStatus = new MuteStatus(); |
| mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId(); |
| mLogger = new PhotoPickerUiEventLogger(); |
| mIsUserSelectForApp = false; |
| mIsLocalOnly = false; |
| |
| initConfigStore(); |
| |
| // When the user opens the PhotoPickerSettingsActivity and changes the cloud provider, it's |
| // possible that system kills PhotoPickerActivity and PickerViewModel while it's in the |
| // background. In these scenarios, content observer will be unregistered and PickerViewModel |
| // will not be able to receive CMP change notifications. |
| initPhotoPickerData(); |
| registerRefreshUiNotificationObserver(); |
| // Add notification content observer for any notifications received for changes in media. |
| NotificationContentObserver contentObserver = new NotificationContentObserver(null); |
| contentObserver.registerKeysToObserverCallback( |
| Arrays.asList(NotificationContentObserver.MEDIA), |
| (dateTakenMs, albumId) -> { |
| onNotificationReceived(); |
| }); |
| contentObserver.register(mAppContext.getContentResolver()); |
| } |
| |
| @Override |
| protected void onCleared() { |
| unregisterRefreshUiNotificationObserver(); |
| |
| // Signal ContentProvider to cancel currently running task. |
| mCancellationSignal.cancel(); |
| |
| clearQueuedTasksInDataLoaderThread(); |
| } |
| |
| private void onNotificationReceived() { |
| Log.d(TAG, "Notification for media update has been received"); |
| mIsNotificationForUpdateReceived = true; |
| if (mEmptyPageDisplayed && mConfigStore.isCloudMediaInPhotoPickerEnabled()) { |
| (new Handler(Looper.getMainLooper())).post(() -> { |
| Log.d(TAG, "Refreshing UI to display new items."); |
| mEmptyPageDisplayed = false; |
| getPaginatedItemsForAction(ACTION_REFRESH_ITEMS, |
| new PaginationParameters(mItemsPageSize, -1, -1)); |
| }); |
| } |
| } |
| |
| @VisibleForTesting |
| protected void initConfigStore() { |
| mConfigStore = MediaApplication.getConfigStore(); |
| } |
| |
| @VisibleForTesting |
| public void setItemsProvider(@NonNull ItemsProvider itemsProvider) { |
| mItemsProvider = itemsProvider; |
| } |
| |
| @VisibleForTesting |
| public void setUserIdManager(@NonNull UserIdManager userIdManager) { |
| mUserIdManager = userIdManager; |
| } |
| |
| @VisibleForTesting |
| public void setBannerManager(@NonNull BannerManager bannerManager) { |
| mBannerManager = bannerManager; |
| } |
| |
| @VisibleForTesting |
| public void setNotificationForUpdateReceived(boolean notificationForUpdateReceived) { |
| mIsNotificationForUpdateReceived = notificationForUpdateReceived; |
| } |
| |
| @VisibleForTesting |
| public void setLogger(@NonNull PhotoPickerUiEventLogger logger) { |
| mLogger = logger; |
| } |
| |
| @VisibleForTesting |
| public void setConfigStore(@NonNull ConfigStore configStore) { |
| mConfigStore = configStore; |
| } |
| |
| public void setEmptyPageDisplayed(boolean emptyPageDisplayed) { |
| mEmptyPageDisplayed = emptyPageDisplayed; |
| } |
| |
| /** |
| * @return the {@link ConfigStore} for this context. |
| */ |
| public ConfigStore getConfigStore() { |
| return mConfigStore; |
| } |
| |
| /** |
| * @return {@link UserIdManager} for this context. |
| */ |
| public UserIdManager getUserIdManager() { |
| return mUserIdManager; |
| } |
| |
| /** |
| * @return {@code mSelection} that manages the selection |
| */ |
| public Selection getSelection() { |
| return mSelection; |
| } |
| |
| |
| /** |
| * @return {@code mMuteStatus} that tracks the volume mute status of the video preview |
| */ |
| public MuteStatus getMuteStatus() { |
| return mMuteStatus; |
| } |
| |
| /** |
| * @return {@code mIsUserSelectForApp} if the picker is currently being used |
| * for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action. |
| */ |
| public boolean isUserSelectForApp() { |
| return mIsUserSelectForApp; |
| } |
| |
| /** |
| * @return a {@link LiveData} that holds the value (once it's fetched) of the |
| * {@link android.content.ContentProvider#mAuthority authority} of the current |
| * {@link android.provider.CloudMediaProvider}. |
| */ |
| @NonNull |
| public LiveData<String> getCloudMediaProviderAuthorityLiveData() { |
| return mBannerManager.getCloudMediaProviderAuthorityLiveData(); |
| } |
| |
| /** |
| * @return a {@link LiveData} that holds the value (once it's fetched) of the label |
| * of the current {@link android.provider.CloudMediaProvider}. |
| */ |
| @NonNull |
| public LiveData<String> getCloudMediaProviderAppTitleLiveData() { |
| return mBannerManager.getCloudMediaProviderAppTitleLiveData(); |
| } |
| |
| /** |
| * @return a {@link LiveData} that holds the value (once it's fetched) of the account name |
| * of the current {@link android.provider.CloudMediaProvider}. |
| */ |
| @NonNull |
| public LiveData<String> getCloudMediaAccountNameLiveData() { |
| return mBannerManager.getCloudMediaAccountNameLiveData(); |
| } |
| |
| /** |
| * @return the account selection activity {@link Intent} of the current |
| * {@link android.provider.CloudMediaProvider}. |
| */ |
| @Nullable |
| public Intent getChooseCloudMediaAccountActivityIntent() { |
| return mBannerManager.getChooseCloudMediaAccountActivityIntent(); |
| } |
| |
| /** |
| * Reset to personal profile mode. |
| */ |
| @UiThread |
| public void resetToPersonalProfile() { |
| mUserIdManager.setPersonalAsCurrentUserProfile(); |
| onSwitchedProfile(); |
| } |
| |
| /** |
| * Reset the content observer & all the content on profile switched. |
| */ |
| @UiThread |
| public void onSwitchedProfile() { |
| resetRefreshUiNotificationObserver(); |
| resetAllContentInCurrentProfile(); |
| } |
| |
| /** |
| * Reset all the content (items, categories & banners) in the current profile. |
| */ |
| @UiThread |
| public void resetAllContentInCurrentProfile() { |
| Log.d(TAG, "Reset all content in current profile"); |
| |
| // Post 'should refresh UI live data' value as false to avoid unnecessary repetitive resets |
| mShouldRefreshUiLiveData.postValue(false); |
| |
| clearQueuedTasksInDataLoaderThread(); |
| |
| initPhotoPickerData(); |
| |
| // Clear the existing content - selection, photos grid, albums grid, banners |
| mSelection.clearSelectedItems(); |
| |
| if (mItemsResult != null) { |
| DataLoaderThread.getHandler().postDelayed(() -> |
| mItemsResult.postValue(new PaginatedItemsResult(List.of(Item.EMPTY_VIEW), |
| ACTION_CLEAR_GRID)), TOKEN, DELAY_MILLIS); |
| } |
| |
| if (mCategoryList != null) { |
| DataLoaderThread.getHandler().postDelayed(() -> |
| mCategoryList.postValue(List.of(Category.EMPTY_VIEW)), TOKEN, DELAY_MILLIS); |
| } |
| |
| mBannerManager.hideAllBanners(); |
| |
| // Update items, categories & banners |
| getPaginatedItemsForAction(ACTION_CLEAR_AND_UPDATE_LIST, null); |
| updateCategories(); |
| mBannerManager.reset(); |
| } |
| |
| /** |
| * Performs required modification to the item list and returns the live data for it. |
| */ |
| public LiveData<PaginatedItemsResult> getPaginatedItemsForAction( |
| @NonNull @ItemsAction.Type int action, |
| @Nullable PaginationParameters paginationParameters) { |
| Objects.requireNonNull(action); |
| switch (action) { |
| case ACTION_VIEW_CREATED: { |
| // Use this when a fresh view is created. If the current list is empty, it will |
| // load the first page and return the list, else it will return previously |
| // existing values. |
| mItemsPageSize = paginationParameters.getPageSize(); |
| if (mItemsResult == null) { |
| updatePaginatedItems(paginationParameters, true, action); |
| } |
| break; |
| } |
| case ACTION_LOAD_NEXT_PAGE: { |
| // Loads next page of the list, using the previously loaded list. |
| // If the current list is empty then it will not perform any actions. |
| if (mItemsResult != null && mItemsResult.getValue() != null) { |
| List<Item> currentItemList = mItemsResult.getValue().getItems(); |
| // If the list is already empty that would mean that the first page was not |
| // loaded since there were no items to be loaded. |
| if (currentItemList != null && !currentItemList.isEmpty()) { |
| // get the last item of the existing list. |
| Item item = currentItemList.get(currentItemList.size() - 1); |
| updatePaginatedItems( |
| new PaginationParameters(mItemsPageSize, item.getDateTaken(), |
| item.getRowId()), false, action); |
| } |
| } |
| break; |
| } |
| case ACTION_CLEAR_AND_UPDATE_LIST: { |
| // Clears the existing list and loads the list with for mItemsPageSize |
| // number of items. This will be equal to page size for pagination if cloud |
| // picker feature flag is enabled, else it will be -1 implying that the complete |
| // list should be loaded. |
| updatePaginatedItems(new PaginationParameters(mItemsPageSize, |
| /*dateBeforeMs*/ Long.MIN_VALUE, /*rowId*/ -1), /* isReset */ true, action); |
| break; |
| } |
| case ACTION_REFRESH_ITEMS: { |
| if (mIsNotificationForUpdateReceived |
| && mItemsResult != null |
| && mItemsResult.getValue() != null) { |
| updatePaginatedItems(paginationParameters, true, action); |
| mIsNotificationForUpdateReceived = false; |
| } |
| break; |
| } |
| default: |
| Log.w(TAG, "Invalid action passed to fetch items"); |
| } |
| return mItemsResult; |
| } |
| |
| /** |
| * Update the item List {@link #mItemsResult}. Loads the page requested represented by the |
| * pagination parameters and replaces/appends it to the existing list of items based on the |
| * reset value. |
| */ |
| private void updatePaginatedItems(PaginationParameters pagingParameters, boolean isReset, |
| @ItemsAction.Type int action) { |
| if (mItemsResult == null) { |
| mItemsResult = new MutableLiveData<>(); |
| } |
| loadItemsAsync(pagingParameters, /* isReset */ isReset, action); |
| } |
| |
| /** |
| * Loads required items and sets it to the {@link PickerViewModel#mItemsResult} while |
| * considering the isReset value. |
| * |
| * @param pagingParameters parameters representing the items that needs to be loaded next. |
| * @param isReset If this is true, clear the pre-existing list and add the newly loaded |
| * items. |
| * @param action This is used while posting the result of the operation. |
| */ |
| private void loadItemsAsync(@NonNull PaginationParameters pagingParameters, boolean isReset, |
| @ItemsAction.Type int action) { |
| final UserId userId = mUserIdManager.getCurrentUserProfileId(); |
| |
| DataLoaderThread.getHandler().postDelayed(() -> { |
| // Load the items as per the pagination parameters passed as params to this method. |
| List<Item> newPageItemList = loadItems(Category.DEFAULT, userId, pagingParameters); |
| |
| // Based on if it is a reset case or not, create an updated list. |
| // If it is a reset case, assign an empty list else use the contents of the pre-existing |
| // list. Then add the newly loaded items. |
| List<Item> updatedList = |
| mItemsResult.getValue() == null || isReset ? new ArrayList<>() |
| : mItemsResult.getValue().getItems(); |
| updatedList.addAll(newPageItemList); |
| Log.d(TAG, "Next page for photos items have been loaded."); |
| if (newPageItemList.isEmpty()) { |
| Log.d(TAG, "All photos items have been loaded."); |
| } |
| |
| // post the result with the action. |
| mItemsResult.postValue(new PaginatedItemsResult(updatedList, action)); |
| mIsSyncInProgress.postValue(false); |
| }, TOKEN, DELAY_MILLIS); |
| } |
| |
| |
| private List<Item> loadItems(Category category, UserId userId, |
| PaginationParameters pagingParameters) { |
| final List<Item> items = new ArrayList<>(); |
| String cloudProviderAuthority = null; // NULL if fetched items have NO cloud only media item |
| |
| try (Cursor cursor = fetchItems(category, userId, pagingParameters)) { |
| if (cursor == null || cursor.getCount() == 0) { |
| Log.d(TAG, "Didn't receive any items for " + category |
| + ", either cursor is null or cursor count is zero"); |
| return items; |
| } |
| |
| while (cursor.moveToNext()) { |
| // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it |
| // here again. |
| final Item item = Item.fromCursor(cursor, userId); |
| String authority = item.getContentUri().getAuthority(); |
| |
| if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) { |
| cloudProviderAuthority = authority; |
| } |
| items.add(item); |
| } |
| |
| Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user " |
| + userId.toString()); |
| return items; |
| } finally { |
| int count = items.size(); |
| if (category.isDefault()) { |
| mLogger.logLoadedMainGridMediaItems(cloudProviderAuthority, mInstanceId, count); |
| } else { |
| mLogger.logLoadedAlbumGridMediaItems(cloudProviderAuthority, mInstanceId, count); |
| } |
| } |
| } |
| |
| private Cursor fetchItems(Category category, UserId userId, |
| PaginationParameters pagingParameters) { |
| try { |
| if (shouldShowOnlyLocalFeatures()) { |
| return mItemsProvider.getLocalItems(category, pagingParameters, |
| mMimeTypeFilters, userId, mCancellationSignal); |
| } else { |
| return mItemsProvider.getAllItems(category, pagingParameters, |
| mMimeTypeFilters, userId, mCancellationSignal); |
| } |
| } catch (RuntimeException ignored) { |
| // Catch OperationCanceledException. |
| Log.e(TAG, "Failed to fetch items due to a runtime exception", ignored); |
| return null; |
| } |
| } |
| |
| /** |
| * Modifies and returns the live data for category items. |
| */ |
| public LiveData<PaginatedItemsResult> getPaginatedCategoryItemsForAction( |
| @NonNull Category category, |
| @ItemsAction.Type int action, @Nullable PaginationParameters paginationParameters) { |
| switch (action) { |
| case ACTION_VIEW_CREATED: { |
| // This call is made only for loading the first page of album media, |
| // so the existing data loader thread tasks for updating the category items should |
| // be cleared and the category and category item list should be refreshed each time. |
| DataLoaderThread.getHandler().removeCallbacksAndMessages( |
| mLoadCategoryItemsThreadToken); |
| mCategoryItemsResult = new MutableLiveData<>(); |
| mCurrentCategory = category; |
| assert paginationParameters != null; |
| mCategoryItemsPageSize = paginationParameters.getPageSize(); |
| updateCategoryItems(paginationParameters, action); |
| break; |
| } |
| case ACTION_LOAD_NEXT_PAGE: { |
| // Loads next page of the list, using the previously loaded list. |
| // If the current list is empty then it will not perform any actions. |
| if (mCategoryItemsResult == null || mCategoryItemsResult.getValue() == null |
| || !TextUtils.equals(mCurrentCategory.getId(), |
| category.getId())) { |
| break; |
| } |
| List<Item> currentItemList = mCategoryItemsResult.getValue().getItems(); |
| // If the categoryItemList does not contain any items, it would mean that the first |
| // page was empty. |
| if (currentItemList != null && !currentItemList.isEmpty()) { |
| Item item = currentItemList.get(currentItemList.size() - 1); |
| PaginationParameters pagingParams = new PaginationParameters( |
| mCategoryItemsPageSize, |
| item.getDateTaken(), |
| item.getRowId()); |
| updateCategoryItems(pagingParams, action); |
| } |
| break; |
| } |
| default: |
| Log.w(TAG, "Invalid action passed to fetch category items"); |
| } |
| return mCategoryItemsResult; |
| } |
| |
| /** |
| * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemsResult} |
| * |
| * @throws IllegalStateException category and category items is not initiated before calling |
| * this method |
| */ |
| @VisibleForTesting |
| public void updateCategoryItems(PaginationParameters pagingParameters, |
| @ItemsAction.Type int action) { |
| if (mCategoryItemsResult == null || mCurrentCategory == null) { |
| throw new IllegalStateException("mCurrentCategory and mCategoryItemsResult are not" |
| + " initiated. Please call getCategoryItems before calling this method"); |
| } |
| loadCategoryItemsAsync(pagingParameters, action != ACTION_LOAD_NEXT_PAGE, action); |
| } |
| |
| /** |
| * Loads required category items and sets it to the {@link PickerViewModel#mCategoryItemsResult} |
| * while considering the isReset value. |
| * |
| * @param pagingParameters parameters representing the items that needs to be loaded next. |
| * @param isReset If this is true, clear the pre-existing list and add the newly loaded |
| * items. |
| * @param action This is used while posting the result of the operation. |
| */ |
| private void loadCategoryItemsAsync(PaginationParameters pagingParameters, boolean isReset, |
| @ItemsAction.Type int action) { |
| final UserId userId = mUserIdManager.getCurrentUserProfileId(); |
| final Category category = mCurrentCategory; |
| |
| DataLoaderThread.getHandler().postDelayed(() -> { |
| if (action == ACTION_LOAD_NEXT_PAGE && mIsAllCategoryItemsLoaded) { |
| return; |
| } |
| // Load the items as per the pagination parameters passed as params to this method. |
| List<Item> newPageItemList = loadItems(category, userId, pagingParameters); |
| |
| // Based on if it is a reset case or not, create an updated list. |
| // If it is a reset case, assign an empty list else use the contents of the pre-existing |
| // list. Then add the newly loaded items. |
| List<Item> updatedList = mCategoryItemsResult.getValue() == null || isReset |
| ? new ArrayList<>() : mCategoryItemsResult.getValue().getItems(); |
| updatedList.addAll(newPageItemList); |
| |
| if (isReset) { |
| mIsAllCategoryItemsLoaded = false; |
| } |
| Log.d(TAG, "Next page for category items have been loaded. Category: " |
| + category + " " + updatedList.size()); |
| if (newPageItemList.isEmpty()) { |
| mIsAllCategoryItemsLoaded = true; |
| Log.d(TAG, "All items have been loaded for category: " + mCurrentCategory); |
| } |
| if (Objects.equals(category, mCurrentCategory)) { |
| mCategoryItemsResult.postValue(new PaginatedItemsResult(updatedList, action)); |
| } |
| }, mLoadCategoryItemsThreadToken, DELAY_MILLIS); |
| } |
| |
| /** |
| * Used only for testing, clears out any data in item list and category item list. |
| */ |
| @VisibleForTesting |
| public void clearItemsAndCategoryItemsList() { |
| mItemsResult = null; |
| mCategoryItemsResult = null; |
| } |
| |
| /** |
| * @return the list of Categories {@link #mCategoryList} |
| */ |
| public LiveData<List<Category>> getCategories() { |
| if (mCategoryList == null) { |
| updateCategories(); |
| } |
| return mCategoryList; |
| } |
| |
| private List<Category> loadCategories(UserId userId) { |
| final List<Category> categoryList = new ArrayList<>(); |
| String cloudProviderAuthority = null; // NULL if fetched albums have NO cloud album |
| try (Cursor cursor = fetchCategories(userId)) { |
| if (cursor == null || cursor.getCount() == 0) { |
| Log.d(TAG, "Didn't receive any categories, either cursor is null or" |
| + " cursor count is zero"); |
| return categoryList; |
| } |
| |
| while (cursor.moveToNext()) { |
| final Category category = Category.fromCursor(cursor, userId); |
| String authority = category.getAuthority(); |
| |
| if (!LOCAL_PICKER_PROVIDER_AUTHORITY.equals(authority)) { |
| cloudProviderAuthority = authority; |
| } |
| categoryList.add(category); |
| } |
| |
| Log.d(TAG, |
| "Loaded " + categoryList.size() + " categories for user " + userId.toString()); |
| CategoryOrganiserUtils.getReorganisedCategoryList(categoryList); |
| return categoryList; |
| } finally { |
| mLogger.logLoadedAlbums(cloudProviderAuthority, mInstanceId, categoryList.size()); |
| } |
| } |
| |
| private Cursor fetchCategories(UserId userId) { |
| try { |
| if (shouldShowOnlyLocalFeatures()) { |
| return mItemsProvider |
| .getLocalCategories(mMimeTypeFilters, userId, mCancellationSignal); |
| } else { |
| return mItemsProvider |
| .getAllCategories(mMimeTypeFilters, userId, mCancellationSignal); |
| } |
| } catch (RuntimeException ignored) { |
| // Catch OperationCanceledException. |
| Log.e(TAG, "Failed to fetch categories due to a runtime exception", ignored); |
| return null; |
| } |
| } |
| |
| private void loadCategoriesAsync() { |
| final UserId userId = mUserIdManager.getCurrentUserProfileId(); |
| DataLoaderThread.getHandler().postDelayed(() -> { |
| mCategoryList.postValue(loadCategories(userId)); |
| }, TOKEN, DELAY_MILLIS); |
| } |
| |
| /** |
| * Update the category List {@link #mCategoryList} |
| */ |
| public void updateCategories() { |
| if (mCategoryList == null) { |
| mCategoryList = new MutableLiveData<>(); |
| } |
| loadCategoriesAsync(); |
| } |
| |
| /** |
| * Return whether the {@link #mMimeTypeFilters} is {@code null} or not |
| */ |
| public boolean hasMimeTypeFilters() { |
| return mMimeTypeFilters != null && mMimeTypeFilters.length > 0; |
| } |
| |
| private boolean isAllImagesFilter() { |
| return mMimeTypeFilters != null && mMimeTypeFilters.length == 1 |
| && MimeUtils.isAllImagesMimeType(mMimeTypeFilters[0]); |
| } |
| |
| private boolean isAllVideosFilter() { |
| return mMimeTypeFilters != null && mMimeTypeFilters.length == 1 |
| && MimeUtils.isAllVideosMimeType(mMimeTypeFilters[0]); |
| } |
| |
| /** |
| * Parse values from {@code intent} and set corresponding fields |
| */ |
| public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException { |
| mUserIdManager.setIntentAndCheckRestrictions(intent); |
| |
| mMimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(intent); |
| |
| mSelection.parseSelectionValuesFromIntent(intent); |
| |
| mIsLocalOnly = intent.getBooleanExtra(EXTRA_LOCAL_ONLY, false); |
| |
| mIsUserSelectForApp = |
| MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intent.getAction()); |
| if (!SdkLevel.isAtLeastU() && mIsUserSelectForApp) { |
| throw new IllegalArgumentException("ACTION_USER_SELECT_IMAGES_FOR_APP is not enabled " |
| + " for this OS version"); |
| } |
| |
| // Ensure that if Photopicker is being used for permissions the target app UID is present |
| // in the extras. |
| if (mIsUserSelectForApp |
| && (intent.getExtras() == null |
| || !intent.getExtras() |
| .containsKey(Intent.EXTRA_UID))) { |
| throw new IllegalArgumentException( |
| "EXTRA_UID is required for" + " ACTION_USER_SELECT_IMAGES_FOR_APP"); |
| } |
| |
| // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates |
| initBannerManager(); |
| } |
| |
| private void initBannerManager() { |
| mBannerManager = shouldShowOnlyLocalFeatures() |
| ? new BannerManager(mAppContext, mUserIdManager, mConfigStore) |
| : new BannerManager.CloudBannerManager(mAppContext, mUserIdManager, mConfigStore); |
| } |
| |
| /** |
| * Set BottomSheet state |
| */ |
| public void setBottomSheetState(int state) { |
| mBottomSheetState = state; |
| } |
| |
| /** |
| * @return BottomSheet state |
| */ |
| public int getBottomSheetState() { |
| return mBottomSheetState; |
| } |
| |
| /** |
| * Log picker opened metrics |
| */ |
| public void logPickerOpened(int callingUid, String callingPackage, String intentAction) { |
| if (getUserIdManager().isManagedUserSelected()) { |
| mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage); |
| } else { |
| mLogger.logPickerOpenPersonal(mInstanceId, callingUid, callingPackage); |
| } |
| |
| // TODO(b/235326735): Optimise logging multiple times on picker opened |
| // TODO(b/235326736): Check if we should add a metric for PICK_IMAGES intent to simplify |
| // metrics reading |
| if (ACTION_GET_CONTENT.equals(intentAction)) { |
| mLogger.logPickerOpenViaGetContent(mInstanceId, callingUid, callingPackage); |
| } |
| |
| if (mBottomSheetState == STATE_COLLAPSED) { |
| mLogger.logPickerOpenInHalfScreen(mInstanceId, callingUid, callingPackage); |
| } else if (mBottomSheetState == STATE_EXPANDED) { |
| mLogger.logPickerOpenInFullScreen(mInstanceId, callingUid, callingPackage); |
| } |
| |
| if (mSelection != null && mSelection.canSelectMultiple()) { |
| mLogger.logPickerOpenInMultiSelect(mInstanceId, callingUid, callingPackage); |
| } else { |
| mLogger.logPickerOpenInSingleSelect(mInstanceId, callingUid, callingPackage); |
| } |
| |
| if (isAllImagesFilter()) { |
| mLogger.logPickerOpenWithFilterAllImages(mInstanceId, callingUid, callingPackage); |
| } else if (isAllVideosFilter()) { |
| mLogger.logPickerOpenWithFilterAllVideos(mInstanceId, callingUid, callingPackage); |
| } else if (hasMimeTypeFilters()) { |
| mLogger.logPickerOpenWithAnyOtherFilter(mInstanceId, callingUid, callingPackage); |
| } |
| |
| maybeLogPickerOpenedWithCloudProvider(); |
| } |
| |
| private void maybeLogPickerOpenedWithCloudProvider() { |
| if (shouldShowOnlyLocalFeatures()) { |
| return; |
| } |
| |
| final LiveData<String> cloudMediaProviderAuthorityLiveData = |
| getCloudMediaProviderAuthorityLiveData(); |
| cloudMediaProviderAuthorityLiveData.observeForever(new Observer<String>() { |
| @Override |
| public void onChanged(@Nullable String providerAuthority) { |
| Log.d(TAG, "logPickerOpenedWithCloudProvider() provider=" + providerAuthority |
| + ", log=" + (providerAuthority != null)); |
| |
| if (providerAuthority != null) { |
| BackgroundThread.getExecutor().execute(() -> |
| logPickerOpenedWithCloudProvider(providerAuthority)); |
| } |
| // We only need to get the value once. |
| cloudMediaProviderAuthorityLiveData.removeObserver(this); |
| } |
| }); |
| } |
| |
| private void logPickerOpenedWithCloudProvider(@NonNull String providerAuthority) { |
| String cloudProviderPackage = providerAuthority; |
| int cloudProviderUid = -1; |
| |
| try { |
| final PackageManager packageManager = |
| UserId.CURRENT_USER.getPackageManager(mAppContext); |
| final ProviderInfo providerInfo = packageManager.resolveContentProvider( |
| providerAuthority, /* flags= */ 0); |
| |
| cloudProviderPackage = providerInfo.applicationInfo.packageName; |
| cloudProviderUid = providerInfo.applicationInfo.uid; |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.d(TAG, "Logging the ui event 'picker open with an active cloud provider' with its " |
| + "authority in place of the package name and a default uid.", e); |
| } |
| |
| mLogger.logPickerOpenWithActiveCloudProvider( |
| mInstanceId, cloudProviderUid, cloudProviderPackage); |
| } |
| |
| /** |
| * Log metrics to notify that the user has clicked Browse to open DocumentsUi |
| */ |
| public void logBrowseToDocumentsUi(int callingUid, String callingPackage) { |
| mLogger.logBrowseToDocumentsUi(mInstanceId, callingUid, callingPackage); |
| } |
| |
| /** |
| * Log metrics to notify that the user has confirmed selection |
| */ |
| public void logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed) { |
| if (getUserIdManager().isManagedUserSelected()) { |
| mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage, |
| countOfItemsConfirmed); |
| } else { |
| mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage, |
| countOfItemsConfirmed); |
| } |
| } |
| |
| /** |
| * Log metrics to notify that the user has exited Picker without any selection |
| */ |
| public void logPickerCancel(int callingUid, String callingPackage) { |
| if (getUserIdManager().isManagedUserSelected()) { |
| mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage); |
| } else { |
| mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage); |
| } |
| } |
| |
| /** |
| * Log metrics to notify that the user has clicked the mute / unmute button in a video preview |
| */ |
| public void logVideoPreviewMuteButtonClick() { |
| mLogger.logVideoPreviewMuteButtonClick(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has clicked the 'view selected' button |
| * |
| * @param selectedItemCount the number of items selected for preview all |
| */ |
| public void logPreviewAllSelected(int selectedItemCount) { |
| mLogger.logPreviewAllSelected(mInstanceId, selectedItemCount); |
| } |
| |
| /** |
| * Log metrics to notify that the 'switch profile' button is visible & enabled |
| */ |
| public void logProfileSwitchButtonEnabled() { |
| mLogger.logProfileSwitchButtonEnabled(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the 'switch profile' button is visible but disabled |
| */ |
| public void logProfileSwitchButtonDisabled() { |
| mLogger.logProfileSwitchButtonDisabled(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has clicked the 'switch profile' button |
| */ |
| public void logProfileSwitchButtonClick() { |
| mLogger.logProfileSwitchButtonClick(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has cancelled the current session by swiping down |
| */ |
| public void logSwipeDownExit() { |
| mLogger.logSwipeDownExit(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has made a back gesture |
| * @param backStackEntryCount the number of fragment entries currently in the back stack |
| */ |
| public void logBackGestureWithStackCount(int backStackEntryCount) { |
| mLogger.logBackGestureWithStackCount(mInstanceId, backStackEntryCount); |
| } |
| |
| /** |
| * Log metrics to notify that the user has clicked the action bar home button |
| * @param backStackEntryCount the number of fragment entries currently in the back stack |
| */ |
| public void logActionBarHomeButtonClick(int backStackEntryCount) { |
| mLogger.logActionBarHomeButtonClick(mInstanceId, backStackEntryCount); |
| } |
| |
| /** |
| * Log metrics to notify that the user has expanded from half screen to full |
| */ |
| public void logExpandToFullScreen() { |
| mLogger.logExpandToFullScreen(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has opened the photo picker menu |
| */ |
| public void logMenuOpened() { |
| mLogger.logMenuOpened(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has switched to the photos tab |
| */ |
| public void logSwitchToPhotosTab() { |
| mLogger.logSwitchToPhotosTab(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has switched to the albums tab |
| */ |
| public void logSwitchToAlbumsTab() { |
| mLogger.logSwitchToAlbumsTab(mInstanceId); |
| } |
| |
| /** |
| * Log metrics to notify that the user has opened an album |
| * |
| * @param category the opened album metadata |
| * @param position the position of the album in the recycler view |
| */ |
| public void logAlbumOpened(@NonNull Category category, int position) { |
| final String albumId = category.getId(); |
| if (ALBUM_ID_FAVORITES.equals(albumId)) { |
| mLogger.logFavoritesAlbumOpened(mInstanceId); |
| } else if (ALBUM_ID_CAMERA.equals(albumId)) { |
| mLogger.logCameraAlbumOpened(mInstanceId); |
| } else if (ALBUM_ID_DOWNLOADS.equals(albumId)) { |
| mLogger.logDownloadsAlbumOpened(mInstanceId); |
| } else if (ALBUM_ID_SCREENSHOTS.equals(albumId)) { |
| mLogger.logScreenshotsAlbumOpened(mInstanceId); |
| } else if (ALBUM_ID_VIDEOS.equals(albumId)) { |
| mLogger.logVideosAlbumOpened(mInstanceId); |
| } else if (!category.isLocal()) { |
| mLogger.logCloudAlbumOpened(mInstanceId, position); |
| } |
| } |
| |
| /** |
| * Log metrics to notify that the user has selected a media item |
| * |
| * @param item the selected item metadata |
| * @param category the category of the item selected, {@link Category#DEFAULT} for main grid |
| * @param position the position of the album in the recycler view |
| */ |
| public void logMediaItemSelected(@NonNull Item item, @NonNull Category category, int position) { |
| if (category.isDefault()) { |
| mLogger.logSelectedMainGridItem(mInstanceId, position); |
| } else { |
| mLogger.logSelectedAlbumItem(mInstanceId, position); |
| } |
| |
| if (!item.isLocal()) { |
| mLogger.logSelectedCloudOnlyItem(mInstanceId, position); |
| } |
| } |
| |
| /** |
| * Log metrics to notify that the user has previewed a media item |
| * |
| * @param item the previewed item metadata |
| * @param category the category of the item previewed, {@link Category#DEFAULT} for main grid |
| * @param position the position of the album in the recycler view |
| */ |
| public void logMediaItemPreviewed( |
| @NonNull Item item, @NonNull Category category, int position) { |
| if (category.isDefault()) { |
| mLogger.logPreviewedMainGridItem( |
| item.getSpecialFormat(), item.getMimeType(), mInstanceId, position); |
| } |
| } |
| |
| /** |
| * Log metrics to notify create surface controller triggered |
| * @param authority the authority of the provider |
| */ |
| public void logCreateSurfaceControllerStart(String authority) { |
| mLogger.logPickerCreateSurfaceControllerStart(mInstanceId, authority); |
| } |
| |
| /** |
| * Log metrics to notify create surface controller ended |
| * @param authority the authority of the provider |
| */ |
| public void logCreateSurfaceControllerEnd(String authority) { |
| mLogger.logPickerCreateSurfaceControllerEnd(mInstanceId, authority); |
| } |
| |
| public InstanceId getInstanceId() { |
| return mInstanceId; |
| } |
| |
| public void setInstanceId(InstanceId parcelable) { |
| mInstanceId = parcelable; |
| } |
| |
| // Return whether hotopicker's launch intent has extra {@link EXTRA_LOCAL_ONLY} set to true |
| // or not. |
| @VisibleForTesting |
| boolean isLocalOnly() { |
| return mIsLocalOnly; |
| } |
| |
| /** |
| * Return whether only the local features should be shown (the cloud features should be hidden). |
| * |
| * Show only the local features in the following cases - |
| * 1. Photo Picker is launched by the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} |
| * action for the permission flow. |
| * 2. Photo Picker is launched with the {@link Intent#EXTRA_LOCAL_ONLY} as {@code true} in the |
| * {@link Intent#ACTION_GET_CONTENT} or {@link MediaStore#ACTION_PICK_IMAGES} action. |
| * 3. Cloud Media in Photo picker is disabled, i.e., |
| * {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}. |
| * |
| * @return {@code true} iff either {@link #isUserSelectForApp()} or {@link #isLocalOnly()} is |
| * {@code true}, OR if {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}. |
| */ |
| public boolean shouldShowOnlyLocalFeatures() { |
| return isUserSelectForApp() || isLocalOnly() |
| || !mConfigStore.isCloudMediaInPhotoPickerEnabled(); |
| } |
| |
| /** |
| * @return the {@link LiveData} of the 'Choose App' banner visibility. |
| */ |
| @NonNull |
| public LiveData<Boolean> shouldShowChooseAppBannerLiveData() { |
| return mBannerManager.shouldShowChooseAppBannerLiveData(); |
| } |
| |
| /** |
| * @return the {@link LiveData} of the 'Cloud Media Available' banner visibility. |
| */ |
| @NonNull |
| public LiveData<Boolean> shouldShowCloudMediaAvailableBannerLiveData() { |
| return mBannerManager.shouldShowCloudMediaAvailableBannerLiveData(); |
| } |
| |
| /** |
| * @return the {@link LiveData} of the 'Account Updated' banner visibility. |
| */ |
| @NonNull |
| public LiveData<Boolean> shouldShowAccountUpdatedBannerLiveData() { |
| return mBannerManager.shouldShowAccountUpdatedBannerLiveData(); |
| } |
| |
| /** |
| * @return the {@link LiveData} of the 'Choose Account' banner visibility. |
| */ |
| @NonNull |
| public LiveData<Boolean> shouldShowChooseAccountBannerLiveData() { |
| return mBannerManager.shouldShowChooseAccountBannerLiveData(); |
| } |
| |
| /** |
| * Dismiss (hide) the 'Choose App' banner for the current user. |
| */ |
| @UiThread |
| public void onUserDismissedChooseAppBanner() { |
| mBannerManager.onUserDismissedChooseAppBanner(); |
| } |
| |
| /** |
| * Dismiss (hide) the 'Cloud Media Available' banner for the current user. |
| */ |
| @UiThread |
| public void onUserDismissedCloudMediaAvailableBanner() { |
| mBannerManager.onUserDismissedCloudMediaAvailableBanner(); |
| } |
| |
| /** |
| * Dismiss (hide) the 'Account Updated' banner for the current user. |
| */ |
| @UiThread |
| public void onUserDismissedAccountUpdatedBanner() { |
| mBannerManager.onUserDismissedAccountUpdatedBanner(); |
| } |
| |
| /** |
| * Dismiss (hide) the 'Choose Account' banner for the current user. |
| */ |
| @UiThread |
| public void onUserDismissedChooseAccountBanner() { |
| mBannerManager.onUserDismissedChooseAccountBanner(); |
| } |
| |
| /** |
| * @return a {@link LiveData} that posts Should Refresh Picker UI as {@code true} when notified. |
| */ |
| @NonNull |
| public LiveData<Boolean> shouldRefreshUiLiveData() { |
| return mShouldRefreshUiLiveData; |
| } |
| |
| private void registerRefreshUiNotificationObserver() { |
| mContentResolver = getContentResolverForSelectedUser(); |
| mContentResolver.registerContentObserver(REFRESH_UI_PICKER_INTERNAL_OBSERVABLE_URI, |
| /* notifyForDescendants */ false, mRefreshUiNotificationObserver); |
| } |
| |
| private void unregisterRefreshUiNotificationObserver() { |
| if (mContentResolver != null) { |
| mContentResolver.unregisterContentObserver(mRefreshUiNotificationObserver); |
| mContentResolver = null; |
| } |
| } |
| |
| private void resetRefreshUiNotificationObserver() { |
| unregisterRefreshUiNotificationObserver(); |
| registerRefreshUiNotificationObserver(); |
| } |
| |
| private ContentResolver getContentResolverForSelectedUser() { |
| final UserId selectedUserId = mUserIdManager.getCurrentUserProfileId(); |
| if (selectedUserId == null) { |
| Log.d(TAG, "Selected user id is NULL; returning the default content resolver."); |
| return mAppContext.getContentResolver(); |
| } |
| try { |
| return selectedUserId.getContentResolver(mAppContext); |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.d(TAG, "Failed to get the content resolver for the selected user id " |
| + selectedUserId + "; returning the default content resolver.", e); |
| return mAppContext.getContentResolver(); |
| } |
| } |
| |
| public LiveData<Boolean> isSyncInProgress() { |
| return mIsSyncInProgress; |
| } |
| |
| /** |
| * Class used to store the result of the item modification operations. |
| */ |
| public class PaginatedItemsResult { |
| private List<Item> mItems = new ArrayList<>(); |
| |
| private int mAction = ACTION_DEFAULT; |
| |
| public PaginatedItemsResult(@NonNull List<Item> itemList, |
| @ItemsAction.Type int action) { |
| mItems = itemList; |
| mAction = action; |
| } |
| |
| public List<Item> getItems() { |
| return mItems; |
| } |
| |
| @ItemsAction.Type |
| public int getAction() { |
| return mAction; |
| } |
| } |
| |
| /** |
| * This will inform the media Provider process that the UI is preparing to load data for the |
| * main photos grid. |
| */ |
| public void initPhotoPickerData() { |
| initPhotoPickerData(Category.DEFAULT); |
| } |
| |
| /** |
| * This will inform the media Provider process that the UI is preparing to load data for main |
| * photos grid or album contents grid. |
| */ |
| public void initPhotoPickerData(@NonNull Category category) { |
| if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) { |
| UserId userId = mUserIdManager.getCurrentUserProfileId(); |
| DataLoaderThread.getHandler().postDelayed(() -> { |
| if (category == Category.DEFAULT) { |
| mIsSyncInProgress.postValue(true); |
| } |
| mItemsProvider.initPhotoPickerData(category.getId(), |
| category.getAuthority(), |
| shouldShowOnlyLocalFeatures(), |
| userId); |
| }, TOKEN, DELAY_MILLIS); |
| } |
| } |
| |
| private void clearQueuedTasksInDataLoaderThread() { |
| DataLoaderThread.getHandler().removeCallbacksAndMessages(TOKEN); |
| DataLoaderThread.getHandler().removeCallbacksAndMessages(mLoadCategoryItemsThreadToken); |
| } |
| } |