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.util.LayoutModeUtils.MODE_ALBUM_PHOTOS_TAB; 19 import static com.android.providers.media.photopicker.util.LayoutModeUtils.MODE_PHOTOS_TAB; 20 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.text.TextUtils; 24 import android.view.View; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.fragment.app.Fragment; 29 import androidx.fragment.app.FragmentManager; 30 import androidx.fragment.app.FragmentTransaction; 31 import androidx.lifecycle.LiveData; 32 import androidx.lifecycle.MutableLiveData; 33 34 import com.android.providers.media.R; 35 import com.android.providers.media.photopicker.data.model.Category; 36 import com.android.providers.media.photopicker.data.model.Item; 37 import com.android.providers.media.photopicker.util.LayoutModeUtils; 38 import com.android.providers.media.util.StringUtils; 39 40 import com.google.android.material.snackbar.Snackbar; 41 42 import java.text.NumberFormat; 43 import java.util.List; 44 import java.util.Locale; 45 46 /** 47 * Photos tab fragment for showing the photos 48 */ 49 public class PhotosTabFragment extends TabFragment { 50 private static final int MINIMUM_SPAN_COUNT = 3; 51 private static final int GRID_COLUMN_COUNT = 3; 52 private static final String FRAGMENT_TAG = "PhotosTabFragment"; 53 54 private Category mCategory = Category.DEFAULT; 55 56 @Override onCreate(Bundle savedInstanceState)57 public void onCreate(Bundle savedInstanceState) { 58 super.onCreate(savedInstanceState); 59 // After the configuration is changed, if the fragment is now shown, onViewCreated will not 60 // be triggered. We need to restore the savedInstanceState in onCreate. 61 // E.g. Click the albums -> preview one item -> rotate the device 62 if (savedInstanceState != null) { 63 mCategory = Category.fromBundle(savedInstanceState); 64 } 65 } 66 67 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)68 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 69 super.onViewCreated(view, savedInstanceState); 70 final Context context = getContext(); 71 72 // We only add the RECENT header on the PhotosTabFragment with CATEGORY_DEFAULT. In this 73 // case, we call this method {loadItems} with null category. When the category is not 74 // empty, we don't show the RECENT header. 75 final boolean showRecentSection = mCategory.isDefault(); 76 77 // We only show the Banners on the PhotosTabFragment with CATEGORY_DEFAULT (Main grid). 78 final boolean shouldShowBanners = mCategory.isDefault(); 79 final LiveData<Boolean> doNotShowBanner = new MutableLiveData<>(false); 80 final LiveData<Boolean> showChooseAppBanner = shouldShowBanners 81 ? mPickerViewModel.shouldShowChooseAppBannerLiveData() : doNotShowBanner; 82 final LiveData<Boolean> showCloudMediaAvailableBanner = shouldShowBanners 83 ? mPickerViewModel.shouldShowCloudMediaAvailableBannerLiveData() : doNotShowBanner; 84 final LiveData<Boolean> showAccountUpdatedBanner = shouldShowBanners 85 ? mPickerViewModel.shouldShowAccountUpdatedBannerLiveData() : doNotShowBanner; 86 final LiveData<Boolean> showChooseAccountBanner = shouldShowBanners 87 ? mPickerViewModel.shouldShowChooseAccountBannerLiveData() : doNotShowBanner; 88 89 final PhotosTabAdapter adapter = new PhotosTabAdapter(showRecentSection, mSelection, 90 mImageLoader, this::onItemClick, this::onItemLongClick, /* lifecycleOwner */ this, 91 mPickerViewModel.getCloudMediaProviderAppTitleLiveData(), 92 mPickerViewModel.getCloudMediaAccountNameLiveData(), showChooseAppBanner, 93 showCloudMediaAvailableBanner, showAccountUpdatedBanner, showChooseAccountBanner, 94 mOnChooseAppBannerEventListener, mOnCloudMediaAvailableBannerEventListener, 95 mOnAccountUpdatedBannerEventListener, mOnChooseAccountBannerEventListener); 96 97 if (mCategory.isDefault()) { 98 setEmptyMessage(R.string.picker_photos_empty_message); 99 // Set the pane title for A11y 100 view.setAccessibilityPaneTitle(getString(R.string.picker_photos)); 101 mPickerViewModel.getItems() 102 .observe(this, itemList -> onChangeMediaItems(itemList, adapter)); 103 } else { 104 setEmptyMessage(R.string.picker_album_media_empty_message); 105 // Set the pane title for A11y 106 view.setAccessibilityPaneTitle(mCategory.getDisplayName(context)); 107 mPickerViewModel.getCategoryItems(mCategory) 108 .observe(this, itemList -> onChangeMediaItems(itemList, adapter)); 109 } 110 111 final PhotosTabItemDecoration itemDecoration = new PhotosTabItemDecoration(context); 112 113 final int spacing = getResources().getDimensionPixelSize(R.dimen.picker_photo_item_spacing); 114 final int photoSize = getResources().getDimensionPixelSize(R.dimen.picker_photo_size); 115 mRecyclerView.setColumnWidth(photoSize + spacing); 116 mRecyclerView.setMinimumSpanCount(MINIMUM_SPAN_COUNT); 117 118 setLayoutManager(adapter, GRID_COLUMN_COUNT); 119 mRecyclerView.setAdapter(adapter); 120 mRecyclerView.addItemDecoration(itemDecoration); 121 } 122 123 /** 124 * Called when owning activity is saving state to be used to restore state during creation. 125 * 126 * @param state Bundle to save state 127 */ onSaveInstanceState(Bundle state)128 public void onSaveInstanceState(Bundle state) { 129 super.onSaveInstanceState(state); 130 mCategory.toBundle(state); 131 } 132 133 @Override onResume()134 public void onResume() { 135 super.onResume(); 136 137 final String title; 138 final LayoutModeUtils.Mode layoutMode; 139 final boolean shouldHideProfileButton; 140 if (mCategory.isDefault()) { 141 title = ""; 142 layoutMode = MODE_PHOTOS_TAB; 143 shouldHideProfileButton = false; 144 } else { 145 title = mCategory.getDisplayName(getContext()); 146 layoutMode = MODE_ALBUM_PHOTOS_TAB; 147 shouldHideProfileButton = true; 148 } 149 150 getPickerActivity().updateCommonLayouts(layoutMode, title); 151 hideProfileButton(shouldHideProfileButton); 152 } 153 onChangeMediaItems(@onNull List<Item> itemList, @NonNull PhotosTabAdapter adapter)154 private void onChangeMediaItems(@NonNull List<Item> itemList, 155 @NonNull PhotosTabAdapter adapter) { 156 adapter.setMediaItems(itemList); 157 // Handle emptyView's visibility 158 updateVisibilityForEmptyView(/* shouldShowEmptyView */ itemList.size() == 0); 159 } 160 onItemClick(@onNull View view)161 private void onItemClick(@NonNull View view) { 162 if (mSelection.canSelectMultiple()) { 163 final boolean isSelectedBefore = view.isSelected(); 164 165 if (isSelectedBefore) { 166 mSelection.removeSelectedItem((Item) view.getTag()); 167 } else { 168 if (!mSelection.isSelectionAllowed()) { 169 final int maxCount = mSelection.getMaxSelectionLimit(); 170 final CharSequence quantityText = 171 StringUtils.getICUFormatString( 172 getResources(), maxCount, R.string.select_up_to); 173 final String itemCountString = NumberFormat.getInstance(Locale.getDefault()) 174 .format(maxCount); 175 final CharSequence message = TextUtils.expandTemplate(quantityText, 176 itemCountString); 177 Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show(); 178 return; 179 } else { 180 final Item item = (Item) view.getTag(); 181 mSelection.addSelectedItem(item); 182 } 183 } 184 view.setSelected(!isSelectedBefore); 185 // There is an issue b/223695510 about not selected in Accessibility mode. It only says 186 // selected state, but it doesn't say not selected state. Add the not selected only to 187 // avoid that it says selected twice. 188 view.setStateDescription(isSelectedBefore ? getString(R.string.not_selected) : null); 189 } else { 190 final Item item = (Item) view.getTag(); 191 mSelection.setSelectedItem(item); 192 getPickerActivity().setResultAndFinishSelf(); 193 } 194 } 195 onItemLongClick(@onNull View view)196 private boolean onItemLongClick(@NonNull View view) { 197 final Item item = (Item) view.getTag(); 198 if (!mSelection.canSelectMultiple()) { 199 // In single select mode, if the item is previewed, we set it as selected item. This is 200 // will assist in "Add" button click to return all selected items. 201 // For multi select, long click only previews the item, and until user selects the item, 202 // it doesn't get added to selected items. Also, there is no "Add" button in the preview 203 // layout that can return selected items. 204 mSelection.setSelectedItem(item); 205 } 206 mSelection.prepareItemForPreviewOnLongPress(item); 207 // Transition to PreviewFragment. 208 PreviewFragment.show(getActivity().getSupportFragmentManager(), 209 PreviewFragment.getArgsForPreviewOnLongPress()); 210 return true; 211 } 212 213 /** 214 * Create the fragment with the category and add it into the FragmentManager 215 * 216 * @param fm the fragment manager 217 * @param category the category 218 */ show(FragmentManager fm, Category category)219 public static void show(FragmentManager fm, Category category) { 220 final FragmentTransaction ft = fm.beginTransaction(); 221 final PhotosTabFragment fragment = new PhotosTabFragment(); 222 fragment.mCategory = category; 223 ft.replace(R.id.fragment_container, fragment, FRAGMENT_TAG); 224 if (!fragment.mCategory.isDefault()) { 225 ft.addToBackStack(FRAGMENT_TAG); 226 } 227 ft.commitAllowingStateLoss(); 228 } 229 230 /** 231 * Get the fragment in the FragmentManager 232 * 233 * @param fm The fragment manager 234 */ get(FragmentManager fm)235 public static Fragment get(FragmentManager fm) { 236 return fm.findFragmentByTag(FRAGMENT_TAG); 237 } 238 } 239