1 /*
2  * Copyright 2018 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.car.media.browse;
18 
19 import android.content.Context;
20 import android.util.Log;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.recyclerview.widget.DiffUtil;
28 import androidx.recyclerview.widget.GridLayoutManager;
29 import androidx.recyclerview.widget.ListAdapter;
30 import androidx.recyclerview.widget.RecyclerView;
31 
32 import com.android.car.media.common.MediaConstants;
33 import com.android.car.media.common.MediaItemMetadata;
34 
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.List;
39 import java.util.Objects;
40 import java.util.function.Consumer;
41 
42 /**
43  * A {@link RecyclerView.Adapter} that can be used to display a single level of a {@link
44  * android.service.media.MediaBrowserService} media tree into a {@link
45  * androidx.car.widget.PagedListView} or any other {@link RecyclerView}.
46  *
47  * <p>This adapter assumes that the attached {@link RecyclerView} uses a {@link GridLayoutManager},
48  * as it can use both grid and list elements to produce the desired representation.
49  *
50  * <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates.
51  */
52 public class BrowseAdapter extends ListAdapter<BrowseViewData, BrowseViewHolder> {
53     private static final String TAG = "BrowseAdapter";
54     @NonNull
55     private final Context mContext;
56     @NonNull
57     private List<Observer> mObservers = new ArrayList<>();
58     @Nullable
59     private CharSequence mTitle;
60     @Nullable
61     private MediaItemMetadata mParentMediaItem;
62     private int mMaxSpanSize = 1;
63 
64     private BrowseItemViewType mRootBrowsableViewType = BrowseItemViewType.LIST_ITEM;
65     private BrowseItemViewType mRootPlayableViewType = BrowseItemViewType.LIST_ITEM;
66 
67     private static final DiffUtil.ItemCallback<BrowseViewData> DIFF_CALLBACK =
68             new DiffUtil.ItemCallback<BrowseViewData>() {
69                 @Override
70                 public boolean areItemsTheSame(@NonNull BrowseViewData oldItem,
71                         @NonNull BrowseViewData newItem) {
72                     return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem)
73                             && Objects.equals(oldItem.mText, newItem.mText);
74                 }
75 
76                 @Override
77                 public boolean areContentsTheSame(@NonNull BrowseViewData oldItem,
78                         @NonNull BrowseViewData newItem) {
79                     return oldItem.equals(newItem);
80                 }
81             };
82 
83     /**
84      * An {@link BrowseAdapter} observer.
85      */
86     public static abstract class Observer {
87 
88         /**
89          * Callback invoked when a user clicks on a playable item.
90          */
onPlayableItemClicked(MediaItemMetadata item)91         protected void onPlayableItemClicked(MediaItemMetadata item) {
92         }
93 
94         /**
95          * Callback invoked when a user clicks on a browsable item.
96          */
onBrowsableItemClicked(MediaItemMetadata item)97         protected void onBrowsableItemClicked(MediaItemMetadata item) {
98         }
99 
100         /**
101          * Callback invoked when the user clicks on the title of the queue.
102          */
onTitleClicked()103         protected void onTitleClicked() {
104         }
105     }
106 
107     /**
108      * Creates a {@link BrowseAdapter} that displays the children of the given media tree node.
109      */
BrowseAdapter(@onNull Context context)110     public BrowseAdapter(@NonNull Context context) {
111         super(DIFF_CALLBACK);
112         mContext = context;
113     }
114 
115     /**
116      * Sets title to be displayed.
117      */
setTitle(CharSequence title)118     public void setTitle(CharSequence title) {
119         mTitle = title;
120     }
121 
122     /**
123      * Registers an {@link Observer}
124      */
registerObserver(Observer observer)125     public void registerObserver(Observer observer) {
126         mObservers.add(observer);
127     }
128 
129     /**
130      * Unregisters an {@link Observer}
131      */
unregisterObserver(Observer observer)132     public void unregisterObserver(Observer observer) {
133         mObservers.remove(observer);
134     }
135 
136     /**
137      * Sets the number of columns that items can take. This method only needs to be used if the
138      * attached {@link RecyclerView} is NOT using a {@link GridLayoutManager}. This class will
139      * automatically determine this value on {@link #onAttachedToRecyclerView(RecyclerView)}
140      * otherwise.
141      */
setMaxSpanSize(int maxSpanSize)142     public void setMaxSpanSize(int maxSpanSize) {
143         mMaxSpanSize = maxSpanSize;
144     }
145 
setRootBrowsableViewType(int hintValue)146     public void setRootBrowsableViewType(int hintValue) {
147         mRootBrowsableViewType = fromMediaHint(hintValue);
148     }
149 
setRootPlayableViewType(int hintValue)150     public void setRootPlayableViewType(int hintValue) {
151         mRootPlayableViewType = fromMediaHint(hintValue);
152     }
153 
154     /**
155      * @return a {@link GridLayoutManager.SpanSizeLookup} that can be used to obtain the span size
156      * of each item in this adapter. This method is only needed if the {@link RecyclerView} is NOT
157      * using a {@link GridLayoutManager}. This class will automatically use it on\ {@link
158      * #onAttachedToRecyclerView(RecyclerView)} otherwise.
159      */
getSpanSizeLookup()160     private GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
161         return new GridLayoutManager.SpanSizeLookup() {
162             @Override
163             public int getSpanSize(int position) {
164                 BrowseItemViewType viewType = getItem(position).mViewType;
165                 return viewType.getSpanSize(mMaxSpanSize);
166             }
167         };
168     }
169 
170     @NonNull
171     @Override
172     public BrowseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
173         int layoutId = BrowseItemViewType.values()[viewType].getLayoutId();
174         View view = LayoutInflater.from(mContext).inflate(layoutId, parent, false);
175         return new BrowseViewHolder(view);
176     }
177 
178     @Override
179     public void onBindViewHolder(@NonNull BrowseViewHolder holder, int position) {
180         BrowseViewData viewData = getItem(position);
181         holder.bind(mContext, viewData);
182     }
183 
184     @Override
185     public void onViewAttachedToWindow(@NonNull BrowseViewHolder holder) {
186         super.onViewAttachedToWindow(holder);
187         holder.onViewAttachedToWindow(mContext);
188     }
189 
190     @Override
191     public void onViewDetachedFromWindow(@NonNull BrowseViewHolder holder) {
192         super.onViewDetachedFromWindow(holder);
193         holder.onViewDetachedFromWindow(mContext);
194     }
195 
196     @Override
197     public int getItemViewType(int position) {
198         return getItem(position).mViewType.ordinal();
199     }
200 
201     public void submitItems(@Nullable MediaItemMetadata parentItem,
202             @Nullable List<MediaItemMetadata> children) {
203         mParentMediaItem = parentItem;
204         if (children == null) {
205             submitList(Collections.emptyList());
206             return;
207         }
208         submitList(generateViewData(children));
209     }
210 
211     private void notify(Consumer<Observer> notification) {
212         for (Observer observer : mObservers) {
213             notification.accept(observer);
214         }
215     }
216 
217     @Override
218     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
219         if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
220             GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager();
221             mMaxSpanSize = manager.getSpanCount();
222             manager.setSpanSizeLookup(getSpanSizeLookup());
223         }
224     }
225 
226     private class ItemsBuilder {
227         private List<BrowseViewData> result = new ArrayList<>();
228 
229         void addItem(MediaItemMetadata item,
230                 BrowseItemViewType viewType, Consumer<Observer> notification) {
231             View.OnClickListener listener = notification != null ?
232                     view -> BrowseAdapter.this.notify(notification) :
233                     null;
234             result.add(new BrowseViewData(item, viewType, listener));
235         }
236 
237         void addTitle(CharSequence title, Consumer<Observer> notification) {
238             if (title == null) {
239                 title = "";
240             }
241             View.OnClickListener listener = notification != null ?
242                     view -> BrowseAdapter.this.notify(notification) :
243                     null;
244             result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener));
245         }
246 
247         void addSpacer() {
248             result.add(new BrowseViewData(BrowseItemViewType.SPACER, null));
249         }
250 
251         List<BrowseViewData> build() {
252             return result;
253         }
254     }
255 
256     /**
257      * Flatten the given collection of item states into a list of {@link BrowseViewData}s. To avoid
258      * flickering, the flatting will stop at the first "loading" section, avoiding unnecessary
259      * insertion animations during the initial data load.
260      */
261     private List<BrowseViewData> generateViewData(List<MediaItemMetadata> items) {
262         ItemsBuilder itemsBuilder = new ItemsBuilder();
263         if (Log.isLoggable(TAG, Log.VERBOSE)) {
264             Log.v(TAG, "Generating browse view from:");
265             for (MediaItemMetadata item : items) {
266                 Log.v(TAG, String.format("[%s%s] '%s' (%s)",
267                         item.isBrowsable() ? "B" : " ",
268                         item.isPlayable() ? "P" : " ",
269                         item.getTitle(),
270                         item.getId()));
271             }
272         }
273 
274         if (mTitle != null) {
275             itemsBuilder.addTitle(mTitle, Observer::onTitleClicked);
276         } else if (!items.isEmpty() && items.get(0).getTitleGrouping() == null) {
277             itemsBuilder.addSpacer();
278         }
279         String currentTitleGrouping = null;
280         for (MediaItemMetadata item : items) {
281             String titleGrouping = item.getTitleGrouping();
282             if (!Objects.equals(currentTitleGrouping, titleGrouping)) {
283                 currentTitleGrouping = titleGrouping;
284                 itemsBuilder.addTitle(titleGrouping, null);
285             }
286             if (item.isBrowsable()) {
287                 itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem),
288                         observer -> observer.onBrowsableItemClicked(item));
289             } else if (item.isPlayable()) {
290                 itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem),
291                         observer -> observer.onPlayableItemClicked(item));
292             }
293         }
294 
295         return itemsBuilder.build();
296     }
297 
298     private BrowseItemViewType getBrowsableViewType(@Nullable MediaItemMetadata mediaItem) {
299         if (mediaItem == null) {
300             return BrowseItemViewType.LIST_ITEM;
301         }
302         if (mediaItem.getBrowsableContentStyleHint() == 0) {
303             return mRootBrowsableViewType;
304         }
305         return fromMediaHint(mediaItem.getBrowsableContentStyleHint());
306     }
307 
308     private BrowseItemViewType getPlayableViewType(@Nullable MediaItemMetadata mediaItem) {
309         if (mediaItem == null) {
310             return BrowseItemViewType.LIST_ITEM;
311         }
312         if (mediaItem.getPlayableContentStyleHint() == 0) {
313             return mRootPlayableViewType;
314         }
315         return fromMediaHint(mediaItem.getPlayableContentStyleHint());
316     }
317 
318     /**
319      * Converts a content style hint to the appropriate {@link BrowseItemViewType}, defaulting to
320      * list items.
321      */
322     private BrowseItemViewType fromMediaHint(int hint) {
323         switch(hint) {
324             case MediaConstants.CONTENT_STYLE_GRID_ITEM_HINT_VALUE:
325                 return BrowseItemViewType.GRID_ITEM;
326             case MediaConstants.CONTENT_STYLE_CATEGORY_GRID_ITEM_HINT_VALUE:
327                 return BrowseItemViewType.ICON_GRID_ITEM;
328             case MediaConstants.CONTENT_STYLE_CATEGORY_LIST_ITEM_HINT_VALUE:
329                 return BrowseItemViewType.ICON_LIST_ITEM;
330             case MediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE:
331             default:
332                 return BrowseItemViewType.LIST_ITEM;
333         }
334     }
335 }
336