/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.car.media.browse; import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import com.android.car.media.common.MediaConstants; import com.android.car.media.common.MediaItemMetadata; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Consumer; /** * A {@link RecyclerView.Adapter} that can be used to display a single level of a {@link * android.service.media.MediaBrowserService} media tree into a {@link * androidx.car.widget.PagedListView} or any other {@link RecyclerView}. * *

This adapter assumes that the attached {@link RecyclerView} uses a {@link GridLayoutManager}, * as it can use both grid and list elements to produce the desired representation. * *

Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates. */ public class BrowseAdapter extends ListAdapter { private static final String TAG = "BrowseAdapter"; @NonNull private final Context mContext; @NonNull private List mObservers = new ArrayList<>(); @Nullable private CharSequence mTitle; @Nullable private MediaItemMetadata mParentMediaItem; private int mMaxSpanSize = 1; private BrowseItemViewType mRootBrowsableViewType = BrowseItemViewType.LIST_ITEM; private BrowseItemViewType mRootPlayableViewType = BrowseItemViewType.LIST_ITEM; private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override public boolean areItemsTheSame(@NonNull BrowseViewData oldItem, @NonNull BrowseViewData newItem) { return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem) && Objects.equals(oldItem.mText, newItem.mText); } @Override public boolean areContentsTheSame(@NonNull BrowseViewData oldItem, @NonNull BrowseViewData newItem) { return oldItem.equals(newItem); } }; /** * An {@link BrowseAdapter} observer. */ public static abstract class Observer { /** * Callback invoked when a user clicks on a playable item. */ protected void onPlayableItemClicked(MediaItemMetadata item) { } /** * Callback invoked when a user clicks on a browsable item. */ protected void onBrowsableItemClicked(MediaItemMetadata item) { } /** * Callback invoked when the user clicks on the title of the queue. */ protected void onTitleClicked() { } } /** * Creates a {@link BrowseAdapter} that displays the children of the given media tree node. */ public BrowseAdapter(@NonNull Context context) { super(DIFF_CALLBACK); mContext = context; } /** * Sets title to be displayed. */ public void setTitle(CharSequence title) { mTitle = title; } /** * Registers an {@link Observer} */ public void registerObserver(Observer observer) { mObservers.add(observer); } /** * Unregisters an {@link Observer} */ public void unregisterObserver(Observer observer) { mObservers.remove(observer); } /** * Sets the number of columns that items can take. This method only needs to be used if the * attached {@link RecyclerView} is NOT using a {@link GridLayoutManager}. This class will * automatically determine this value on {@link #onAttachedToRecyclerView(RecyclerView)} * otherwise. */ public void setMaxSpanSize(int maxSpanSize) { mMaxSpanSize = maxSpanSize; } public void setRootBrowsableViewType(int hintValue) { mRootBrowsableViewType = fromMediaHint(hintValue); } public void setRootPlayableViewType(int hintValue) { mRootPlayableViewType = fromMediaHint(hintValue); } /** * @return a {@link GridLayoutManager.SpanSizeLookup} that can be used to obtain the span size * of each item in this adapter. This method is only needed if the {@link RecyclerView} is NOT * using a {@link GridLayoutManager}. This class will automatically use it on\ {@link * #onAttachedToRecyclerView(RecyclerView)} otherwise. */ private GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { return new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { BrowseItemViewType viewType = getItem(position).mViewType; return viewType.getSpanSize(mMaxSpanSize); } }; } @NonNull @Override public BrowseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { int layoutId = BrowseItemViewType.values()[viewType].getLayoutId(); View view = LayoutInflater.from(mContext).inflate(layoutId, parent, false); return new BrowseViewHolder(view); } @Override public void onBindViewHolder(@NonNull BrowseViewHolder holder, int position) { BrowseViewData viewData = getItem(position); holder.bind(mContext, viewData); } @Override public int getItemViewType(int position) { return getItem(position).mViewType.ordinal(); } public void submitItems(@Nullable MediaItemMetadata parentItem, @Nullable List children) { mParentMediaItem = parentItem; if (children == null) { submitList(Collections.emptyList()); return; } submitList(generateViewData(children)); } private void notify(Consumer notification) { for (Observer observer : mObservers) { notification.accept(observer); } } @Override public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager(); mMaxSpanSize = manager.getSpanCount(); manager.setSpanSizeLookup(getSpanSizeLookup()); } } private class ItemsBuilder { private List result = new ArrayList<>(); void addItem(MediaItemMetadata item, BrowseItemViewType viewType, Consumer notification) { View.OnClickListener listener = notification != null ? view -> BrowseAdapter.this.notify(notification) : null; result.add(new BrowseViewData(item, viewType, listener)); } void addTitle(CharSequence title, Consumer notification) { if (title == null) { title = ""; } View.OnClickListener listener = notification != null ? view -> BrowseAdapter.this.notify(notification) : null; result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener)); } List build() { return result; } } /** * Flatten the given collection of item states into a list of {@link BrowseViewData}s. To avoid * flickering, the flatting will stop at the first "loading" section, avoiding unnecessary * insertion animations during the initial data load. */ private List generateViewData(Collection items) { ItemsBuilder itemsBuilder = new ItemsBuilder(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Generating browse view from:"); for (MediaItemMetadata item : items) { Log.v(TAG, String.format("[%s%s] '%s' (%s)", item.isBrowsable() ? "B" : " ", item.isPlayable() ? "P" : " ", item.getTitle(), item.getId())); } } if (mTitle != null) { itemsBuilder.addTitle(mTitle, Observer::onTitleClicked); } String currentTitleGrouping = null; for (MediaItemMetadata item : items) { String titleGrouping = item.getTitleGrouping(); if (!Objects.equals(currentTitleGrouping, titleGrouping)) { currentTitleGrouping = titleGrouping; itemsBuilder.addTitle(titleGrouping, null); } if (item.isPlayable()) { itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem), observer -> observer.onPlayableItemClicked(item)); } else if (item.isBrowsable()) { itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem), observer -> observer.onBrowsableItemClicked(item)); } } return itemsBuilder.build(); } private BrowseItemViewType getBrowsableViewType(@Nullable MediaItemMetadata mediaItem) { if (mediaItem == null) { return BrowseItemViewType.LIST_ITEM; } if (mediaItem.getBrowsableContentStyleHint() == 0) { return mRootBrowsableViewType; } return fromMediaHint(mediaItem.getBrowsableContentStyleHint()); } private BrowseItemViewType getPlayableViewType(@Nullable MediaItemMetadata mediaItem) { if (mediaItem == null) { return BrowseItemViewType.LIST_ITEM; } if (mediaItem.getPlayableContentStyleHint() == 0) { return mRootPlayableViewType; } return fromMediaHint(mediaItem.getPlayableContentStyleHint()); } /** * Converts a content style hint to the appropriate {@link BrowseItemViewType}, defaulting to * list items. */ private BrowseItemViewType fromMediaHint(int hint) { switch(hint) { case MediaConstants.CONTENT_STYLE_GRID_ITEM_HINT_VALUE: return BrowseItemViewType.GRID_ITEM; case MediaConstants.CONTENT_STYLE_LIST_ITEM_HINT_VALUE: return BrowseItemViewType.LIST_ITEM; default: return BrowseItemViewType.LIST_ITEM; } } }