summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2018-04-09 07:24:38 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2018-04-09 07:24:38 +0000
commitbc339a36c69e8379fc7e66f1136061ef7f421e89 (patch)
tree5d7b46b797ab075441ec6df25029badf6df499e6
parent3c7f9582c73ffcfb100164d007b88d974eed9ef8 (diff)
parent7e6aeee1cb8d64ad271a25c0bc8e0bbe4068ca9f (diff)
downloadMedia-bc339a36c69e8379fc7e66f1136061ef7f421e89.tar.gz
Snap for 4707594 from 7e6aeee1cb8d64ad271a25c0bc8e0bbe4068ca9f to pi-release
Change-Id: Ib1d47f18ce32d565e28a1f522307fc30a2783782
-rw-r--r--res/layout/fragment_playback.xml7
-rw-r--r--res/layout/media_browse_grid_item.xml26
-rw-r--r--res/layout/media_browse_header_item.xml13
-rw-r--r--res/layout/media_browse_list_item.xml41
-rw-r--r--res/layout/media_browse_more_footer.xml15
-rw-r--r--res/layout/media_browse_panel_item.xml38
-rw-r--r--res/values/strings.xml2
-rw-r--r--src/com/android/car/media/PlaybackFragment.java192
-rw-r--r--src/com/android/car/media/browse/BrowseAdapter.java542
-rw-r--r--src/com/android/car/media/browse/BrowseItemViewType.java70
-rw-r--r--src/com/android/car/media/browse/BrowseViewData.java102
-rw-r--r--src/com/android/car/media/browse/BrowseViewHolder.java70
-rw-r--r--src/com/android/car/media/browse/ContentForwardStrategy.java120
13 files changed, 1165 insertions, 73 deletions
diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml
index e37a712..601844f 100644
--- a/res/layout/fragment_playback.xml
+++ b/res/layout/fragment_playback.xml
@@ -56,12 +56,9 @@
android:id="@+id/browse_list"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1"
- app:showPagedListViewDivider="true"
- app:listDividerColor="@color/car_list_divider_inverse"
+ android:layout_marginBottom="@dimen/car_padding_4"
+ app:showPagedListViewDivider="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
- app:dividerStartMargin="@dimen/car_keyline_1"
- app:dividerEndMargin="@dimen/car_keyline_1"
app:layout_constraintTop_toBottomOf="@+id/metadata_container"
app:layout_constraintBottom_toTopOf="@+id/playback_controls"/>
diff --git a/res/layout/media_browse_grid_item.xml b/res/layout/media_browse_grid_item.xml
new file mode 100644
index 0000000..086f11a
--- /dev/null
+++ b/res/layout/media_browse_grid_item.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.android.car.media.common.FixedRatioImageView
+ android:id="@+id/thumbnail"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:scaleType="centerCrop"
+ app:aspect_ratio="1"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/car_padding_2"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.Car.Body2.Light"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/media_browse_header_item.xml b/res/layout/media_browse_header_item.xml
new file mode 100644
index 0000000..b4db506
--- /dev/null
+++ b/res/layout/media_browse_header_item.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:textAppearance="@style/TextAppearance.Car.Body1.Light"/>
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/media_browse_list_item.xml b/res/layout/media_browse_list_item.xml
new file mode 100644
index 0000000..f20b20d
--- /dev/null
+++ b/res/layout/media_browse_list_item.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/thumbnail"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+ android:layout_gravity="center_vertical"
+ android:scaleType="centerCrop" />
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_padding_1"
+ android:includeFontPadding="false"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.Car.Body1.Light" />
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAppearance="@style/TextAppearance.Car.Body2.Light" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/media_browse_more_footer.xml b/res/layout/media_browse_more_footer.xml
new file mode 100644
index 0000000..ea79378
--- /dev/null
+++ b/res/layout/media_browse_more_footer.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:id="@+id/title"
+ android:text="@string/media_browse_more"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_padding_1"
+ android:includeFontPadding="false"
+ android:textAppearance="@style/TextAppearance.Car.Body1.Light"/>
+</FrameLayout>
diff --git a/res/layout/media_browse_panel_item.xml b/res/layout/media_browse_panel_item.xml
new file mode 100644
index 0000000..3b56c36
--- /dev/null
+++ b/res/layout/media_browse_panel_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/thumbnail"
+ android:layout_width="@dimen/car_drawer_list_item_icon_size"
+ android:layout_height="@dimen/car_drawer_list_item_icon_size"
+ android:layout_marginEnd="@dimen/car_drawer_list_item_icon_end_margin"
+ android:layout_gravity="center_vertical"
+ android:scaleType="centerCrop" />
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/car_text_vertical_margin"
+ android:maxLines="1"
+ android:textAppearance="?attr/drawerItemTitleTextAppearance" />
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAppearance="?attr/drawerItemBodyTextAppearance" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index dbe3930..84d60fd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -26,4 +26,6 @@
<string name="loading">Getting your selection&#8230;</string>
<!-- The text for unknown playback error. [CHAR LIMIT=50] -->
<string name="unknown_error">Something went wrong</string>
+ <!-- Text of the button displayed at the bottom of a media browse section to provide access to additional items. [CHAR LIMIT=100] -->
+ <string name="media_browse_more">More …</string>
</resources>
diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java
index 69ca0b0..bf1e9ec 100644
--- a/src/com/android/car/media/PlaybackFragment.java
+++ b/src/com/android/car/media/PlaybackFragment.java
@@ -1,12 +1,11 @@
package com.android.car.media;
-import static java.security.AccessController.getContext;
-
-import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
+import android.media.browse.MediaBrowser;
import android.os.Bundle;
import android.support.v4.app.Fragment;
+import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -16,29 +15,29 @@ import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
-import com.android.car.apps.common.ImageUtils;
+import com.android.car.media.browse.BrowseAdapter;
+import com.android.car.media.browse.ContentForwardStrategy;
+import com.android.car.media.common.GridSpacingItemDecoration;
import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.MediaSource;
import com.android.car.media.common.PlaybackControls;
import com.android.car.media.common.PlaybackModel;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
-import java.util.List;
import java.util.Locale;
+import java.util.Objects;
-import androidx.car.widget.ListItem;
-import androidx.car.widget.ListItemAdapter;
-import androidx.car.widget.ListItemProvider;
import androidx.car.widget.PagedListView;
-import androidx.car.widget.TextListItem;
/**
* A {@link Fragment} that implements both the playback and the content forward browsing experience.
* It observes a {@link PlaybackModel} and updates its information depending on the currently
- * playing media source through the {@link MediaSession} API.
+ * playing media source through the {@link android.media.session.MediaSession} API.
*/
-public class PlaybackFragment extends Fragment {
+public class PlaybackFragment extends Fragment implements PlaybackModel.PlaybackObserver,
+ MediaSource.Observer, BrowseAdapter.Observer {
private static final String TAG = "PlaybackFragment";
private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US);
@@ -51,64 +50,18 @@ public class PlaybackFragment extends Fragment {
private TextView mSubtitle;
private SeekBar mSeekbar;
private PagedListView mBrowseList;
- private ListItemAdapter mMediaAdapter;
private int mBackgroundRawImageSize;
private float mBackgroundBlurRadius;
private float mBackgroundBlurScale;
-
- private PlaybackModel.PlaybackObserver mObserver = new PlaybackModel.PlaybackObserver() {
- @Override
- public void onPlaybackStateChanged() {
- updateState();
- }
-
- @Override
- public void onSourceChanged() {
- updateState();
- updateMetadata();
- updateAccentColor();
- updateBrowse();
- }
-
- @Override
- public void onMetadataChanged() {
- updateMetadata();
- }
- };
-
- private ListItemProvider mMediaItemsProvider = new ListItemProvider() {
- @Override
- public ListItem get(int position) {
- if (mModel == null || !mModel.hasQueue()) {
- return null;
- }
- List<MediaItemMetadata> queue = mModel.getQueue();
- if (position < 0 || position >= queue.size()) {
- return null;
- }
- MediaItemMetadata item = queue.get(position);
- TextListItem textListItem = new TextListItem(getContext());
- textListItem.setTitle(item.getTitle().toString());
- textListItem.setBody(item.getSubtitle().toString());
- textListItem.setOnClickListener(v -> mModel.onSkipToQueueItem(item.getQueueId()));
- return textListItem;
- }
-
- @Override
- public int size() {
- if (mModel == null || !mModel.hasQueue()) {
- return 0;
- }
- return mModel.getQueue().size();
- }
- };
+ private MediaSource mMediaSource;
+ private BrowseAdapter mBrowseAdapter;
@Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_playback, container, false);
mModel = new PlaybackModel(getContext());
- mModel.registerObserver(mObserver);
+ mModel.registerObserver(this);
mAlbumBackground = view.findViewById(R.id.album_background);
mPlaybackControls = view.findViewById(R.id.playback_controls);
mPlaybackControls.setModel(mModel);
@@ -120,12 +73,14 @@ public class PlaybackFragment extends Fragment {
mSeekbar = view.findViewById(R.id.seek_bar);
mTime = view.findViewById(R.id.time);
mBrowseList = view.findViewById(R.id.browse_list);
- mMediaAdapter = new ListItemAdapter(getContext(), mMediaItemsProvider);
- mBrowseList.setAdapter(mMediaAdapter);
- RecyclerView recyclerView = mBrowseList.findViewById(R.id.recycler_view);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 4);
+ RecyclerView recyclerView = mBrowseList.getRecyclerView();
+ recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setVerticalFadingEdgeEnabled(true);
recyclerView.setFadingEdgeLength(getResources()
- .getDimensionPixelSize(R.dimen.car_padding_3));
+ .getDimensionPixelSize(R.dimen.car_padding_4));
+ recyclerView.addItemDecoration(new GridSpacingItemDecoration(getResources()
+ .getDimensionPixelSize(R.dimen.car_padding_4)));
TypedValue outValue = new TypedValue();
getResources().getValue(R.dimen.playback_background_blur_radius, outValue, true);
mBackgroundBlurRadius = outValue.getFloat();
@@ -140,14 +95,42 @@ public class PlaybackFragment extends Fragment {
public void onStart() {
super.onStart();
mModel.start();
- mMediaAdapter.start();
+ if (mMediaSource != null) {
+ mMediaSource.subscribe(this);
+ }
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.start();
+ }
}
@Override
public void onStop() {
super.onStop();
mModel.stop();
- mMediaAdapter.stop();
+ if (mMediaSource != null) {
+ mMediaSource.unsubscribe(this);
+ }
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.stop();
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged() {
+ updateState();
+ }
+
+ @Override
+ public void onSourceChanged() {
+ updateState();
+ updateMetadata();
+ updateAccentColor();
+ updateBrowse();
+ }
+
+ @Override
+ public void onMetadataChanged() {
+ updateMetadata();
}
private void updateState() {
@@ -159,6 +142,10 @@ public class PlaybackFragment extends Fragment {
mSeekbar.removeCallbacks(mSeekBarRunnable);
}
mBrowseList.setVisibility(mModel.hasQueue() ? View.VISIBLE : View.GONE);
+
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.setQueue(mModel.getQueue(), mModel.getQueueTitle());
+ }
}
private void updateMetadata() {
@@ -186,7 +173,8 @@ public class PlaybackFragment extends Fragment {
}
private void updateAccentColor() {
- int color = mModel.getAccentColor();
+ int defaultColor = getResources().getColor(android.R.color.background_dark, null);
+ int color = mModel.getMediaSource().getAccentColor(defaultColor);
mSeekbar.getProgressDrawable().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
@@ -225,7 +213,75 @@ public class PlaybackFragment extends Fragment {
}
private void updateBrowse() {
+ MediaSource newSource = mModel.getMediaSource();
+ if (Objects.equals(mMediaSource, newSource)) {
+ return;
+ }
+ if (mMediaSource != null) {
+ mMediaSource.unsubscribe(this);
+ }
+ mMediaSource = newSource;
+ mMediaSource.subscribe(this);
MediaManager.getInstance(getContext())
- .setMediaClientComponent(mModel.getMediaBrowseServiceComponent());
+ .setMediaClientComponent(mMediaSource.getBrowseServiceComponentName());
+ }
+
+ @Override
+ public void onBrowseConnected(MediaBrowser mediaBrowser) {
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.stop();
+ mBrowseAdapter = null;
+ }
+ if (mediaBrowser == null) {
+ mBrowseList.setVisibility(View.GONE);
+ // TODO(b/77647430) implement intermediate states.
+ return;
+ }
+ mBrowseAdapter = new BrowseAdapter(getContext(), mediaBrowser, null,
+ ContentForwardStrategy.DEFAULT_STRATEGY);
+ mBrowseList.setAdapter(mBrowseAdapter);
+ mBrowseAdapter.registerObserver(this);
+ mBrowseAdapter.start();
+ }
+
+ @Override
+ public void onBrowseDisconnected() {
+ mBrowseAdapter.stop();
+ }
+
+ @Override
+ public void onDirty() {
+ mBrowseAdapter.update();
+ if (mBrowseAdapter.getItemCount() > 0) {
+ mBrowseList.setVisibility(View.VISIBLE);
+ } else {
+ mBrowseList.setVisibility(View.GONE);
+ // TODO(b/77647430) implement intermediate states.
+ }
+ }
+
+ @Override
+ public void onPlayableItemClicked(MediaItemMetadata item) {
+ mModel.onPlayItem(item.getId());
+ }
+
+ @Override
+ public void onBrowseableItemClicked(MediaItemMetadata item) {
+ // TODO(b/77527398): Drill down in the navigation.
+ }
+
+ @Override
+ public void onMoreButtonClicked(MediaItemMetadata item) {
+ // TODO(b/77527398): Drill down in the navigation
+ }
+
+ @Override
+ public void onQueueTitleClicked() {
+ // TODO(b/77527398): Show full queue
+ }
+
+ @Override
+ public void onQueueItemClicked(MediaItemMetadata item) {
+ mModel.onSkipToQueueItem(item.getQueueId());
}
}
diff --git a/src/com/android/car/media/browse/BrowseAdapter.java b/src/com/android/car/media/browse/BrowseAdapter.java
new file mode 100644
index 0000000..be50b9b
--- /dev/null
+++ b/src/com/android/car/media/browse/BrowseAdapter.java
@@ -0,0 +1,542 @@
+/*
+ * 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.media.browse.MediaBrowser;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.util.DiffUtil;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.car.media.common.MediaItemMetadata;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * 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> The actual strategy to group and expand media items has to be supplied by providing an
+ * instance of {@link ContentForwardStrategy}.
+ *
+ * <p> The adapter will only start updating once {@link #start()} is invoked. At this point, the
+ * provided {@link MediaBrowser} must be already in connected state.
+ *
+ * <p>Resources and asynchronous data loading must be released by callign {@link #stop()}.
+ *
+ * <p>No views will be actually updated until {@link #update()} is invoked (normally as a result of
+ * the {@link Observer#onDirty()} event. This way, the consumer of this adapter has the opportunity
+ * to decide whether updates should be displayd immediately, or if they should be delayed to
+ * prevent flickering.
+ *
+ * <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates.
+ */
+public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
+ private static final String TAG = "MediaBrowseAdapter";
+ @NonNull
+ private final Context mContext;
+ private final MediaBrowser mMediaBrowser;
+ private final MediaItemMetadata mParentMediaItem;
+ private final ContentForwardStrategy mCFBStrategy;
+ private LinkedHashMap<String, MediaItemState> mItemStates = new LinkedHashMap<>();
+ private List<BrowseViewData> mViewData = new ArrayList<>();
+ private String mParentMediaItemId;
+ private List<Observer> mObservers = new ArrayList<>();
+ private List<MediaItemMetadata> mQueue;
+ private CharSequence mQueueTitle;
+ private int mMaxSpanSize = 1;
+ private BrowseViewData.State mState = BrowseViewData.State.IDLE;
+
+ /**
+ * An {@link BrowseAdapter} observer.
+ */
+ public interface Observer {
+ /**
+ * Callback invoked anytime there is more information to be displayed, or if there is a
+ * change in the overall state of the adapter.
+ */
+ void onDirty();
+
+ /**
+ * Callback invoked when a user clicks on a playable item.
+ */
+ void onPlayableItemClicked(MediaItemMetadata item);
+
+ /**
+ * Callback invoked when a user clicks on a browsable item.
+ */
+ void onBrowseableItemClicked(MediaItemMetadata item);
+
+ /**
+ * Callback invoked when a user clicks on a the "more items" button on a section.
+ */
+ void onMoreButtonClicked(MediaItemMetadata item);
+
+ /**
+ * Callback invoked when the user clicks on the title of the queue.
+ */
+ void onQueueTitleClicked();
+
+ /**
+ * Callback invoked when the user clicks on a queue item.
+ */
+ void onQueueItemClicked(MediaItemMetadata item);
+ }
+
+ private MediaBrowser.SubscriptionCallback mSubscriptionCallback =
+ new MediaBrowser.SubscriptionCallback() {
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
+ onItemsLoaded(parentId, children);
+ }
+
+ @Override
+ public void onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children,
+ Bundle options) {
+ onItemsLoaded(parentId, children);
+ }
+
+ @Override
+ public void onError(String parentId) {
+ onLoadingError(parentId);
+ }
+
+ @Override
+ public void onError(String parentId, Bundle options) {
+ onLoadingError(parentId);
+ }
+ };
+
+
+ /**
+ * Represents the loading state of children of a single {@link MediaItemMetadata} in the
+ * {@link BrowseAdapter}
+ */
+ private class MediaItemState {
+ /**
+ * {@link com.android.car.media.common.MediaItemMetadata} whose children are being loaded
+ */
+ final MediaItemMetadata mItem;
+ /** Current loading state for this item */
+ BrowseViewData.State mState = BrowseViewData.State.LOADING;
+ /** Playable children of the given item */
+ List<MediaItemMetadata> mPlayableChildren = new ArrayList<>();
+ /** Browsable children of the given item */
+ List<MediaItemMetadata> mBrowsableChildren = new ArrayList<>();
+ /** Whether we are subscribed to updates for this item or not */
+ boolean mIsSubscribed;
+
+ MediaItemState(MediaBrowser.MediaItem item) {
+ mItem = new MediaItemMetadata(item);
+ }
+
+ void setChildren(List<MediaBrowser.MediaItem> children) {
+ mPlayableChildren.clear();
+ mBrowsableChildren.clear();
+ for (MediaBrowser.MediaItem child : children) {
+ if (child.isBrowsable()) {
+ // Browsable items could also be playable
+ mBrowsableChildren.add(new MediaItemMetadata(child));
+ } else if (child.isPlayable()) {
+ mPlayableChildren.add(new MediaItemMetadata(child));
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link BrowseAdapter} that displays the children of the given media tree node.
+ *
+ * @param mediaBrowser the {@link MediaBrowser} to get data from.
+ * @param parentItem the node to display children of, or NULL if the
+ * @param strategy a {@link ContentForwardStrategy} that would determine which items would be
+ * expanded and how.
+ */
+ public BrowseAdapter(Context context, @NonNull MediaBrowser mediaBrowser,
+ @Nullable MediaItemMetadata parentItem, @NonNull ContentForwardStrategy strategy) {
+ mContext = context;
+ mMediaBrowser = mediaBrowser;
+ mParentMediaItem = parentItem;
+ mCFBStrategy = strategy;
+ }
+
+ /**
+ * Initiates or resumes the data loading process and subscribes to updates. The client can use
+ * {@link #registerObserver(Observer)} to receive updates on the progress.
+ */
+ public void start() {
+ mParentMediaItemId = mParentMediaItem != null
+ ? mParentMediaItem.getId()
+ : mMediaBrowser.getRoot();
+ mMediaBrowser.subscribe(mParentMediaItemId, mSubscriptionCallback);
+ for (MediaItemState itemState : mItemStates.values()) {
+ subscribe(itemState);
+ }
+ }
+
+ /**
+ * Stops the data loading and releases any subscriptions.
+ */
+ public void stop() {
+ if (mParentMediaItemId == null) {
+ // Not started
+ return;
+ }
+ mMediaBrowser.unsubscribe(mParentMediaItemId, mSubscriptionCallback);
+ for (MediaItemState itemState : mItemStates.values()) {
+ unsubscribe(itemState);
+ }
+ mParentMediaItemId = null;
+ }
+
+ /**
+ * Sets media queue items into this adapter.
+ */
+ public void setQueue(List<MediaItemMetadata> items, CharSequence queueTitle) {
+ mQueue = items;
+ mQueueTitle = queueTitle;
+ notify(Observer::onDirty);
+ }
+
+ /**
+ * Registers an {@link Observer}
+ */
+ public void registerObserver(Observer observer) {
+ mObservers.add(observer);
+ }
+
+ /**
+ * Unregisters an {@link Observer}
+ */
+ public void unregisterObserver(Observer observer) {
+ mObservers.remove(observer);
+ }
+
+ /**
+ * @return the global loading state. Consumers can use this state to determine if more
+ * information is still pending to arrive or not. This method will report
+ * {@link BrowseViewData.State#ERROR} only if the list of immediate children fails to load.
+ */
+ public BrowseViewData.State getState() {
+ return mState;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @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.
+ */
+ public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() {
+ return new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ BrowseItemViewType viewType = mViewData.get(position).mViewType;
+ return viewType.getSpanSize(mMaxSpanSize);
+ }
+ };
+ }
+
+ /**
+ * Updates the {@link RecyclerView} with newly loaded information. This normally should be
+ * invoked as a result of a {@link Observer#onDirty()} callback.
+ *
+ * This method is idempotent and can be used at any time (even delayed if needed). Additions,
+ * removals and insertions would be notified to the {@link RecyclerView} so it can be
+ * animated appropriately.
+ */
+ public void update() {
+ List<BrowseViewData> newItems = generateViewData(mItemStates.values());
+ List<BrowseViewData> oldItems = mViewData;
+ mViewData = newItems;
+ DiffUtil.DiffResult result = DiffUtil.calculateDiff(createDiffUtil(oldItems, newItems));
+ result.dispatchUpdatesTo(this);
+ }
+
+ private void subscribe(MediaItemState state) {
+ if (!state.mIsSubscribed && state.mItem.isBrowsable()) {
+ mMediaBrowser.subscribe(state.mItem.getId(), mSubscriptionCallback);
+ state.mIsSubscribed = true;
+ }
+ }
+
+ private void unsubscribe(MediaItemState state) {
+ if (state.mIsSubscribed) {
+ mMediaBrowser.unsubscribe(state.mItem.getId(), mSubscriptionCallback);
+ state.mIsSubscribed = false;
+ }
+ }
+
+ @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 = mViewData.get(position);
+ holder.bind(mContext, viewData);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mViewData.size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return mViewData.get(position).mViewType.ordinal();
+ }
+
+ private void onItemsLoaded(String parentId, List<MediaBrowser.MediaItem> children) {
+ if (parentId.equals(mParentMediaItemId)) {
+ // Direct children from the requested media item id. Update subscription list.
+ LinkedHashMap<String, MediaItemState> newItemStates = new LinkedHashMap<>();
+ for (MediaBrowser.MediaItem item : children) {
+ MediaItemState itemState = mItemStates.get(item.getMediaId());
+ if (itemState != null) {
+ // Reuse existing section.
+ newItemStates.put(item.getMediaId(), itemState);
+ mItemStates.remove(item.getMediaId());
+ } else {
+ // New section, subscribe to it.
+ itemState = new MediaItemState(item);
+ newItemStates.put(item.getMediaId(), itemState);
+ subscribe(itemState);
+ }
+ }
+ // Remove unused sections
+ for (MediaItemState itemState : mItemStates.values()) {
+ unsubscribe(itemState);
+ }
+ mItemStates = newItemStates;
+ } else {
+ MediaItemState itemState = mItemStates.get(parentId);
+ if (itemState == null) {
+ Log.w(TAG, "Loaded children for a section we don't have: " + parentId);
+ return;
+ }
+ itemState.setChildren(children);
+ itemState.mState = BrowseViewData.State.LOADED;
+ }
+ updateGlobalState();
+ notify(Observer::onDirty);
+ }
+
+ private void notify(Consumer<Observer> notification) {
+ for (Observer observer : mObservers) {
+ notification.accept(observer);
+ }
+ }
+
+ private void onLoadingError(String parentId) {
+ if (parentId.equals(mParentMediaItemId)) {
+ mState = BrowseViewData.State.ERROR;
+ } else {
+ MediaItemState state = mItemStates.get(parentId);
+ if (state == null) {
+ Log.w(TAG, "Error loading children for a section we don't have: " + parentId);
+ return;
+ }
+ state.setChildren(new ArrayList<>());
+ state.mState = BrowseViewData.State.ERROR;
+ }
+ updateGlobalState();
+ notify(Observer::onDirty);
+ }
+
+ private void updateGlobalState() {
+ for (MediaItemState state: mItemStates.values()) {
+ if (state.mState == BrowseViewData.State.LOADING) {
+ mState = BrowseViewData.State.LOADING;
+ return;
+ }
+ }
+ mState = BrowseViewData.State.LOADED;
+ }
+
+ private DiffUtil.Callback createDiffUtil(List<BrowseViewData> oldList,
+ List<BrowseViewData> newList) {
+ return new DiffUtil.Callback() {
+ @Override
+ public int getOldListSize() {
+ return oldList.size();
+ }
+
+ @Override
+ public int getNewListSize() {
+ return newList.size();
+ }
+
+ @Override
+ public boolean areItemsTheSame(int oldPos, int newPos) {
+ BrowseViewData oldItem = oldList.get(oldPos);
+ BrowseViewData newItem = newList.get(newPos);
+
+ return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem)
+ && Objects.equals(oldItem.mText, newItem.mText);
+ }
+
+ @Override
+ public boolean areContentsTheSame(int oldPos, int newPos) {
+ BrowseViewData oldItem = oldList.get(oldPos);
+ BrowseViewData newItem = newList.get(newPos);
+
+ return oldItem.equals(newItem);
+ }
+ };
+ }
+
+ @Override
+ public void onAttachedToRecyclerView(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, BrowseViewData.State state,
+ BrowseItemViewType viewType, Consumer<Observer> notification) {
+ result.add(new BrowseViewData(item, viewType, state,
+ view -> BrowseAdapter.this.notify(notification)));
+ }
+
+ void addItems(List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxRows) {
+ int spanSize = viewType.getSpanSize(mMaxSpanSize);
+ int maxChildren = maxRows * (mMaxSpanSize / spanSize);
+ result.addAll(items.stream()
+ .limit(maxChildren)
+ .map(item -> {
+ Consumer<Observer> notification = item.getQueueId() != null
+ ? observer -> observer.onQueueItemClicked(item)
+ : item.isBrowsable()
+ ? observer -> observer.onBrowseableItemClicked(item)
+ : observer -> observer.onPlayableItemClicked(item);
+ return new BrowseViewData(item, viewType, null, view ->
+ BrowseAdapter.this.notify(notification));
+ })
+ .collect(Collectors.toList()));
+ }
+
+ void addTitle(CharSequence title, Consumer<Observer> notification) {
+ result.add(new BrowseViewData(title, BrowseItemViewType.HEADER,
+ view -> BrowseAdapter.this.notify(notification)));
+
+ }
+
+ void addBrowseBlock(MediaItemMetadata header, BrowseViewData.State state,
+ List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxChildren,
+ boolean showHeader, boolean showMoreFooter) {
+ if (showHeader) {
+ addItem(header, state, BrowseItemViewType.HEADER, null);
+ }
+ addItems(items, viewType, maxChildren);
+ if (showMoreFooter) {
+ addItem(header, null, BrowseItemViewType.MORE_FOOTER,
+ observer -> observer.onMoreButtonClicked(header));
+ }
+ }
+
+ 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(Collection<MediaItemState> itemStates) {
+ ItemsBuilder itemsBuilder = new ItemsBuilder();
+
+ if (mQueue != null && !mQueue.isEmpty() && mCFBStrategy.getMaxQueueRows() > 0
+ && mCFBStrategy.getQueueViewType() != null) {
+ if (mQueueTitle != null) {
+ itemsBuilder.addTitle(mQueueTitle, Observer::onQueueTitleClicked);
+ }
+ itemsBuilder.addItems(mQueue, mCFBStrategy.getQueueViewType(),
+ mCFBStrategy.getMaxQueueRows());
+ }
+ for (MediaItemState itemState : itemStates) {
+ MediaItemMetadata item = itemState.mItem;
+ if (itemState.mItem.isBrowsable()) {
+ if (!itemState.mBrowsableChildren.isEmpty()
+ && !itemState.mPlayableChildren.isEmpty()
+ || !mCFBStrategy.shouldBeExpanded(item)) {
+ itemsBuilder.addItem(item, itemState.mState,
+ mCFBStrategy.getBrowsableViewType(mParentMediaItem), null);
+ } else if (!itemState.mPlayableChildren.isEmpty()) {
+ itemsBuilder.addBrowseBlock(item,
+ itemState.mState,
+ itemState.mPlayableChildren,
+ mCFBStrategy.getPlayableViewType(item),
+ mCFBStrategy.getMaxRows(item, mCFBStrategy.getPlayableViewType(item)),
+ mCFBStrategy.showMoreButton(item),
+ mCFBStrategy.includeHeader(item));
+ } else if (!itemState.mBrowsableChildren.isEmpty()) {
+ itemsBuilder.addBrowseBlock(item,
+ itemState.mState,
+ itemState.mBrowsableChildren,
+ mCFBStrategy.getBrowsableViewType(item),
+ mCFBStrategy.getMaxRows(item, mCFBStrategy.getBrowsableViewType(item)),
+ mCFBStrategy.showMoreButton(item),
+ mCFBStrategy.includeHeader(item));
+ }
+ } else if (item.isPlayable()) {
+ itemsBuilder.addItem(item, itemState.mState,
+ mCFBStrategy.getPlayableViewType(mParentMediaItem), null);
+ }
+ }
+
+ return itemsBuilder.build();
+ }
+}
diff --git a/src/com/android/car/media/browse/BrowseItemViewType.java b/src/com/android/car/media/browse/BrowseItemViewType.java
new file mode 100644
index 0000000..cfbdee7
--- /dev/null
+++ b/src/com/android/car/media/browse/BrowseItemViewType.java
@@ -0,0 +1,70 @@
+/*
+ * 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.support.annotation.LayoutRes;
+
+/**
+ * Possible view types that would be used by the {@link BrowseAdapter}.
+ */
+public enum BrowseItemViewType {
+ /** A section header */
+ HEADER(com.android.car.media.common.R.layout.media_browse_header_item),
+ /** A grid item including an image and a title */
+ GRID_ITEM(com.android.car.media.common.R.layout.media_browse_grid_item, 1),
+ /** A list item including title and subtitle */
+ LIST_ITEM(com.android.car.media.common.R.layout.media_browse_list_item),
+ /** An item in a panel of items (menu) */
+ PANEL_ITEM(com.android.car.media.common.R.layout.media_browse_panel_item),
+ /** A footer that can be used to navigate to an expanded version of a section */
+ MORE_FOOTER(com.android.car.media.common.R.layout.media_browse_more_footer),
+ ;
+ private final @LayoutRes int mLayoutId;
+ private final int mSpanSize;
+
+ /**
+ * {@link BrowseItemViewType} that take the whole width of the
+ * {@link android.support.v7.widget.RecyclerView}
+ */
+ BrowseItemViewType(@LayoutRes int layoutId) {
+ mLayoutId = layoutId;
+ mSpanSize = -1;
+ }
+
+ /**
+ * {@link BrowseItemViewType} that only takes the given number of columns.
+ */
+ BrowseItemViewType(@LayoutRes int layoutId, int spanSize) {
+ mLayoutId = layoutId;
+ mSpanSize = spanSize;
+ }
+
+ /**
+ * @param maxSpanSize maximum number of columns of the underlying grid
+ * @return number of columns this view wants to use.
+ */
+ public int getSpanSize(int maxSpanSize) {
+ return mSpanSize < 0 ? maxSpanSize : mSpanSize;
+ }
+
+ /**
+ * @return layout that should be inflated to generate this view type.
+ */
+ public @LayoutRes int getLayoutId() {
+ return mLayoutId;
+ }
+}
diff --git a/src/com/android/car/media/browse/BrowseViewData.java b/src/com/android/car/media/browse/BrowseViewData.java
new file mode 100644
index 0000000..31d0588
--- /dev/null
+++ b/src/com/android/car/media/browse/BrowseViewData.java
@@ -0,0 +1,102 @@
+/*
+ * 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.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+
+import com.android.car.media.common.MediaItemMetadata;
+
+import java.util.Objects;
+
+/**
+ * Information necessary to update a {@link BrowseViewHolder}
+ */
+class BrowseViewData {
+ /** {@link com.android.car.media.common.MediaItemMetadata} associated with this item */
+ public final MediaItemMetadata mMediaItem;
+ /** View type associated with this item */
+ @NonNull public final BrowseItemViewType mViewType;
+ /** Current state of this item */
+ public final State mState;
+ /** Text associated with this item */
+ public final CharSequence mText;
+ /** Click listener to set for this item */
+ public final View.OnClickListener mOnClickListener;
+
+ /**
+ * Possible states of a view
+ */
+ public enum State {
+ /** Loading of this item hasn't started yet */
+ IDLE,
+ /** There is pending information before this item can be displayed */
+ LOADING,
+ /** It was not possible to load metadata for this item */
+ ERROR,
+ /** Metadata for this items has been correctly loaded */
+ LOADED
+ }
+
+ /**
+ * Creates a {@link BrowseViewData} for a particular {@link MediaItemMetadata}.
+ *
+ * @param mediaItem {@link MediaItemMetadata} metadata
+ * @param viewType view type to use to represent this item
+ * @param state current item state
+ * @param onClickListener optional {@link android.view.View.OnClickListener}
+ */
+ BrowseViewData(MediaItemMetadata mediaItem, @NonNull BrowseItemViewType viewType,
+ @NonNull State state, View.OnClickListener onClickListener) {
+ mMediaItem = mediaItem;
+ mViewType = viewType;
+ mState = state;
+ mText = null;
+ mOnClickListener = onClickListener;
+ }
+
+ /**
+ * Creates a {@link BrowseViewData} for a given text (normally used for headers or footers)
+ * @param text text to set
+ * @param viewType view type to use
+ * @param onClickListener optional {@link android.view.View.OnClickListener}
+ */
+ BrowseViewData(@NonNull CharSequence text, @NonNull BrowseItemViewType viewType,
+ View.OnClickListener onClickListener) {
+ mText = text;
+ mViewType = viewType;
+ mMediaItem = null;
+ mState = null;
+ mOnClickListener = onClickListener;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BrowseViewData item = (BrowseViewData) o;
+ return Objects.equals(mMediaItem, item.mMediaItem) &&
+ mState == item.mState &&
+ mViewType == item.mViewType;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMediaItem, mState, mViewType);
+ }
+}
diff --git a/src/com/android/car/media/browse/BrowseViewHolder.java b/src/com/android/car/media/browse/BrowseViewHolder.java
new file mode 100644
index 0000000..28ec64c
--- /dev/null
+++ b/src/com/android/car/media/browse/BrowseViewHolder.java
@@ -0,0 +1,70 @@
+/*
+ * 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.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.car.media.common.MediaItemMetadata;
+
+/**
+ * Generic {@link RecyclerView.ViewHolder} to use for all views in the {@link BrowseAdapter}
+ */
+class BrowseViewHolder extends RecyclerView.ViewHolder {
+ final TextView mTitle;
+ final TextView mSubtitle;
+ final ImageView mAlbumArt;
+ final ViewGroup mContainer;
+
+ /**
+ * Creates a {@link BrowseViewHolder} for the given view.
+ */
+ BrowseViewHolder(View itemView) {
+ super(itemView);
+ mTitle = itemView.findViewById(com.android.car.media.common.R.id.title);
+ mSubtitle = itemView.findViewById(com.android.car.media.common.R.id.subtitle);
+ mAlbumArt = itemView.findViewById(com.android.car.media.common.R.id.thumbnail);
+ mContainer = itemView.findViewById(com.android.car.media.common.R.id.container);
+ }
+
+ /**
+ * Updates this {@link BrowseViewHolder} with the given data
+ */
+ public void bind(Context context, BrowseViewData data) {
+ if (mTitle != null) {
+ mTitle.setText(data.mText != null
+ ? data.mText : data.mMediaItem != null
+ ? data.mMediaItem.getTitle()
+ : null);
+ }
+ if (mSubtitle != null) {
+ mSubtitle.setText(data.mMediaItem != null
+ ? data.mMediaItem.getSubtitle()
+ : null);
+ }
+ if (mAlbumArt != null) {
+ MediaItemMetadata.updateImageView(context, data.mMediaItem, mAlbumArt, 0);
+ }
+ if (mContainer != null && data.mOnClickListener != null) {
+ mContainer.setOnClickListener(data.mOnClickListener);
+ }
+ }
+}
diff --git a/src/com/android/car/media/browse/ContentForwardStrategy.java b/src/com/android/car/media/browse/ContentForwardStrategy.java
new file mode 100644
index 0000000..7720366
--- /dev/null
+++ b/src/com/android/car/media/browse/ContentForwardStrategy.java
@@ -0,0 +1,120 @@
+/*
+ * 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 com.android.car.media.common.MediaItemMetadata;
+
+/**
+ * Strategy used to group and expand media items in the {@link BrowseAdapter}
+ */
+public interface ContentForwardStrategy {
+ /**
+ * @return true if a header should be included when expanding the given media item into a
+ * section. Only used if {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
+ */
+ boolean includeHeader(MediaItemMetadata mediaItem);
+
+ /**
+ * @return maximum number of rows to use when when expanding the given media item into a
+ * section. The number can be different depending on the {@link BrowseItemViewType} that
+ * will be used to represent media item children (i.e.: we might allow more rows for lists
+ * than for grids). Only used if {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
+ */
+ int getMaxRows(MediaItemMetadata mediaItem, BrowseItemViewType viewType);
+
+ /**
+ * @return whether the given media item should be expanded or not. If not expanded, the item
+ * will be displayed according to its parent preferred view type.
+ */
+ boolean shouldBeExpanded(MediaItemMetadata mediaItem);
+
+ /**
+ * @return view type to use to render browsable children of the given media item. Only used if
+ * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
+ */
+ BrowseItemViewType getBrowsableViewType(MediaItemMetadata mediaItem);
+
+ /**
+ * @return view type to use to render playable children fo the given media item. Only used if
+ * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
+ */
+ BrowseItemViewType getPlayableViewType(MediaItemMetadata mediaItem);
+
+ /**
+ * @return true if a "more" button should be displayed as a footer for a section displaying the
+ * given media item, in case that there item has more children than the ones that can be
+ * displayed according to {@link #getMaxQueueRows()}. Only used if
+ * {@link #shouldBeExpanded(MediaItemMetadata)} returns true.
+ */
+ boolean showMoreButton(MediaItemMetadata mediaItem);
+
+ /**
+ * @return maximum number of items to show for the media queue, if one is provided.
+ */
+ int getMaxQueueRows();
+
+ /**
+ * @return view type to use to display queue items.
+ */
+ BrowseItemViewType getQueueViewType();
+
+ /**
+ * Default strategy
+ * TODO(b/77646944): Expand this implementation to honor the media source expectations.
+ */
+ ContentForwardStrategy DEFAULT_STRATEGY = new ContentForwardStrategy() {
+ @Override
+ public boolean includeHeader(MediaItemMetadata mediaItem) {
+ return false;
+ }
+
+ @Override
+ public int getMaxRows(MediaItemMetadata mediaItem, BrowseItemViewType viewType) {
+ return viewType == BrowseItemViewType.GRID_ITEM ? 2 : 8;
+ }
+
+ @Override
+ public boolean shouldBeExpanded(MediaItemMetadata mediaItem) {
+ return true;
+ }
+
+ @Override
+ public BrowseItemViewType getBrowsableViewType(MediaItemMetadata mediaItem) {
+ return BrowseItemViewType.PANEL_ITEM;
+ }
+
+ @Override
+ public BrowseItemViewType getPlayableViewType(MediaItemMetadata mediaItem) {
+ return BrowseItemViewType.GRID_ITEM;
+ }
+
+ @Override
+ public boolean showMoreButton(MediaItemMetadata mediaItem) {
+ return true;
+ }
+
+ @Override
+ public int getMaxQueueRows() {
+ return 8;
+ }
+
+ @Override
+ public BrowseItemViewType getQueueViewType() {
+ return BrowseItemViewType.LIST_ITEM;
+ }
+ };
+}