summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorandroid-build-team Robot <android-build-team-robot@google.com>2018-04-24 07:21:13 +0000
committerandroid-build-team Robot <android-build-team-robot@google.com>2018-04-24 07:21:13 +0000
commit9f07c6c37dc0d26178ade6e6c6f54e3743e07c39 (patch)
tree5721e38526d6739035d9e76f88942ee8e9418d34
parent9a05aa74c41d087b4f2f28e9b2bf1676ef9e84cf (diff)
parentd1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837 (diff)
downloadMedia-9f07c6c37dc0d26178ade6e6c6f54e3743e07c39.tar.gz
Snap for 4739962 from d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837 to pi-release
Change-Id: I03083a1a3b9cc49bf579479a3af4c7609c1efca4
-rw-r--r--Android.mk1
-rw-r--r--res/drawable/browse_gradient_scrim.xml24
-rw-r--r--res/layout/fragment_browse.xml29
-rw-r--r--res/layout/fragment_playback.xml34
-rw-r--r--res/layout/media_activity.xml48
-rw-r--r--res/layout/now_playing_screen.xml85
-rw-r--r--res/layout/tab_view.xml39
-rw-r--r--res/values-h1200dp/bools.xml20
-rw-r--r--res/values/bools.xml24
-rw-r--r--res/values/colors.xml14
-rw-r--r--res/values/dimens.xml6
-rw-r--r--res/values/integers.xml6
-rw-r--r--res/values/styles.xml10
-rw-r--r--src/com/android/car/media/BrowseFragment.java276
-rw-r--r--src/com/android/car/media/CrossfadeImageView.java168
-rw-r--r--src/com/android/car/media/MediaActivity.java370
-rw-r--r--src/com/android/car/media/MediaPlaybackModel.java9
-rw-r--r--src/com/android/car/media/PlaybackFragment.java241
-rw-r--r--src/com/android/car/media/browse/BrowseAdapter.java64
-rw-r--r--src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java14
-rw-r--r--src/com/android/car/media/drawer/MediaDrawerAdapter.java22
-rw-r--r--src/com/android/car/media/drawer/MediaDrawerController.java3
-rw-r--r--src/com/android/car/media/util/widgets/MediaItemTabView.java51
23 files changed, 986 insertions, 572 deletions
diff --git a/Android.mk b/Android.mk
index e88ab0e..f442d59 100644
--- a/Android.mk
+++ b/Android.mk
@@ -40,6 +40,7 @@ LOCAL_DEX_PREOPT := false
LOCAL_STATIC_ANDROID_LIBRARIES += \
android-support-car \
android-support-constraint-layout \
+ android-support-design-widget \
car-apps-common \
car-media-common
diff --git a/res/drawable/browse_gradient_scrim.xml b/res/drawable/browse_gradient_scrim.xml
new file mode 100644
index 0000000..a3f0f12
--- /dev/null
+++ b/res/drawable/browse_gradient_scrim.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:startColor="#000000"
+ android:endColor="#00000000"
+ android:angle="270" />
+</shape> \ No newline at end of file
diff --git a/res/layout/fragment_browse.xml b/res/layout/fragment_browse.xml
new file mode 100644
index 0000000..0d1e109
--- /dev/null
+++ b/res/layout/fragment_browse.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.car.widget.PagedListView
+ android:id="@+id/browse_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:showPagedListViewDivider="false"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml
index 867d0e9..f3a3ee2 100644
--- a/res/layout/fragment_playback.xml
+++ b/res/layout/fragment_playback.xml
@@ -22,38 +22,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <com.android.car.media.CrossfadeImageView
- android:id="@+id/album_background"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:background="@color/car_dark_blue_grey_800"
- android:scaleType="centerCrop"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="@+id/playback_scrim_bottom"/>
-
- <View
- android:id="@+id/playback_scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/car_dark_blue_grey_900"
- android:alpha="@dimen/playback_initial_scrim_alpha"/>
-
- <View
- android:id="@+id/playback_scrim_bottom"
- android:layout_width="match_parent"
- android:layout_height="@dimen/playback_scrim_transition_height"
- android:background="@drawable/car_playback_bottom_scrim"
- app:layout_constraintTop_toBottomOf="@+id/metadata_container"/>
-
- <View
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:background="@android:color/black"
- app:layout_constraintTop_toBottomOf="@+id/playback_scrim_bottom"
- app:layout_constraintBottom_toBottomOf="parent"/>
-
<android.support.v4.widget.Space
android:id="@+id/app_bar_space"
android:layout_width="match_parent"
@@ -74,7 +42,7 @@
</FrameLayout>
<androidx.car.widget.PagedListView
- android:id="@+id/browse_list"
+ android:id="@+id/queue_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="@dimen/car_padding_4"
diff --git a/res/layout/media_activity.xml b/res/layout/media_activity.xml
index 4abfe04..e92308d 100644
--- a/res/layout/media_activity.xml
+++ b/res/layout/media_activity.xml
@@ -14,8 +14,50 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
+<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/fragment_container"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/application"
+ android:animateLayoutChanges="true"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent">
+
+ <com.android.car.media.common.CrossfadeImageView
+ android:id="@+id/media_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/media_template_background"
+ android:scaleType="centerCrop"/>
+
+ <View
+ android:id="@+id/media_scrim"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/media_scrim_background"
+ android:alpha="@dimen/playback_initial_scrim_alpha"/>
+
+ <View
+ android:id="@+id/browse_scrim"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/browse_gradient_scrim"/>
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/browse_tab_height"
+ app:tabIndicatorHeight="0dp"
+ app:tabGravity="fill"
+ app:tabMode="fixed"
+ app:tabPadding="0dp"
+ app:tabMaxWidth="0dp"
+ app:layout_constraintTop_toTopOf="parent"/>
+
+ <FrameLayout
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintTop_toBottomOf="@+id/tabs"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+
+</android.support.constraint.ConstraintLayout>
diff --git a/res/layout/now_playing_screen.xml b/res/layout/now_playing_screen.xml
deleted file mode 100644
index bca7a15..0000000
--- a/res/layout/now_playing_screen.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2016, 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.
--->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:animateLayoutChanges="true" >
-
- <com.android.car.media.CrossfadeImageView
- android:id="@+id/album_art"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="centerCrop" />
-
- <View
- android:id="@+id/scrim"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@android:color/black"
- android:alpha="@dimen/media_scrim_alpha" />
-
- <View
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/album_art_color_overlay" />
-
- <LinearLayout
- android:layout_marginTop="@dimen/car_app_bar_height"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical" >
-
- <FrameLayout
- android:id="@+id/metadata"
- android:layout_width="@dimen/metadata_width"
- android:layout_height="0dp"
- android:layout_weight="1"
- android:layout_marginStart="@dimen/car_keyline_1"
- android:layout_marginTop="@dimen/now_playing_metadata_top_margin"
- android:visibility="gone" >
-
- <LinearLayout
- android:layout_gravity="top"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical" >
-
- <TextView
- android:id="@+id/title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:fontFamily="sans-serif-medium"
- android:maxLines="@integer/media_title_max_lines"
- style="@style/TextAppearance.Car.Headline1.Light" />
- <TextView
- android:id="@+id/artist"
- android:layout_marginTop="@dimen/metadata_inter_line_space"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="@integer/media_artist_max_lines"
- style="@style/TextAppearance.Car.Headline1.Light" />
- </LinearLayout>
- </FrameLayout>
-
- <include layout="@layout/media_controls" />
- </LinearLayout>
-
- <include layout="@layout/initial_no_content" />
-</FrameLayout>
diff --git a/res/layout/tab_view.xml b/res/layout/tab_view.xml
new file mode 100644
index 0000000..68fef69
--- /dev/null
+++ b/res/layout/tab_view.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/car_primary_icon_size"
+ android:layout_height="@dimen/car_primary_icon_size"
+ android:scaleType="fitCenter"
+ android:layout_gravity="center_horizontal"
+ android:tint="@color/car_tint_inverse"
+ android:layout_marginTop="@dimen/car_padding_2"/>
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="@dimen/car_padding_1"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:textAppearance="@style/TextAppearance.Car.Body5.Light"/>
+
+</merge> \ No newline at end of file
diff --git a/res/values-h1200dp/bools.xml b/res/values-h1200dp/bools.xml
new file mode 100644
index 0000000..34eae64
--- /dev/null
+++ b/res/values-h1200dp/bools.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <bool name="forward_content_browse_enabled">true</bool>
+ <bool name="force_browse_tabs">true</bool>
+</resources> \ No newline at end of file
diff --git a/res/values/bools.xml b/res/values/bools.xml
new file mode 100644
index 0000000..b9bd29e
--- /dev/null
+++ b/res/values/bools.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <!-- Whether forward content browse is enabled -->
+ <bool name="forward_content_browse_enabled">false</bool>
+
+ <!-- Force the presentation of tabs even if the number of browsable items exceeds the
+ maximum number of allowed tabs (this is mainly for demo purposes) -->
+ <bool name="force_browse_tabs">false</bool>
+</resources> \ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 19a9700..30f7d00 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -15,8 +15,16 @@
-->
<resources>
- <color name="music_default_artwork">#78909c</color>
- <color name="music_loading_view_background">#11181d</color>
<color name="no_content_text_color">#ffffff</color>
- <color name="album_art_color_overlay">#99000000</color>
+
+ <color name="car_body5_light">@android:color/white</color>
+ <color name="car_body5_dark">@android:color/black</color>
+ <color name="car_body5">@color/car_body5_dark</color>
+
+ <!-- Color displayed behind the transport controls while browsing -->
+ <color name="browse_playback_bg">@android:color/black</color>
+ <!-- Default media background -->
+ <color name="media_template_background">@color/car_dark_blue_grey_800</color>
+ <!-- Scrim displayed on top of playback album art background -->
+ <color name="media_scrim_background">@color/car_dark_blue_grey_900</color>
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 79da22b..254ebd5 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -33,6 +33,8 @@
<dimen name="controls_spacing_inner">16dp</dimen>
<dimen name="controls_spacing_outer">81dp</dimen>
+ <!-- Playback seekbar height -->
+ <dimen name="playback_seekbar_height">8dp</dimen>
<!-- Height of the gradient scrim over the background image -->
<dimen name="playback_scrim_transition_height">256dp</dimen>
<!-- Image size used to generate the background -->
@@ -45,4 +47,8 @@
<dimen name="playback_album_art_height">190dp</dimen>
<dimen name="playback_album_art_width">200dp</dimen>
+ <!-- Tab height -->
+ <dimen name="browse_tab_height">100dp</dimen>
+ <!-- Tab max width -->
+ <dimen name="browse_tab_width">92dp</dimen>
</resources>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index 41eb8a8..13ad859 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -23,4 +23,10 @@
<!-- The maximum number of lines for the artist of a currently playing media item. -->
<integer name="media_artist_max_lines">1</integer>
+
+ <!-- Maximum number of browse tabs -->
+ <integer name="max_browse_tabs">4</integer>
+
+ <!-- Number of columns in the browse view -->
+ <integer name="num_browse_columns">3</integer>
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index dd0ef69..f58306e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -25,4 +25,14 @@
<item name="android:textSize">@dimen/car_body2_size</item>
<item name="android:textColor">@color/no_content_text_color</item>
</style>
+
+ <style name="TextAppearance.Car.Body5">
+ <item name="android:textStyle">normal</item>
+ <item name="android:textSize">@dimen/car_body5_size</item>
+ <item name="android:textColor">@color/car_body5</item>
+ </style>
+
+ <style name="TextAppearance.Car.Body5.Light">
+ <item name="android:textColor">@color/car_body5_light</item>
+ </style>
</resources>
diff --git a/src/com/android/car/media/BrowseFragment.java b/src/com/android/car/media/BrowseFragment.java
new file mode 100644
index 0000000..64c5eaa
--- /dev/null
+++ b/src/com/android/car/media/BrowseFragment.java
@@ -0,0 +1,276 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+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 java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+import androidx.car.widget.PagedListView;
+
+/**
+ * A {@link Fragment} that implements the content forward browsing experience.
+ */
+public class BrowseFragment extends Fragment {
+ private static final String TAG = "BrowseFragment";
+ private static final String TOP_MEDIA_ITEM_KEY = "top_media_item";
+ private static final String MEDIA_SOURCE_PACKAGE_NAME_KEY = "media_source";
+ private static final String BROWSE_STACK_KEY = "browse_stack";
+
+ private PagedListView mBrowseList;
+ private MediaSource mMediaSource;
+ private BrowseAdapter mBrowseAdapter;
+ private String mMediaSourcePackageName;
+ private MediaItemMetadata mTopMediaItem;
+ private Callbacks mCallbacks;
+ private Stack<MediaItemMetadata> mBrowseStack = new Stack<>();
+ private MediaSource.Observer mBrowseObserver = new MediaSource.Observer() {
+ @Override
+ protected void onBrowseConnected(boolean success) {
+ BrowseFragment.this.onBrowseConnected(success);
+ }
+
+ @Override
+ protected void onBrowseDisconnected() {
+ BrowseFragment.this.onBrowseDisconnected();
+ }
+ };
+ private BrowseAdapter.Observer mBrowseAdapterObserver = new BrowseAdapter.Observer() {
+ @Override
+ protected void onDirty() {
+ mBrowseAdapter.update();
+ if (mBrowseAdapter.getItemCount() > 0) {
+ mBrowseList.setVisibility(View.VISIBLE);
+ } else {
+ mBrowseList.setVisibility(View.GONE);
+ // TODO(b/77647430) implement intermediate states.
+ }
+ }
+
+ @Override
+ protected void onPlayableItemClicked(MediaItemMetadata item) {
+ mCallbacks.onPlayableItemClicked(mMediaSource, item);
+ }
+
+ @Override
+ protected void onBrowseableItemClicked(MediaItemMetadata item) {
+ navigateInto(item);
+ }
+
+ @Override
+ protected void onMoreButtonClicked(MediaItemMetadata item) {
+ navigateInto(item);
+ }
+ };
+
+ /**
+ * Fragment callbacks (implemented by the hosting Activity)
+ */
+ public interface Callbacks {
+ /**
+ * @return a {@link MediaSource} corresponding to the given package name
+ */
+ MediaSource getMediaSource(String packageName);
+
+ /**
+ * Method invoked when the back stack changes (for example, when the user moves up or down
+ * the media tree)
+ */
+ void onBackStackChanged();
+
+ /**
+ * Method invoked when the user clicks on a playable item
+ *
+ * @param mediaSource {@link MediaSource} the playable item belongs to
+ * @param item item to be played.
+ */
+ void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item);
+ }
+
+ /**
+ * Moves the user one level up in the browse tree, if possible.
+ */
+ public void navigateBack() {
+ mBrowseStack.pop();
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.setParentMediaItemId(getCurrentMediaItem());
+ }
+ if (mCallbacks != null) {
+ mCallbacks.onBackStackChanged();
+ }
+ }
+
+ /**
+ * @return whether the user is in a level other than the top.
+ */
+ public boolean isBackEnabled() {
+ return !mBrowseStack.isEmpty();
+ }
+
+ /**
+ * Creates a new instance of this fragment.
+ *
+ * @param item media tree node to display on this fragment.
+ * @return a fully initialized {@link BrowseFragment}
+ */
+ public static BrowseFragment newInstance(MediaSource mediaSource, MediaItemMetadata item) {
+ BrowseFragment fragment = new BrowseFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(TOP_MEDIA_ITEM_KEY, item);
+ args.putString(MEDIA_SOURCE_PACKAGE_NAME_KEY, mediaSource.getPackageName());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle arguments = getArguments();
+ if (arguments != null) {
+ mTopMediaItem = arguments.getParcelable(TOP_MEDIA_ITEM_KEY);
+ mMediaSourcePackageName = arguments.getString(MEDIA_SOURCE_PACKAGE_NAME_KEY);
+ }
+ if (savedInstanceState != null) {
+ List<MediaItemMetadata> savedStack =
+ savedInstanceState.getParcelableArrayList(BROWSE_STACK_KEY);
+ mBrowseStack.clear();
+ if (savedStack != null) {
+ mBrowseStack.addAll(savedStack);
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, final ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_browse, container, false);
+ mBrowseList = view.findViewById(R.id.browse_list);
+ int numColumns = getContext().getResources().getInteger(R.integer.num_browse_columns);
+ GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), numColumns);
+ RecyclerView recyclerView = mBrowseList.getRecyclerView();
+ recyclerView.setVerticalFadingEdgeEnabled(true);
+ recyclerView.setFadingEdgeLength(getResources()
+ .getDimensionPixelSize(R.dimen.car_padding_4));
+ recyclerView.setLayoutManager(gridLayoutManager);
+ recyclerView.addItemDecoration(new GridSpacingItemDecoration(
+ getResources().getDimensionPixelSize(R.dimen.car_padding_4),
+ getResources().getDimensionPixelSize(R.dimen.car_keyline_1),
+ getResources().getDimensionPixelSize(R.dimen.car_keyline_1)
+ ));
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mCallbacks = (Callbacks) context;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = null;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mMediaSource = mCallbacks.getMediaSource(mMediaSourcePackageName);
+ if (mMediaSource != null) {
+ mMediaSource.subscribe(mBrowseObserver);
+ }
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.start();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mMediaSource != null) {
+ mMediaSource.unsubscribe(mBrowseObserver);
+ }
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.stop();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ ArrayList<MediaItemMetadata> stack = new ArrayList<>(mBrowseStack);
+ outState.putParcelableArrayList(BROWSE_STACK_KEY, stack);
+ }
+
+ private void onBrowseConnected(boolean success) {
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.stop();
+ mBrowseAdapter = null;
+ }
+ if (!success) {
+ mBrowseList.setVisibility(View.GONE);
+ // TODO(b/77647430) implement intermediate states.
+ return;
+ }
+ mBrowseAdapter = new BrowseAdapter(getContext(), mMediaSource.getMediaBrowser(),
+ getCurrentMediaItem(), ContentForwardStrategy.DEFAULT_STRATEGY);
+ mBrowseList.setAdapter(mBrowseAdapter);
+ mBrowseAdapter.registerObserver(mBrowseAdapterObserver);
+ mBrowseAdapter.start();
+ }
+
+ private void onBrowseDisconnected() {
+ if (mBrowseAdapter != null) {
+ mBrowseAdapter.stop();
+ mBrowseAdapter = null;
+ }
+ }
+
+ private void navigateInto(MediaItemMetadata item) {
+ mBrowseStack.push(item);
+ mBrowseAdapter.setParentMediaItemId(item);
+ mCallbacks.onBackStackChanged();
+ }
+
+ /**
+ * @return the current item being displayed
+ */
+ public MediaItemMetadata getCurrentMediaItem() {
+ if (mBrowseStack.isEmpty()) {
+ return mTopMediaItem;
+ } else {
+ return mBrowseStack.lastElement();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/car/media/CrossfadeImageView.java b/src/com/android/car/media/CrossfadeImageView.java
deleted file mode 100644
index 413442a..0000000
--- a/src/com/android/car/media/CrossfadeImageView.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-
-/**
- * A view where updating the image will show certain animations. Current animations include fading
- * in and scaling down the new image.
- */
-public class CrossfadeImageView extends FrameLayout {
- // ColorFilters can't currently be modified (b/17262092) so creating a saturation fade with
- // color filters would normally require creating a ton of small objects. We get around this by
- // caching color filters and limit the saturation to increments of 0.1.
- // 0-0.09 -> [0]
- // 0.10-0.19 -> [1]
- // ...
- // 1.0 -> [10]
- private final ColorFilter[] mSaturationColorFilters = new ColorFilter[11];
- private final ColorMatrix mColorMatrix = new ColorMatrix();
- private final ImageView mImageView1;
- private final ImageView mImageView2;
-
- private ImageView mActiveImageView;
- private ImageView mInactiveImageView;
-
- private Bitmap mCurrentBitmap = null;
- private Integer mCurrentColor = null;
- private Animation mImageInAnimation;
- private Animation mImageOutAnimation;
-
- public CrossfadeImageView(Context context) {
- this(context, null);
- }
-
- public CrossfadeImageView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CrossfadeImageView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public CrossfadeImageView(Context context, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- LayoutParams lp = new LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- ImageView imageViewBackground = new ImageView(context, attrs, defStyleAttr, defStyleRes);
- imageViewBackground.setLayoutParams(lp);
- imageViewBackground.setBackgroundColor(Color.BLACK);
- addView(imageViewBackground);
- mImageView1 = new ImageView(context, attrs, defStyleAttr, defStyleRes);
- mImageView1.setLayoutParams(lp);
- addView(mImageView1);
- mImageView2 = new ImageView(context, attrs, defStyleAttr, defStyleRes);
- mImageView2.setLayoutParams(lp);
- addView(mImageView2);
-
- mActiveImageView = mImageView1;
- mInactiveImageView = mImageView2;
-
- mImageInAnimation = AnimationUtils.loadAnimation(context, R.anim.image_in);
- mImageInAnimation.setInterpolator(new DecelerateInterpolator());
- mImageOutAnimation = AnimationUtils.loadAnimation(context, R.anim.image_out);
- }
-
- public void setImageBitmap(Bitmap bitmap, boolean showAnimation) {
- if (bitmap == null) {
- return;
- }
-
- if (mCurrentBitmap != null && bitmap.sameAs(mCurrentBitmap)) {
- return;
- }
-
- mCurrentBitmap = bitmap;
- mCurrentColor = null;
- mInactiveImageView.setImageBitmap(bitmap);
- if (showAnimation) {
- animateViews();
- } else {
- mActiveImageView.setImageBitmap(bitmap);
- }
- }
-
- @Override
- public void setBackgroundColor(int color) {
- if (mCurrentColor != null && mCurrentColor == color) {
- return;
- }
- mInactiveImageView.setImageBitmap(null);
- mCurrentBitmap = null;
- mCurrentColor = color;
- mInactiveImageView.setBackgroundColor(color);
- animateViews();
- }
-
- public void setSaturation(float saturation) {
- int i = (int) ((saturation * 100) / 10);
- ColorFilter cf = mSaturationColorFilters[i];
- if (cf == null) {
- mColorMatrix.setSaturation((10 * i) / 100f);
- cf = new ColorMatrixColorFilter(mColorMatrix);
- mSaturationColorFilters[i] = cf;
- }
-
- mImageView1.setColorFilter(cf);
- mImageView2.setColorFilter(cf);
- }
-
- private final Animation.AnimationListener mAnimationListener =
- new Animation.AnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- if (mInactiveImageView != null) {
- mInactiveImageView.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onAnimationStart(Animation animation) { }
-
- @Override
- public void onAnimationRepeat(Animation animation) { }
- };
-
- private void animateViews() {
- mInactiveImageView.setVisibility(View.VISIBLE);
- mInactiveImageView.startAnimation(mImageInAnimation);
- mInactiveImageView.bringToFront();
- mActiveImageView.startAnimation(mImageOutAnimation);
- mImageOutAnimation.setAnimationListener(mAnimationListener);
- if (mActiveImageView == mImageView1) {
- mActiveImageView = mImageView2;
- mInactiveImageView = mImageView1;
- } else {
- mActiveImageView = mImageView1;
- mInactiveImageView = mImageView2;
- }
- }
-}
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index b3ddd12..9723d4d 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -17,12 +17,27 @@ package com.android.car.media;
import android.content.ComponentName;
import android.content.Intent;
+import android.graphics.Bitmap;
import android.os.Bundle;
-import android.provider.MediaStore;
+import android.support.design.widget.TabLayout;
+import android.support.design.widget.AppBarLayout;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import com.android.car.media.common.CrossfadeImageView;
+import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.MediaSource;
+import com.android.car.media.common.PlaybackModel;
import com.android.car.media.drawer.MediaDrawerController;
+import com.android.car.media.util.widgets.MediaItemTabView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
import androidx.car.drawer.CarDrawerActivity;
import androidx.car.drawer.CarDrawerAdapter;
@@ -31,27 +46,128 @@ import androidx.car.drawer.CarDrawerAdapter;
* This activity controls the UI of media. It also updates the connection status for the media app
* by broadcast. Drawer menu is controlled by {@link MediaDrawerController}.
*/
-public class MediaActivity extends CarDrawerActivity {
+public class MediaActivity extends CarDrawerActivity implements BrowseFragment.Callbacks {
private static final String TAG = "MediaActivity";
+ /** Intent extra specifying the package with the MediaBrowser **/
+ public static final String KEY_MEDIA_PACKAGE = "media_package";
+
+ /** Configuration (controlled from resources) */
+ private boolean mContentForwardBrowseEnabled;
+ private boolean mForceBrowseTabs;
+ private int mMaxBrowserTabs;
+ private float mBackgroundBlurRadius;
+ private float mBackgroundBlurScale;
+
+ /** Models */
private MediaDrawerController mDrawerController;
+ private MediaSource mMediaSource;
+ private PlaybackModel mPlaybackModel;
+
+ /** Layout views */
+ private TabLayout mTabLayout;
+ private CrossfadeImageView mAlbumBackground;
private PlaybackFragment mPlaybackFragment;
+ private AppBarLayout mAppBarLayout;
+ private View mBrowseScrim;
+
+ /** Current state */
+ private MediaItemMetadata mCurrentMetadata;
+ private Fragment mCurrentFragment;
+ private Mode mMode = Mode.BROWSING;
+
+ private MediaSource.Observer mMediaSourceObserver = new MediaSource.Observer() {
+ @Override
+ protected void onBrowseConnected(boolean success) {
+ MediaActivity.this.onBrowseConnected(success);
+ }
+
+ @Override
+ protected void onBrowseDisconnected() {
+ MediaActivity.this.onBrowseConnected(false);
+ }
+ };
+ private MediaSource.ItemsSubscription mRootItemsSubscription =
+ (parentId, items) -> updateTabs(items);
+ private PlaybackModel.PlaybackObserver mPlaybackObserver =
+ new PlaybackModel.PlaybackObserver() {
+ @Override
+ public void onSourceChanged() {
+ updateMetadata();
+ updateBrowseSource();
+ }
+
+ @Override
+ public void onMetadataChanged() {
+ updateMetadata();
+ }
+ };
+ private TabLayout.OnTabSelectedListener mTabSelectedListener =
+ new TabLayout.OnTabSelectedListener() {
+ @Override
+ public void onTabSelected(TabLayout.Tab tab) {
+ mMode = Mode.BROWSING;
+ updateBrowseFragment((MediaItemMetadata) tab.getTag());
+ updateMetadata();
+ }
+
+ @Override
+ public void onTabUnselected(TabLayout.Tab tab) {
+ // Nothing to do
+ }
+
+ @Override
+ public void onTabReselected(TabLayout.Tab tab) {
+ mMode = Mode.BROWSING;
+ updateBrowseFragment((MediaItemMetadata) tab.getTag());
+ updateMetadata();
+ }
+ };
+
+ private enum Mode {
+ BROWSING,
+ PLAYBACK
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+
+ setMainContent(R.layout.media_activity);
+
setToolbarElevation(0f);
- mDrawerController = new MediaDrawerController(this /* context */, getDrawerController());
+ mContentForwardBrowseEnabled = getResources()
+ .getBoolean(R.bool.forward_content_browse_enabled);
+ mForceBrowseTabs = getResources()
+ .getBoolean(R.bool.force_browse_tabs);
+ mDrawerController = new MediaDrawerController(this, getDrawerController());
getDrawerController().setRootAdapter(getRootAdapter());
+ mTabLayout = findViewById(R.id.tabs);
+ mTabLayout.addOnTabSelectedListener(mTabSelectedListener);
+ mPlaybackFragment = new PlaybackFragment();
+ mPlaybackModel = new PlaybackModel(this);
+ mMaxBrowserTabs = getResources().getInteger(R.integer.max_browse_tabs);
+ mAppBarLayout = findViewById(androidx.car.R.id.appbar);
+ mAlbumBackground = findViewById(R.id.media_background);
+ mBrowseScrim = findViewById(R.id.browse_scrim);
+ TypedValue outValue = new TypedValue();
+ getResources().getValue(R.dimen.playback_background_blur_radius, outValue, true);
+ mBackgroundBlurRadius = outValue.getFloat();
+ getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true);
+ mBackgroundBlurScale = outValue.getFloat();
+ }
- setMainContent(R.layout.media_activity);
- MediaManager.getInstance(this).addListener(mListener);
+ @Override
+ public void onStart() {
+ super.onStart();
+ mPlaybackModel.registerObserver(mPlaybackObserver);
+ }
- mPlaybackFragment = new PlaybackFragment();
- getSupportFragmentManager().beginTransaction()
- .replace(R.id.fragment_container, mPlaybackFragment)
- .commit();
+ @Override
+ public void onStop() {
+ super.onStop();
+ mPlaybackModel.unregisterObserver(mPlaybackObserver);
}
@Override
@@ -74,7 +190,7 @@ public class MediaActivity extends CarDrawerActivity {
setIntent(intent);
getDrawerController().closeDrawer();
- handleIntent(getIntent());
+ handleIntent();
}
@Override
@@ -86,70 +202,214 @@ public class MediaActivity extends CarDrawerActivity {
@Override
protected void onResumeFragments() {
super.onResumeFragments();
- handleIntent(getIntent());
+ updateBrowseSource();
}
- private void handleIntent(Intent intent) {
- Bundle extras = null;
- if (intent != null) {
- extras = intent.getExtras();
+ private void onBrowseConnected(boolean success) {
+ if (!success) {
+ updateTabs(new ArrayList<>());
+ mMediaSource.unsubscribeChildren(null);
+ return;
}
+ mMediaSource.subscribeChildren(null, mRootItemsSubscription);
+ }
- // If the intent has a media component name set, connect to it directly
- if (extras != null && extras.containsKey(MediaManager.KEY_MEDIA_PACKAGE)) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Media component in intent.");
- }
-
- String packageName = intent.getStringExtra(MediaManager.KEY_MEDIA_PACKAGE);
- MediaSource mediaSource = new MediaSource(this, packageName);
- ComponentName component = mediaSource.getBrowseServiceComponentName();
+ private void handleIntent() {
+ updateBrowseSource();
+ switchToMode(getRequestedMediaPackageName() == null || !mContentForwardBrowseEnabled
+ ? Mode.PLAYBACK
+ : Mode.BROWSING);
+ }
- Log.i(TAG, "Browsing: " + component + " from " + packageName);
+ /**
+ * Updates the media source being browsed. This could be necessary when the source playing
+ * changes, or if the user requests to connect to a different source.
+ */
+ private void updateBrowseSource() {
+ MediaSource mediaSource = getCurrentMediaSource();
+ if (Objects.equals(mediaSource, mMediaSource)) {
+ // No change, nothing to do.
+ return;
+ }
+ if (mMediaSource != null) {
+ mMediaSource.unsubscribe(mMediaSourceObserver);
+ updateTabs(new ArrayList<>());
+ }
+ mMediaSource = mediaSource;
+ if (mMediaSource != null) {
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "Browsing: " + mediaSource.getName());
+ }
+ ComponentName component = mMediaSource.getBrowseServiceComponentName();
MediaManager.getInstance(this).setMediaClientComponent(component);
- mPlaybackFragment.updateBrowse();
- } else {
- // TODO (b/77334804): Implement the correct initialization logic when no component is
- // given. For example, it should either connect the user to the currently playing
- // session, bring the user to the app selector, or open the last known media source.
- mPlaybackFragment.updateBrowse();
+ // If content forward browsing is disabled, then no need to subscribe to this media
+ // source.
+ if (mContentForwardBrowseEnabled) {
+ Log.i(TAG, "Content forward is enabled: subscribing to " +
+ mMediaSource.getPackageName());
+ mMediaSource.subscribe(mMediaSourceObserver);
+ }
}
+ }
- if (isSearchIntent(intent)) {
- MediaManager.getInstance(this).processSearchIntent(intent);
- setIntent(null);
- }
+ /**
+ * @return the media source that should be browsed. If the user expressed the intention of
+ * browsing something different than what is being played, we will return that. Otherwise
+ * we return the souce that is playing.
+ */
+ private MediaSource getCurrentMediaSource() {
+ String packageName = getRequestedMediaPackageName();
+ return packageName != null
+ ? new MediaSource(this, packageName)
+ : mPlaybackModel.getMediaSource();
}
/**
- * Returns {@code true} if the given intent is one that contains a search query for the
- * attached media application.
+ * @return the package name of the media source requested by the incoming {@link Intent} or
+ * null if no source was indicated.
*/
- private boolean isSearchIntent(Intent intent) {
- return (intent != null && intent.getAction() != null &&
- intent.getAction().equals(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH));
+ private String getRequestedMediaPackageName() {
+ return getIntent() != null
+ ? getIntent().getStringExtra(KEY_MEDIA_PACKAGE)
+ : null;
}
- private void sendMediaConnectionStatusBroadcast(ComponentName componentName,
- String connectionStatus) {
- // There will be no current component if no media app has been chosen before.
- if (componentName == null) {
+ private boolean isCurrentMediaSourcePlaying() {
+ return Objects.equals(mMediaSource, mPlaybackModel.getMediaSource());
+ }
+
+ private void updateTabs(List<MediaItemMetadata> items) {
+ List<MediaItemMetadata> browsableTopLevel = items.stream()
+ .filter(item -> item.isBrowsable())
+ .collect(Collectors.toList());
+ List<MediaItemMetadata> playableTopLevel = items.stream()
+ .filter(item -> item.isPlayable())
+ .collect(Collectors.toList());
+
+ Log.i(TAG, "Updating top level: " + browsableTopLevel.size() + " browsable items, "
+ + playableTopLevel.size() + " playable items");
+ mTabLayout.removeAllTabs();
+ // Show tabs if:
+ // - We have some browesable items and we are forced to show them as tabs,
+ // - or we have only browsable items on the top level and they are not too many.
+ if ((!browsableTopLevel.isEmpty() && mForceBrowseTabs)
+ || (playableTopLevel.isEmpty() && !browsableTopLevel.isEmpty()
+ && browsableTopLevel.size() <= mMaxBrowserTabs)) {
+ mAppBarLayout.setVisibility(View.GONE);
+ mTabLayout.setVisibility(View.VISIBLE);
+ int count = 0;
+ for (MediaItemMetadata item : browsableTopLevel) {
+ MediaItemTabView tab = new MediaItemTabView(this, item);
+ mTabLayout.addTab(mTabLayout.newTab().setCustomView(tab).setTag(item));
+ count++;
+ if (count >= mMaxBrowserTabs) {
+ break;
+ }
+ }
+ updateBrowseFragment(browsableTopLevel.get(0));
+ } else {
+ mAppBarLayout.setVisibility(View.VISIBLE);
+ mTabLayout.setVisibility(View.INVISIBLE);
+ updateBrowseFragment(null);
+ }
+ }
+
+ private void switchToMode(Mode mode) {
+ mMode = mode;
+ switch(mode) {
+ case PLAYBACK:
+ showFragment(mPlaybackFragment);
+ break;
+ case BROWSING:
+ // Browse fragment will be loaded once we have the top level items.
+ showFragment(null);
+ updateMetadata();
+ break;
+ }
+ }
+
+ private void updateBrowseFragment(MediaItemMetadata topItem) {
+ if (mMode != Mode.BROWSING) {
return;
}
+ showFragment(mContentForwardBrowseEnabled
+ ? BrowseFragment.newInstance(mMediaSource, topItem)
+ : null);
+ }
+
+ private void updateMetadata() {
+ if (isCurrentMediaSourcePlaying()) {
+ mAlbumBackground.setVisibility(View.VISIBLE);
+ mBrowseScrim.setVisibility(mCurrentFragment == mPlaybackFragment
+ ? View.GONE
+ : View. VISIBLE);
+ MediaItemMetadata metadata = mPlaybackModel.getMetadata();
+ if (Objects.equals(mCurrentMetadata, metadata)) {
+ return;
+ }
+ mCurrentMetadata = metadata;
+ Log.i(TAG, "Updating metadata: " + metadata);
+ if (metadata != null) {
+ metadata.getAlbumArt(this,
+ mAlbumBackground.getWidth(),
+ mAlbumBackground.getHeight(),
+ false)
+ .thenAccept(this::setBackgroundImage);
+ } else {
+ mAlbumBackground.setImageBitmap(null, true);
+ }
+ } else {
+ mAlbumBackground.setVisibility(View.GONE);
+ mBrowseScrim.setVisibility(View.GONE);
+ }
+ }
- Intent intent = new Intent(MediaConstants.ACTION_MEDIA_STATUS);
- intent.setPackage(componentName.getPackageName());
- intent.putExtra(MediaConstants.MEDIA_CONNECTION_STATUS, connectionStatus);
- sendBroadcast(intent);
+ private void showFragment(Fragment fragment) {
+ FragmentManager manager = getSupportFragmentManager();
+ if (fragment == null) {
+ if (mCurrentFragment != null) {
+ manager.beginTransaction()
+ .remove(mCurrentFragment)
+ .commit();
+ }
+ } else {
+ manager.beginTransaction()
+ .replace(R.id.fragment_container, fragment)
+ .commit();
+ }
+ mCurrentFragment = fragment;
}
- private final MediaManager.Listener mListener = new MediaManager.Listener() {
- @Override
- public void onMediaAppChanged(ComponentName componentName) {
- sendMediaConnectionStatusBroadcast(componentName, MediaConstants.MEDIA_CONNECTED);
+ private void setBackgroundImage(Bitmap bitmap) {
+ // TODO(b/77551865): Implement image blurring once the following issue is solved:
+ // b/77551557
+ // bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale,
+ // mBackgroundBlurRadius);
+ mAlbumBackground.setImageBitmap(bitmap, true);
+ }
+
+ @Override
+ public MediaSource getMediaSource(String packageName) {
+ if (mMediaSource != null && mMediaSource.getPackageName().equals(packageName)) {
+ return mMediaSource;
+ }
+ if (mPlaybackModel.getMediaSource() != null &&
+ mPlaybackModel.getMediaSource().getPackageName().equals(packageName)) {
+ return mPlaybackModel.getMediaSource();
}
+ return new MediaSource(this, packageName);
+ }
- @Override
- public void onStatusMessageChanged(String msg) {}
- };
+ @Override
+ public void onBackStackChanged() {
+ // TODO: Update ActionBar
+ }
+
+ @Override
+ public void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item) {
+ mPlaybackModel.onStop();
+ mediaSource.getPlaybackModel().onPlayItem(item.getId());
+ setIntent(null);
+ switchToMode(Mode.PLAYBACK);
+ }
}
diff --git a/src/com/android/car/media/MediaPlaybackModel.java b/src/com/android/car/media/MediaPlaybackModel.java
index 96cfe59..a223893 100644
--- a/src/com/android/car/media/MediaPlaybackModel.java
+++ b/src/com/android/car/media/MediaPlaybackModel.java
@@ -299,6 +299,11 @@ public class MediaPlaybackModel {
mController.unregisterCallback(mMediaControllerCallback);
mController = null;
}
+
+ final ComponentName currentName = mCurrentComponentName;
+ notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
+ mCurrentComponentName = name;
+
mBrowser.connect();
// reset the colors and views if we switch to another app.
@@ -306,10 +311,6 @@ public class MediaPlaybackModel {
mPrimaryColor = manager.getMediaClientPrimaryColor();
mAccentColor = manager.getMediaClientAccentColor();
mPrimaryColorDark = manager.getMediaClientPrimaryColorDark();
-
- final ComponentName currentName = mCurrentComponentName;
- notifyListeners((listener) -> listener.onMediaAppChanged(currentName, name));
- mCurrentComponentName = name;
});
}
diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java
index e14dbc8..c5772a5 100644
--- a/src/com/android/car/media/PlaybackFragment.java
+++ b/src/com/android/car/media/PlaybackFragment.java
@@ -16,6 +16,7 @@
package com.android.car.media;
+import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.os.Bundle;
@@ -43,35 +44,34 @@ 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 android.media.session.MediaSession} API.
*/
-public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer {
+public class PlaybackFragment extends Fragment {
private static final String TAG = "PlaybackFragment";
private static final DateFormat TIME_FORMAT = new SimpleDateFormat("m:ss", Locale.US);
private PlaybackModel mModel;
- private CrossfadeImageView mAlbumBackground;
private PlaybackControls mPlaybackControls;
private ImageView mAlbumArt;
private TextView mTitle;
private TextView mTime;
private TextView mSubtitle;
private SeekBar mSeekbar;
- private PagedListView mBrowseList;
- private int mBackgroundRawImageSize;
- private float mBackgroundBlurRadius;
- private float mBackgroundBlurScale;
- private MediaSource mMediaSource;
- private BrowseAdapter mBrowseAdapter;
- private ViewGroup mMetadataContainer;
+ private PagedListView mQueueList;
+ private QueueItemsAdapter mQueueAdapter;
private MediaItemMetadata mCurrentMetadata;
private PlaybackModel.PlaybackObserver mPlaybackObserver = new PlaybackModel.PlaybackObserver() {
@Override
@@ -84,7 +84,6 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
updateState();
updateMetadata();
updateAccentColor();
- updateBrowse();
}
@Override
@@ -92,24 +91,48 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
updateMetadata();
}
};
- private MediaSource.Observer mMediaSourceObserver = new MediaSource.Observer() {
+ private ListItemProvider mQueueItemsProvider = new ListItemProvider() {
@Override
- protected void onBrowseConnected(boolean success) {
- PlaybackFragment.this.onBrowseConnected(success);
+ public ListItem get(int position) {
+ if (!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 -> onQueueItemClicked(item));
+ return textListItem;
}
-
@Override
- protected void onBrowseDisconnected() {
- PlaybackFragment.this.onBrowseDisconnected();
+ public int size() {
+ if (!mModel.hasQueue()) {
+ return 0;
+ }
+ return mModel.getQueue().size();
}
};
+ private static class QueueItemsAdapter extends ListItemAdapter {
+ QueueItemsAdapter(Context context, ListItemProvider itemProvider) {
+ super(context, itemProvider, BackgroundStyle.SOLID);
+ }
+
+ void refresh() {
+ // TODO: Perform a diff between current and new content and trigger the proper
+ // RecyclerView updates.
+ this.notifyDataSetChanged();
+ }
+ }
@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());
- mAlbumBackground = view.findViewById(R.id.album_background);
mPlaybackControls = view.findViewById(R.id.playback_controls);
mPlaybackControls.setModel(mModel);
ViewGroup playbackContainer = view.findViewById(R.id.playback_container);
@@ -119,23 +142,14 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
mSubtitle = view.findViewById(R.id.subtitle);
mSeekbar = view.findViewById(R.id.seek_bar);
mTime = view.findViewById(R.id.time);
- mBrowseList = view.findViewById(R.id.browse_list);
- mMetadataContainer = view.findViewById(R.id.metadata_container);
- GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 4);
- RecyclerView recyclerView = mBrowseList.getRecyclerView();
- recyclerView.setLayoutManager(gridLayoutManager);
+ mQueueList = view.findViewById(R.id.queue_list);
+ RecyclerView recyclerView = mQueueList.getRecyclerView();
recyclerView.setVerticalFadingEdgeEnabled(true);
recyclerView.setFadingEdgeLength(getResources()
.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();
- getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true);
- mBackgroundBlurScale = outValue.getFloat();
- mBackgroundRawImageSize = getContext().getResources().getDimensionPixelSize(
- R.dimen.playback_background_raw_image_size);
+ mQueueAdapter = new QueueItemsAdapter(getContext(), mQueueItemsProvider);
+ mQueueList.setAdapter(mQueueAdapter);
+
return view;
}
@@ -143,21 +157,13 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
public void onStart() {
super.onStart();
mModel.registerObserver(mPlaybackObserver);
- if (mMediaSource != null) {
- mMediaSource.subscribe(mMediaSourceObserver);
- }
}
@Override
public void onStop() {
super.onStop();
mModel.unregisterObserver(mPlaybackObserver);
- if (mMediaSource != null) {
- mMediaSource.unsubscribe(mMediaSourceObserver);
- }
- if (mBrowseAdapter != null) {
- mBrowseAdapter.stop();
- }
+ mCurrentMetadata = null;
}
private void updateState() {
@@ -168,11 +174,8 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
} else {
mSeekbar.removeCallbacks(mSeekBarRunnable);
}
- mBrowseList.setVisibility(mModel.hasQueue() ? View.VISIBLE : View.GONE);
- if (mBrowseAdapter != null) {
- mBrowseAdapter.setQueue(mModel.getQueue(), mModel.getQueueTitle());
- }
+ mQueueAdapter.refresh();
}
private void updateMetadata() {
@@ -184,23 +187,6 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
mTitle.setText(metadata != null ? metadata.getTitle() : null);
mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null);
MediaItemMetadata.updateImageView(getContext(), metadata, mAlbumArt, 0);
- if (metadata != null) {
- metadata.getAlbumArt(getContext(),
- mBackgroundRawImageSize,
- mBackgroundRawImageSize,
- false)
- .thenAccept(this::setBackgroundImage);
- } else {
- mAlbumBackground.setImageBitmap(null, true);
- }
- }
-
- private void setBackgroundImage(Bitmap bitmap) {
- // TODO(b/77551865): Implement image blurring once the following issue is solved:
- // b/77551557
- // bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale,
- // mBackgroundBlurRadius);
- mAlbumBackground.setImageBitmap(bitmap, true);
}
private void updateAccentColor() {
@@ -237,139 +223,14 @@ public class PlaybackFragment extends Fragment implements BrowseAdapter.Observer
mSeekbar.setProgress((int) mModel.getProgress());
}
+ private void onQueueItemClicked(MediaItemMetadata item) {
+ mModel.onSkipToQueueItem(item.getQueueId());
+ }
+
/**
* Collapses the playback controls.
*/
public void closeOverflowMenu() {
mPlaybackControls.close();
}
-
- /**
- * Updates the information on the media source being browsed.
- */
- public void updateBrowse() {
- MediaSource newSource = getCurrentMediaSource();
-
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "Updating browse: new source "
- + (newSource != null ? newSource.getPackageName() : null)
- + ", current source: "
- + (mMediaSource != null ? mMediaSource.getPackageName() : null)
- + ", currently playing: "
- + (mModel.getMediaSource() != null
- ? mModel.getMediaSource().getPackageName() : null));
- }
-
- // Visibility might change both because the browsed source changed or because the
- // source being played changed.
- if (Objects.equals(newSource, mModel.getMediaSource())) {
- // We are playing: show everything
- mAlbumBackground.setVisibility(View.VISIBLE);
- mPlaybackControls.setVisibility(View.VISIBLE);
- mAlbumArt.setVisibility(View.VISIBLE);
- mTitle.setVisibility(View.VISIBLE);
- mTime.setVisibility(View.VISIBLE);
- mSubtitle.setVisibility(View.VISIBLE);
- mSeekbar.setVisibility(View.VISIBLE);
- mMetadataContainer.setVisibility(View.VISIBLE);
- } else {
- // Hide playback
- mAlbumBackground.setVisibility(View.INVISIBLE);
- mPlaybackControls.setVisibility(View.GONE);
- mAlbumArt.setVisibility(View.VISIBLE);
- mTitle.setVisibility(View.GONE);
- mTime.setVisibility(View.GONE);
- mSubtitle.setVisibility(View.GONE);
- mSeekbar.setVisibility(View.GONE);
- mMetadataContainer.setVisibility(View.GONE);
- }
-
- if (Objects.equals(mMediaSource, newSource)) {
- // Browse information hasn't changed. Nothing to do.
- return;
- }
-
- if (mMediaSource != null) {
- mMediaSource.unsubscribe(mMediaSourceObserver);
- }
- mMediaSource = newSource;
- if (newSource == null) return;
- mMediaSource.subscribe(mMediaSourceObserver);
-
- MediaManager.getInstance(getContext())
- .setMediaClientComponent(mMediaSource.getBrowseServiceComponentName());
- }
-
- private MediaSource getCurrentMediaSource() {
- if (getActivity() == null || getActivity().getIntent() == null
- || !getActivity().getIntent().hasExtra(
- MediaManager.KEY_MEDIA_PACKAGE)) {
- return mModel.getMediaSource();
- } else {
- String packageName = getActivity().getIntent().getStringExtra(
- MediaManager.KEY_MEDIA_PACKAGE);
- return new MediaSource(getContext(), packageName);
- }
- }
-
- private void onBrowseConnected(boolean success) {
- if (mBrowseAdapter != null) {
- mBrowseAdapter.stop();
- mBrowseAdapter = null;
- }
- if (!success) {
- mBrowseList.setVisibility(View.GONE);
- // TODO(b/77647430) implement intermediate states.
- return;
- } else {
- mBrowseList.setVisibility(View.VISIBLE);
- }
- mBrowseAdapter = new BrowseAdapter(getContext(), mMediaSource, null,
- ContentForwardStrategy.DEFAULT_STRATEGY);
- mBrowseList.setAdapter(mBrowseAdapter);
- mBrowseAdapter.registerObserver(this);
- mBrowseAdapter.start();
- }
-
- private 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.onStop();
- mMediaSource.getPlaybackModel().onPlayItem(item.getId());
- getActivity().setIntent(null);
- }
-
- @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
index 0b93ed6..0092c5a 100644
--- a/src/com/android/car/media/browse/BrowseAdapter.java
+++ b/src/com/android/car/media/browse/BrowseAdapter.java
@@ -30,7 +30,6 @@ import android.view.View;
import android.view.ViewGroup;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.MediaSource;
import java.util.ArrayList;
import java.util.Collection;
@@ -67,9 +66,9 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
private static final String TAG = "MediaBrowseAdapter";
@NonNull
private final Context mContext;
- private final MediaSource mMediaSource;
- private final MediaItemMetadata mParentMediaItem;
+ private final MediaBrowser mMediaBrowser;
private final ContentForwardStrategy mCFBStrategy;
+ private MediaItemMetadata mParentMediaItem;
private LinkedHashMap<String, MediaItemState> mItemStates = new LinkedHashMap<>();
private List<BrowseViewData> mViewData = new ArrayList<>();
private String mParentMediaItemId;
@@ -82,37 +81,37 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
/**
* An {@link BrowseAdapter} observer.
*/
- public interface Observer {
+ public static abstract class 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();
+ protected void onDirty() {};
/**
* Callback invoked when a user clicks on a playable item.
*/
- void onPlayableItemClicked(MediaItemMetadata item);
+ protected void onPlayableItemClicked(MediaItemMetadata item) {};
/**
* Callback invoked when a user clicks on a browsable item.
*/
- void onBrowseableItemClicked(MediaItemMetadata item);
+ protected void onBrowseableItemClicked(MediaItemMetadata item) {};
/**
* Callback invoked when a user clicks on a the "more items" button on a section.
*/
- void onMoreButtonClicked(MediaItemMetadata item);
+ protected void onMoreButtonClicked(MediaItemMetadata item) {};
/**
* Callback invoked when the user clicks on the title of the queue.
*/
- void onQueueTitleClicked();
+ protected void onQueueTitleClicked() {};
/**
* Callback invoked when the user clicks on a queue item.
*/
- void onQueueItemClicked(MediaItemMetadata item);
+ protected void onQueueItemClicked(MediaItemMetadata item) {};
}
private MediaBrowser.SubscriptionCallback mSubscriptionCallback =
@@ -179,15 +178,15 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
/**
* Creates a {@link BrowseAdapter} that displays the children of the given media tree node.
*
- * @param mediaSource the {@link MediaSource} to get data from.
+ * @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 MediaSource mediaSource,
+ public BrowseAdapter(Context context, @NonNull MediaBrowser mediaBrowser,
@Nullable MediaItemMetadata parentItem, @NonNull ContentForwardStrategy strategy) {
mContext = context;
- mMediaSource = mediaSource;
+ mMediaBrowser = mediaBrowser;
mParentMediaItem = parentItem;
mCFBStrategy = strategy;
}
@@ -199,8 +198,8 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
public void start() {
mParentMediaItemId = mParentMediaItem != null
? mParentMediaItem.getId()
- : mMediaSource.getMediaBrowser().getRoot();
- mMediaSource.getMediaBrowser().subscribe(mParentMediaItemId, mSubscriptionCallback);
+ : mMediaBrowser.getRoot();
+ mMediaBrowser.subscribe(mParentMediaItemId, mSubscriptionCallback);
for (MediaItemState itemState : mItemStates.values()) {
subscribe(itemState);
}
@@ -214,7 +213,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
// Not started
return;
}
- mMediaSource.getMediaBrowser().unsubscribe(mParentMediaItemId, mSubscriptionCallback);
+ mMediaBrowser.unsubscribe(mParentMediaItemId, mSubscriptionCallback);
for (MediaItemState itemState : mItemStates.values()) {
unsubscribe(itemState);
}
@@ -222,6 +221,25 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
}
/**
+ * Replaces the media item whose children are being displayed in this adapter. The content of
+ * the adapter will be replaced once the children of the new item are loaded.
+ *
+ * @param parentItem new media item to expand.
+ */
+ public void setParentMediaItemId(@Nullable MediaItemMetadata parentItem) {
+ String newParentMediaItemId = parentItem != null
+ ? parentItem.getId()
+ : mMediaBrowser.getRoot();
+ if (Objects.equals(newParentMediaItemId, mParentMediaItemId)) {
+ return;
+ }
+ mMediaBrowser.unsubscribe(mParentMediaItemId, mSubscriptionCallback);
+ mParentMediaItem = parentItem;
+ mParentMediaItemId = newParentMediaItemId;
+ mMediaBrowser.subscribe(mParentMediaItemId, mSubscriptionCallback);
+ }
+
+ /**
* Sets media queue items into this adapter.
*/
public void setQueue(List<MediaItemMetadata> items, CharSequence queueTitle) {
@@ -297,14 +315,14 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
private void subscribe(MediaItemState state) {
if (!state.mIsSubscribed && state.mItem.isBrowsable()) {
- mMediaSource.getMediaBrowser().subscribe(state.mItem.getId(), mSubscriptionCallback);
+ mMediaBrowser.subscribe(state.mItem.getId(), mSubscriptionCallback);
state.mIsSubscribed = true;
}
}
private void unsubscribe(MediaItemState state) {
if (state.mIsSubscribed) {
- mMediaSource.getMediaBrowser().unsubscribe(state.mItem.getId(), mSubscriptionCallback);
+ mMediaBrowser.unsubscribe(state.mItem.getId(), mSubscriptionCallback);
state.mIsSubscribed = false;
}
}
@@ -369,6 +387,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
}
private void notify(Consumer<Observer> notification) {
+ Log.i(TAG, "Notifying: " + notification);
for (Observer observer : mObservers) {
notification.accept(observer);
}
@@ -446,8 +465,10 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
void addItem(MediaItemMetadata item, BrowseViewData.State state,
BrowseItemViewType viewType, Consumer<Observer> notification) {
- result.add(new BrowseViewData(item, viewType, state,
- view -> BrowseAdapter.this.notify(notification)));
+ View.OnClickListener listener = notification != null ?
+ view -> BrowseAdapter.this.notify(notification) :
+ null;
+ result.add(new BrowseViewData(item, viewType, state, listener));
}
void addItems(List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxRows) {
@@ -534,7 +555,8 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> {
}
} else if (item.isPlayable()) {
itemsBuilder.addItem(item, itemState.mState,
- mCFBStrategy.getPlayableViewType(mParentMediaItem), null);
+ mCFBStrategy.getPlayableViewType(mParentMediaItem),
+ observer -> observer.onPlayableItemClicked(item));
}
}
diff --git a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
index 22d8920..ab2593c 100644
--- a/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
+++ b/src/com/android/car/media/drawer/MediaBrowserItemsFetcher.java
@@ -69,7 +69,12 @@ class MediaBrowserItemsFetcher implements MediaItemsFetcher {
public void start(ItemsUpdatedCallback callback) {
mCallback = callback;
updateQueueAvailability();
- mMediaPlaybackModel.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);
+ if (mMediaPlaybackModel.isConnected()) {
+ mMediaPlaybackModel.getMediaBrowser().subscribe(mMediaId, mSubscriptionCallback);
+ } else {
+ mItems.clear();
+ callback.onItemsUpdated();
+ }
mMediaPlaybackModel.addListener(mModelListener);
}
@@ -83,6 +88,12 @@ class MediaBrowserItemsFetcher implements MediaItemsFetcher {
public void onSessionDestroyed(CharSequence destroyedMediaClientName) {
updateQueueAvailability();
}
+ @Override
+ public void onMediaConnectionSuspended() {
+ if (mCallback != null) {
+ mCallback.onItemsUpdated();
+ }
+ }
};
private final MediaBrowser.SubscriptionCallback mSubscriptionCallback =
@@ -96,7 +107,6 @@ class MediaBrowserItemsFetcher implements MediaItemsFetcher {
@Override
public void onError(String parentId) {
- Log.e(TAG, "Error loading children of: " + mMediaId);
mItems.clear();
mCallback.onItemsUpdated();
}
diff --git a/src/com/android/car/media/drawer/MediaDrawerAdapter.java b/src/com/android/car/media/drawer/MediaDrawerAdapter.java
index acf816e..c1f5e07 100644
--- a/src/com/android/car/media/drawer/MediaDrawerAdapter.java
+++ b/src/com/android/car/media/drawer/MediaDrawerAdapter.java
@@ -72,16 +72,15 @@ class MediaDrawerAdapter extends CarDrawerAdapter {
*/
void setFetcherAndInvoke(MediaItemsFetcher fetcher) {
setFetcher(fetcher);
- mCurrentFetcher.start(() -> {
- if (mFetchCallback != null) {
- mFetchCallback.onFetchEnd();
- }
- notifyDataSetChanged();
- });
if (mFetchCallback != null) {
mFetchCallback.onFetchStart();
}
+
+ mCurrentFetcher.start(() -> {
+ closeFetch();
+ notifyDataSetChanged();
+ });
}
void setFetcher(MediaItemsFetcher fetcher) {
@@ -89,6 +88,7 @@ class MediaDrawerAdapter extends CarDrawerAdapter {
mCurrentFetcher.cleanup();
}
mCurrentFetcher = fetcher;
+ notifyDataSetChanged();
}
@Override
@@ -124,8 +124,16 @@ class MediaDrawerAdapter extends CarDrawerAdapter {
if (mCurrentFetcher != null) {
mCurrentFetcher.cleanup();
mCurrentFetcher = null;
+ notifyDataSetChanged();
+ }
+ closeFetch();
+ }
+
+ private void closeFetch() {
+ if (mFetchCallback != null) {
+ mFetchCallback.onFetchEnd();
+ mFetchCallback = null;
}
- mFetchCallback = null;
}
public void scrollToCurrent() {
diff --git a/src/com/android/car/media/drawer/MediaDrawerController.java b/src/com/android/car/media/drawer/MediaDrawerController.java
index 2745b2e..06b2068 100644
--- a/src/com/android/car/media/drawer/MediaDrawerController.java
+++ b/src/com/android/car/media/drawer/MediaDrawerController.java
@@ -149,7 +149,7 @@ public class MediaDrawerController implements MediaDrawerAdapter.MediaFetchCallb
mDrawerController.removeDrawerListener(mQueueDrawerListener);
mRootAdapter.cleanup();
mMediaPlaybackModel.removeListener(mModelListener);
- mMediaPlaybackModel.stop();
+ mMediaPlaybackModel.stop();
}
/**
@@ -215,6 +215,7 @@ public class MediaDrawerController implements MediaDrawerAdapter.MediaFetchCallb
@Nullable ComponentName newName) {
// Only store MediaManager instance to a local variable when it is short lived.
MediaManager mediaManager = MediaManager.getInstance(mContext);
+ mRootAdapter.cleanup();
mRootAdapter.setTitle(mediaManager.getMediaClientName());
}
diff --git a/src/com/android/car/media/util/widgets/MediaItemTabView.java b/src/com/android/car/media/util/widgets/MediaItemTabView.java
new file mode 100644
index 0000000..2bcd4d4
--- /dev/null
+++ b/src/com/android/car/media/util/widgets/MediaItemTabView.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util.widgets;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.car.media.R;
+import com.android.car.media.common.MediaItemMetadata;
+
+/**
+ * A view representing a media item to be included in the tab bar at the top of the UI.
+ */
+public class MediaItemTabView extends LinearLayout {
+ private TextView mTitleView;
+ private ImageView mImageView;
+
+ /**
+ * Creates a new tab for the given media item.
+ */
+ public MediaItemTabView(Context context, MediaItemMetadata item) {
+ super(context);
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.tab_view, this, true);
+ setOrientation(LinearLayout.VERTICAL);
+
+ mImageView = findViewById(R.id.icon);
+ MediaItemMetadata.updateImageView(context, item, mImageView, 0);
+ mTitleView = findViewById(R.id.title);
+ mTitleView.setText(item.getTitle());
+ }
+}