• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.photopicker.viewmodel;
18 
19 import static android.content.Intent.ACTION_GET_CONTENT;
20 import static android.content.Intent.EXTRA_LOCAL_ONLY;
21 
22 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
23 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
24 
25 import android.annotation.SuppressLint;
26 import android.app.Application;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.database.Cursor;
30 import android.provider.MediaStore;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.annotation.UiThread;
37 import androidx.annotation.VisibleForTesting;
38 import androidx.lifecycle.AndroidViewModel;
39 import androidx.lifecycle.LiveData;
40 import androidx.lifecycle.MutableLiveData;
41 import androidx.lifecycle.Observer;
42 
43 import com.android.internal.logging.InstanceId;
44 import com.android.internal.logging.InstanceIdSequence;
45 import com.android.modules.utils.build.SdkLevel;
46 import com.android.providers.media.ConfigStore;
47 import com.android.providers.media.MediaApplication;
48 import com.android.providers.media.photopicker.data.ItemsProvider;
49 import com.android.providers.media.photopicker.data.MuteStatus;
50 import com.android.providers.media.photopicker.data.Selection;
51 import com.android.providers.media.photopicker.data.UserIdManager;
52 import com.android.providers.media.photopicker.data.model.Category;
53 import com.android.providers.media.photopicker.data.model.Item;
54 import com.android.providers.media.photopicker.data.model.UserId;
55 import com.android.providers.media.photopicker.metrics.PhotoPickerUiEventLogger;
56 import com.android.providers.media.photopicker.util.MimeFilterUtils;
57 import com.android.providers.media.util.ForegroundThread;
58 import com.android.providers.media.util.MimeUtils;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * PickerViewModel to store and handle data for PhotoPickerActivity.
65  */
66 public class PickerViewModel extends AndroidViewModel {
67     public static final String TAG = "PhotoPicker";
68 
69     private static final int RECENT_MINIMUM_COUNT = 12;
70 
71     private static final int INSTANCE_ID_MAX = 1 << 15;
72 
73     @NonNull
74     @SuppressLint("StaticFieldLeak")
75     private final Context mAppContext;
76 
77     private final Selection mSelection;
78     private final MuteStatus mMuteStatus;
79 
80     // TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the
81     //  data set to reduce memories.
82     // The list of Items with all photos and videos
83     private MutableLiveData<List<Item>> mItemList;
84     // The list of Items with all photos and videos in category
85     private MutableLiveData<List<Item>> mCategoryItemList;
86     // The list of categories.
87     private MutableLiveData<List<Category>> mCategoryList;
88 
89     private ItemsProvider mItemsProvider;
90     private UserIdManager mUserIdManager;
91     private BannerManager mBannerManager;
92 
93     private InstanceId mInstanceId;
94     private PhotoPickerUiEventLogger mLogger;
95 
96     private String[] mMimeTypeFilters = null;
97     private int mBottomSheetState;
98 
99     private Category mCurrentCategory;
100 
101     // Note - Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
102     private boolean mIsUserSelectForApp;
103     private boolean mIsLocalOnly;
104 
PickerViewModel(@onNull Application application)105     public PickerViewModel(@NonNull Application application) {
106         super(application);
107         mAppContext = application.getApplicationContext();
108         mItemsProvider = new ItemsProvider(mAppContext);
109         mSelection = new Selection();
110         mUserIdManager = UserIdManager.create(mAppContext);
111         mMuteStatus = new MuteStatus();
112         mInstanceId = new InstanceIdSequence(INSTANCE_ID_MAX).newInstanceId();
113         mLogger = new PhotoPickerUiEventLogger();
114         mIsUserSelectForApp = false;
115         mIsLocalOnly = false;
116         // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
117         initBannerManager();
118     }
119 
120     @VisibleForTesting
setItemsProvider(@onNull ItemsProvider itemsProvider)121     public void setItemsProvider(@NonNull ItemsProvider itemsProvider) {
122         mItemsProvider = itemsProvider;
123     }
124 
125     @VisibleForTesting
setUserIdManager(@onNull UserIdManager userIdManager)126     public void setUserIdManager(@NonNull UserIdManager userIdManager) {
127         mUserIdManager = userIdManager;
128     }
129 
130     /**
131      * @return {@link UserIdManager} for this context.
132      */
getUserIdManager()133     public UserIdManager getUserIdManager() {
134         return mUserIdManager;
135     }
136 
137     /**
138      * @return {@code mSelection} that manages the selection
139      */
getSelection()140     public Selection getSelection() {
141         return mSelection;
142     }
143 
144 
145     /**
146      * @return {@code mMuteStatus} that tracks the volume mute status of the video preview
147      */
getMuteStatus()148     public MuteStatus getMuteStatus() {
149         return mMuteStatus;
150     }
151 
152     /**
153      * @return {@code mIsUserSelectForApp} if the picker is currently being used
154      *         for the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP} action.
155      */
isUserSelectForApp()156     public boolean isUserSelectForApp() {
157         return mIsUserSelectForApp;
158     }
159 
160     /**
161      * @return a {@link LiveData} that holds the value (once it's fetched) of the
162      *         {@link android.content.ContentProvider#mAuthority authority} of the current
163      *         {@link android.provider.CloudMediaProvider}.
164      */
165     @NonNull
getCloudMediaProviderAuthorityLiveData()166     public LiveData<String> getCloudMediaProviderAuthorityLiveData() {
167         return mBannerManager.getCloudMediaProviderAuthorityLiveData();
168     }
169 
170     /**
171      * @return a {@link LiveData} that holds the value (once it's fetched) of the label
172      *         of the current {@link android.provider.CloudMediaProvider}.
173      */
174     @NonNull
getCloudMediaProviderAppTitleLiveData()175     public LiveData<String> getCloudMediaProviderAppTitleLiveData() {
176         return mBannerManager.getCloudMediaProviderAppTitleLiveData();
177     }
178 
179     /**
180      * @return a {@link LiveData} that holds the value (once it's fetched) of the account name
181      *         of the current {@link android.provider.CloudMediaProvider}.
182      */
183     @NonNull
getCloudMediaAccountNameLiveData()184     public LiveData<String> getCloudMediaAccountNameLiveData() {
185         return mBannerManager.getCloudMediaAccountNameLiveData();
186     }
187 
188     /**
189      * Reset PickerViewModel.
190      * @param switchToPersonalProfile is true then set personal profile as current profile.
191      */
192     @UiThread
reset(boolean switchToPersonalProfile)193     public void reset(boolean switchToPersonalProfile) {
194         // 1. Clear Selected items
195         mSelection.clearSelectedItems();
196         // 2. Change profile to personal user
197         if (switchToPersonalProfile) {
198             mUserIdManager.setPersonalAsCurrentUserProfile();
199         }
200         // 3. Update Item and Category lists
201         updateItems();
202         updateCategories();
203         // 4. Update Banners
204         // Note - Banners should always be updated after the items & categories to ensure a
205         // consistent UI.
206         mBannerManager.maybeResetAllBannerData();
207         mBannerManager.maybeUpdateBannerLiveDatas();
208     }
209 
210     /**
211      * Update items, categories & banners on profile switched by the user.
212      */
213     @UiThread
onUserSwitchedProfile()214     public void onUserSwitchedProfile() {
215         updateItems();
216         updateCategories();
217         // Note - Banners should always be updated after the items & categories to ensure a
218         // consistent UI.
219         mBannerManager.maybeUpdateBannerLiveDatas();
220     }
221 
222     /**
223      * @return the list of Items with all photos and videos {@link #mItemList} on the device.
224      */
getItems()225     public LiveData<List<Item>> getItems() {
226         if (mItemList == null) {
227             updateItems();
228         }
229         return mItemList;
230     }
231 
loadItems(Category category, UserId userId)232     private List<Item> loadItems(Category category, UserId userId) {
233         final List<Item> items = new ArrayList<>();
234 
235         try (Cursor cursor = fetchItems(category, userId)) {
236             if (cursor == null || cursor.getCount() == 0) {
237                 Log.d(TAG, "Didn't receive any items for " + category
238                         + ", either cursor is null or cursor count is zero");
239                 return items;
240             }
241 
242             while (cursor.moveToNext()) {
243                 // TODO(b/188394433): Return userId in the cursor so that we do not need to pass it
244                 //  here again.
245                 items.add(Item.fromCursor(cursor, userId));
246             }
247         }
248 
249         Log.d(TAG, "Loaded " + items.size() + " items in " + category + " for user "
250                 + userId.toString());
251         return items;
252     }
253 
fetchItems(Category category, UserId userId)254     private Cursor fetchItems(Category category, UserId userId) {
255         if (shouldShowOnlyLocalFeatures()) {
256             return mItemsProvider.getLocalItems(category, /* limit */ -1, mMimeTypeFilters, userId);
257         } else {
258             return mItemsProvider.getAllItems(category, /* limit */ -1, mMimeTypeFilters, userId);
259         }
260     }
261 
loadItemsAsync()262     private void loadItemsAsync() {
263         final UserId userId = mUserIdManager.getCurrentUserProfileId();
264         ForegroundThread.getExecutor().execute(() -> {
265                     mItemList.postValue(loadItems(Category.DEFAULT, userId));
266         });
267     }
268 
269     /**
270      * Update the item List {@link #mItemList}
271      */
updateItems()272     public void updateItems() {
273         if (mItemList == null) {
274             mItemList = new MutableLiveData<>();
275         }
276         loadItemsAsync();
277     }
278 
279     /**
280      * Get the list of all photos and videos with the specific {@code category} on the device.
281      *
282      * In our use case, we only keep the list of current category {@link #mCurrentCategory} in
283      * {@link #mCategoryItemList}. If the {@code category} and {@link #mCurrentCategory} are
284      * different, we will create the new LiveData to {@link #mCategoryItemList}.
285      *
286      * @param category the category we want to be queried
287      * @return the list of all photos and videos with the specific {@code category}
288      *         {@link #mCategoryItemList}
289      */
getCategoryItems(@onNull Category category)290     public LiveData<List<Item>> getCategoryItems(@NonNull Category category) {
291         if (mCategoryItemList == null || !TextUtils.equals(mCurrentCategory.getId(),
292                 category.getId())) {
293             mCategoryItemList = new MutableLiveData<>();
294             mCurrentCategory = category;
295         }
296         updateCategoryItems();
297         return mCategoryItemList;
298     }
299 
loadCategoryItemsAsync()300     private void loadCategoryItemsAsync() {
301         final UserId userId = mUserIdManager.getCurrentUserProfileId();
302         ForegroundThread.getExecutor().execute(() -> {
303             mCategoryItemList.postValue(loadItems(mCurrentCategory, userId));
304         });
305     }
306 
307     /**
308      * Update the item List with the {@link #mCurrentCategory} {@link #mCategoryItemList}
309      *
310      * @throws IllegalStateException category and category items is not initiated before calling
311      *     this method
312      */
313     @VisibleForTesting
updateCategoryItems()314     public void updateCategoryItems() {
315         if (mCategoryItemList == null || mCurrentCategory == null) {
316             throw new IllegalStateException("mCurrentCategory and mCategoryItemList are not"
317                     + " initiated. Please call getCategoryItems before calling this method");
318         }
319         loadCategoryItemsAsync();
320     }
321 
322     /**
323      * @return the list of Categories {@link #mCategoryList}
324      */
getCategories()325     public LiveData<List<Category>> getCategories() {
326         if (mCategoryList == null) {
327             updateCategories();
328         }
329         return mCategoryList;
330     }
331 
loadCategories(UserId userId)332     private List<Category> loadCategories(UserId userId) {
333         final List<Category> categoryList = new ArrayList<>();
334         try (Cursor cursor = fetchCategories(userId)) {
335             if (cursor == null || cursor.getCount() == 0) {
336                 Log.d(TAG, "Didn't receive any categories, either cursor is null or"
337                         + " cursor count is zero");
338                 return categoryList;
339             }
340 
341             while (cursor.moveToNext()) {
342                 final Category category = Category.fromCursor(cursor, userId);
343                 categoryList.add(category);
344             }
345 
346             Log.d(TAG,
347                     "Loaded " + categoryList.size() + " categories for user " + userId.toString());
348         }
349         return categoryList;
350     }
351 
fetchCategories(UserId userId)352     private Cursor fetchCategories(UserId userId) {
353         if (shouldShowOnlyLocalFeatures()) {
354             return mItemsProvider.getLocalCategories(mMimeTypeFilters, userId);
355         } else {
356             return mItemsProvider.getAllCategories(mMimeTypeFilters, userId);
357         }
358     }
359 
loadCategoriesAsync()360     private void loadCategoriesAsync() {
361         final UserId userId = mUserIdManager.getCurrentUserProfileId();
362         ForegroundThread.getExecutor().execute(() -> {
363             mCategoryList.postValue(loadCategories(userId));
364         });
365     }
366 
367     /**
368      * Update the category List {@link #mCategoryList}
369      */
updateCategories()370     public void updateCategories() {
371         if (mCategoryList == null) {
372             mCategoryList = new MutableLiveData<>();
373         }
374         loadCategoriesAsync();
375     }
376 
377     /**
378      * Return whether the {@link #mMimeTypeFilters} is {@code null} or not
379      */
hasMimeTypeFilters()380     public boolean hasMimeTypeFilters() {
381         return mMimeTypeFilters != null && mMimeTypeFilters.length > 0;
382     }
383 
isAllImagesFilter()384     private boolean isAllImagesFilter() {
385         return mMimeTypeFilters != null && mMimeTypeFilters.length == 1
386                 && MimeUtils.isAllImagesMimeType(mMimeTypeFilters[0]);
387     }
388 
isAllVideosFilter()389     private boolean isAllVideosFilter() {
390         return mMimeTypeFilters != null && mMimeTypeFilters.length == 1
391                 && MimeUtils.isAllVideosMimeType(mMimeTypeFilters[0]);
392     }
393 
394     /**
395      * Parse values from {@code intent} and set corresponding fields
396      */
parseValuesFromIntent(Intent intent)397     public void parseValuesFromIntent(Intent intent) throws IllegalArgumentException {
398         mUserIdManager.setIntentAndCheckRestrictions(intent);
399 
400         mMimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(intent);
401 
402         mSelection.parseSelectionValuesFromIntent(intent);
403 
404         mIsLocalOnly = intent.getBooleanExtra(EXTRA_LOCAL_ONLY, false);
405 
406         mIsUserSelectForApp =
407                 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(intent.getAction());
408         if (!SdkLevel.isAtLeastU() && mIsUserSelectForApp) {
409             throw new IllegalArgumentException("ACTION_USER_SELECT_IMAGES_FOR_APP is not enabled "
410                     + " for this OS version");
411         }
412 
413         // Ensure that if Photopicker is being used for permissions the target app UID is present
414         // in the extras.
415         if (mIsUserSelectForApp
416                 && (intent.getExtras() == null
417                         || !intent.getExtras()
418                                 .containsKey(Intent.EXTRA_UID))) {
419             throw new IllegalArgumentException(
420                     "EXTRA_UID is required for" + " ACTION_USER_SELECT_IMAGES_FOR_APP");
421         }
422 
423         // Must init banner manager on mIsUserSelectForApp / mIsLocalOnly updates
424         initBannerManager();
425     }
426 
initBannerManager()427     private void initBannerManager() {
428         mBannerManager = shouldShowOnlyLocalFeatures()
429                 ? new BannerManager(mAppContext, mUserIdManager)
430                 : new BannerManager.CloudBannerManager(mAppContext, mUserIdManager);
431     }
432 
433     /**
434      * Set BottomSheet state
435      */
setBottomSheetState(int state)436     public void setBottomSheetState(int state) {
437         mBottomSheetState = state;
438     }
439 
440     /**
441      * @return BottomSheet state
442      */
getBottomSheetState()443     public int getBottomSheetState() {
444         return mBottomSheetState;
445     }
446 
447     /**
448      * Log picker opened metrics
449      */
logPickerOpened(int callingUid, String callingPackage, String intentAction)450     public void logPickerOpened(int callingUid, String callingPackage, String intentAction) {
451         if (getUserIdManager().isManagedUserSelected()) {
452             mLogger.logPickerOpenWork(mInstanceId, callingUid, callingPackage);
453         } else {
454             mLogger.logPickerOpenPersonal(mInstanceId, callingUid, callingPackage);
455         }
456 
457         // TODO(b/235326735): Optimise logging multiple times on picker opened
458         // TODO(b/235326736): Check if we should add a metric for PICK_IMAGES intent to simplify
459         // metrics reading
460         if (ACTION_GET_CONTENT.equals(intentAction)) {
461             mLogger.logPickerOpenViaGetContent(mInstanceId, callingUid, callingPackage);
462         }
463 
464         if (mBottomSheetState == STATE_COLLAPSED) {
465             mLogger.logPickerOpenInHalfScreen(mInstanceId, callingUid, callingPackage);
466         } else if (mBottomSheetState == STATE_EXPANDED) {
467             mLogger.logPickerOpenInFullScreen(mInstanceId, callingUid, callingPackage);
468         }
469 
470         if (mSelection != null && mSelection.canSelectMultiple()) {
471             mLogger.logPickerOpenInMultiSelect(mInstanceId, callingUid, callingPackage);
472         } else {
473             mLogger.logPickerOpenInSingleSelect(mInstanceId, callingUid, callingPackage);
474         }
475 
476         if (isAllImagesFilter()) {
477             mLogger.logPickerOpenWithFilterAllImages(mInstanceId, callingUid, callingPackage);
478         } else if (isAllVideosFilter()) {
479             mLogger.logPickerOpenWithFilterAllVideos(mInstanceId, callingUid, callingPackage);
480         } else if (hasMimeTypeFilters()) {
481             mLogger.logPickerOpenWithAnyOtherFilter(mInstanceId, callingUid, callingPackage);
482         }
483 
484         maybeLogPickerOpenedWithCloudProvider();
485     }
486 
487     // TODO(b/245745412): Fix log params (uid & package name)
488     // TODO(b/245745424): Solve for active cloud provider without a logged in account
maybeLogPickerOpenedWithCloudProvider()489     private void maybeLogPickerOpenedWithCloudProvider() {
490         if (shouldShowOnlyLocalFeatures()) {
491             return;
492         }
493 
494         final LiveData<String> cloudMediaProviderAuthorityLiveData =
495                 getCloudMediaProviderAuthorityLiveData();
496         cloudMediaProviderAuthorityLiveData.observeForever(new Observer<String>() {
497             @Override
498             public void onChanged(@Nullable String providerAuthority) {
499                 Log.d(TAG, "logPickerOpenedWithCloudProvider() provider=" + providerAuthority
500                         + ", log=" + (providerAuthority != null));
501 
502                 if (providerAuthority != null) {
503                     mLogger.logPickerOpenWithActiveCloudProvider(
504                             mInstanceId, /* cloudProviderUid */ -1, providerAuthority);
505                 }
506                 // We only need to get the value once.
507                 cloudMediaProviderAuthorityLiveData.removeObserver(this);
508             }
509         });
510     }
511 
512     /**
513      * Log metrics to notify that the user has clicked Browse to open DocumentsUi
514      */
logBrowseToDocumentsUi(int callingUid, String callingPackage)515     public void logBrowseToDocumentsUi(int callingUid, String callingPackage) {
516         mLogger.logBrowseToDocumentsUi(mInstanceId, callingUid, callingPackage);
517     }
518 
519     /**
520      * Log metrics to notify that the user has confirmed selection
521      */
logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed)522     public void logPickerConfirm(int callingUid, String callingPackage, int countOfItemsConfirmed) {
523         if (getUserIdManager().isManagedUserSelected()) {
524             mLogger.logPickerConfirmWork(mInstanceId, callingUid, callingPackage,
525                     countOfItemsConfirmed);
526         } else {
527             mLogger.logPickerConfirmPersonal(mInstanceId, callingUid, callingPackage,
528                     countOfItemsConfirmed);
529         }
530     }
531 
532     /**
533      * Log metrics to notify that the user has exited Picker without any selection
534      */
logPickerCancel(int callingUid, String callingPackage)535     public void logPickerCancel(int callingUid, String callingPackage) {
536         if (getUserIdManager().isManagedUserSelected()) {
537             mLogger.logPickerCancelWork(mInstanceId, callingUid, callingPackage);
538         } else {
539             mLogger.logPickerCancelPersonal(mInstanceId, callingUid, callingPackage);
540         }
541     }
542 
getInstanceId()543     public InstanceId getInstanceId() {
544         return mInstanceId;
545     }
546 
setInstanceId(InstanceId parcelable)547     public void setInstanceId(InstanceId parcelable) {
548         mInstanceId = parcelable;
549     }
550 
551     // Return whether hotopicker's launch intent has extra {@link EXTRA_LOCAL_ONLY} set to true
552     // or not.
553     @VisibleForTesting
isLocalOnly()554     boolean isLocalOnly() {
555         return mIsLocalOnly;
556     }
557 
558     /**
559      * Return whether only the local features should be shown (the cloud features should be hidden).
560      *
561      * Show only the local features in the following cases -
562      * 1. Photo Picker is launched by the {@link MediaStore#ACTION_USER_SELECT_IMAGES_FOR_APP}
563      *    action for the permission flow.
564      * 2. Photo Picker is launched with the {@link Intent#EXTRA_LOCAL_ONLY} as {@code true} in the
565      *    {@link Intent#ACTION_GET_CONTENT} or {@link MediaStore#ACTION_PICK_IMAGES} action.
566      * 3. Cloud Media in Photo picker is disabled, i.e.,
567      *    {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}.
568      *
569      * @return {@code true} iff either {@link #isUserSelectForApp()} or {@link #isLocalOnly()} is
570      * {@code true}, OR if {@link ConfigStore#isCloudMediaInPhotoPickerEnabled()} is {@code false}.
571      */
shouldShowOnlyLocalFeatures()572     public boolean shouldShowOnlyLocalFeatures() {
573         return isUserSelectForApp() || isLocalOnly()
574                 || !getConfigStore().isCloudMediaInPhotoPickerEnabled();
575     }
576 
577     @VisibleForTesting
getConfigStore()578     protected ConfigStore getConfigStore() {
579         return MediaApplication.getConfigStore();
580     }
581 
582     /**
583      * @return the {@link LiveData} of the 'Choose App' banner visibility.
584      */
585     @NonNull
shouldShowChooseAppBannerLiveData()586     public LiveData<Boolean> shouldShowChooseAppBannerLiveData() {
587         return mBannerManager.shouldShowChooseAppBannerLiveData();
588     }
589 
590     /**
591      * @return the {@link LiveData} of the 'Cloud Media Available' banner visibility.
592      */
593     @NonNull
shouldShowCloudMediaAvailableBannerLiveData()594     public LiveData<Boolean> shouldShowCloudMediaAvailableBannerLiveData() {
595         return mBannerManager.shouldShowCloudMediaAvailableBannerLiveData();
596     }
597 
598     /**
599      * @return the {@link LiveData} of the 'Account Updated' banner visibility.
600      */
601     @NonNull
shouldShowAccountUpdatedBannerLiveData()602     public LiveData<Boolean> shouldShowAccountUpdatedBannerLiveData() {
603         return mBannerManager.shouldShowAccountUpdatedBannerLiveData();
604     }
605 
606     /**
607      * @return the {@link LiveData} of the 'Choose Account' banner visibility.
608      */
609     @NonNull
shouldShowChooseAccountBannerLiveData()610     public LiveData<Boolean> shouldShowChooseAccountBannerLiveData() {
611         return mBannerManager.shouldShowChooseAccountBannerLiveData();
612     }
613 
614     /**
615      * Dismiss (hide) the 'Choose App' banner for the current user.
616      */
617     @UiThread
onUserDismissedChooseAppBanner()618     public void onUserDismissedChooseAppBanner() {
619         mBannerManager.onUserDismissedChooseAppBanner();
620     }
621 
622     /**
623      * Dismiss (hide) the 'Cloud Media Available' banner for the current user.
624      */
625     @UiThread
onUserDismissedCloudMediaAvailableBanner()626     public void onUserDismissedCloudMediaAvailableBanner() {
627         mBannerManager.onUserDismissedCloudMediaAvailableBanner();
628     }
629 
630     /**
631      * Dismiss (hide) the 'Account Updated' banner for the current user.
632      */
633     @UiThread
onUserDismissedAccountUpdatedBanner()634     public void onUserDismissedAccountUpdatedBanner() {
635         mBannerManager.onUserDismissedAccountUpdatedBanner();
636     }
637 
638     /**
639      * Dismiss (hide) the 'Choose Account' banner for the current user.
640      */
641     @UiThread
onUserDismissedChooseAccountBanner()642     public void onUserDismissedChooseAccountBanner() {
643         mBannerManager.onUserDismissedChooseAccountBanner();
644     }
645 }
646