1 /*
2  * Copyright (C) 2022 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.util.MimeUtils.isVideoMimeType;
19 
20 import android.graphics.Color;
21 import android.graphics.drawable.GradientDrawable;
22 import android.graphics.drawable.LayerDrawable;
23 import android.os.Bundle;
24 import android.provider.MediaStore;
25 import android.util.Log;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.core.content.ContextCompat;
33 import androidx.fragment.app.Fragment;
34 import androidx.fragment.app.FragmentManager;
35 import androidx.fragment.app.FragmentTransaction;
36 import androidx.lifecycle.ViewModelProvider;
37 import androidx.recyclerview.widget.RecyclerView;
38 import androidx.viewpager2.widget.CompositePageTransformer;
39 import androidx.viewpager2.widget.ViewPager2;
40 
41 import com.android.providers.media.R;
42 import com.android.providers.media.photopicker.util.AccentColorResources;
43 import com.android.providers.media.photopicker.util.MimeFilterUtils;
44 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
45 
46 import com.google.android.material.bottomsheet.BottomSheetBehavior;
47 import com.google.android.material.tabs.TabLayout;
48 import com.google.android.material.tabs.TabLayoutMediator;
49 
50 import java.lang.ref.WeakReference;
51 import java.lang.reflect.Field;
52 
53 /**
54  * The tab container fragment
55  */
56 public class TabContainerFragment extends Fragment {
57     private static final String TAG = "TabContainerFragment";
58     private static final int PHOTOS_TAB_POSITION = 0;
59     private static final int ALBUMS_TAB_POSITION = 1;
60 
61     private TabContainerAdapter mTabContainerAdapter;
62     private TabLayoutMediator mTabLayoutMediator;
63     private ViewPager2 mViewPager;
64     private PickerViewModel mPickerViewModel;
65 
66     @Override
67     @NonNull
onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)68     public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
69             Bundle savedInstanceState) {
70         super.onCreateView(inflater, container, savedInstanceState);
71         return inflater.inflate(R.layout.fragment_picker_tab_container, container, false);
72     }
73 
74     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)75     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
76         super.onViewCreated(view, savedInstanceState);
77         mViewPager = view.findViewById(R.id.picker_tab_viewpager);
78         final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity());
79         mPickerViewModel = viewModelProvider.get(PickerViewModel.class);
80         mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this);
81         mViewPager.setAdapter(mTabContainerAdapter);
82 
83         // Launch in albums tab if the app requests so
84         if (mPickerViewModel.getPickerLaunchTab() == MediaStore.PICK_IMAGES_TAB_ALBUMS) {
85             // Launch the picker in Albums tab without any switch animation
86             mViewPager.setCurrentItem(ALBUMS_TAB_POSITION, /* smoothScroll */ false);
87         }
88 
89         // If the ViewPager2 has more than one page with BottomSheetBehavior, the scrolled view
90         // (e.g. RecyclerView) on the second page can't be scrolled. The workaround is to update
91         // nestedScrollingChildRef to the scrolled view on the current page. b/145334244
92         Field fieldNestedScrollingChildRef = null;
93         try {
94             fieldNestedScrollingChildRef = BottomSheetBehavior.class.getDeclaredField(
95                     "nestedScrollingChildRef");
96             fieldNestedScrollingChildRef.setAccessible(true);
97         } catch (NoSuchFieldException ex) {
98             Log.d(TAG, "Can't get the field nestedScrollingChildRef from BottomSheetBehavior", ex);
99         }
100 
101         final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(
102                 getActivity().findViewById(R.id.bottom_sheet));
103 
104         final CompositePageTransformer compositePageTransformer = new CompositePageTransformer();
105         mViewPager.setPageTransformer(compositePageTransformer);
106         compositePageTransformer.addTransformer(new AnimationPageTransformer());
107         compositePageTransformer.addTransformer(
108                 new NestedScrollPageTransformer(bottomSheetBehavior, fieldNestedScrollingChildRef));
109 
110         // The BottomSheetBehavior looks for the first nested scrolling child to determine how to
111         // handle nested scrolls, it finds the inner recyclerView on ViewPager2 in this case. So, we
112         // need to work around it by setNestedScrollingEnabled false. b/145351873
113         final View firstChild = mViewPager.getChildAt(0);
114         if (firstChild instanceof RecyclerView) {
115             mViewPager.getChildAt(0).setNestedScrollingEnabled(false);
116         }
117 
118         final TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
119 
120         if (mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet()) {
121             tabLayout.setBackgroundColor(
122                     mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
123                     AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT,
124                     AccentColorResources.SURFACE_CONTAINER_COLOR_DARK));
125             LayerDrawable pickerTabDrawable = (LayerDrawable) ContextCompat.getDrawable(
126                     getActivity(), R.drawable.picker_tab_background);
127             GradientDrawable pickerTab =
128                     (GradientDrawable) pickerTabDrawable.findDrawableByLayerId(R.id.picker_tab);
129             pickerTab.setColor(mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
130                     AccentColorResources.SURFACE_CONTAINER_HIGHEST_LIGHT,
131                     AccentColorResources.SURFACE_CONTAINER_HIGHEST_DARK));
132             tabLayout.setSelectedTabIndicatorColor(
133                     mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor());
134             setTabTextColors(tabLayout);
135         }
136 
137         mTabLayoutMediator = new TabLayoutMediator(tabLayout, mViewPager, (tab, pos) -> {
138             if (pos == PHOTOS_TAB_POSITION) {
139                 if (isOnlyVideoMimeTypeFilterAvailable()) {
140                     tab.setText(R.string.picker_videos);
141                 } else {
142                     tab.setText(R.string.picker_photos);
143                 }
144             } else if (pos == ALBUMS_TAB_POSITION) {
145                 tab.setText(R.string.picker_albums);
146             }
147         });
148 
149         mTabLayoutMediator.attach();
150         // TabLayout only supports colorDrawable in xml. And if we set the color in the drawable by
151         // setSelectedTabIndicator method, it doesn't apply the color. So, we set color in xml and
152         // set the drawable for the shape here.
153         tabLayout.setSelectedTabIndicator(R.drawable.picker_tab_indicator);
154         tabLayout.addOnTabSelectedListener(mOnTabSelectedListener);
155     }
156 
setTabTextColors(TabLayout tabLayout)157     private void setTabTextColors(TabLayout tabLayout) {
158         String selectedTabTextColor =
159                 mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright()
160                         ? AccentColorResources.DARK_TEXT_COLOR
161                         : AccentColorResources.LIGHT_TEXT_COLOR;
162         // Unselected tab text color in dark mode will be white and vice versa
163         tabLayout.setTabTextColors(
164                 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor(
165                         AccentColorResources.DARK_TEXT_COLOR,
166                         AccentColorResources.LIGHT_TEXT_COLOR),
167                 Color.parseColor(selectedTabTextColor));
168     }
169 
isOnlyVideoMimeTypeFilterAvailable()170     private boolean isOnlyVideoMimeTypeFilterAvailable() {
171         String [] mimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(getActivity().getIntent());
172         boolean hasVideoMimeTypeFilterOnly = false;
173         if (mimeTypeFilters != null && mimeTypeFilters.length > 0) {
174             for (String mimeTypeFilter : mimeTypeFilters) {
175                 if (isVideoMimeType(mimeTypeFilter)) {
176                     hasVideoMimeTypeFilterOnly = true;
177                 } else {
178                     hasVideoMimeTypeFilterOnly = false;
179                     break;
180                 }
181             }
182         }
183         return hasVideoMimeTypeFilterOnly;
184     }
185 
186     @Override
onDestroyView()187     public void onDestroyView() {
188         mTabLayoutMediator.detach();
189         super.onDestroyView();
190     }
191 
192     /**
193      * Create the fragment and add it into the FragmentManager
194      *
195      * @param fm the fragment manager
196      */
show(FragmentManager fm)197     public static void show(FragmentManager fm) {
198         final FragmentTransaction ft = fm.beginTransaction();
199         final TabContainerFragment fragment = new TabContainerFragment();
200         ft.replace(R.id.fragment_container, fragment, TAG);
201         ft.commitAllowingStateLoss();
202     }
203 
204     private final TabLayout.OnTabSelectedListener mOnTabSelectedListener =
205             new TabLayout.OnTabSelectedListener() {
206                 @Override
207                 public void onTabSelected(TabLayout.Tab tab) {
208                     int position = tab.getPosition();
209                     if (position == PHOTOS_TAB_POSITION) {
210                         mPickerViewModel.logSwitchToPhotosTab();
211                     } else if (position == ALBUMS_TAB_POSITION) {
212                         mPickerViewModel.logSwitchToAlbumsTab();
213                     }
214                 }
215 
216                 @Override
217                 public void onTabUnselected(TabLayout.Tab tab) {
218                     // No=op
219                 }
220 
221                 @Override
222                 public void onTabReselected(TabLayout.Tab tab) {
223                     // No-op
224                 }
225             };
226 
227     private static class AnimationPageTransformer implements ViewPager2.PageTransformer {
228 
229         @Override
transformPage(@onNull View view, float pos)230         public void transformPage(@NonNull View view, float pos) {
231             view.setAlpha(1.0f - Math.abs(pos));
232         }
233     }
234 
235     private static class NestedScrollPageTransformer implements ViewPager2.PageTransformer {
236         private Field mFieldNestedScrollingChildRef;
237         private BottomSheetBehavior mBottomSheetBehavior;
238 
NestedScrollPageTransformer(BottomSheetBehavior bottomSheetBehavior, Field field)239         public NestedScrollPageTransformer(BottomSheetBehavior bottomSheetBehavior, Field field) {
240             mBottomSheetBehavior = bottomSheetBehavior;
241             mFieldNestedScrollingChildRef = field;
242         }
243 
244         @Override
transformPage(@onNull View view, float pos)245         public void transformPage(@NonNull View view, float pos) {
246             // If pos != 0, it is not in current page, don't update the nested scrolling child
247             // reference.
248             if (pos != 0 || mFieldNestedScrollingChildRef == null) {
249                 return;
250             }
251 
252             try {
253                 final View childView = view.findViewById(R.id.picker_tab_recyclerview);
254                 if (childView != null) {
255                     mFieldNestedScrollingChildRef.set(mBottomSheetBehavior,
256                             new WeakReference(childView));
257                 }
258             } catch (IllegalAccessException ex) {
259                 Log.d(TAG, "Set nestedScrollingChildRef to BottomSheetBehavior fail", ex);
260             }
261         }
262     }
263 }
264