blob: f947096bb83113d0aba7af63426891b18729b30a [file] [log] [blame]
/*
* 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}.
*
* <p>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.
*
* <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates.
*/
public class BrowseAdapter extends ListAdapter<BrowseViewData, BrowseViewHolder> {
private static final String TAG = "BrowseAdapter";
@NonNull
private final Context mContext;
@NonNull
private List<Observer> 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<BrowseViewData> DIFF_CALLBACK =
new DiffUtil.ItemCallback<BrowseViewData>() {
@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<MediaItemMetadata> children) {
mParentMediaItem = parentItem;
if (children == null) {
submitList(Collections.emptyList());
return;
}
submitList(generateViewData(children));
}
private void notify(Consumer<Observer> 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<BrowseViewData> result = new ArrayList<>();
void addItem(MediaItemMetadata item,
BrowseItemViewType viewType, Consumer<Observer> notification) {
View.OnClickListener listener = notification != null ?
view -> BrowseAdapter.this.notify(notification) :
null;
result.add(new BrowseViewData(item, viewType, listener));
}
void addTitle(CharSequence title, Consumer<Observer> notification) {
if (title == null) {
title = "";
}
View.OnClickListener listener = notification != null ?
view -> BrowseAdapter.this.notify(notification) :
null;
result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, listener));
}
void addSpacer() {
result.add(new BrowseViewData(BrowseItemViewType.SPACER, null));
}
List<BrowseViewData> 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<BrowseViewData> generateViewData(List<MediaItemMetadata> 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);
} else if (!items.isEmpty() && items.get(0).getTitleGrouping() == null) {
itemsBuilder.addSpacer();
}
String currentTitleGrouping = null;
for (MediaItemMetadata item : items) {
String titleGrouping = item.getTitleGrouping();
if (!Objects.equals(currentTitleGrouping, titleGrouping)) {
currentTitleGrouping = titleGrouping;
itemsBuilder.addTitle(titleGrouping, null);
}
if (item.isBrowsable()) {
itemsBuilder.addItem(item, getBrowsableViewType(mParentMediaItem),
observer -> observer.onBrowsableItemClicked(item));
} else if (item.isPlayable()) {
itemsBuilder.addItem(item, getPlayableViewType(mParentMediaItem),
observer -> observer.onPlayableItemClicked(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;
}
}
}