• 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 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.res.ColorStateList;
28 import android.content.res.TypedArray;
29 import android.graphics.drawable.Drawable;
30 import android.os.Build;
31 import android.os.Bundle;
32 import android.text.TextUtils;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.accessibility.AccessibilityManager;
37 import android.view.animation.Animation;
38 import android.view.animation.AnimationUtils;
39 import android.widget.Button;
40 import android.widget.TextView;
41 
42 import androidx.annotation.ColorInt;
43 import androidx.annotation.NonNull;
44 import androidx.annotation.Nullable;
45 import androidx.annotation.RequiresApi;
46 import androidx.fragment.app.Fragment;
47 import androidx.lifecycle.LiveData;
48 import androidx.lifecycle.MutableLiveData;
49 import androidx.lifecycle.ViewModelProvider;
50 import androidx.recyclerview.widget.GridLayoutManager;
51 import androidx.recyclerview.widget.RecyclerView;
52 
53 import com.android.modules.utils.build.SdkLevel;
54 import com.android.providers.media.R;
55 import com.android.providers.media.photopicker.PhotoPickerActivity;
56 import com.android.providers.media.photopicker.data.Selection;
57 import com.android.providers.media.photopicker.data.UserIdManager;
58 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
59 
60 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
61 
62 import java.text.NumberFormat;
63 import java.util.Locale;
64 
65 /**
66  * The base abstract Tab fragment
67  */
68 public abstract class TabFragment extends Fragment {
69 
70     protected PickerViewModel mPickerViewModel;
71     protected Selection mSelection;
72     protected ImageLoader mImageLoader;
73     protected AutoFitRecyclerView mRecyclerView;
74 
75     private ExtendedFloatingActionButton mProfileButton;
76     private UserIdManager mUserIdManager;
77     private boolean mHideProfileButton;
78     private View mEmptyView;
79     private TextView mEmptyTextView;
80     private boolean mIsAccessibilityEnabled;
81 
82     private Button mAddButton;
83     private View mBottomBar;
84     private Animation mSlideUpAnimation;
85     private Animation mSlideDownAnimation;
86 
87     @ColorInt
88     private int mButtonIconAndTextColor;
89 
90     @ColorInt
91     private int mButtonBackgroundColor;
92 
93     @ColorInt
94     private int mButtonDisabledIconAndTextColor;
95 
96     @ColorInt
97     private int mButtonDisabledBackgroundColor;
98 
99     private int mRecyclerViewBottomPadding;
100 
101     private final MutableLiveData<Boolean> mIsBottomBarVisible = new MutableLiveData<>(false);
102     private final MutableLiveData<Boolean> mIsProfileButtonVisible = new MutableLiveData<>(false);
103 
104     @Override
105     @NonNull
onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)106     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
107             Bundle savedInstanceState) {
108         super.onCreateView(inflater, container, savedInstanceState);
109         return inflater.inflate(R.layout.fragment_picker_tab, container, false);
110     }
111 
112     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)113     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
114         super.onViewCreated(view, savedInstanceState);
115 
116         final Context context = getContext();
117         mImageLoader = new ImageLoader(context);
118         mRecyclerView = view.findViewById(R.id.picker_tab_recyclerview);
119         mRecyclerView.setHasFixedSize(true);
120         final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity());
121         mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
122         mSelection = mPickerViewModel.getSelection();
123         mRecyclerViewBottomPadding = getResources().getDimensionPixelSize(
124                 R.dimen.picker_recycler_view_bottom_padding);
125 
126         mIsBottomBarVisible.observe(this, val -> updateRecyclerViewBottomPadding());
127         mIsProfileButtonVisible.observe(this, val -> updateRecyclerViewBottomPadding());
128 
129         mEmptyView = view.findViewById(android.R.id.empty);
130         mEmptyTextView = mEmptyView.findViewById(R.id.empty_text_view);
131 
132         final int[] attrsDisabled =
133                 new int[]{R.attr.pickerDisabledProfileButtonColor,
134                         R.attr.pickerDisabledProfileButtonTextColor};
135         final TypedArray taDisabled = context.obtainStyledAttributes(attrsDisabled);
136         mButtonDisabledBackgroundColor = taDisabled.getColor(/* index */ 0, /* defValue */ -1);
137         mButtonDisabledIconAndTextColor = taDisabled.getColor(/* index */ 1, /* defValue */ -1);
138         taDisabled.recycle();
139 
140         final int[] attrs =
141                 new int[]{R.attr.pickerProfileButtonColor, R.attr.pickerProfileButtonTextColor};
142         final TypedArray ta = context.obtainStyledAttributes(attrs);
143         mButtonBackgroundColor = ta.getColor(/* index */ 0, /* defValue */ -1);
144         mButtonIconAndTextColor = ta.getColor(/* index */ 1, /* defValue */ -1);
145         ta.recycle();
146 
147         mProfileButton = getActivity().findViewById(R.id.profile_button);
148         mUserIdManager = mPickerViewModel.getUserIdManager();
149 
150         final boolean canSelectMultiple = mSelection.canSelectMultiple();
151         if (canSelectMultiple) {
152             mAddButton = getActivity().findViewById(R.id.button_add);
153             mAddButton.setOnClickListener(v -> {
154                 ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
155             });
156 
157             final Button viewSelectedButton = getActivity().findViewById(R.id.button_view_selected);
158             // Transition to PreviewFragment on clicking "View Selected".
159             viewSelectedButton.setOnClickListener(v -> {
160                 mSelection.prepareSelectedItemsForPreviewAll();
161                 PreviewFragment.show(getActivity().getSupportFragmentManager(),
162                         PreviewFragment.getArgsForPreviewOnViewSelected());
163             });
164 
165             mBottomBar = getActivity().findViewById(R.id.picker_bottom_bar);
166             mSlideUpAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_up);
167             mSlideDownAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_down);
168 
169             mSelection.getSelectedItemCount().observe(this, selectedItemListSize -> {
170                 updateProfileButtonVisibility();
171                 updateVisibilityAndAnimateBottomBar(selectedItemListSize);
172             });
173         }
174 
175         // Initial setup
176         setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
177 
178         // Observe for cross profile access changes.
179         final LiveData<Boolean> crossProfileAllowed = mUserIdManager.getCrossProfileAllowed();
180         if (crossProfileAllowed != null) {
181             crossProfileAllowed.observe(this, isCrossProfileAllowed -> {
182                 setUpProfileButton();
183             });
184         }
185 
186         // Observe for multi-user changes.
187         final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles();
188         if (isMultiUserProfiles != null) {
189             isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners);
190         }
191 
192         final AccessibilityManager accessibilityManager =
193                 context.getSystemService(AccessibilityManager.class);
194         mIsAccessibilityEnabled = accessibilityManager.isEnabled();
195         accessibilityManager.addAccessibilityStateChangeListener(enabled -> {
196             mIsAccessibilityEnabled = enabled;
197             updateProfileButtonVisibility();
198         });
199     }
200 
updateRecyclerViewBottomPadding()201     private void updateRecyclerViewBottomPadding() {
202         final int recyclerViewBottomPadding;
203         if (mIsProfileButtonVisible.getValue() || mIsBottomBarVisible.getValue()) {
204             recyclerViewBottomPadding = mRecyclerViewBottomPadding;
205         } else {
206             recyclerViewBottomPadding = 0;
207         }
208 
209         mRecyclerView.setPadding(0, 0, 0, recyclerViewBottomPadding);
210     }
211 
updateVisibilityAndAnimateBottomBar(int selectedItemListSize)212     private void updateVisibilityAndAnimateBottomBar(int selectedItemListSize) {
213         if (!mSelection.canSelectMultiple()) {
214             return;
215         }
216 
217         if (selectedItemListSize == 0) {
218             if (mBottomBar.getVisibility() == View.VISIBLE) {
219                 mBottomBar.setVisibility(View.GONE);
220                 mBottomBar.startAnimation(mSlideDownAnimation);
221             }
222         } else {
223             if (mBottomBar.getVisibility() == View.GONE) {
224                 mBottomBar.setVisibility(View.VISIBLE);
225                 mBottomBar.startAnimation(mSlideUpAnimation);
226             }
227             mAddButton.setText(generateAddButtonString(getContext(), selectedItemListSize));
228         }
229         mIsBottomBarVisible.setValue(selectedItemListSize > 0);
230     }
231 
setUpListenersForProfileButton()232     private void setUpListenersForProfileButton() {
233         mProfileButton.setOnClickListener(v -> onClickProfileButton());
234         mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
235             @Override
236             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
237                 super.onScrolled(recyclerView, dx, dy);
238 
239                 // Do not change profile button visibility on scroll if Accessibility mode is
240                 // enabled. This is done to enhance button visibility in Accessibility mode.
241                 if (mIsAccessibilityEnabled) {
242                     return;
243                 }
244 
245                 if (dy > 0) {
246                     mProfileButton.hide();
247                 } else {
248                     updateProfileButtonVisibility();
249                 }
250             }
251         });
252     }
253 
254     @Override
onDestroy()255     public void onDestroy() {
256         super.onDestroy();
257         if (mRecyclerView != null) {
258             mRecyclerView.clearOnScrollListeners();
259         }
260     }
261 
setUpProfileButtonWithListeners(boolean isMultiUserProfile)262     private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) {
263         if (isMultiUserProfile) {
264             setUpListenersForProfileButton();
265         } else {
266             mRecyclerView.clearOnScrollListeners();
267         }
268         setUpProfileButton();
269     }
270 
setUpProfileButton()271     private void setUpProfileButton() {
272         updateProfileButtonVisibility();
273         if (!mUserIdManager.isMultiUserProfiles()) {
274             return;
275         }
276 
277         updateProfileButtonContent(mUserIdManager.isManagedUserSelected());
278         updateProfileButtonColor(/* isDisabled */ !mUserIdManager.isCrossProfileAllowed());
279     }
280 
shouldShowProfileButton()281     private boolean shouldShowProfileButton() {
282         return mUserIdManager.isMultiUserProfiles()
283                 && !mHideProfileButton
284                 && !mPickerViewModel.isUserSelectForApp()
285                 && (!mSelection.canSelectMultiple()
286                         || mSelection.getSelectedItemCount().getValue() == 0);
287     }
288 
onClickProfileButton()289     private void onClickProfileButton() {
290         if (!mUserIdManager.isCrossProfileAllowed()) {
291             ProfileDialogFragment.show(getActivity().getSupportFragmentManager());
292         } else {
293             changeProfile();
294         }
295     }
296 
changeProfile()297     private void changeProfile() {
298         if (mUserIdManager.isManagedUserSelected()) {
299             // TODO(b/190024747): Add caching for performance before switching data to and fro
300             // work profile
301             mUserIdManager.setPersonalAsCurrentUserProfile();
302 
303         } else {
304             // TODO(b/190024747): Add caching for performance before switching data to and fro
305             // work profile
306             mUserIdManager.setManagedAsCurrentUserProfile();
307         }
308 
309         updateProfileButtonContent(mUserIdManager.isManagedUserSelected());
310 
311         mPickerViewModel.onUserSwitchedProfile();
312     }
313 
updateProfileButtonContent(boolean isManagedUserSelected)314     private void updateProfileButtonContent(boolean isManagedUserSelected) {
315         final Drawable icon;
316         final String text;
317         if (isManagedUserSelected) {
318             icon = getContext().getDrawable(R.drawable.ic_personal_mode);
319             text = getSwitchToPersonalMessage();
320         } else {
321             icon = getWorkProfileIcon();
322             text = getSwitchToWorkMessage();
323         }
324         mProfileButton.setIcon(icon);
325         mProfileButton.setText(text);
326     }
327 
getSwitchToPersonalMessage()328     private String getSwitchToPersonalMessage() {
329         if (SdkLevel.isAtLeastT()) {
330             return getUpdatedEnterpriseString(
331                     SWITCH_TO_PERSONAL_MESSAGE, R.string.picker_personal_profile);
332         } else {
333             return getContext().getString(R.string.picker_personal_profile);
334         }
335     }
336 
getSwitchToWorkMessage()337     private String getSwitchToWorkMessage() {
338         if (SdkLevel.isAtLeastT()) {
339             return getUpdatedEnterpriseString(
340                     SWITCH_TO_WORK_MESSAGE, R.string.picker_work_profile);
341         } else {
342             return getContext().getString(R.string.picker_work_profile);
343         }
344     }
345 
346     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getUpdatedEnterpriseString(String updatableStringId, int defaultStringId)347     private String getUpdatedEnterpriseString(String updatableStringId, int defaultStringId) {
348         final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
349         return dpm.getResources().getString(updatableStringId, () -> getString(defaultStringId));
350     }
351 
getWorkProfileIcon()352     private Drawable getWorkProfileIcon() {
353         if (SdkLevel.isAtLeastT()) {
354             return getUpdatedWorkProfileIcon();
355         } else {
356             return getContext().getDrawable(R.drawable.ic_work_outline);
357         }
358     }
359 
360     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
getUpdatedWorkProfileIcon()361     private Drawable getUpdatedWorkProfileIcon() {
362         DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
363         return dpm.getResources().getDrawable(WORK_PROFILE_ICON, OUTLINE, () ->
364                 getContext().getDrawable(R.drawable.ic_work_outline));
365     }
366 
updateProfileButtonColor(boolean isDisabled)367     private void updateProfileButtonColor(boolean isDisabled) {
368         final int textAndIconColor =
369                 isDisabled ? mButtonDisabledIconAndTextColor : mButtonIconAndTextColor;
370         final int backgroundTintColor =
371                 isDisabled ? mButtonDisabledBackgroundColor : mButtonBackgroundColor;
372 
373         mProfileButton.setTextColor(ColorStateList.valueOf(textAndIconColor));
374         mProfileButton.setIconTint(ColorStateList.valueOf(textAndIconColor));
375         mProfileButton.setBackgroundTintList(ColorStateList.valueOf(backgroundTintColor));
376     }
377 
hideProfileButton(boolean hide)378     protected void hideProfileButton(boolean hide) {
379         mHideProfileButton = hide;
380         updateProfileButtonVisibility();
381     }
382 
updateProfileButtonVisibility()383     private void updateProfileButtonVisibility() {
384         final boolean shouldShowProfileButton = shouldShowProfileButton();
385         if (shouldShowProfileButton) {
386             mProfileButton.show();
387         } else {
388             mProfileButton.hide();
389         }
390         mIsProfileButtonVisible.setValue(shouldShowProfileButton);
391     }
392 
setEmptyMessage(int resId)393     protected void setEmptyMessage(int resId) {
394         mEmptyTextView.setText(resId);
395     }
396 
397     /**
398      * If we show the {@link #mEmptyView}, hide the {@link #mRecyclerView}. If we don't hide the
399      * {@link #mEmptyView}, show the {@link #mRecyclerView}
400      */
updateVisibilityForEmptyView(boolean shouldShowEmptyView)401     protected void updateVisibilityForEmptyView(boolean shouldShowEmptyView) {
402         mEmptyView.setVisibility(shouldShowEmptyView ? View.VISIBLE : View.GONE);
403         mRecyclerView.setVisibility(shouldShowEmptyView ? View.GONE : View.VISIBLE);
404     }
405 
406     /**
407      * Generates the Button Label for the {@link TabFragment#mAddButton}.
408      *
409      * @param context The current application context.
410      * @param size The current size of the selection.
411      * @return Localized, formatted string.
412      */
generateAddButtonString(Context context, int size)413     private String generateAddButtonString(Context context, int size) {
414         final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size);
415         final String template =
416                 mPickerViewModel.isUserSelectForApp()
417                         ? context.getString(R.string.picker_add_button_multi_select_permissions)
418                         : context.getString(R.string.picker_add_button_multi_select);
419 
420         return TextUtils.expandTemplate(template, sizeString).toString();
421     }
422 
getPickerActivity()423     protected final PhotoPickerActivity getPickerActivity() {
424         return (PhotoPickerActivity) getActivity();
425     }
426 
setLayoutManager(@onNull TabAdapter adapter, int spanCount)427     protected final void setLayoutManager(@NonNull TabAdapter adapter, int spanCount) {
428         final GridLayoutManager layoutManager =
429                 new GridLayoutManager(getContext(), spanCount);
430         final GridLayoutManager.SpanSizeLookup lookup = new GridLayoutManager.SpanSizeLookup() {
431             @Override
432             public int getSpanSize(int position) {
433                 final int itemViewType = adapter.getItemViewType(position);
434                 // For the item view types ITEM_TYPE_BANNER and ITEM_TYPE_SECTION, it is full
435                 // span, return the span count of the layoutManager.
436                 if (itemViewType == ITEM_TYPE_BANNER || itemViewType == ITEM_TYPE_SECTION) {
437                     return layoutManager.getSpanCount();
438                 } else {
439                     return 1;
440                 }
441             }
442         };
443         layoutManager.setSpanSizeLookup(lookup);
444         mRecyclerView.setLayoutManager(layoutManager);
445     }
446 
447     private abstract class OnBannerEventListener implements TabAdapter.OnBannerEventListener {
448         @Override
onActionButtonClick()449         public void onActionButtonClick() {
450             dismissBanner();
451             getPickerActivity().startSettingsActivity();
452         }
453 
454         @Override
onDismissButtonClick()455         public void onDismissButtonClick() {
456             dismissBanner();
457         }
458 
459         @Override
onBannerAdded()460         public void onBannerAdded() {
461             // Should scroll to the banner only if the first completely visible item is the one
462             // just below it. The possible adapter item positions of such an item are 0 and 1.
463             // During onViewCreated, before restoring the state, the first visible item position
464             // is -1, and we should not scroll to position 0 in such cases, else the previously
465             // saved recycler view position may get overridden.
466             int firstItemPosition = -1;
467 
468             final RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
469             if (layoutManager instanceof GridLayoutManager) {
470                 firstItemPosition = ((GridLayoutManager) layoutManager)
471                         .findFirstCompletelyVisibleItemPosition();
472             }
473 
474             if (firstItemPosition == 0 || firstItemPosition == 1) {
475                 mRecyclerView.scrollToPosition(/* position */ 0);
476             }
477         }
478 
dismissBanner()479         abstract void dismissBanner();
480     }
481 
482     protected final OnBannerEventListener mOnChooseAppBannerEventListener =
483             new OnBannerEventListener() {
484                 @Override
485                 void dismissBanner() {
486                     mPickerViewModel.onUserDismissedChooseAppBanner();
487                 }
488             };
489 
490     protected final OnBannerEventListener mOnCloudMediaAvailableBannerEventListener =
491             new OnBannerEventListener() {
492                 @Override
493                 void dismissBanner() {
494                     mPickerViewModel.onUserDismissedCloudMediaAvailableBanner();
495                 }
496             };
497 
498     protected final OnBannerEventListener mOnAccountUpdatedBannerEventListener =
499             new OnBannerEventListener() {
500                 @Override
501                 void dismissBanner() {
502                     mPickerViewModel.onUserDismissedAccountUpdatedBanner();
503                 }
504             };
505 
506     protected final OnBannerEventListener mOnChooseAccountBannerEventListener =
507             new OnBannerEventListener() {
508                 @Override
509                 void dismissBanner() {
510                     mPickerViewModel.onUserDismissedChooseAccountBanner();
511                 }
512             };
513 }
514