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