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