• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.photopicker.ui;
18 
19 import android.content.Context;
20 import android.content.res.Configuration;
21 import android.graphics.Color;
22 import android.os.Bundle;
23 import android.text.TextUtils;
24 import android.util.Log;
25 import android.view.LayoutInflater;
26 import android.view.Menu;
27 import android.view.MenuInflater;
28 import android.view.MenuItem;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.Button;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.fragment.app.Fragment;
36 import androidx.fragment.app.FragmentManager;
37 import androidx.lifecycle.ViewModelProvider;
38 import androidx.viewpager2.widget.ViewPager2;
39 
40 import com.android.providers.media.R;
41 import com.android.providers.media.photopicker.PhotoPickerActivity;
42 import com.android.providers.media.photopicker.data.MuteStatus;
43 import com.android.providers.media.photopicker.data.Selection;
44 import com.android.providers.media.photopicker.data.model.Item;
45 import com.android.providers.media.photopicker.util.LayoutModeUtils;
46 import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
47 
48 import java.text.NumberFormat;
49 import java.util.List;
50 import java.util.Locale;
51 
52 /**
53  * Displays a selected items in one up view. Supports deselecting items.
54  */
55 public class PreviewFragment extends Fragment {
56     private static String TAG = "PreviewFragment";
57 
58     private static final String PREVIEW_TYPE = "preview_type";
59     private static final int PREVIEW_ON_LONG_PRESS = 1;
60     private static final int PREVIEW_ON_VIEW_SELECTED = 2;
61 
62     private static final Bundle sPreviewOnLongPressArgs = new Bundle();
63     static {
sPreviewOnLongPressArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_LONG_PRESS)64         sPreviewOnLongPressArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_LONG_PRESS);
65     }
66     private static final Bundle sPreviewOnViewSelectedArgs = new Bundle();
67     static {
sPreviewOnViewSelectedArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_VIEW_SELECTED)68         sPreviewOnViewSelectedArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_VIEW_SELECTED);
69     }
70 
71     private Selection mSelection;
72     private PickerViewModel mPickerViewModel;
73     private ViewPager2Wrapper mViewPager2Wrapper;
74     private boolean mShouldShowGifBadge;
75     private boolean mShouldShowMotionPhotoBadge;
76     private MuteStatus mMuteStatus;
77 
78     @Override
onCreate(Bundle savedInstanceState)79     public void onCreate(Bundle savedInstanceState) {
80         super.onCreate(savedInstanceState);
81         // Register with the activity to inform the system that the app bar fragment is
82         // participating in the population of the options menu
83         setHasOptionsMenu(true);
84     }
85 
86     @Override
onCreateOptionsMenu(@onNull Menu menu, @NonNull MenuInflater inflater)87     public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
88         inflater.inflate(R.menu.picker_preview_menu, menu);
89     }
90 
91     @Override
onPrepareOptionsMenu(@onNull Menu menu)92     public void onPrepareOptionsMenu(@NonNull Menu menu) {
93         super.onPrepareOptionsMenu(menu);
94         // All logic to hide/show an item in the menu must be in this method
95         final MenuItem gifItem = menu.findItem(R.id.preview_gif);
96         final MenuItem motionPhotoItem = menu.findItem(R.id.preview_motion_photo);
97         gifItem.setVisible(mShouldShowGifBadge);
98         motionPhotoItem.setVisible(mShouldShowMotionPhotoBadge);
99     }
100 
101     @Override
onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)102     public View onCreateView(LayoutInflater inflater, ViewGroup parent,
103             Bundle savedInstanceState) {
104         mPickerViewModel = new ViewModelProvider(requireActivity()).get(PickerViewModel.class);
105         mSelection = mPickerViewModel.getSelection();
106         mMuteStatus = mPickerViewModel.getMuteStatus();
107         return inflater.inflate(R.layout.fragment_preview, parent, /* attachToRoot */ false);
108     }
109 
110     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)111     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
112         // Set the pane title for A11y.
113         view.setAccessibilityPaneTitle(getString(R.string.picker_preview));
114         final List<Item> selectedItemsList = mSelection.getSelectedItemsForPreview();
115         final int selectedItemsListSize = selectedItemsList.size();
116 
117         if (selectedItemsListSize <= 0) {
118             // This can happen if we lost PickerViewModel to optimize memory.
119             Log.e(TAG, "No items to preview. Returning back to photo grid");
120             requireActivity().getSupportFragmentManager().popBackStack();
121         } else if (selectedItemsListSize > 1 && !mSelection.canSelectMultiple()) {
122             // This should never happen
123             throw new IllegalStateException("Found more than one preview items in single select"
124                     + " mode. Selected items count: " + selectedItemsListSize);
125         }
126 
127         // Initialize ViewPager2 to swipe between multiple pictures/videos in preview
128         final ViewPager2 viewPager = view.findViewById(R.id.preview_viewPager);
129         if (viewPager == null) {
130             throw new IllegalStateException("Expected to find ViewPager2 in " + view
131                     + ", but found null");
132         }
133         mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList, mMuteStatus);
134 
135         setUpPreviewLayout(view, getArguments());
136         setupScrimLayerAndBottomBar(view);
137     }
138 
setupScrimLayerAndBottomBar(View fragmentView)139     private void setupScrimLayerAndBottomBar(View fragmentView) {
140         final boolean isLandscape = getResources().getConfiguration().orientation
141                 == Configuration.ORIENTATION_LANDSCAPE;
142 
143         // Show the scrim layers in Landscape mode. The default visibility is GONE.
144         if (isLandscape) {
145             final View topScrim = fragmentView.findViewById(R.id.preview_top_scrim);
146             topScrim.setVisibility(View.VISIBLE);
147 
148             final View bottomScrim = fragmentView.findViewById(R.id.preview_bottom_scrim);
149             bottomScrim.setVisibility(View.VISIBLE);
150         }
151 
152         // Set appropriate background color for the bottom bar
153         final int bottomBarColor;
154         if (isLandscape) {
155             bottomBarColor = Color.TRANSPARENT;
156         } else {
157             bottomBarColor = getContext().getColor(R.color.preview_scrim_solid_color);
158         }
159         final View bottomBar = fragmentView.findViewById(R.id.preview_bottom_bar);
160         bottomBar.setBackgroundColor(bottomBarColor);
161     }
162 
setUpPreviewLayout(@onNull View view, @Nullable Bundle args)163     private void setUpPreviewLayout(@NonNull View view, @Nullable Bundle args) {
164         if (args == null) {
165             // We are willing to crash PhotoPickerActivity because this error might only happen
166             // during development.
167             throw new IllegalArgumentException("Can't determine the type of the Preview, arguments"
168                     + " is not set");
169         }
170 
171         final int previewType = args.getInt(PREVIEW_TYPE, -1);
172         if (previewType == PREVIEW_ON_LONG_PRESS) {
173             setUpPreviewLayoutForLongPress(view);
174         } else if (previewType == PREVIEW_ON_VIEW_SELECTED) {
175             setUpPreviewLayoutForViewSelected(view);
176         } else {
177             // We are willing to crash PhotoPickerActivity because this error might only happen
178             // during development.
179             throw new IllegalArgumentException("No preview type specified");
180         }
181     }
182 
183     /**
184      * Adjusts the select/add button layout for preview on LongPress
185      */
setUpPreviewLayoutForLongPress(@onNull View view)186     private void setUpPreviewLayoutForLongPress(@NonNull View view) {
187         final Button addOrSelectButton = view.findViewById(R.id.preview_add_or_select_button);
188 
189         // Preview on Long Press will reuse AddOrSelect button as
190         // * Add button - Button with text "Add" - for single select mode
191         // * Select button - Button with text "Select"/"Deselect" based on the selection state of
192         //                   the item - for multi select mode
193         if (!mSelection.canSelectMultiple()) {
194             // On clicking add button we return the picker result to calling app.
195             // This destroys PickerActivity and all fragments.
196             addOrSelectButton.setOnClickListener(v -> {
197                 ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
198             });
199         } else {
200             // For preview on long press, we always preview only one item.
201             // Selection#getSelectedItemsForPreview is guaranteed to return only one item. Hence,
202             // we can always use position=0 as current position.
203             updateSelectButtonText(addOrSelectButton,
204                     mSelection.isItemSelected(mViewPager2Wrapper.getItemAt(/* position */ 0)));
205             addOrSelectButton.setOnClickListener(v -> onClickSelectButton(addOrSelectButton));
206         }
207 
208         // Set the appropriate special format icon based on the item in the preview
209         updateSpecialFormatIcon(mViewPager2Wrapper.getItemAt(/* position */ 0));
210     }
211 
212     /**
213      * Adjusts the layout based on Multi select and adds appropriate onClick listeners
214      */
setUpPreviewLayoutForViewSelected(@onNull View view)215     private void setUpPreviewLayoutForViewSelected(@NonNull View view) {
216         // Hide addOrSelect button of long press, we have a separate add button for view selected
217         final Button addOrSelectButton = view.findViewById(R.id.preview_add_or_select_button);
218         addOrSelectButton.setVisibility(View.GONE);
219 
220         final Button viewSelectedAddButton = view.findViewById(R.id.preview_add_button);
221         viewSelectedAddButton.setVisibility(View.VISIBLE);
222         // On clicking add button we return the picker result to calling app.
223         // This destroys PickerActivity and all fragments.
224         viewSelectedAddButton.setOnClickListener(v -> {
225             ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf();
226         });
227 
228         final Button selectedCheckButton = view.findViewById(R.id.preview_selected_check_button);
229         selectedCheckButton.setVisibility(View.VISIBLE);
230         // Update the select icon and text according to the state of selection while swiping
231         // between photos
232         mViewPager2Wrapper.addOnPageChangeCallback(new OnPageChangeCallback(selectedCheckButton));
233 
234         // Update add button text to include number of items selected.
235         mSelection
236                 .getSelectedItemCount()
237                 .observe(
238                         this,
239                         selectedItemCount -> {
240                             viewSelectedAddButton.setText(
241                                     generateAddButtonString(
242                                             /* context= */ getContext(),
243                                             /* size= */ selectedItemCount,
244                                             /* isUserSelectForApp= */ mPickerViewModel
245                                                     .isUserSelectForApp()));
246                         });
247 
248         selectedCheckButton.setOnClickListener(
249                 v -> onClickSelectedCheckButton(selectedCheckButton));
250     }
251 
252     @Override
onResume()253     public void onResume() {
254         super.onResume();
255 
256         ((PhotoPickerActivity) getActivity()).updateCommonLayouts(LayoutModeUtils.MODE_PREVIEW,
257                 /* title */"");
258     }
259 
260     @Override
onStop()261     public void onStop() {
262         super.onStop();
263 
264         if (mViewPager2Wrapper != null) {
265             mViewPager2Wrapper.onStop();
266         }
267     }
268 
269     @Override
onStart()270     public void onStart() {
271         super.onStart();
272 
273         if (mViewPager2Wrapper != null) {
274             mViewPager2Wrapper.onStart();
275         }
276     }
277 
278     @Override
onDestroy()279     public void onDestroy() {
280         super.onDestroy();
281         if (mViewPager2Wrapper != null) {
282             mViewPager2Wrapper.onDestroy();
283         }
284     }
285 
onClickSelectButton(@onNull Button selectButton)286     private void onClickSelectButton(@NonNull Button selectButton) {
287         final boolean isSelectedNow = updateSelectionAndGetState();
288         updateSelectButtonText(selectButton, isSelectedNow);
289     }
290 
onClickSelectedCheckButton(@onNull Button selectedCheckButton)291     private void onClickSelectedCheckButton(@NonNull Button selectedCheckButton) {
292         final boolean isSelectedNow = updateSelectionAndGetState();
293         updateSelectedCheckButtonStateAndText(selectedCheckButton, isSelectedNow);
294     }
295 
updateSelectionAndGetState()296     private boolean updateSelectionAndGetState() {
297         final Item currentItem = mViewPager2Wrapper.getCurrentItem();
298         final boolean wasSelectedBefore = mSelection.isItemSelected(currentItem);
299 
300         if (wasSelectedBefore) {
301             // If the item is previously selected, current user action is to deselect the item
302             mSelection.removeSelectedItem(currentItem);
303         } else {
304             // If the item is not previously selected, current user action is to select the item
305             mSelection.addSelectedItem(currentItem);
306         }
307 
308         // After the user has clicked the button, current state of the button should be opposite of
309         // the previous state.
310         // If the previous state was to "Select" the item, and user clicks "Select" button,
311         // wasSelectedBefore = false. And item will be added to selected items. Now, user can only
312         // deselect the item. Hence, isSelectedNow is opposite of previous state,
313         // i.e., isSelectedNow = true.
314         return !wasSelectedBefore;
315     }
316 
317     private class OnPageChangeCallback extends ViewPager2.OnPageChangeCallback {
318         private final Button mSelectedCheckButton;
319 
OnPageChangeCallback(@onNull Button selectedCheckButton)320         public OnPageChangeCallback(@NonNull Button selectedCheckButton) {
321             mSelectedCheckButton = selectedCheckButton;
322         }
323 
324         @Override
onPageSelected(int position)325         public void onPageSelected(int position) {
326             // No action to take as we don't have deselect view here.
327             if (!mSelection.canSelectMultiple()) return;
328 
329             final Item item = mViewPager2Wrapper.getItemAt(position);
330             // Set the appropriate select/deselect state for each item in each page based on the
331             // selection list.
332             updateSelectedCheckButtonStateAndText(mSelectedCheckButton,
333                     mSelection.isItemSelected(item));
334 
335             // Set the appropriate special format icon based on the item in the preview
336             updateSpecialFormatIcon(item);
337         }
338     }
339 
updateSelectButtonText(@onNull Button selectButton, boolean isSelected)340     private static void updateSelectButtonText(@NonNull Button selectButton,
341             boolean isSelected) {
342         selectButton.setText(isSelected ? R.string.deselect : R.string.select);
343     }
344 
updateSelectedCheckButtonStateAndText(@onNull Button selectedCheckButton, boolean isSelected)345     private static void updateSelectedCheckButtonStateAndText(@NonNull Button selectedCheckButton,
346             boolean isSelected) {
347         selectedCheckButton.setText(isSelected ? R.string.selected : R.string.deselected);
348         selectedCheckButton.setSelected(isSelected);
349     }
350 
updateSpecialFormatIcon(Item item)351     private void updateSpecialFormatIcon(Item item) {
352         mShouldShowGifBadge = item.isGifOrAnimatedWebp();
353         mShouldShowMotionPhotoBadge = item.isMotionPhoto();
354         // Invalidating options menu calls onPrepareOptionsMenu() where the logic for
355         // hiding/showing menu items is placed.
356         requireActivity().invalidateOptionsMenu();
357     }
358 
show(@onNull FragmentManager fm, @NonNull Bundle args)359     public static void show(@NonNull FragmentManager fm, @NonNull Bundle args) {
360         if (fm.isStateSaved()) {
361             Log.d(TAG, "Skip show preview fragment because state saved");
362             return;
363         }
364 
365         final PreviewFragment fragment = new PreviewFragment();
366         fragment.setArguments(args);
367         fm.beginTransaction()
368                 .replace(R.id.fragment_container, fragment, TAG)
369                 .addToBackStack(TAG)
370                 .commitAllowingStateLoss();
371     }
372 
373     /**
374      * Get the fragment in the FragmentManager
375      * @param fm the fragment manager
376      */
get(@onNull FragmentManager fm)377     public static Fragment get(@NonNull FragmentManager fm) {
378         return fm.findFragmentByTag(TAG);
379     }
380 
getArgsForPreviewOnLongPress()381     public static Bundle getArgsForPreviewOnLongPress() {
382         return sPreviewOnLongPressArgs;
383     }
384 
getArgsForPreviewOnViewSelected()385     public static Bundle getArgsForPreviewOnViewSelected() {
386         return sPreviewOnViewSelectedArgs;
387     }
388 
389     // TODO: There is a same method in TabFragment. To find a way to reuse it.
generateAddButtonString( @onNull Context context, int size, boolean isUserSelectForApp)390     private static String generateAddButtonString(
391             @NonNull Context context, int size, boolean isUserSelectForApp) {
392         final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size);
393         final String template =
394                 isUserSelectForApp
395                         ? context.getString(R.string.picker_add_button_multi_select_permissions)
396                         : context.getString(R.string.picker_add_button_multi_select);
397         return TextUtils.expandTemplate(template, sizeString).toString();
398     }
399 }
400