• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 int getItemViewType(int position) {
186         return getItem(position).mViewType.ordinal();
187     }
188 
189     public void submitItems(@Nullable MediaItemMetadata parentItem,
190             @Nullable List<MediaItemMetadata> children) {
191         mParentMediaItem = parentItem;
192         if (children == null) {
193             submitList(Collections.emptyList());
194             return;
195         }
196         submitList(generateViewData(children));
197     }
198 
199     private void notify(Consumer<Observer> notification) {
200         for (Observer observer : mObservers) {
201             notification.accept(observer);
202         }
203     }
204 
205     @Override
206     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
207         if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
208             GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager();
209             mMaxSpanSize = manager.getSpanCount();
210             manager.setSpanSizeLookup(getSpanSizeLookup());
211         }
212     }
213 
214     private class ItemsBuilder {
215         private List<BrowseViewData> result = new ArrayList<>();
216 
217         void addItem(MediaItemMetadata item,
218                 BrowseItemViewType viewType, Consumer<Observer> notification) {
219             View.OnClickListener listener = notification != null ?
220                     view -> BrowseAdapter.this.notify(notification) :
221                     null;
222             result.add(new BrowseViewData(item, viewType, listener));
223         }
224 
225         void addTitle(CharSequence title, Consumer<Observer> notification) {
226             if (title == null) {
227                 title = "";
228             }
229             View.OnClickListener listener = notification != null ?
230                     view -> BrowseAdapter.this.notify(notification) :
231                     null;
232             result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener));
233         }
234 
235         void addSpacer() {
236             result.add(new BrowseViewData(BrowseItemViewType.SPACER, null));
237         }
238 
239         List<BrowseViewData> build() {
240             return result;
241         }
242     }
243 
244     /**
245      * Flatten the given collection of item states into a list of {@link BrowseViewData}s. To avoid
246      * flickering, the flatting will stop at the first "loading" section, avoiding unnecessary
247      * insertion animations during the initial data load.
248      */
249     private List<BrowseViewData> generateViewData(List<MediaItemMetadata> items) {
250         ItemsBuilder itemsBuilder = new ItemsBuilder();
251         if (Log.isLoggable(TAG, Log.VERBOSE)) {
252             Log.v(TAG, "Generating browse view from:");
253             for (MediaItemMetadata item : items) {
254                 Log.v(TAG, String.format("[%s%s] '%s' (%s)",
255                         item.isBrowsable() ? "B" : " ",
256                         item.isPlayable() ? "P" : " ",
257                         item.getTitle(),
258                         item.getId()));
259             }
260         }
261 
262         if (mTitle != null) {
263             itemsBuilder.addTitle(mTitle, Observer::onTitleClicked);
264         } else if (!items.isEmpty() && items.get(0).getTitleGrouping() == null) {
265             itemsBuilder.addSpacer();
266         }
267         String currentTitleGrouping = null;
268         for (MediaItemMetadata item : items) {
269             String titleGrouping = item.getTitleGrouping();
270             if (!Objects.equals(currentTitleGrouping, titleGrouping)) {
271                 currentTitleGrouping = titleGrouping;
272                 itemsBuilder.addTitle(titleGrouping, null);
273             }
274             if (item.isBrowsable()) {
275                 itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem),
276                         observer -> observer.onBrowsableItemClicked(item));
277             } else if (item.isPlayable()) {
278                 itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem),
279                         observer -> observer.onPlayableItemClicked(item));
280             }
281         }
282 
283         return itemsBuilder.build();
284     }
285 
286     private BrowseItemViewType getBrowsableViewType(@Nullable MediaItemMetadata mediaItem) {
287         if (mediaItem == null) {
288             return BrowseItemViewType.LIST_ITEM;
289         }
290         if (mediaItem.getBrowsableContentStyleHint() == 0) {
291             return mRootBrowsableViewType;
292         }
293         return fromMediaHint(mediaItem.getBrowsableContentStyleHint());
294     }
295 
296     private BrowseItemViewType getPlayableViewType(@Nullable MediaItemMetadata mediaItem) {
297         if (mediaItem == null) {
298             return BrowseItemViewType.LIST_ITEM;
299         }
300         if (mediaItem.getPlayableContentStyleHint() == 0) {
301             return mRootPlayableViewType;
302         }
303         return fromMediaHint(mediaItem.getPlayableContentStyleHint());
304     }
305 
306     /**
307      * Converts a content style hint to the appropriate {@link BrowseItemViewType}, defaulting to
308      * list items.
309      */
310     private BrowseItemViewType fromMediaHint(int hint) {
311         switch(hint) {
312             case MediaConstants.CONTENT_STYLE_GRID_ITEM_HINT_VALUE:
313                 return BrowseItemViewType.GRID_ITEM;
314             case MediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE:
315                 return BrowseItemViewType.LIST_ITEM;
316             default:
317                 return BrowseItemViewType.LIST_ITEM;
318         }
319     }
320 }
321