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