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