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 17 package com.android.providers.media.photopicker.ui; 18 19 import android.content.Context; 20 import android.view.LayoutInflater; 21 import android.view.View; 22 import android.view.ViewGroup; 23 import android.widget.Button; 24 import android.widget.TextView; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 import androidx.annotation.StringRes; 29 import androidx.annotation.VisibleForTesting; 30 import androidx.lifecycle.LifecycleOwner; 31 import androidx.lifecycle.LiveData; 32 import androidx.recyclerview.widget.RecyclerView; 33 34 import com.android.providers.media.R; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * Adapts from model to something RecyclerView understands. 41 */ 42 @VisibleForTesting 43 public abstract class TabAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { 44 45 @VisibleForTesting 46 public static final int ITEM_TYPE_BANNER = 0; 47 // Date header sections for "Photos" tab 48 static final int ITEM_TYPE_SECTION = 1; 49 // Media items (a.k.a. Items) for "Photos" tab, Albums (a.k.a. Categories) for "Albums" tab 50 private static final int ITEM_TYPE_MEDIA_ITEM = 2; 51 52 @NonNull final ImageLoader mImageLoader; 53 @NonNull private final LiveData<String> mCloudMediaProviderAppTitle; 54 @NonNull private final LiveData<String> mCloudMediaAccountName; 55 56 @Nullable private Banner mBanner; 57 @Nullable private OnBannerEventListener mOnBannerEventListener; 58 /** 59 * Combined list of Sections and Media Items, ordered based on their position in the view. 60 * 61 * (List of {@link com.android.providers.media.photopicker.ui.PhotosTabAdapter.DateHeader} and 62 * {@link com.android.providers.media.photopicker.data.model.Item} for the "Photos" tab) 63 * 64 * (List of {@link com.android.providers.media.photopicker.data.model.Category} for the "Albums" 65 * tab) 66 */ 67 @NonNull 68 private final List<Object> mAllItems = new ArrayList<>(); 69 TabAdapter(@onNull ImageLoader imageLoader, @NonNull LifecycleOwner lifecycleOwner, @NonNull LiveData<String> cloudMediaProviderAppTitle, @NonNull LiveData<String> cloudMediaAccountName, @NonNull LiveData<Boolean> shouldShowChooseAppBanner, @NonNull LiveData<Boolean> shouldShowCloudMediaAvailableBanner, @NonNull LiveData<Boolean> shouldShowAccountUpdatedBanner, @NonNull LiveData<Boolean> shouldShowChooseAccountBanner, @NonNull OnBannerEventListener onChooseAppBannerEventListener, @NonNull OnBannerEventListener onCloudMediaAvailableBannerEventListener, @NonNull OnBannerEventListener onAccountUpdatedBannerEventListener, @NonNull OnBannerEventListener onChooseAccountBannerEventListener)70 TabAdapter(@NonNull ImageLoader imageLoader, @NonNull LifecycleOwner lifecycleOwner, 71 @NonNull LiveData<String> cloudMediaProviderAppTitle, 72 @NonNull LiveData<String> cloudMediaAccountName, 73 @NonNull LiveData<Boolean> shouldShowChooseAppBanner, 74 @NonNull LiveData<Boolean> shouldShowCloudMediaAvailableBanner, 75 @NonNull LiveData<Boolean> shouldShowAccountUpdatedBanner, 76 @NonNull LiveData<Boolean> shouldShowChooseAccountBanner, 77 @NonNull OnBannerEventListener onChooseAppBannerEventListener, 78 @NonNull OnBannerEventListener onCloudMediaAvailableBannerEventListener, 79 @NonNull OnBannerEventListener onAccountUpdatedBannerEventListener, 80 @NonNull OnBannerEventListener onChooseAccountBannerEventListener) { 81 mImageLoader = imageLoader; 82 mCloudMediaProviderAppTitle = cloudMediaProviderAppTitle; 83 mCloudMediaAccountName = cloudMediaAccountName; 84 85 shouldShowChooseAppBanner.observe(lifecycleOwner, isVisible -> 86 setBannerVisibility(isVisible, Banner.CHOOSE_APP, onChooseAppBannerEventListener)); 87 shouldShowCloudMediaAvailableBanner.observe(lifecycleOwner, isVisible -> 88 setBannerVisibility(isVisible, Banner.CLOUD_MEDIA_AVAILABLE, 89 onCloudMediaAvailableBannerEventListener)); 90 shouldShowAccountUpdatedBanner.observe(lifecycleOwner, isVisible -> 91 setBannerVisibility(isVisible, Banner.ACCOUNT_UPDATED, 92 onAccountUpdatedBannerEventListener)); 93 shouldShowChooseAccountBanner.observe(lifecycleOwner, isVisible -> 94 setBannerVisibility(isVisible, Banner.CHOOSE_ACCOUNT, 95 onChooseAccountBannerEventListener)); 96 } 97 98 @NonNull 99 @Override onCreateViewHolder(@onNull ViewGroup viewGroup, int viewType)100 public final RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, 101 int viewType) { 102 switch (viewType) { 103 case ITEM_TYPE_BANNER: 104 return createBannerViewHolder(viewGroup); 105 case ITEM_TYPE_SECTION: 106 return createSectionViewHolder(viewGroup); 107 case ITEM_TYPE_MEDIA_ITEM: 108 return createMediaItemViewHolder(viewGroup); 109 default: 110 throw new IllegalArgumentException("Unknown item view type " + viewType); 111 } 112 } 113 114 @Override onBindViewHolder(@onNull RecyclerView.ViewHolder itemHolder, int position)115 public final void onBindViewHolder(@NonNull RecyclerView.ViewHolder itemHolder, int position) { 116 final int itemViewType = getItemViewType(position); 117 switch (itemViewType) { 118 case ITEM_TYPE_BANNER: 119 onBindBannerViewHolder(itemHolder); 120 break; 121 case ITEM_TYPE_SECTION: 122 onBindSectionViewHolder(itemHolder, position); 123 break; 124 case ITEM_TYPE_MEDIA_ITEM: 125 onBindMediaItemViewHolder(itemHolder, position); 126 break; 127 default: 128 throw new IllegalArgumentException("Unknown item view type " + itemViewType); 129 } 130 } 131 132 @Override getItemCount()133 public final int getItemCount() { 134 return getBannerCount() + getAllItemsCount(); 135 } 136 137 @Override getItemViewType(int position)138 public final int getItemViewType(int position) { 139 if (position < 0) { 140 throw new IllegalStateException("Get item view type for negative position " + position); 141 } 142 if (isItemTypeBanner(position)) { 143 return ITEM_TYPE_BANNER; 144 } else if (isItemTypeSection(position)) { 145 return ITEM_TYPE_SECTION; 146 } else if (isItemTypeMediaItem(position)) { 147 return ITEM_TYPE_MEDIA_ITEM; 148 } else { 149 throw new IllegalStateException("Item at position " + position 150 + " is of neither of the defined types"); 151 } 152 } 153 154 @NonNull createBannerViewHolder(@onNull ViewGroup viewGroup)155 private RecyclerView.ViewHolder createBannerViewHolder(@NonNull ViewGroup viewGroup) { 156 final View view = getView(viewGroup, R.layout.item_banner); 157 return new BannerHolder(view); 158 } 159 160 @NonNull createSectionViewHolder(@onNull ViewGroup viewGroup)161 RecyclerView.ViewHolder createSectionViewHolder(@NonNull ViewGroup viewGroup) { 162 // A descendant must override this method if and only if {@link isItemTypeSection} is 163 // implemented and may return {@code true} for them. 164 throw new IllegalStateException("Attempt to create an unimplemented section view holder"); 165 } 166 167 @NonNull createMediaItemViewHolder(@onNull ViewGroup viewGroup)168 abstract RecyclerView.ViewHolder createMediaItemViewHolder(@NonNull ViewGroup viewGroup); 169 onBindBannerViewHolder(@onNull RecyclerView.ViewHolder itemHolder)170 private void onBindBannerViewHolder(@NonNull RecyclerView.ViewHolder itemHolder) { 171 final BannerHolder bannerVH = (BannerHolder) itemHolder; 172 bannerVH.bind(mBanner, mCloudMediaProviderAppTitle.getValue(), 173 mCloudMediaAccountName.getValue(), mOnBannerEventListener); 174 } 175 onBindSectionViewHolder(@onNull RecyclerView.ViewHolder itemHolder, int position)176 void onBindSectionViewHolder(@NonNull RecyclerView.ViewHolder itemHolder, int position) { 177 // no-op: descendants may implement 178 } 179 onBindMediaItemViewHolder(@onNull RecyclerView.ViewHolder itemHolder, int position)180 abstract void onBindMediaItemViewHolder(@NonNull RecyclerView.ViewHolder itemHolder, 181 int position); 182 getBannerCount()183 private int getBannerCount() { 184 return mBanner != null ? 1 : 0; 185 } 186 getAllItemsCount()187 private int getAllItemsCount() { 188 return mAllItems.size(); 189 } 190 isItemTypeBanner(int position)191 private boolean isItemTypeBanner(int position) { 192 return position > -1 && position < getBannerCount(); 193 } 194 isItemTypeSection(int position)195 boolean isItemTypeSection(int position) { 196 // no-op: descendants may implement 197 return false; 198 } 199 isItemTypeMediaItem(int position)200 abstract boolean isItemTypeMediaItem(int position); 201 202 /** 203 * Update the banner visibility in tab adapter 204 */ setBannerVisibility(boolean isVisible, @NonNull Banner banner, @NonNull OnBannerEventListener onBannerEventListener)205 private void setBannerVisibility(boolean isVisible, @NonNull Banner banner, 206 @NonNull OnBannerEventListener onBannerEventListener) { 207 if (isVisible) { 208 if (mBanner == null) { 209 mBanner = banner; 210 mOnBannerEventListener = onBannerEventListener; 211 notifyItemInserted(/* position */ 0); 212 mOnBannerEventListener.onBannerAdded(); 213 } else { 214 mBanner = banner; 215 mOnBannerEventListener = onBannerEventListener; 216 notifyItemChanged(/* position */ 0); 217 } 218 } else if (mBanner == banner) { 219 mBanner = null; 220 mOnBannerEventListener = null; 221 notifyItemRemoved(/* position */ 0); 222 } 223 } 224 225 /** 226 * Update the List of all items (excluding the banner) in tab adapter {@link #mAllItems} 227 */ setAllItems(@onNull List<?> items)228 protected final void setAllItems(@NonNull List<?> items) { 229 mAllItems.clear(); 230 mAllItems.addAll(items); 231 notifyDataSetChanged(); 232 } 233 234 @NonNull getAdapterItem(int position)235 final Object getAdapterItem(int position) { 236 if (position < 0) { 237 throw new IllegalStateException("Get adapter item for negative position " + position); 238 } 239 if (isItemTypeBanner(position)) { 240 return mBanner; 241 } 242 243 final int effectiveItemIndex = position - getBannerCount(); 244 return mAllItems.get(effectiveItemIndex); 245 } 246 247 @NonNull getView(@onNull ViewGroup viewGroup, int layout)248 final View getView(@NonNull ViewGroup viewGroup, int layout) { 249 final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); 250 return inflater.inflate(layout, viewGroup, /* attachToRoot */ false); 251 } 252 253 private static class BannerHolder extends RecyclerView.ViewHolder { 254 final TextView mPrimaryText; 255 final TextView mSecondaryText; 256 final Button mDismissButton; 257 final Button mActionButton; 258 BannerHolder(@onNull View itemView)259 BannerHolder(@NonNull View itemView) { 260 super(itemView); 261 mPrimaryText = itemView.findViewById(R.id.banner_primary_text); 262 mSecondaryText = itemView.findViewById(R.id.banner_secondary_text); 263 mDismissButton = itemView.findViewById(R.id.dismiss_button); 264 mActionButton = itemView.findViewById(R.id.action_button); 265 } 266 bind(@onNull Banner banner, String cloudAppName, String cloudUserAccount, @NonNull OnBannerEventListener onBannerEventListener)267 void bind(@NonNull Banner banner, String cloudAppName, String cloudUserAccount, 268 @NonNull OnBannerEventListener onBannerEventListener) { 269 final Context context = itemView.getContext(); 270 271 itemView.setOnClickListener(v -> onBannerEventListener.onBannerClick()); 272 273 mPrimaryText.setText(banner.getPrimaryText(context, cloudAppName)); 274 mSecondaryText.setText(banner.getSecondaryText(context, cloudAppName, 275 cloudUserAccount)); 276 277 mDismissButton.setOnClickListener(v -> onBannerEventListener.onDismissButtonClick()); 278 279 if (banner.mActionButtonText != -1) { 280 mActionButton.setText(banner.mActionButtonText); 281 mActionButton.setVisibility(View.VISIBLE); 282 mActionButton.setOnClickListener(v -> onBannerEventListener.onActionButtonClick()); 283 } else { 284 mActionButton.setVisibility(View.GONE); 285 } 286 } 287 } 288 289 private enum Banner { 290 // TODO(b/274426228): Replace `CLOUD_MEDIA_AVAILABLE` `mActionButtonText` from `-1` to 291 // `R.string.picker_banner_cloud_change_account_button`, post change cloud account 292 // functionality implementation from the Picker settings (b/261999521). 293 CLOUD_MEDIA_AVAILABLE(R.string.picker_banner_cloud_first_time_available_title, 294 R.string.picker_banner_cloud_first_time_available_desc, /* no action button */ -1), 295 ACCOUNT_UPDATED(R.string.picker_banner_cloud_account_changed_title, 296 R.string.picker_banner_cloud_account_changed_desc, /* no action button */ -1), 297 // TODO(b/274426228): Replace `CHOOSE_ACCOUNT` `mActionButtonText` from `-1` to 298 // `R.string.picker_banner_cloud_choose_account_button`, post change cloud account 299 // functionality implementation from the Picker settings (b/261999521). 300 CHOOSE_ACCOUNT(R.string.picker_banner_cloud_choose_account_title, 301 R.string.picker_banner_cloud_choose_account_desc, /* no action button */ -1), 302 CHOOSE_APP(R.string.picker_banner_cloud_choose_app_title, 303 R.string.picker_banner_cloud_choose_app_desc, 304 R.string.picker_banner_cloud_choose_app_button); 305 306 @StringRes final int mPrimaryText; 307 @StringRes final int mSecondaryText; 308 @StringRes final int mActionButtonText; 309 Banner(int primaryText, int secondaryText, int actionButtonText)310 Banner(int primaryText, int secondaryText, int actionButtonText) { 311 mPrimaryText = primaryText; 312 mSecondaryText = secondaryText; 313 mActionButtonText = actionButtonText; 314 } 315 getPrimaryText(@onNull Context context, String appName)316 String getPrimaryText(@NonNull Context context, String appName) { 317 switch (this) { 318 case CLOUD_MEDIA_AVAILABLE: 319 // fall-through 320 case CHOOSE_APP: 321 return context.getString(mPrimaryText); 322 case ACCOUNT_UPDATED: 323 // fall-through 324 case CHOOSE_ACCOUNT: 325 return context.getString(mPrimaryText, appName); 326 default: 327 throw new IllegalStateException("Unknown banner type " + name()); 328 } 329 } 330 getSecondaryText(@onNull Context context, String appName, String userAccount)331 String getSecondaryText(@NonNull Context context, String appName, String userAccount) { 332 switch (this) { 333 case CLOUD_MEDIA_AVAILABLE: 334 return context.getString(mSecondaryText, appName, userAccount); 335 case ACCOUNT_UPDATED: 336 return context.getString(mSecondaryText, userAccount); 337 case CHOOSE_ACCOUNT: 338 return context.getString(mSecondaryText, appName); 339 case CHOOSE_APP: 340 return context.getString(mSecondaryText); 341 default: 342 throw new IllegalStateException("Unknown banner type " + name()); 343 } 344 } 345 } 346 347 interface OnBannerEventListener { onActionButtonClick()348 void onActionButtonClick(); 349 onDismissButtonClick()350 void onDismissButtonClick(); 351 onBannerClick()352 default void onBannerClick() { 353 onActionButtonClick(); 354 } 355 onBannerAdded()356 void onBannerAdded(); 357 } 358 } 359