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 package com.android.providers.media.photopicker.ui;
17 
18 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.Style.OUTLINE;
19 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
20 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_PERSONAL_MESSAGE;
21 import static com.android.providers.media.photopicker.ui.DevicePolicyResources.Strings.SWITCH_TO_WORK_MESSAGE;
22 import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_BANNER;
23 import static com.android.providers.media.photopicker.ui.TabAdapter.ITEM_TYPE_SECTION;
24 
25 import android.app.admin.DevicePolicyManager;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.UserProperties;
29 import android.content.res.ColorStateList;
30 import android.content.res.TypedArray;
31 import android.graphics.Color;
32 import android.graphics.drawable.Drawable;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Pair;
38 import android.view.LayoutInflater;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.accessibility.AccessibilityManager;
42 import android.view.animation.Animation;
43 import android.view.animation.AnimationUtils;
44 import android.widget.Button;
45 import android.widget.LinearLayout;
46 import android.widget.PopupWindow;
47 import android.widget.TextView;
48 
49 import androidx.annotation.ColorInt;
50 import androidx.annotation.NonNull;
51 import androidx.annotation.Nullable;
52 import androidx.annotation.RequiresApi;
53 import androidx.core.content.ContextCompat;
54 import androidx.core.graphics.drawable.DrawableCompat;
55 import androidx.fragment.app.Fragment;
56 import androidx.fragment.app.FragmentActivity;
57 import androidx.lifecycle.LiveData;
58 import androidx.lifecycle.MutableLiveData;
59 import androidx.lifecycle.ViewModelProvider;
60 import androidx.recyclerview.widget.GridLayoutManager;
61 import androidx.recyclerview.widget.RecyclerView;
62 
63 import com.android.modules.utils.build.SdkLevel;
64 import com.android.providers.media.ConfigStore;
65 import com.android.providers.media.R;
66 import com.android.providers.media.photopicker.PhotoPickerActivity;
67 import com.android.providers.media.photopicker.data.Selection;
68 import com.android.providers.media.photopicker.data.UserIdManager;
69 import com.android.providers.media.photopicker.data.UserManagerState;
70 import com.android.providers.media.photopicker.data.model.UserId;
71 import com.android.providers.media.photopicker.util.AccentColorResources;
72 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
73 
74 import com.google.android.material.button.MaterialButton;
75 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
76 
77 import java.text.NumberFormat;
78 import java.util.Locale;
79 import java.util.Map;
80 
81 /**
82  * The base abstract Tab fragment
83  */
84 public abstract class TabFragment extends Fragment {
85     private static final String TAG = TabFragment.class.getSimpleName();
86     protected PickerViewModel mPickerViewModel;
87     protected Selection mSelection;
88     protected ImageLoader mImageLoader;
89     protected AutoFitRecyclerView mRecyclerView;
90 
91     private ExtendedFloatingActionButton mProfileButton;
92     private ExtendedFloatingActionButton mProfileMenuButton;
93     private UserIdManager mUserIdManager;
94     private UserManagerState mUserManagerState;
95     private boolean mHideProfileButtonAndProfileMenuButton;
96     private View mEmptyView;
97     private TextView mEmptyTextView;
98     private boolean mIsAccessibilityEnabled;
99     private AccessibilityManager mAccessibilityManager;
100     private AccessibilityManager.AccessibilityStateChangeListener mAccessibilityStateChangeListener;
101 
102     private Button mAddButton;
103 
104     private MaterialButton mViewSelectedButton;
105     private View mBottomBar;
106     private Animation mSlideUpAnimation;
107     private Animation mSlideDownAnimation;
108 
109     @ColorInt
110     private int mButtonIconAndTextColor;
111 
112     @ColorInt
113     private int mProfileMenuButtonIconAndTextColor;
114     @ColorInt
115     private int mButtonBackgroundColor;
116 
117     @ColorInt
118     private int mButtonDisabledIconAndTextColor;
119 
120     @ColorInt
121     private int mButtonDisabledBackgroundColor;
122 
123     private int mRecyclerViewBottomPadding;
124     private boolean mIsProfileButtonVisible = false;
125     private boolean mIsProfileMenuButtonVisible = false;
126     private static PopupWindow sProfileMenuWindow = null;
127 
128     private RecyclerView.OnScrollListener mOnScrollListenerForMultiProfileButton;
129 
130     private final MutableLiveData<Boolean> mIsBottomBarVisible = new MutableLiveData<>(false);
131     private final MutableLiveData<Boolean> mIsProfileButtonOrProfileMenuButtonVisible =
132             new MutableLiveData<>(false);
133     private ConfigStore mConfigStore;
134     private boolean mIsCustomPickerColorSet = false;
135 
136     /**
137      * In case of multiuser profile, it represents the number of profiles that are off
138      * (In quiet mode) with {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN}. Such profiles
139      * in quiet mode will not appear in photopicker.
140      */
141     private int mHideProfileCount = 0;
142 
143     /**
144      * This member variable is relevant to get the userId (other than current user) when only two
145      * number of profiles those either unlocked/on or don't have
146      * {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN},  are available on the device.
147      * we are using this variable to get label and icon of a userId to update the content
148      * in {@link #mProfileButton}, and at the time when user will press {@link #mProfileButton}
149      * to change the current profile.
150      */
151     private UserId mPotentialUserForProfileButton;
152 
153     @Override
154     @NonNull
onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)155     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
156             Bundle savedInstanceState) {
157         super.onCreateView(inflater, container, savedInstanceState);
158         return inflater.inflate(R.layout.fragment_picker_tab, container, false);
159     }
160 
161     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)162     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
163         super.onViewCreated(view, savedInstanceState);
164 
165         final Context context = requireContext();
166         final FragmentActivity activity = requireActivity();
167 
168         mImageLoader = new ImageLoader(context);
169         mRecyclerView = view.findViewById(R.id.picker_tab_recyclerview);
170         mRecyclerView.setHasFixedSize(true);
171         final ViewModelProvider viewModelProvider = new ViewModelProvider(activity);
172         mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
173         mIsCustomPickerColorSet =
174                 mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet();
175         mConfigStore = mPickerViewModel.getConfigStore();
176         mSelection = mPickerViewModel.getSelection();
177         mRecyclerViewBottomPadding = getResources().getDimensionPixelSize(
178                 R.dimen.picker_recycler_view_bottom_padding);
179 
180         mIsBottomBarVisible.observe(this, val -> updateRecyclerViewBottomPadding());
181         mIsProfileButtonOrProfileMenuButtonVisible.observe(
182                 this, val -> updateRecyclerViewBottomPadding());
183 
184         mEmptyView = view.findViewById(android.R.id.empty);
185         mEmptyTextView = mEmptyView.findViewById(R.id.empty_text_view);
186 
187         final int[] attrsDisabled =
188                 new int[]{R.attr.pickerDisabledProfileButtonColor,
189                         R.attr.pickerDisabledProfileButtonTextColor};
190         final TypedArray taDisabled = context.obtainStyledAttributes(attrsDisabled);
191         mButtonDisabledBackgroundColor = taDisabled.getColor(/* index */ 0, /* defValue */ -1);
192         mButtonDisabledIconAndTextColor = taDisabled.getColor(/* index */ 1, /* defValue */ -1);
193         taDisabled.recycle();
194 
195         final int[] attrs =
196                 new int[]{R.attr.pickerProfileButtonColor, R.attr.pickerProfileButtonTextColor,
197                         android.R.attr.textColorPrimary};
198         final TypedArray ta = context.obtainStyledAttributes(attrs);
199         mButtonBackgroundColor = ta.getColor(/* index */ 0, /* defValue */ -1);
200         mButtonIconAndTextColor = ta.getColor(/* index */ 1, /* defValue */ -1);
201         mProfileMenuButtonIconAndTextColor = ta.getColor(/* index */ 2, /* defValue */ -1);
202         ta.recycle();
203 
204         mProfileButton = activity.findViewById(R.id.profile_button);
205         mProfileMenuButton = activity.findViewById(R.id.profile_menu_button);
206         mUserManagerState = mPickerViewModel.getUserManagerState();
207         mUserIdManager = mPickerViewModel.getUserIdManager();
208 
209         final boolean canSelectMultiple = mSelection.canSelectMultiple();
210         if (canSelectMultiple) {
211             mAddButton = activity.findViewById(R.id.button_add);
212 
213             mViewSelectedButton = activity.findViewById(R.id.button_view_selected);
214 
215             if (mIsCustomPickerColorSet) {
216                 setCustomPickerButtonColors(
217                         mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor());
218             }
219             mAddButton.setOnClickListener(v -> {
220                 try {
221                     requirePickerActivity().setResultAndFinishSelf();
222                 } catch (RuntimeException e) {
223                     Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
224                 }
225             });
226             // Transition to PreviewFragment on clicking "View Selected".
227             mViewSelectedButton.setOnClickListener(v -> {
228                 // Load items for preview that are pre granted but not yet loaded for UI. This is an
229                 // async call. Until the items are loaded, we can still preview already available
230                 // items
231                 mPickerViewModel.getRemainingPreGrantedItems();
232                 mSelection.prepareSelectedItemsForPreviewAll();
233 
234                 int selectedItemCount = mSelection.getSelectedItemCount().getValue();
235                 mPickerViewModel.logPreviewAllSelected(selectedItemCount);
236 
237                 try {
238                     PreviewFragment.show(requireActivity().getSupportFragmentManager(),
239                             PreviewFragment.getArgsForPreviewOnViewSelected());
240                 } catch (RuntimeException e) {
241                     Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
242                 }
243             });
244 
245             mBottomBar = activity.findViewById(R.id.picker_bottom_bar);
246 
247             if (mIsCustomPickerColorSet) {
248                 mBottomBar.setBackgroundColor(
249                         mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
250                                 AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT,
251                                 AccentColorResources.SURFACE_CONTAINER_COLOR_DARK
252                         ));
253             }
254             // consume the event so that it doesn't get passed through to the next view b/287661737
255             mBottomBar.setOnClickListener(v -> {
256             });
257             mSlideUpAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_up);
258             mSlideDownAnimation = AnimationUtils.loadAnimation(context, R.anim.slide_down);
259 
260             mSelection.getSelectedItemCount().observe(this, selectedItemListSize -> {
261                 // Fetch activity or context again instead of capturing existing variable in lambdas
262                 // to avoid memory leaks.
263                 try {
264                     if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled()
265                             && SdkLevel.isAtLeastS()) {
266                         updateProfileButtonAndProfileMenuButtonVisibility();
267                     } else {
268                         updateProfileButtonVisibility();
269                     }
270                     updateVisibilityAndAnimateBottomBar(requireContext(), selectedItemListSize);
271                 } catch (RuntimeException e) {
272                     Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
273                 }
274             });
275         }
276 
277         if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled() && SdkLevel.isAtLeastS()) {
278             setUpObserverForCrossProfileAndMultiUserChangeGeneric();
279 
280             // Initial setup
281             setUpProfileButtonAndProfileMenuButtonWithListeners(
282                     mUserManagerState.isMultiUserProfiles());
283 
284         } else {
285             setupObserverForCrossProfileAccess();
286 
287             // Initial setup
288             setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
289         }
290 
291 
292         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
293         mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
294         mAccessibilityStateChangeListener =
295                 enabled -> {
296                     mIsAccessibilityEnabled = enabled;
297                     if (mConfigStore.isPrivateSpaceInPhotoPickerEnabled()
298                             && SdkLevel.isAtLeastS()) {
299                         setUpProfileButtonAndProfileMenuButtonWithListeners(
300                                 mUserManagerState.isMultiUserProfiles());
301                     } else {
302                         setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
303                     }
304                 };
305         mAccessibilityManager.addAccessibilityStateChangeListener(
306                 mAccessibilityStateChangeListener);
307     }
308 
309     @Override
onDestroyView()310     public void onDestroyView() {
311         super.onDestroyView();
312         if (mAccessibilityManager != null) {
313             mAccessibilityManager.removeAccessibilityStateChangeListener(
314                     mAccessibilityStateChangeListener);
315         }
316     }
317 
setupObserverForCrossProfileAccess()318     private void setupObserverForCrossProfileAccess() {
319         // Observe for cross profile access changes.
320         final LiveData<Boolean> crossProfileAllowed = mUserIdManager.getCrossProfileAllowed();
321         if (crossProfileAllowed != null) {
322             crossProfileAllowed.observe(this, isCrossProfileAllowed -> {
323                 setUpProfileButton();
324                 if (Boolean.TRUE.equals(
325                         mIsProfileButtonOrProfileMenuButtonVisible.getValue())) {
326                     if (isCrossProfileAllowed) {
327                         mPickerViewModel.logProfileSwitchButtonEnabled();
328                     } else {
329                         mPickerViewModel.logProfileSwitchButtonDisabled();
330                     }
331                 }
332             });
333         }
334 
335         // Observe for multi-user changes.
336         final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles();
337         if (isMultiUserProfiles != null) {
338             isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners);
339         }
340     }
341 
342     @RequiresApi(Build.VERSION_CODES.S)
setUpObserverForCrossProfileAndMultiUserChangeGeneric()343     private void setUpObserverForCrossProfileAndMultiUserChangeGeneric() {
344         // Observe for cross profile access changes.
345         final LiveData<Map<UserId, Boolean>> crossProfileAllowed =
346                 mUserManagerState.getCrossProfileAllowed();
347         if (crossProfileAllowed != null) {
348             crossProfileAllowed.observe(this, crossProfileAllowedStatus -> {
349                 setUpProfileButtonAndProfileMenuButton();
350                 if (mIsProfileButtonVisible) {
351                     boolean isDisabled = true;
352                     UserId userIdToSwitch = getUserToSwitchFromProfileButton();
353                     if (userIdToSwitch != null) {
354                         isDisabled = !canSwitchToUser(userIdToSwitch);
355                     }
356                     if (isDisabled) {
357                         mPickerViewModel.logProfileSwitchButtonDisabled();
358                     } else {
359                         mPickerViewModel.logProfileSwitchButtonEnabled();
360                     }
361                 } else if (mIsProfileMenuButtonVisible) {
362                     mPickerViewModel.logProfileSwitchMenuButtonVisible();
363                 }
364             });
365         }
366 
367         // Observe for multi-user changes.
368         final LiveData<Boolean> isMultiUserProfiles =
369                 mUserManagerState.getIsMultiUserProfiles();
370         if (isMultiUserProfiles != null) {
371             isMultiUserProfiles.observe(this, isMultiUserProfilesAvailable -> {
372                 setUpProfileButtonAndProfileMenuButtonWithListeners(isMultiUserProfilesAvailable);
373             });
374         }
375     }
376 
377     @RequiresApi(Build.VERSION_CODES.S)
updateUserForProfileButtonAndHideProfileCount()378     private void updateUserForProfileButtonAndHideProfileCount() {
379         mHideProfileCount = 0;
380         mPotentialUserForProfileButton = null;
381         for (UserId userId : mUserManagerState.getAllUserProfileIds()) {
382             if (isProfileHideInQuietMode(userId)) {
383                 mHideProfileCount += 1;
384             } else if (!userId.equals(UserId.CURRENT_USER)) {
385                 mPotentialUserForProfileButton = userId;
386             }
387         }
388 
389         // we will use {@link #mPotentialUserForProfileButton} only to show profile button and
390         // profile button will only be visible when two profiles are available on the device
391         if (mUserManagerState.getProfileCount() - mHideProfileCount != 2) {
392             mPotentialUserForProfileButton = null;
393         }
394     }
395 
isProfileHideInQuietMode(UserId userId)396     private boolean isProfileHideInQuietMode(UserId userId) {
397         if (!SdkLevel.isAtLeastV()) {
398             return false;
399         }
400         /*
401          * Any profile with {@link UserProperties.SHOW_IN_QUIET_MODE_HIDDEN}  will not appear in
402          * quiet mode in Photopicker.
403          */
404         return mUserManagerState.isProfileOff(userId)
405                 && mUserManagerState.getShowInQuietMode(userId)
406                 == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
407     }
408 
setCustomPickerButtonColors(int accentColor)409     private void setCustomPickerButtonColors(int accentColor) {
410         String addButtonTextColor =
411                 mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright()
412                         ? AccentColorResources.DARK_TEXT_COLOR
413                         : AccentColorResources.LIGHT_TEXT_COLOR;
414         mAddButton.setBackgroundColor(accentColor);
415         mAddButton.setTextColor(Color.parseColor(addButtonTextColor));
416         mViewSelectedButton.setTextColor(accentColor);
417         mViewSelectedButton.setIconTint(ColorStateList.valueOf(accentColor));
418 
419     }
420 
updateRecyclerViewBottomPadding()421     private void updateRecyclerViewBottomPadding() {
422         final int recyclerViewBottomPadding;
423         if (mIsProfileButtonOrProfileMenuButtonVisible.getValue()
424                 || mIsBottomBarVisible.getValue()) {
425             recyclerViewBottomPadding = mRecyclerViewBottomPadding;
426         } else {
427             recyclerViewBottomPadding = 0;
428         }
429 
430         mRecyclerView.setPadding(0, 0, 0, recyclerViewBottomPadding);
431     }
432 
updateVisibilityAndAnimateBottomBar(@onNull Context context, int selectedItemListSize)433     private void updateVisibilityAndAnimateBottomBar(@NonNull Context context,
434             int selectedItemListSize) {
435         if (!mSelection.canSelectMultiple()) {
436             return;
437         }
438 
439         if (mPickerViewModel.isManagedSelectionEnabled()) {
440             animateAndShowBottomBar(context, selectedItemListSize);
441             if (selectedItemListSize == 0) {
442                 mViewSelectedButton.setVisibility(View.INVISIBLE);
443                 // Update the add button to show "Allow none".
444                 mAddButton.setText(R.string.picker_add_button_allow_none_option);
445             }
446         } else {
447             if (selectedItemListSize == 0) {
448                 animateAndHideBottomBar();
449             } else {
450                 animateAndShowBottomBar(context, selectedItemListSize);
451             }
452         }
453         mIsBottomBarVisible.setValue(
454                 mPickerViewModel.isManagedSelectionEnabled() || selectedItemListSize > 0);
455     }
456 
animateAndShowBottomBar(Context context, int selectedItemListSize)457     private void animateAndShowBottomBar(Context context, int selectedItemListSize) {
458         if (mBottomBar.getVisibility() == View.GONE) {
459             mBottomBar.setVisibility(View.VISIBLE);
460             mBottomBar.startAnimation(mSlideUpAnimation);
461         }
462         mViewSelectedButton.setVisibility(View.VISIBLE);
463         mAddButton.setText(generateAddButtonString(context, selectedItemListSize));
464     }
465 
animateAndHideBottomBar()466     private void animateAndHideBottomBar() {
467         if (mBottomBar.getVisibility() == View.VISIBLE) {
468             mBottomBar.setVisibility(View.GONE);
469             mBottomBar.startAnimation(mSlideDownAnimation);
470         }
471     }
472 
setUpListenersForProfileButton()473     private void setUpListenersForProfileButton() {
474         mProfileButton.setOnClickListener(v -> onClickProfileButton());
475         mOnScrollListenerForMultiProfileButton = new RecyclerView.OnScrollListener() {
476             @Override
477             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
478                 super.onScrolled(recyclerView, dx, dy);
479 
480                 // Do not change profile button visibility on scroll if Accessibility mode is
481                 // enabled. This is done to enhance button visibility in Accessibility mode.
482                 if (mIsAccessibilityEnabled) {
483                     return;
484                 }
485 
486                 if (dy > 0) {
487                     mProfileButton.hide();
488                 } else {
489                     updateProfileButtonVisibility();
490                 }
491             }
492         };
493         mRecyclerView.addOnScrollListener(mOnScrollListenerForMultiProfileButton);
494     }
495 
496     @RequiresApi(Build.VERSION_CODES.S)
setUpListenersForProfileButtonAndProfileMenuButton()497     private void setUpListenersForProfileButtonAndProfileMenuButton() {
498         mProfileButton.setOnClickListener(v -> onClickProfileButtonGeneric());
499         mProfileMenuButton.setOnClickListener(v -> onClickProfileMenuButton(v));
500         mOnScrollListenerForMultiProfileButton = new RecyclerView.OnScrollListener() {
501             @Override
502             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
503                 super.onScrolled(recyclerView, dx, dy);
504 
505                 // Do not change profile button visibility on scroll if Accessibility mode is
506                 // enabled. This is done to enhance button visibility in Accessibility mode.
507                 if (mIsAccessibilityEnabled) {
508                     return;
509                 }
510 
511                 if (dy > 0) {
512                     mProfileButton.hide();
513                     mProfileMenuButton.hide();
514                 } else {
515                     updateProfileButtonAndProfileMenuButtonVisibility();
516                 }
517             }
518         };
519         mRecyclerView.addOnScrollListener(mOnScrollListenerForMultiProfileButton);
520     }
521 
522     @RequiresApi(Build.VERSION_CODES.S)
onClickProfileMenuButton(View view)523     private void onClickProfileMenuButton(View view) {
524         mPickerViewModel.logProfileSwitchMenuButtonClick();
525         initialiseProfileMenuWindow();
526         View profileMenuView = LayoutInflater.from(requireContext()).inflate(
527                 R.layout.profile_menu_layout, null);
528         sProfileMenuWindow.setContentView(profileMenuView);
529         LinearLayout profileMenuContainer = profileMenuView.findViewById(
530                 R.id.profile_menu_container);
531 
532         Map<UserId, Drawable> profileBadges = mUserManagerState.getProfileBadgeForAll();
533         Map<UserId, String> profileLabels = mUserManagerState.getProfileLabelsForAll();
534 
535         // Add profile menu items to profile menu.
536         for (UserId userId : mUserManagerState.getAllUserProfileIds()) {
537             if (!isProfileHideInQuietMode(userId)) {
538                 View profileMenuItemView = LayoutInflater.from(requireContext()).inflate(
539                         R.layout.profile_menu_item, profileMenuContainer, false);
540 
541                 // Set label and icon in profile menu item
542                 TextView profileMenuItem = profileMenuItemView.findViewById(R.id.profile_label);
543                 String label = profileLabels.get(userId);
544                 Drawable icon = profileBadges.get(userId);
545                 boolean isSwitchingAllowed = canSwitchToUser(userId);
546                 final int textAndIconColor = isSwitchingAllowed
547                         ? mProfileMenuButtonIconAndTextColor : mButtonDisabledIconAndTextColor;
548                 DrawableCompat.setTintList(icon, ColorStateList.valueOf(textAndIconColor));
549                 profileMenuItem.setTextColor(ColorStateList.valueOf(textAndIconColor));
550                 profileMenuItem.setText(label);
551                 profileMenuItem.setCompoundDrawablesWithIntrinsicBounds(
552                         icon, null, null, null);
553                 // Set padding between icon anf label in profile menu button
554                 int paddingDp = getResources().getDimensionPixelSize(
555                         R.dimen.popup_window_title_icon_padding);
556                 int paddingPixels = (int) (paddingDp * getResources().getDisplayMetrics().density);
557                 profileMenuItem.setCompoundDrawablePadding(paddingPixels);
558 
559                 // Add click listener
560                 profileMenuItemView.setOnClickListener(v -> onClickProfileMenuItem(userId));
561                 profileMenuContainer.addView(profileMenuItemView);
562             }
563         }
564 
565         /*
566          * we need estimated dimensions of {@link #sProfileMenuWindow} to open dropdown just above
567          * the {@link #mProfileMenuButton}
568          */
569         sProfileMenuWindow.showAsDropDown(
570                 view, view.getWidth() / 2 - getProfileMenuWindowDimensions().first / 2,
571                 -(getProfileMenuWindowDimensions().second + view.getHeight()));
572     }
573 
initialiseProfileMenuWindow()574     private void initialiseProfileMenuWindow() {
575         if (sProfileMenuWindow != null) {
576             sProfileMenuWindow.dismiss();
577         }
578         sProfileMenuWindow = new PopupWindow(requireContext());
579         sProfileMenuWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
580         sProfileMenuWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
581         sProfileMenuWindow.setFocusable(true);
582         sProfileMenuWindow.setBackgroundDrawable(
583                 ContextCompat.getDrawable(requireContext(), R.drawable.profile_menu_background));
584     }
585 
586     @RequiresApi(Build.VERSION_CODES.S)
canSwitchToUser(UserId userId)587     private boolean canSwitchToUser(UserId userId) {
588         return mUserManagerState.getCrossProfileAllowedStatusForAll().get(userId);
589     }
590 
591     @RequiresApi(Build.VERSION_CODES.S)
onClickProfileMenuItem(UserId userId)592     private void onClickProfileMenuItem(UserId userId) {
593         // Check if current user profileId is not same as given userId, where user want to switch
594         if (!userId.equals(mUserManagerState.getCurrentUserProfileId())) {
595             if (canSwitchToUser(userId)) {
596                 changeProfileGeneric(userId);
597             } else {
598                 try {
599                     ProfileDialogFragment.show(
600                             requireActivity().getSupportFragmentManager(), userId);
601                 } catch (RuntimeException e) {
602                     Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
603                 }
604             }
605         }
606         sProfileMenuWindow.dismiss();
607     }
608 
609     /**
610      * To get estimated dimensions of {@link #sProfileMenuWindow};
611      *
612      * @return a pair of two Integers, first represents width and second represents height
613      */
getProfileMenuWindowDimensions()614     private Pair<Integer, Integer> getProfileMenuWindowDimensions() {
615         int width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
616         int height = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
617         sProfileMenuWindow.getContentView().measure(width, height);
618 
619         return new Pair<>(sProfileMenuWindow.getContentView().getMeasuredWidth(),
620                 sProfileMenuWindow.getContentView().getMeasuredHeight());
621     }
622 
623     @Override
onDestroy()624     public void onDestroy() {
625         super.onDestroy();
626         if (mRecyclerView != null) {
627             mRecyclerView.clearOnScrollListeners();
628         }
629     }
630 
setUpProfileButtonWithListeners(boolean isMultiUserProfile)631     private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) {
632         if (mOnScrollListenerForMultiProfileButton != null) {
633             mRecyclerView.removeOnScrollListener(mOnScrollListenerForMultiProfileButton);
634         }
635         if (isMultiUserProfile) {
636             setUpListenersForProfileButton();
637         }
638         setUpProfileButton();
639     }
640 
641     @RequiresApi(Build.VERSION_CODES.S)
setUpProfileButtonAndProfileMenuButtonWithListeners(boolean isMultiUserProfile)642     private void setUpProfileButtonAndProfileMenuButtonWithListeners(boolean isMultiUserProfile) {
643         if (mOnScrollListenerForMultiProfileButton != null) {
644             mRecyclerView.removeOnScrollListener(mOnScrollListenerForMultiProfileButton);
645         }
646         if (isMultiUserProfile) {
647             setUpListenersForProfileButtonAndProfileMenuButton();
648         }
649         setUpProfileButtonAndProfileMenuButton();
650 
651     }
652 
setUpProfileButton()653     private void setUpProfileButton() {
654         updateProfileButtonVisibility();
655         if (!mUserIdManager.isMultiUserProfiles()) {
656             return;
657         }
658 
659         updateProfileButtonContent(mUserIdManager.isManagedUserSelected());
660         updateProfileButtonColor(/* isDisabled */ !mUserIdManager.isCrossProfileAllowed());
661     }
662 
663     @RequiresApi(Build.VERSION_CODES.S)
setUpProfileButtonAndProfileMenuButton()664     private void setUpProfileButtonAndProfileMenuButton() {
665         // Dismiss profile menu if user remove/lock any profile in the background while profile
666         // menu window was opened.
667         if (sProfileMenuWindow != null) {
668             sProfileMenuWindow.dismiss();
669         }
670         updateUserForProfileButtonAndHideProfileCount();
671         updateProfileButtonAndProfileMenuButtonVisibility();
672         if (!mUserManagerState.isMultiUserProfiles()) {
673             return;
674         }
675 
676         updateProfileButtonAndProfileMenuButtonContent();
677         updateProfileButtonAndProfileMenuButtonColor();
678     }
679 
shouldShowProfileButton()680     private boolean shouldShowProfileButton() {
681         return mUserIdManager.isMultiUserProfiles()
682                 && !mHideProfileButtonAndProfileMenuButton
683                 && !mPickerViewModel.isUserSelectForApp()
684                 && (!mSelection.canSelectMultiple()
685                 || mSelection.getSelectedItemCount().getValue() == 0);
686     }
687 
688     @RequiresApi(Build.VERSION_CODES.S)
shouldShowProfileButtonOrProfileMenuButton()689     private boolean shouldShowProfileButtonOrProfileMenuButton() {
690         return mUserManagerState.isMultiUserProfiles()
691                 && (mUserManagerState.getProfileCount() - mHideProfileCount) > 1
692                 && !mHideProfileButtonAndProfileMenuButton
693                 && !mPickerViewModel.isUserSelectForApp()
694                 && (!mSelection.canSelectMultiple()
695                 || mSelection.getSelectedItemCount().getValue() == 0);
696     }
697 
onClickProfileButton()698     private void onClickProfileButton() {
699         mPickerViewModel.logProfileSwitchButtonClick();
700 
701         if (!mUserIdManager.isCrossProfileAllowed()) {
702             try {
703                 ProfileDialogFragment.show(requireActivity().getSupportFragmentManager(),
704                         (UserId) null);
705             } catch (RuntimeException e) {
706                 Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
707             }
708         } else {
709             changeProfile();
710         }
711     }
712 
713 
714     /**
715      * This method is relevant to get the userId (other than current user profile) when only two
716      * number of profiles those either unlocked/on or don't have
717      * {@link UserProperties#SHOW_IN_QUIET_MODE_HIDDEN},  are available on the device.
718      * we are using this method to get label and icon of a userId to update the content
719      * in {@link #mProfileButton}, and at the time when user will press {@link #mProfileButton}
720      * to change the current profile.
721      */
722     @RequiresApi(Build.VERSION_CODES.S)
getUserToSwitchFromProfileButton()723     private UserId getUserToSwitchFromProfileButton() {
724         if (mPotentialUserForProfileButton != null
725                 && mPotentialUserForProfileButton.equals(
726                 mUserManagerState.getCurrentUserProfileId())) {
727             return UserId.CURRENT_USER;
728         }
729         return mPotentialUserForProfileButton;
730     }
731 
732     @RequiresApi(Build.VERSION_CODES.S)
onClickProfileButtonGeneric()733     private void onClickProfileButtonGeneric() {
734         mPickerViewModel.logProfileSwitchButtonClick();
735         UserId userIdToSwitch = getUserToSwitchFromProfileButton();
736         if (userIdToSwitch != null) {
737             if (canSwitchToUser(userIdToSwitch)) {
738                 changeProfileGeneric(userIdToSwitch);
739             } else {
740                 try {
741                     ProfileDialogFragment.show(
742                             requireActivity().getSupportFragmentManager(), userIdToSwitch);
743                 } catch (RuntimeException e) {
744                     Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
745                 }
746             }
747         }
748     }
749 
changeProfile()750     private void changeProfile() {
751         if (mUserIdManager.isManagedUserSelected()) {
752             // TODO(b/190024747): Add caching for performance before switching data to and fro
753             // work profile
754             mUserIdManager.setPersonalAsCurrentUserProfile();
755 
756         } else {
757             // TODO(b/190024747): Add caching for performance before switching data to and fro
758             // work profile
759             mUserIdManager.setManagedAsCurrentUserProfile();
760         }
761 
762         updateProfileButtonContent(mUserIdManager.isManagedUserSelected());
763 
764         mPickerViewModel.onSwitchedProfile();
765     }
766 
767     @RequiresApi(Build.VERSION_CODES.S)
changeProfileGeneric(UserId userIdSwitchTo)768     private void changeProfileGeneric(UserId userIdSwitchTo) {
769         mUserManagerState.setUserAsCurrentUserProfile(userIdSwitchTo);
770         updateProfileButtonAndProfileMenuButtonContent();
771 
772         mPickerViewModel.onSwitchedProfile();
773     }
774 
updateProfileButtonContent(boolean isManagedUserSelected)775     private void updateProfileButtonContent(boolean isManagedUserSelected) {
776         final Drawable icon;
777         final String text;
778         final Context context;
779         try {
780             context = requireContext();
781         } catch (RuntimeException e) {
782             Log.e(TAG, "Could not update profile button content because the fragment is not"
783                     + " attached.");
784             return;
785         }
786 
787         if (isManagedUserSelected) {
788             icon = context.getDrawable(R.drawable.ic_personal_mode);
789             text = getSwitchToPersonalMessage(context);
790         } else {
791             icon = getWorkProfileIcon(context);
792             text = getSwitchToWorkMessage(context);
793         }
794         mProfileButton.setIcon(icon);
795         mProfileButton.setText(text);
796     }
797 
798     @RequiresApi(Build.VERSION_CODES.S)
updateProfileButtonAndProfileMenuButtonContent()799     private void updateProfileButtonAndProfileMenuButtonContent() {
800         final Context context;
801         final Drawable profileButtonIcon, profileMenuButtonIcon;
802         final String profileButtonText, profileMenuButtonText;
803         final UserId currentUserProfileId = mUserManagerState.getCurrentUserProfileId();
804         try {
805             context = requireContext();
806         } catch (RuntimeException e) {
807             Log.e(TAG, "Could not update profile button content because the fragment is not"
808                     + " attached.");
809             return;
810         }
811 
812         if (mIsProfileMenuButtonVisible) {
813             profileMenuButtonIcon =
814                     mUserManagerState.getProfileBadgeForAll().get(currentUserProfileId);
815             profileMenuButtonText =
816                     mUserManagerState.getProfileLabelsForAll().get(currentUserProfileId);
817             mProfileMenuButton.setIcon(profileMenuButtonIcon);
818             mProfileMenuButton.setText(profileMenuButtonText);
819         }
820 
821         if (mIsProfileButtonVisible) {
822             UserId userIdToSwitch = getUserToSwitchFromProfileButton();
823             if (userIdToSwitch != null) {
824                 if (SdkLevel.isAtLeastV()) {
825                     profileButtonIcon =
826                             mUserManagerState.getProfileBadgeForAll().get(userIdToSwitch);
827                     profileButtonText = context.getString(R.string.picker_profile_switch_message,
828                             mUserManagerState.getProfileLabelsForAll().get(userIdToSwitch));
829                 } else {
830                     if (mUserManagerState.isManagedUserProfile(currentUserProfileId)) {
831                         profileButtonIcon = context.getDrawable(R.drawable.ic_personal_mode);
832                         profileButtonText = getSwitchToPersonalMessage(context);
833                     } else {
834                         profileButtonIcon = getWorkProfileIcon(context);
835                         profileButtonText = getSwitchToWorkMessage(context);
836                     }
837                 }
838                 mProfileButton.setIcon(profileButtonIcon);
839                 mProfileButton.setText(profileButtonText);
840             }
841         }
842     }
843 
844 
getSwitchToPersonalMessage(@onNull Context context)845     private String getSwitchToPersonalMessage(@NonNull Context context) {
846         if (SdkLevel.isAtLeastT()) {
847             return getUpdatedEnterpriseString(
848                     context, SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
849         } else {
850             return context.getString(R.string.picker_personal_profile);
851         }
852     }
853 
getSwitchToWorkMessage(@onNull Context context)854     private String getSwitchToWorkMessage(@NonNull Context context) {
855         if (SdkLevel.isAtLeastT()) {
856             return getUpdatedEnterpriseString(
857                     context, SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
858         } else {
859             return context.getString(R.string.picker_work_profile);
860         }
861     }
862 
863     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getUpdatedEnterpriseString(@onNull Context context, @NonNull String updatableStringId, int defaultStringId)864     private String getUpdatedEnterpriseString(@NonNull Context context,
865             @NonNull String updatableStringId,
866             int defaultStringId) {
867         final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
868         return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
869     }
870 
getWorkProfileIcon(@onNull Context context)871     private Drawable getWorkProfileIcon(@NonNull Context context) {
872         if (SdkLevel.isAtLeastT()) {
873             return getUpdatedWorkProfileIcon(context);
874         } else {
875             return context.getDrawable(R.drawable.ic_work_outline);
876         }
877     }
878 
879     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getUpdatedWorkProfileIcon(@onNull Context context)880     private Drawable getUpdatedWorkProfileIcon(@NonNull Context context) {
881         DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
882         return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () -> {
883             // Fetch activity or context again instead of capturing existing variable in
884             // lambdas to avoid memory leaks.
885             try {
886                 return requireContext().getDrawable(R.drawable.ic_work_outline);
887             } catch (RuntimeException e) {
888                 Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
889                 return null;
890             }
891         });
892     }
893 
894     private void updateProfileButtonColor(boolean isDisabled) {
895         int textAndIconColor =
896                 isDisabled ? mButtonDisabledIconAndTextColor : mButtonIconAndTextColor;
897         int backgroundTintColor =
898                 isDisabled ? mButtonDisabledBackgroundColor : mButtonBackgroundColor;
899 
900         if (mIsCustomPickerColorSet) {
901             textAndIconColor = mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
902                     AccentColorResources.ON_SURFACE_VARIANT_LIGHT,
903                     AccentColorResources.ON_SURFACE_VARIANT_DARK
904             );
905             backgroundTintColor =
906                     mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
907                             AccentColorResources.SURFACE_CONTAINER_LOW_LIGHT,
908                             AccentColorResources.SURFACE_CONTAINER_LOW_DARK
909                     );
910         }
911 
912         mProfileButton.setTextColor(ColorStateList.valueOf(textAndIconColor));
913         mProfileButton.setIconTint(ColorStateList.valueOf(textAndIconColor));
914         mProfileButton.setBackgroundTintList(ColorStateList.valueOf(backgroundTintColor));
915     }
916 
917     @RequiresApi(Build.VERSION_CODES.S)
918     private void updateProfileButtonAndProfileMenuButtonColor() {
919         if (mIsProfileButtonVisible) {
920             boolean isDisabled = true;
921             UserId userIdToSwitch = getUserToSwitchFromProfileButton();
922             if (userIdToSwitch != null) {
923                 isDisabled = !canSwitchToUser(userIdToSwitch);
924             }
925 
926             final int textAndIconColor =
927                     isDisabled ? mButtonDisabledIconAndTextColor : mButtonIconAndTextColor;
928             final int backgroundTintColor =
929                     isDisabled ? mButtonDisabledBackgroundColor : mButtonBackgroundColor;
930 
931             mProfileButton.setTextColor(ColorStateList.valueOf(textAndIconColor));
932             mProfileButton.setIconTint(ColorStateList.valueOf(textAndIconColor));
933             mProfileButton.setBackgroundTintList(ColorStateList.valueOf(backgroundTintColor));
934         }
935 
936         if (mIsProfileMenuButtonVisible) {
937             mProfileMenuButton.setIconTint(ColorStateList.valueOf(mButtonIconAndTextColor));
938         }
939     }
940 
941 
942     protected void hideProfileButton(boolean hide) {
943         mHideProfileButtonAndProfileMenuButton = hide;
944         updateProfileButtonVisibility();
945     }
946 
947     @RequiresApi(Build.VERSION_CODES.S)
948     protected void hideProfileButtonAndProfileMenuButton(boolean hide) {
949         mHideProfileButtonAndProfileMenuButton = hide;
950         updateProfileButtonAndProfileMenuButtonVisibility();
951     }
952 
953 
954     private void updateProfileButtonVisibility() {
955         final boolean shouldShowProfileButton = shouldShowProfileButton();
956         if (shouldShowProfileButton) {
957             mIsProfileButtonVisible = true;
958             mProfileButton.show();
959         } else {
960             mIsProfileButtonVisible = false;
961             mProfileButton.hide();
962         }
963         mIsProfileButtonOrProfileMenuButtonVisible.setValue(shouldShowProfileButton);
964     }
965 
966     @RequiresApi(Build.VERSION_CODES.S)
967     private void updateProfileButtonAndProfileMenuButtonVisibility() {
968         // The button could be either profile button or profile menu button.
969         final boolean shouldShowButton = shouldShowProfileButtonOrProfileMenuButton();
970         setProfileButtonVisibility(shouldShowButton);
971         setProfileMenuButtonVisibility(shouldShowButton);
972         mIsProfileButtonOrProfileMenuButtonVisible.setValue(
973                 shouldShowButton);
974     }
975 
976     @RequiresApi(Build.VERSION_CODES.S)
977     private void setProfileMenuButtonVisibility(boolean shouldShowProfileMenuButton) {
978         mIsProfileMenuButtonVisible = false;
979         /*
980          *  Check if the total number of profiles that will appear separately in PhotoPicker
981          *  is less than three. If more than two such profiles are available, we will show a
982          *  dropdown menu with {@link #mProfileMenuButton} representing all available visible
983          *  profiles instead of showing a {@link #mProfileMenuButton}
984          */
985         if (shouldShowProfileMenuButton
986                 && (mUserManagerState.getProfileCount() - mHideProfileCount) >= 3) {
987             mIsProfileMenuButtonVisible = true;
988             mProfileMenuButton.show();
989         }
990 
991         if (!mIsProfileMenuButtonVisible) {
992             mProfileMenuButton.hide();
993         }
994     }
995 
996     @RequiresApi(Build.VERSION_CODES.S)
997     private void setProfileButtonVisibility(boolean shouldShowProfileButton) {
998         mIsProfileButtonVisible = false;
999         /*
1000          *  Check if the total number of profiles that will appear separately in PhotoPicker
1001          *  is less than three. If more than two such profiles are available, we will show a
1002          *  dropdown menu with {@link #mProfileMenuButton} representing all available visible
1003          *  profiles instead of showing a {@link #mProfileMenuButton}
1004          */
1005         if (shouldShowProfileButton
1006                 && (mUserManagerState.getProfileCount() - mHideProfileCount) < 3) {
1007             mIsProfileButtonVisible = true;
1008             mProfileButton.show();
1009         }
1010         if (!mIsProfileButtonVisible) {
1011             mProfileButton.hide();
1012         }
1013     }
1014 
1015     protected void setEmptyMessage(int resId) {
1016         mEmptyTextView.setText(resId);
1017     }
1018 
1019     /**
1020      * If we show the {@link #mEmptyView}, hide the {@link #mRecyclerView}. If we don't hide the
1021      * {@link #mEmptyView}, show the {@link #mRecyclerView}
1022      * when user switches the profile ,till the time when updated profile data is loading,
1023      * on the UI we hide {@link #mEmptyView} and show Empty {@link #mRecyclerView}
1024      */
1025     protected void updateVisibilityForEmptyView(boolean shouldShowEmptyView) {
1026         mEmptyView.setVisibility(shouldShowEmptyView ? View.VISIBLE : View.GONE);
1027         mRecyclerView.setVisibility(shouldShowEmptyView ? View.GONE : View.VISIBLE);
1028     }
1029 
1030     /**
1031      * Generates the Button Label for the {@link TabFragment#mAddButton}.
1032      *
1033      * @param context The current application context.
1034      * @param size    The current size of the selection.
1035      * @return Localized, formatted string.
1036      */
1037     private String generateAddButtonString(Context context, int size) {
1038         final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size);
1039         final String template =
1040                 mPickerViewModel.isUserSelectForApp()
1041                         ? context.getString(R.string.picker_add_button_multi_select_permissions)
1042                         : context.getString(R.string.picker_add_button_multi_select);
1043 
1044         return TextUtils.expandTemplate(template, sizeString).toString();
1045     }
1046 
1047     /**
1048      * Returns {@link PhotoPickerActivity} if the fragment is attached to one. Otherwise, throws an
1049      * {@link IllegalStateException}.
1050      */
1051     protected final PhotoPickerActivity requirePickerActivity() throws IllegalStateException {
1052         return (PhotoPickerActivity) requireActivity();
1053     }
1054 
1055     protected final void setLayoutManager(@NonNull Context context,
1056             @NonNull TabAdapter adapter, int spanCount) {
1057         final GridLayoutManager layoutManager =
1058                 new GridLayoutManager(context, spanCount);
1059         final GridLayoutManager.SpanSizeLookup lookup = new GridLayoutManager.SpanSizeLookup() {
1060             @Override
1061             public int getSpanSize(int position) {
1062                 final int itemViewType = adapter.getItemViewType(position);
1063                 // For the item view types ITEM_TYPE_BANNER and ITEM_TYPE_SECTION, it is full
1064                 // span, return the span count of the layoutManager.
1065                 if (itemViewType == ITEM_TYPE_BANNER || itemViewType == ITEM_TYPE_SECTION) {
1066                     return layoutManager.getSpanCount();
1067                 } else {
1068                     return 1;
1069                 }
1070             }
1071         };
1072         layoutManager.setSpanSizeLookup(lookup);
1073         mRecyclerView.setLayoutManager(layoutManager);
1074     }
1075 
1076     private abstract class OnBannerEventListener implements TabAdapter.OnBannerEventListener {
1077         @Override
1078         public void onActionButtonClick() {
1079             mPickerViewModel.logBannerActionButtonClicked();
1080             dismissBanner();
1081             launchCloudProviderSettings();
1082         }
1083 
1084         @Override
1085         public void onDismissButtonClick() {
1086             mPickerViewModel.logBannerDismissed();
1087             dismissBanner();
1088         }
1089 
1090         @Override
1091         public void onBannerClick() {
1092             mPickerViewModel.logBannerClicked();
1093             dismissBanner();
1094             launchCloudProviderSettings();
1095         }
1096 
1097         @Override
1098         public void onBannerAdded(@NonNull String name) {
1099             mPickerViewModel.logBannerAdded(name);
1100 
1101             // Should scroll to the banner only if the first completely visible item is the one
1102             // just below it. The possible adapter item positions of such an item are 0 and 1.
1103             // During onViewCreated, before restoring the state, the first visible item position
1104             // is -1, and we should not scroll to position 0 in such cases, else the previously
1105             // saved recycler view position may get overridden.
1106             int firstItemPosition = -1;
1107 
1108             final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
1109             if (layoutManager instanceof GridLayoutManager) {
1110                 firstItemPosition = ((GridLayoutManager) layoutManager)
1111                         .findFirstCompletelyVisibleItemPosition();
1112             }
1113 
1114             if (firstItemPosition == 0 || firstItemPosition == 1) {
1115                 mRecyclerView.scrollToPosition(/* position */ 0);
1116             }
1117         }
1118 
1119         abstract void dismissBanner();
1120 
1121         private void launchCloudProviderSettings() {
1122             final Intent accountChangeIntent =
1123                     mPickerViewModel.getChooseCloudMediaAccountActivityIntent();
1124 
1125             try {
1126                 if (accountChangeIntent != null) {
1127                     requirePickerActivity().startActivity(accountChangeIntent);
1128                 } else {
1129                     requirePickerActivity().startSettingsActivity();
1130                 }
1131             } catch (RuntimeException e) {
1132                 Log.e(TAG, "Fragment is likely not attached to an activity. ", e);
1133             }
1134         }
1135     }
1136 
1137     protected final OnBannerEventListener mOnChooseAppBannerEventListener =
1138             new OnBannerEventListener() {
1139                 @Override
1140                 void dismissBanner() {
1141                     mPickerViewModel.onUserDismissedChooseAppBanner();
1142                 }
1143             };
1144 
1145     protected final OnBannerEventListener mOnCloudMediaAvailableBannerEventListener =
1146             new OnBannerEventListener() {
1147                 @Override
1148                 void dismissBanner() {
1149                     mPickerViewModel.onUserDismissedCloudMediaAvailableBanner();
1150                 }
1151 
1152                 @Override
1153                 public boolean shouldShowActionButton() {
1154                     return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null;
1155                 }
1156             };
1157 
1158     protected final OnBannerEventListener mOnAccountUpdatedBannerEventListener =
1159             new OnBannerEventListener() {
1160                 @Override
1161                 void dismissBanner() {
1162                     mPickerViewModel.onUserDismissedAccountUpdatedBanner();
1163                 }
1164             };
1165 
1166     protected final OnBannerEventListener mOnChooseAccountBannerEventListener =
1167             new OnBannerEventListener() {
1168                 @Override
1169                 void dismissBanner() {
1170                     mPickerViewModel.onUserDismissedChooseAccountBanner();
1171                 }
1172 
1173                 @Override
1174                 public boolean shouldShowActionButton() {
1175                     return mPickerViewModel.getChooseCloudMediaAccountActivityIntent() != null;
1176                 }
1177             };
1178 }
1179