• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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