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.ColorStateList; 21 import android.content.res.Configuration; 22 import android.graphics.Color; 23 import android.os.Bundle; 24 import android.text.TextUtils; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.Menu; 28 import android.view.MenuInflater; 29 import android.view.MenuItem; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.Button; 33 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 import androidx.fragment.app.Fragment; 37 import androidx.fragment.app.FragmentManager; 38 import androidx.lifecycle.ViewModelProvider; 39 import androidx.viewpager2.widget.ViewPager2; 40 41 import com.android.providers.media.R; 42 import com.android.providers.media.photopicker.PhotoPickerActivity; 43 import com.android.providers.media.photopicker.data.MuteStatus; 44 import com.android.providers.media.photopicker.data.Selection; 45 import com.android.providers.media.photopicker.data.model.Item; 46 import com.android.providers.media.photopicker.util.AccentColorResources; 47 import com.android.providers.media.photopicker.util.LayoutModeUtils; 48 import com.android.providers.media.photopicker.viewmodel.PickerViewModel; 49 50 import java.text.NumberFormat; 51 import java.util.List; 52 import java.util.Locale; 53 import java.util.Objects; 54 55 /** 56 * Displays a selected items in one up view. Supports deselecting items. 57 */ 58 public class PreviewFragment extends Fragment { 59 private static String TAG = "PreviewFragment"; 60 61 private static final String PREVIEW_TYPE = "preview_type"; 62 private static final int PREVIEW_ON_LONG_PRESS = 1; 63 private static final int PREVIEW_ON_VIEW_SELECTED = 2; 64 65 private static final Bundle sPreviewOnLongPressArgs = new Bundle(); 66 static { sPreviewOnLongPressArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_LONG_PRESS)67 sPreviewOnLongPressArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_LONG_PRESS); 68 } 69 private static final Bundle sPreviewOnViewSelectedArgs = new Bundle(); 70 static { sPreviewOnViewSelectedArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_VIEW_SELECTED)71 sPreviewOnViewSelectedArgs.putInt(PREVIEW_TYPE, PREVIEW_ON_VIEW_SELECTED); 72 } 73 74 private Selection mSelection; 75 private PickerViewModel mPickerViewModel; 76 private ViewPager2Wrapper mViewPager2Wrapper; 77 private boolean mShouldShowGifBadge; 78 private boolean mShouldShowMotionPhotoBadge; 79 private MuteStatus mMuteStatus; 80 private boolean mIsCustomPickerColorSet = false; 81 82 @Override onCreate(Bundle savedInstanceState)83 public void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 // Register with the activity to inform the system that the app bar fragment is 86 // participating in the population of the options menu 87 setHasOptionsMenu(true); 88 } 89 90 @Override onCreateOptionsMenu(@onNull Menu menu, @NonNull MenuInflater inflater)91 public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { 92 inflater.inflate(R.menu.picker_preview_menu, menu); 93 } 94 95 @Override onPrepareOptionsMenu(@onNull Menu menu)96 public void onPrepareOptionsMenu(@NonNull Menu menu) { 97 super.onPrepareOptionsMenu(menu); 98 // All logic to hide/show an item in the menu must be in this method 99 final MenuItem gifItem = menu.findItem(R.id.preview_gif); 100 final MenuItem motionPhotoItem = menu.findItem(R.id.preview_motion_photo); 101 gifItem.setVisible(mShouldShowGifBadge); 102 motionPhotoItem.setVisible(mShouldShowMotionPhotoBadge); 103 } 104 105 @Override onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState)106 public View onCreateView(LayoutInflater inflater, ViewGroup parent, 107 Bundle savedInstanceState) { 108 mPickerViewModel = new ViewModelProvider(requireActivity()).get(PickerViewModel.class); 109 mIsCustomPickerColorSet = 110 mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet(); 111 mSelection = mPickerViewModel.getSelection(); 112 mMuteStatus = mPickerViewModel.getMuteStatus(); 113 return inflater.inflate(R.layout.fragment_preview, parent, /* attachToRoot */ false); 114 } 115 116 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)117 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 118 // Set the pane title for A11y. 119 view.setAccessibilityPaneTitle(getString(R.string.picker_preview)); 120 final List<Item> selectedItemsList = mSelection.getSelectedItemsForPreview(); 121 final int selectedItemsListSize = selectedItemsList.size(); 122 123 if (selectedItemsListSize > 1 && !mSelection.canSelectMultiple()) { 124 // This should never happen 125 throw new IllegalStateException("Found more than one preview items in single select" 126 + " mode. Selected items count: " + selectedItemsListSize); 127 } 128 129 // Initialize ViewPager2 to swipe between multiple pictures/videos in preview 130 final ViewPager2 viewPager = view.findViewById(R.id.preview_viewPager); 131 if (viewPager == null) { 132 throw new IllegalStateException("Expected to find ViewPager2 in " + view 133 + ", but found null"); 134 } 135 mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList, mMuteStatus, 136 mOnCreateSurfaceController, mPickerViewModel::logVideoPreviewMuteButtonClick); 137 138 setUpPreviewLayout(view, getArguments()); 139 setupScrimLayerAndBottomBar(view); 140 // Don't add any code post this line. The lazy loading setup should be the last thing we do 141 // to avoid the UI getting overwritten. 142 setUpUIForLazyLoading(view, selectedItemsListSize); 143 } 144 setUpUIForLazyLoading(View view, int selectedItemsListSize)145 private void setUpUIForLazyLoading(View view, int selectedItemsListSize) { 146 final Button selectedCheckButton = view.findViewById(R.id.preview_selected_check_button); 147 Objects.requireNonNull(selectedCheckButton); 148 if (selectedItemsListSize == 0) { 149 // This can happen in two cases - 150 // 1. ACTION_USER_SELECT_IMAGES_FOR_APP launched the Photo Picker UI, and we are waiting 151 // for items that's not preloaded due to pagination 152 // 2. PreviewFragment was launched from SavedPreference state but PickerViewModel was 153 // killed and hence there is no selected items. 154 // In both these cases, user will see a blank UI with only Add/Allow button 155 selectedCheckButton.setVisibility(View.GONE); 156 Log.i(TAG, "No items to preview yet" + selectedCheckButton.getVisibility()); 157 } 158 159 if (mPickerViewModel.isManagedSelectionEnabled()) { 160 mPickerViewModel.getIsAllPreGrantedMediaLoaded().observe(this, (isLoadComplete) -> { 161 if (!isLoadComplete) return; 162 163 selectedCheckButton.setVisibility(View.VISIBLE); 164 165 mSelection.prepareSelectedItemsForPreviewAll(); 166 mViewPager2Wrapper.updateList(mSelection.getSelectedItemsForPreview()); 167 }); 168 } 169 } 170 setupScrimLayerAndBottomBar(View fragmentView)171 private void setupScrimLayerAndBottomBar(View fragmentView) { 172 final boolean isLandscape = getResources().getConfiguration().orientation 173 == Configuration.ORIENTATION_LANDSCAPE; 174 175 // Show the scrim layers in Landscape mode. The default visibility is GONE. 176 if (isLandscape) { 177 final View topScrim = fragmentView.findViewById(R.id.preview_top_scrim); 178 topScrim.setVisibility(View.VISIBLE); 179 180 final View bottomScrim = fragmentView.findViewById(R.id.preview_bottom_scrim); 181 bottomScrim.setVisibility(View.VISIBLE); 182 } 183 184 // Set appropriate background color for the bottom bar 185 final int bottomBarColor; 186 if (isLandscape) { 187 bottomBarColor = Color.TRANSPARENT; 188 } else { 189 bottomBarColor = getContext().getColor(R.color.preview_scrim_solid_color); 190 } 191 final View bottomBar = fragmentView.findViewById(R.id.preview_bottom_bar); 192 bottomBar.setBackgroundColor(bottomBarColor); 193 } 194 setUpPreviewLayout(@onNull View view, @Nullable Bundle args)195 private void setUpPreviewLayout(@NonNull View view, @Nullable Bundle args) { 196 if (args == null) { 197 // We are willing to crash PhotoPickerActivity because this error might only happen 198 // during development. 199 throw new IllegalArgumentException("Can't determine the type of the Preview, arguments" 200 + " is not set"); 201 } 202 203 final int previewType = args.getInt(PREVIEW_TYPE, -1); 204 if (previewType == PREVIEW_ON_LONG_PRESS) { 205 setUpPreviewLayoutForLongPress(view); 206 } else if (previewType == PREVIEW_ON_VIEW_SELECTED) { 207 setUpPreviewLayoutForViewSelected(view); 208 } else { 209 // We are willing to crash PhotoPickerActivity because this error might only happen 210 // during development. 211 throw new IllegalArgumentException("No preview type specified"); 212 } 213 } 214 215 /** 216 * Adjusts the select/add button layout for preview on LongPress 217 */ setUpPreviewLayoutForLongPress(@onNull View view)218 private void setUpPreviewLayoutForLongPress(@NonNull View view) { 219 final Button addOrSelectButton = view.findViewById(R.id.preview_add_or_select_button); 220 if (mIsCustomPickerColorSet) { 221 setCustomButtonColorsInLongPressPreviewMode( 222 addOrSelectButton, 223 mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor()); 224 } 225 226 // Preview on Long Press will reuse AddOrSelect button as 227 // * Add button - Button with text "Add" - for single select mode 228 // * Select button - Button with text "Select"/"Deselect" based on the selection state of 229 // the item - for multi select mode 230 if (!mSelection.canSelectMultiple()) { 231 // On clicking add button we return the picker result to calling app. 232 // This destroys PickerActivity and all fragments. 233 addOrSelectButton.setOnClickListener(v -> { 234 ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf(); 235 }); 236 } else { 237 // For preview on long press, we always preview only one item. 238 // Selection#getSelectedItemsForPreview is guaranteed to return only one item. Hence, 239 // we can always use position=0 as current position. 240 updateSelectButtonTextAndVisibility(addOrSelectButton, 241 mSelection.isItemSelected(mViewPager2Wrapper.getItemAt(/* position */ 0))); 242 addOrSelectButton.setOnClickListener(v -> onClickSelectButton(addOrSelectButton)); 243 } 244 245 // Set the appropriate special format icon based on the item in the preview 246 updateSpecialFormatIcon(mViewPager2Wrapper.getItemAt(/* position */ 0)); 247 } 248 setCustomButtonColorsInLongPressPreviewMode( Button addOrSelectButton, int buttonBackgroundColor)249 private void setCustomButtonColorsInLongPressPreviewMode( 250 Button addOrSelectButton, int buttonBackgroundColor) { 251 String textColor = mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright() 252 ? AccentColorResources.DARK_TEXT_COLOR : AccentColorResources.LIGHT_TEXT_COLOR; 253 addOrSelectButton.setBackgroundColor(buttonBackgroundColor); 254 addOrSelectButton.setTextColor(Color.parseColor(textColor)); 255 } 256 257 /** 258 * Adjusts the layout based on Multi select and adds appropriate onClick listeners 259 */ setUpPreviewLayoutForViewSelected(@onNull View view)260 private void setUpPreviewLayoutForViewSelected(@NonNull View view) { 261 // Hide addOrSelect button of long press, we have a separate add button for view selected 262 final Button addOrSelectButton = view.findViewById(R.id.preview_add_or_select_button); 263 addOrSelectButton.setVisibility(View.GONE); 264 265 final Button viewSelectedAddButton = view.findViewById(R.id.preview_add_button); 266 viewSelectedAddButton.setVisibility(View.VISIBLE); 267 // On clicking add button we return the picker result to calling app. 268 // This destroys PickerActivity and all fragments. 269 viewSelectedAddButton.setOnClickListener(v -> { 270 ((PhotoPickerActivity) getActivity()).setResultAndFinishSelf(); 271 }); 272 273 final Button selectedCheckButton = view.findViewById(R.id.preview_selected_check_button); 274 selectedCheckButton.setVisibility(View.VISIBLE); 275 276 if (mIsCustomPickerColorSet) { 277 setCustomButtonColorsInViewSelectedPreviewMode( 278 viewSelectedAddButton, selectedCheckButton, 279 mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor(), 280 AccentColorResources.LIGHT_TEXT_COLOR); 281 } 282 // Update the select icon and text according to the state of selection while swiping 283 // between photos 284 mViewPager2Wrapper.addOnPageChangeCallback(new OnPageChangeCallback(selectedCheckButton)); 285 286 // Update add button text to include number of items selected. 287 mSelection 288 .getSelectedItemCount() 289 .observe( 290 this, 291 selectedItemCount -> { 292 viewSelectedAddButton.setText( 293 generateAddButtonString( 294 /* context= */ getContext(), 295 /* size= */ selectedItemCount, 296 /* isUserSelectForApp= */ mPickerViewModel 297 .isUserSelectForApp(), 298 /* isManagedSelectionEnabled */ 299 mPickerViewModel.isManagedSelectionEnabled())); 300 }); 301 302 selectedCheckButton.setOnClickListener( 303 v -> onClickSelectedCheckButton(selectedCheckButton)); 304 } 305 setCustomButtonColorsInViewSelectedPreviewMode( Button addButton, Button viewSelectedButton, int buttonFillColor, String viewSelectedButtonTextColor)306 private void setCustomButtonColorsInViewSelectedPreviewMode( 307 Button addButton, Button viewSelectedButton, int buttonFillColor, 308 String viewSelectedButtonTextColor) { 309 // Set add button colors 310 String addButtonTextColor = 311 mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright() 312 ? AccentColorResources.DARK_TEXT_COLOR 313 : AccentColorResources.LIGHT_TEXT_COLOR; 314 addButton.setBackgroundColor(buttonFillColor); 315 addButton.setTextColor(Color.parseColor(addButtonTextColor)); 316 // Set view-selected button colors 317 viewSelectedButton.setTextColor(Color.parseColor(viewSelectedButtonTextColor)); 318 viewSelectedButton.setCompoundDrawableTintList( 319 ColorStateList.valueOf(buttonFillColor)); 320 } 321 322 @Override onResume()323 public void onResume() { 324 super.onResume(); 325 326 ((PhotoPickerActivity) getActivity()).updateCommonLayouts(LayoutModeUtils.MODE_PREVIEW, 327 /* title */""); 328 } 329 330 @Override onStop()331 public void onStop() { 332 super.onStop(); 333 334 if (mViewPager2Wrapper != null) { 335 mViewPager2Wrapper.onStop(); 336 } 337 } 338 339 @Override onStart()340 public void onStart() { 341 super.onStart(); 342 343 if (mViewPager2Wrapper != null) { 344 mViewPager2Wrapper.onStart(); 345 } 346 } 347 348 @Override onDestroy()349 public void onDestroy() { 350 super.onDestroy(); 351 if (mViewPager2Wrapper != null) { 352 mViewPager2Wrapper.onDestroy(); 353 } 354 } 355 onClickSelectButton(@onNull Button selectButton)356 private void onClickSelectButton(@NonNull Button selectButton) { 357 final boolean isSelectedNow = updateSelectionAndGetState(); 358 updateSelectButtonTextAndVisibility(selectButton, isSelectedNow); 359 } 360 onClickSelectedCheckButton(@onNull Button selectedCheckButton)361 private void onClickSelectedCheckButton(@NonNull Button selectedCheckButton) { 362 final boolean isSelectedNow = updateSelectionAndGetState(); 363 updateSelectedCheckButtonStateAndText(selectedCheckButton, isSelectedNow); 364 } 365 updateSelectionAndGetState()366 private boolean updateSelectionAndGetState() { 367 final Item currentItem = mViewPager2Wrapper.getCurrentItem(); 368 final boolean wasSelectedBefore = mSelection.isItemSelected(currentItem); 369 370 if (wasSelectedBefore) { 371 // If the item is previously selected, current user action is to deselect the item 372 mSelection.removeSelectedItem(currentItem); 373 } else { 374 // If the item is not previously selected, current user action is to select the item 375 mSelection.addSelectedItem(currentItem); 376 } 377 378 // After the user has clicked the button, current state of the button should be opposite of 379 // the previous state. 380 // If the previous state was to "Select" the item, and user clicks "Select" button, 381 // wasSelectedBefore = false. And item will be added to selected items. Now, user can only 382 // deselect the item. Hence, isSelectedNow is opposite of previous state, 383 // i.e., isSelectedNow = true. 384 return !wasSelectedBefore; 385 } 386 387 private class OnPageChangeCallback extends ViewPager2.OnPageChangeCallback { 388 private final Button mSelectedCheckButton; 389 OnPageChangeCallback(@onNull Button selectedCheckButton)390 public OnPageChangeCallback(@NonNull Button selectedCheckButton) { 391 mSelectedCheckButton = selectedCheckButton; 392 } 393 394 @Override onPageSelected(int position)395 public void onPageSelected(int position) { 396 // No action to take as we don't have deselect view here. 397 if (!mSelection.canSelectMultiple()) return; 398 399 final Item item = mViewPager2Wrapper.getItemAt(position); 400 // Set the appropriate select/deselect state for each item in each page based on the 401 // selection list. 402 updateSelectedCheckButtonStateAndText(mSelectedCheckButton, 403 mSelection.isItemSelected(item)); 404 405 // Set the appropriate special format icon based on the item in the preview 406 updateSpecialFormatIcon(item); 407 } 408 } 409 updateSelectButtonTextAndVisibility(@onNull Button selectButton, boolean isSelected)410 private void updateSelectButtonTextAndVisibility(@NonNull Button selectButton, 411 boolean isSelected) { 412 selectButton.setText(isSelected ? R.string.deselect : R.string.select); 413 selectButton.setVisibility( 414 (isSelected || mSelection.isSelectionAllowed()) ? View.VISIBLE : View.GONE); 415 } 416 updateSelectedCheckButtonStateAndText(@onNull Button selectedCheckButton, boolean isSelected)417 private static void updateSelectedCheckButtonStateAndText(@NonNull Button selectedCheckButton, 418 boolean isSelected) { 419 selectedCheckButton.setText(isSelected ? R.string.selected : R.string.deselected); 420 selectedCheckButton.setSelected(isSelected); 421 } 422 updateSpecialFormatIcon(Item item)423 private void updateSpecialFormatIcon(Item item) { 424 mShouldShowGifBadge = item.isGifOrAnimatedWebp(); 425 mShouldShowMotionPhotoBadge = item.isMotionPhoto(); 426 // Invalidating options menu calls onPrepareOptionsMenu() where the logic for 427 // hiding/showing menu items is placed. 428 requireActivity().invalidateOptionsMenu(); 429 } 430 show(@onNull FragmentManager fm, @NonNull Bundle args)431 public static void show(@NonNull FragmentManager fm, @NonNull Bundle args) { 432 if (fm.isStateSaved()) { 433 Log.d(TAG, "Skip show preview fragment because state saved"); 434 return; 435 } 436 437 final PreviewFragment fragment = new PreviewFragment(); 438 fragment.setArguments(args); 439 fm.beginTransaction() 440 .replace(R.id.fragment_container, fragment, TAG) 441 .addToBackStack(TAG) 442 .commitAllowingStateLoss(); 443 } 444 445 /** 446 * Get the fragment in the FragmentManager 447 * @param fm the fragment manager 448 */ get(@onNull FragmentManager fm)449 public static Fragment get(@NonNull FragmentManager fm) { 450 return fm.findFragmentByTag(TAG); 451 } 452 getArgsForPreviewOnLongPress()453 public static Bundle getArgsForPreviewOnLongPress() { 454 return sPreviewOnLongPressArgs; 455 } 456 getArgsForPreviewOnViewSelected()457 public static Bundle getArgsForPreviewOnViewSelected() { 458 return sPreviewOnViewSelectedArgs; 459 } 460 461 // TODO: There is a same method in TabFragment. To find a way to reuse it. generateAddButtonString( @onNull Context context, int size, boolean isUserSelectForApp, boolean isManagedSelection)462 private static String generateAddButtonString( 463 @NonNull Context context, int size, boolean isUserSelectForApp, 464 boolean isManagedSelection) { 465 if (isManagedSelection && size == 0) { 466 return context.getString(R.string.picker_add_button_allow_none_option); 467 } 468 final String sizeString = NumberFormat.getInstance(Locale.getDefault()).format(size); 469 final String template = 470 isUserSelectForApp 471 ? context.getString(R.string.picker_add_button_multi_select_permissions) 472 : context.getString(R.string.picker_add_button_multi_select); 473 return TextUtils.expandTemplate(template, sizeString).toString(); 474 } 475 476 private final PreviewAdapter.OnCreateSurfaceController mOnCreateSurfaceController = 477 new PreviewAdapter.OnCreateSurfaceController() { 478 @Override 479 public void logStart(String authority) { 480 mPickerViewModel.logCreateSurfaceControllerStart(authority); 481 } 482 483 @Override 484 public void logEnd(String authority) { 485 mPickerViewModel.logCreateSurfaceControllerEnd(authority); 486 } 487 }; 488 } 489