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