diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2018-04-24 07:21:13 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2018-04-24 07:21:13 +0000 |
commit | 9f07c6c37dc0d26178ade6e6c6f54e3743e07c39 (patch) | |
tree | 5721e38526d6739035d9e76f88942ee8e9418d34 | |
parent | 9a05aa74c41d087b4f2f28e9b2bf1676ef9e84cf (diff) | |
parent | d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837 (diff) | |
download | Media-9f07c6c37dc0d26178ade6e6c6f54e3743e07c39.tar.gz |
Snap for 4739962 from d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837 to pi-release
Change-Id: I03083a1a3b9cc49bf579479a3af4c7609c1efca4
23 files changed, 986 insertions, 572 deletions
@@ -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()); + } +} |