diff options
author | Roberto Perez <robertoalexis@google.com> | 2018-05-01 09:41:37 -0700 |
---|---|---|
committer | Roberto Perez <robertoalexis@google.com> | 2018-05-03 23:32:41 -0700 |
commit | 450c73a79e566f1c4a8758648525b457005e9f9a (patch) | |
tree | 27420a62a591e61d3b32c89a417cd9396845a385 | |
parent | 839543fa68e92663908eb074b65c74e801168aed (diff) | |
download | Media-450c73a79e566f1c4a8758648525b457005e9f9a.tar.gz |
DO NOT MERGE Remember last browsed application and improving transitions.
Bug: 78895772
Test: Launched on Big Dog and Mojave
Change-Id: I7db5cbcc458dc2d8a94f85808bda687eb7f68dc3
-rw-r--r-- | AndroidManifest.xml | 2 | ||||
-rw-r--r-- | res/layout/appbar_view.xml | 1 | ||||
-rw-r--r-- | res/layout/fragment_browse.xml | 33 | ||||
-rw-r--r-- | res/layout/fragment_metadata.xml | 25 | ||||
-rw-r--r-- | res/layout/fragment_metadata_with_queue.xml | 3 | ||||
-rw-r--r-- | res/layout/initial_no_content.xml | 59 | ||||
-rw-r--r-- | res/layout/media_browse_grid_item.xml | 2 | ||||
-rw-r--r-- | res/layout/metadata_normal.xml | 1 | ||||
-rw-r--r-- | res/values/dimens.xml | 2 | ||||
-rw-r--r-- | res/values/integers.xml | 3 | ||||
-rw-r--r-- | res/values/strings.xml | 4 | ||||
-rw-r--r-- | src/com/android/car/media/BrowseFragment.java | 95 | ||||
-rw-r--r-- | src/com/android/car/media/MediaActivity.java | 158 | ||||
-rw-r--r-- | src/com/android/car/media/PlaybackFragment.java | 27 | ||||
-rw-r--r-- | src/com/android/car/media/browse/BrowseAdapter.java | 44 | ||||
-rw-r--r-- | src/com/android/car/media/browse/BrowseViewData.java | 18 | ||||
-rw-r--r-- | src/com/android/car/media/browse/ContentForwardStrategy.java | 2 | ||||
-rw-r--r-- | src/com/android/car/media/widgets/AppBarView.java | 16 | ||||
-rw-r--r-- | src/com/android/car/media/widgets/MediaItemTabView.java | 1 |
19 files changed, 309 insertions, 187 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 08b2d59..47da7be 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -19,6 +19,8 @@ package="com.android.car.media" > <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-sdk android:minSdkVersion="24" diff --git a/res/layout/appbar_view.xml b/res/layout/appbar_view.xml index 31e965e..7e15e8c 100644 --- a/res/layout/appbar_view.xml +++ b/res/layout/appbar_view.xml @@ -30,7 +30,6 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal" - android:animateLayoutChanges="true" android:gravity="center" android:layout_alignParentLeft="true" android:layout_centerInParent="true"/> diff --git a/res/layout/fragment_browse.xml b/res/layout/fragment_browse.xml index 508292e..42c28dc 100644 --- a/res/layout/fragment_browse.xml +++ b/res/layout/fragment_browse.xml @@ -14,19 +14,48 @@ See the License for the specific language governing permissions and limitations under the License. --> -<FrameLayout +<RelativeLayout 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"> + <ProgressBar + android:id="@+id/loading_spinner" + android:layout_width="match_parent" + android:layout_height="@dimen/car_double_line_list_item_height" + android:layout_centerInParent="true" + android:layout_alignWithParentIfMissing="true" + android:indeterminateDrawable="@drawable/music_buffering" + android:visibility="gone" /> + + <ImageView + android:id="@+id/error_icon" + android:layout_width="@dimen/missing_permission_icon_size" + android:layout_height="@dimen/missing_permission_icon_size" + android:layout_centerInParent="true" + android:src="@drawable/error_illustration" + android:visibility="gone" /> + + <TextView + android:id="@+id/error_message" + style="@style/TextAppearance.NoContent" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@+id/error_icon" + android:gravity="center" + android:maxLines="3" + android:text="@string/nothing_to_play" + android:visibility="gone" /> + <androidx.car.widget.PagedListView android:id="@+id/browse_list" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" android:focusable="true" + android:visibility="gone" app:dayNightStyle="force_night" app:showPagedListViewDivider="false"/> -</FrameLayout>
\ No newline at end of file +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/fragment_metadata.xml b/res/layout/fragment_metadata.xml index e43ceb6..9775c04 100644 --- a/res/layout/fragment_metadata.xml +++ b/res/layout/fragment_metadata.xml @@ -36,26 +36,29 @@ app:layout_constraintVertical_chainStyle="packed" tools:src="@drawable/ic_person"/> - <include android:id="@+id/metadata_subcontainer" - layout="@layout/metadata_normal" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/car_padding_5" - app:layout_constraintBottom_toTopOf="@+id/playback_controls" - app:layout_constraintEnd_toEndOf="@+id/album_art" - app:layout_constraintStart_toStartOf="@+id/album_art" - app:layout_constraintTop_toBottomOf="@+id/album_art"/> + <include + android:id="@+id/metadata_subcontainer" + layout="@layout/metadata_normal" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/car_padding_5" + android:layout_marginBottom="@dimen/playback_controls_margin" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@+id/album_art" + app:layout_constraintStart_toStartOf="@+id/album_art" + app:layout_constraintTop_toBottomOf="@+id/album_art"/> <androidx.car.widget.PagedListView android:id="@+id/queue_list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="@dimen/playback_album_art_size_normal" - android:visibility="invisible" + android:layout_marginBottom="@dimen/playback_controls_margin" + android:visibility="gone" app:dividerEndMargin="@dimen/car_keyline_1" app:dividerStartMargin="@dimen/car_keyline_1" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:layout_constraintBottom_toTopOf="@+id/playback_controls" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/margin_top" app:listDividerColor="@color/car_list_divider_inverse"/> </merge> diff --git a/res/layout/fragment_metadata_with_queue.xml b/res/layout/fragment_metadata_with_queue.xml index d2d4eb3..a87d12f 100644 --- a/res/layout/fragment_metadata_with_queue.xml +++ b/res/layout/fragment_metadata_with_queue.xml @@ -50,10 +50,11 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="@dimen/car_padding_4" + android:layout_marginBottom="@dimen/playback_controls_margin" app:dividerEndMargin="@dimen/car_keyline_1" app:dividerStartMargin="@dimen/car_keyline_1" app:layout_behavior="@string/appbar_scrolling_view_behavior" - app:layout_constraintBottom_toTopOf="@+id/playback_controls" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/album_art" app:listDividerColor="@color/car_list_divider_inverse"/> diff --git a/res/layout/initial_no_content.xml b/res/layout/initial_no_content.xml deleted file mode 100644 index bc7a199..0000000 --- a/res/layout/initial_no_content.xml +++ /dev/null @@ -1,59 +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" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:animateLayoutChanges="true" > - - <RelativeLayout - android:id="@+id/initial_view" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center_horizontal"> - - <ProgressBar - android:id="@+id/loading_spinner" - android:layout_width="match_parent" - android:layout_height="@dimen/car_double_line_list_item_height" - android:layout_centerInParent="true" - android:layout_alignWithParentIfMissing="true" - android:indeterminateDrawable="@drawable/music_buffering" - android:visibility="gone" /> - - <ImageView - android:id="@+id/error_icon" - android:layout_width="@dimen/missing_permission_icon_size" - android:layout_height="@dimen/missing_permission_icon_size" - android:layout_centerInParent="true" - android:src="@drawable/error_illustration" - android:visibility="gone" /> - - <TextView - android:id="@+id/tap_to_select_item" - style="@style/TextAppearance.NoContent" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_alignParentBottom="true" - android:layout_marginBottom="48dp" - android:gravity="center" - android:maxLines="3" - android:text="@string/nothing_to_play" - android:visibility="gone" /> - </RelativeLayout> -</FrameLayout> diff --git a/res/layout/media_browse_grid_item.xml b/res/layout/media_browse_grid_item.xml index c6112d6..3cc10d7 100644 --- a/res/layout/media_browse_grid_item.xml +++ b/res/layout/media_browse_grid_item.xml @@ -18,6 +18,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/container" + android:focusable="true" + android:clickable="true" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> diff --git a/res/layout/metadata_normal.xml b/res/layout/metadata_normal.xml index a98e3e0..f280429 100644 --- a/res/layout/metadata_normal.xml +++ b/res/layout/metadata_normal.xml @@ -66,7 +66,6 @@ android:layout_width="match_parent" android:layout_height="@dimen/playback_seekbar_height" android:layout_marginTop="@dimen/car_padding_2" - android:background="@color/car_grey_900" android:paddingEnd="0dp" android:paddingStart="0dp" android:progressDrawable="@drawable/seekbar_background" diff --git a/res/values/dimens.xml b/res/values/dimens.xml index cf3e68a..67bf4fb 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -47,6 +47,8 @@ <dimen name="playback_album_art_size_normal">156dp</dimen> <!-- Size of the album art thumbnail when large --> <dimen name="playback_album_art_size_large">574dp</dimen> + <!-- Bottom marging for the playback queue --> + <dimen name="playback_controls_margin">192dp</dimen> <!-- Tab height --> <dimen name="browse_tab_height">76dp</dimen> diff --git a/res/values/integers.xml b/res/values/integers.xml index eac1eef..8a70b8a 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -32,4 +32,7 @@ <!-- The amount of time it takes for the app selector to fade in and out --> <integer name="app_selector_fade_duration">500</integer> + + <!-- Time allowed for a process to complete before we show a progress indicator --> + <integer name="progress_indicator_delay">500</integer> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 84d60fd..bdc7e92 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -17,7 +17,7 @@ <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <!-- Music --> <!-- Prompt text to display when the user hasn't picked any songs to play yet. [CHAR LIMIT=57]--> - <string name="nothing_to_play">To play something, open the menu at the top left.</string> + <string name="nothing_to_play">This folder is empty.</string> <!-- Prompt text to display when we failed to connect to a media app. [CHAR LIMIT=50]--> <string name="cannot_connect_to_app"><xliff:g name="app">%s</xliff:g> doesn\'t seem to be working right now.</string> <!-- Prompt text to display when connecting to a media app. [CHAR LIMIT=50] --> @@ -28,4 +28,6 @@ <string name="unknown_error">Something went wrong</string> <!-- Text of the button displayed at the bottom of a media browse section to provide access to additional items. [CHAR LIMIT=100] --> <string name="media_browse_more">More …</string> + <!-- Media template title --> + <string name="media_app_title">Media</string> </resources> diff --git a/src/com/android/car/media/BrowseFragment.java b/src/com/android/car/media/BrowseFragment.java index 3c56c0e..7c6b8b4 100644 --- a/src/com/android/car/media/BrowseFragment.java +++ b/src/com/android/car/media/BrowseFragment.java @@ -16,16 +16,23 @@ package com.android.car.media; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.os.Bundle; +import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; import com.android.car.media.browse.BrowseAdapter; import com.android.car.media.browse.ContentForwardStrategy; @@ -49,11 +56,17 @@ public class BrowseFragment extends Fragment { private static final String BROWSE_STACK_KEY = "browse_stack"; private PagedListView mBrowseList; + private ProgressBar mProgressBar; + private ImageView mErrorIcon; + private TextView mErrorMessage; private MediaSource mMediaSource; private BrowseAdapter mBrowseAdapter; private String mMediaSourcePackageName; private MediaItemMetadata mTopMediaItem; private Callbacks mCallbacks; + private int mFadeDuration; + private int mProgressBarDelay; + private Handler mHandler = new Handler(); private Stack<MediaItemMetadata> mBrowseStack = new Stack<>(); private MediaSource.Observer mBrowseObserver = new MediaSource.Observer() { @Override @@ -69,12 +82,27 @@ public class BrowseFragment extends Fragment { 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. + switch (mBrowseAdapter.getState()) { + case LOADING: + case IDLE: + // Still loading... nothing to do. + break; + case LOADED: + stopLoadingIndicator(); + mBrowseAdapter.update(); + if (mBrowseAdapter.getItemCount() > 0) { + showViewAnimated(mBrowseList); + } else { + mErrorMessage.setText(R.string.nothing_to_play); + showViewAnimated(mErrorMessage); + } + break; + case ERROR: + stopLoadingIndicator(); + mErrorMessage.setText(R.string.unknown_error); + showViewAnimated(mErrorMessage); + showViewAnimated(mErrorIcon); + break; } } @@ -175,7 +203,14 @@ public class BrowseFragment extends Fragment { public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_browse, container, false); + mProgressBar = view.findViewById(R.id.loading_spinner); + mProgressBarDelay = getContext().getResources() + .getInteger(R.integer.progress_indicator_delay); mBrowseList = view.findViewById(R.id.browse_list); + mErrorIcon = view.findViewById(R.id.error_icon); + mErrorMessage = view.findViewById(R.id.error_message); + mFadeDuration = getContext().getResources().getInteger( + R.integer.new_album_art_fade_in_duration); int numColumns = getContext().getResources().getInteger(R.integer.num_browse_columns); GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), numColumns); RecyclerView recyclerView = mBrowseList.getRecyclerView(); @@ -206,6 +241,7 @@ public class BrowseFragment extends Fragment { @Override public void onStart() { super.onStart(); + startLoadingIndicator(); mMediaSource = mCallbacks.getMediaSource(mMediaSourcePackageName); if (mMediaSource != null) { mMediaSource.subscribe(mBrowseObserver); @@ -215,9 +251,28 @@ public class BrowseFragment extends Fragment { } } + private Runnable mProgressIndicatorRunnable = new Runnable() { + @Override + public void run() { + showViewAnimated(mProgressBar); + } + }; + + private void startLoadingIndicator() { + // Display the indicator after a certain time, to avoid flashing the indicator constantly, + // even when performance is acceptable. + mHandler.postDelayed(mProgressIndicatorRunnable, mProgressBarDelay); + } + + private void stopLoadingIndicator() { + mHandler.removeCallbacks(mProgressIndicatorRunnable); + hideViewAnimated(mProgressBar); + } + @Override public void onStop() { super.onStop(); + stopLoadingIndicator(); if (mMediaSource != null) { mMediaSource.unsubscribe(mBrowseObserver); } @@ -239,8 +294,11 @@ public class BrowseFragment extends Fragment { mBrowseAdapter = null; } if (!success) { - mBrowseList.setVisibility(View.GONE); - // TODO(b/77647430) implement intermediate states. + hideViewAnimated(mBrowseList); + stopLoadingIndicator(); + mErrorMessage.setText(R.string.cannot_connect_to_app); + showViewAnimated(mErrorIcon); + showViewAnimated(mErrorMessage); return; } mBrowseAdapter = new BrowseAdapter(getContext(), mMediaSource.getMediaBrowser(), @@ -273,4 +331,25 @@ public class BrowseFragment extends Fragment { return mBrowseStack.lastElement(); } } + + private void hideViewAnimated(View view) { + view.animate() + .alpha(0f) + .setDuration(mFadeDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + } + }); + } + + private void showViewAnimated(View view) { + view.setAlpha(0f); + view.setVisibility(View.VISIBLE); + view.animate() + .alpha(1f) + .setDuration(mFadeDuration) + .setListener(null); + } } diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java index db3a023..d158e60 100644 --- a/src/com/android/car/media/MediaActivity.java +++ b/src/com/android/car/media/MediaActivity.java @@ -15,8 +15,12 @@ */ package com.android.car.media; +import android.annotation.NonNull; +import android.car.Car; import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.Bitmap; import android.os.Bundle; import android.support.design.widget.AppBarLayout; @@ -39,12 +43,8 @@ import com.android.car.media.widgets.AppBarView; import com.android.car.media.widgets.MetadataView; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.stream.Collectors; import androidx.car.drawer.CarDrawerActivity; @@ -58,22 +58,15 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C AppSelectionFragment.Callbacks { private static final String TAG = "MediaActivity"; - /** Intent extra specifying the package with the MediaBrowser **/ + /** Intent extra specifying the package with the MediaBrowser */ public static final String KEY_MEDIA_PACKAGE = "media_package"; - - /** - * Custom media sources which won't be rendered inside the template but will be offered on the - * app selector. - */ - private static final Set<String> CUSTOM_MEDIA_SOURCES = new HashSet<>(); - static { - CUSTOM_MEDIA_SOURCES.add("com.android.car.radio"); - } + /** Shared preferences files */ + public static final String SHARED_PREF = "com.android.car.media"; + /** Shared preference containing the last controlled source */ + public static final String LAST_MEDIA_SOURCE_SHARED_PREF_KEY = "last_media_source"; /** Configuration (controlled from resources) */ private boolean mContentForwardBrowseEnabled; - private boolean mForceBrowseTabs; - private int mMaxBrowserTabs; private float mBackgroundBlurRadius; private float mBackgroundBlurScale; @@ -82,6 +75,7 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C private MediaSource mMediaSource; private PlaybackModel mPlaybackModel; private MediaSourcesManager mMediaSourcesManager; + private SharedPreferences mSharedPreferences; /** Layout views */ private AppBarView mAppBarView; @@ -94,7 +88,6 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C private ViewGroup mBrowseControlsContainer; /** Current state */ - private MediaItemMetadata mCurrentMetadata; private Fragment mCurrentFragment; private Mode mMode = Mode.BROWSING; private boolean mIsAppSelectorOpen; @@ -117,7 +110,6 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C @Override public void onSourceChanged() { updateMetadata(); - updateBrowseSource(); } @Override @@ -188,7 +180,6 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C mAppSelectionFragment.setExitTransition(new Fade().setDuration(fadeDuration)); mPlaybackModel = new PlaybackModel(this); mMediaSourcesManager = new MediaSourcesManager(this); - mMaxBrowserTabs = getResources().getInteger(R.integer.max_browse_tabs); mDrawerBarLayout = findViewById(androidx.car.R.id.appbar); mDrawerBarLayout.setVisibility(forceBrowseTabs ? View.GONE : View.VISIBLE); mAlbumBackground = findViewById(R.id.media_background); @@ -203,6 +194,7 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C mBackgroundBlurRadius = outValue.getFloat(); getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true); mBackgroundBlurScale = outValue.getFloat(); + mSharedPreferences = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE); } @Override @@ -240,7 +232,6 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C } setIntent(intent); - getDrawerController().closeDrawer(); handleIntent(); } @@ -260,36 +251,74 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C if (!success) { updateTabs(new ArrayList<>()); mMediaSource.unsubscribeChildren(null); - Intent intent = getPackageManager(). - getLaunchIntentForPackage(mMediaSource.getPackageName()); - startActivity(intent); return; } mMediaSource.subscribeChildren(null, mRootItemsSubscription); } private void handleIntent() { - updateBrowseSource(); - switchToMode(getRequestedMediaPackageName() == null || !mContentForwardBrowseEnabled - ? Mode.PLAYBACK - : Mode.BROWSING); + Intent intent = getIntent(); + String action = intent != null ? intent.getAction() : null; + + getDrawerController().closeDrawer(); + + if (Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE.equals(action)) { + // The user either wants to browse a particular media source or switch to the + // playback UI. + String packageName = intent.getStringExtra(KEY_MEDIA_PACKAGE); + if (packageName != null) { + // We were told to navigate to a particular package: we open browse for it. + updateBrowseSource(new MediaSource(this, packageName)); + switchToMode(Mode.BROWSING); + return; + } + + // If didn't receive a package name and we are playing something: show the playback + // UI for the playing media source. + MediaSource mediaSource = mPlaybackModel.getMediaSource(); + if (mediaSource != null) { + updateBrowseSource(mPlaybackModel.getMediaSource()); + switchToMode(Mode.PLAYBACK); + return; + } + } + + // In any other case, if we were already browsing something: just close drawers/overlays + // and display what we have. + if (mMediaSource != null) { + closeAppSelector(); + updateMetadata(); + return; + } + + // If we don't have a current media source, we try with the last one we remember. + MediaSource lastMediaSource = getLastMediaSource(); + if (lastMediaSource != null) { + closeAppSelector(); + updateBrowseSource(lastMediaSource); + updateMetadata(); + } else { + // If we don't have anything from before: open the app selector. + openAppSelector(); + } } /** - * 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. + * Updates the media source being browsed. This could be necessary when the user selects + * a different media source. */ - private void updateBrowseSource() { - MediaSource mediaSource = getCurrentMediaSource(); + private void updateBrowseSource(MediaSource mediaSource) { if (Objects.equals(mediaSource, mMediaSource)) { // No change, nothing to do. return; } if (mMediaSource != null) { + mMediaSource.unsubscribeChildren(null); mMediaSource.unsubscribe(mMediaSourceObserver); updateTabs(new ArrayList<>()); } mMediaSource = mediaSource; + setLastMediaSource(mMediaSource); mAppBarView.setState(AppBarView.State.IDLE); if (mMediaSource != null) { if (Log.isLoggable(TAG, Log.INFO)) { @@ -313,18 +342,6 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C } /** - * @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(); - } - - /** * @return the package name of the media source requested by the incoming {@link Intent} or * null if no source was indicated. */ @@ -339,6 +356,7 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C } private void updateTabs(List<MediaItemMetadata> items) { + items = customizeTabs(mMediaSource, items); List<MediaItemMetadata> browsableTopLevel = items.stream() .filter(item -> item.isBrowsable()) .collect(Collectors.toList()); @@ -354,6 +372,20 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C } } + /** + * Extension point used to customize media items displayed on the tabs. + * + * @param mediaSource media source these items belong to. + * @param items items to override. + * @return an updated list of items. + * @deprecated This method will be removed on b/79089344 + */ + @Deprecated + protected List<MediaItemMetadata> customizeTabs(MediaSource mediaSource, + List<MediaItemMetadata> items) { + return items; + } + private void switchToMode(Mode mode) { Log.i(TAG, "Switch mode to " + mode + " (intent package: " + getRequestedMediaPackageName() + ")"); @@ -381,16 +413,10 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C mBrowseControlsContainer.setVisibility(mMode == Mode.PLAYBACK ? View.GONE : View.VISIBLE); - MediaItemMetadata metadata = mPlaybackModel.getMetadata(); - if (Objects.equals(mCurrentMetadata, metadata)) { - return; - } - mCurrentMetadata = metadata; mUpdateAlbumArtRunnable.run(); } else { mAlbumBackground.setImageBitmap(null, true); mBrowseControlsContainer.setVisibility(View.GONE); - mCurrentMetadata = null; } } @@ -495,24 +521,40 @@ public class MediaActivity extends CarDrawerActivity implements BrowseFragment.C return mMediaSourcesManager.getMediaSources() .stream() .filter(source -> source.getMediaBrowser() != null - || CUSTOM_MEDIA_SOURCES.contains(source.getPackageName())) + || source.isCustom()) .collect(Collectors.toList()); } @Override public void onMediaSourceSelected(MediaSource mediaSource) { closeAppSelector(); - String packageName = mediaSource.getPackageName(); - if (mediaSource.getMediaBrowser() != null - && !CUSTOM_MEDIA_SOURCES.contains(packageName)) { - Intent intent = new Intent(); - intent.putExtra(KEY_MEDIA_PACKAGE, packageName); - setIntent(intent); - getDrawerController().closeDrawer(); - handleIntent(); + if (mediaSource.getMediaBrowser() != null && !mediaSource.isCustom()) { + updateBrowseSource(mediaSource); + switchToMode(Mode.BROWSING); } else { + String packageName = mediaSource.getPackageName(); Intent intent = getPackageManager().getLaunchIntentForPackage(packageName); startActivity(intent); } } + + private MediaSource getLastMediaSource() { + String packageName = mSharedPreferences.getString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, null); + if (packageName == null) { + return null; + } + // Verify that the stored package name corresponds to a currently installed media source. + for (MediaSource mediaSource : mMediaSourcesManager.getMediaSources()) { + if (mediaSource.getPackageName().equals(packageName)) { + return mediaSource; + } + } + return null; + } + + private void setLastMediaSource(@NonNull MediaSource mediaSource) { + mSharedPreferences.edit() + .putString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, mediaSource.getPackageName()) + .apply(); + } } diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java index 0478d8a..da42832 100644 --- a/src/com/android/car/media/PlaybackFragment.java +++ b/src/com/android/car/media/PlaybackFragment.java @@ -34,12 +34,6 @@ import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; -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; - import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.MediaSource; import com.android.car.media.common.PlaybackControls; @@ -49,6 +43,12 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +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 @@ -60,6 +60,7 @@ public class PlaybackFragment extends Fragment { private PlaybackModel mModel; private PlaybackControls mPlaybackControls; private QueueItemsAdapter mQueueAdapter; + private PagedListView mQueue; private MetadataController mMetadataController; private ConstraintLayout mRootView; @@ -96,6 +97,7 @@ public class PlaybackFragment extends Fragment { textListItem.setTitle(item.getTitle() != null ? item.getTitle().toString() : null); textListItem.setBody(item.getSubtitle() != null ? item.getSubtitle().toString() : null); textListItem.setOnClickListener(v -> onQueueItemClicked(item)); + return textListItem; } @Override @@ -103,9 +105,10 @@ public class PlaybackFragment extends Fragment { return mQueueItems.size(); } }; - private static class QueueItemsAdapter extends ListItemAdapter { + private class QueueItemsAdapter extends ListItemAdapter { QueueItemsAdapter(Context context, ListItemProvider itemProvider) { super(context, itemProvider, BackgroundStyle.SOLID); + setHasStableIds(true); } void refresh() { @@ -113,6 +116,11 @@ public class PlaybackFragment extends Fragment { // RecyclerView updates. this.notifyDataSetChanged(); } + + @Override + public long getItemId(int position) { + return mQueueItems.get(position).getQueueId(); + } } private PlaybackControls.Listener mPlaybackControlsListener = new PlaybackControls.Listener() { @@ -130,9 +138,10 @@ public class PlaybackFragment extends Fragment { View view = inflater.inflate(R.layout.fragment_playback, container, false); mRootView = view.findViewById(R.id.playback_container); mModel = new PlaybackModel(getContext()); + mQueue = view.findViewById(R.id.queue_list); initPlaybackControls(view.findViewById(R.id.playback_controls)); - initQueue(view.findViewById(R.id.queue_list)); + initQueue(mQueue); initMetadataController(view); return view; } @@ -203,6 +212,7 @@ public class PlaybackFragment extends Fragment { updateState(); } mMetadataController.resumeUpdates(); + mQueue.getRecyclerView().scrollToPosition(0); } }); TransitionManager.beginDelayedTransition(mRootView, transition); @@ -225,7 +235,6 @@ public class PlaybackFragment extends Fragment { mQueueAdapter.refresh(); } - private void updateAccentColor() { int defaultColor = getResources().getColor(android.R.color.background_dark, null); MediaSource mediaSource = mModel.getMediaSource(); diff --git a/src/com/android/car/media/browse/BrowseAdapter.java b/src/com/android/car/media/browse/BrowseAdapter.java index 0092c5a..6016688 100644 --- a/src/com/android/car/media/browse/BrowseAdapter.java +++ b/src/com/android/car/media/browse/BrowseAdapter.java @@ -63,7 +63,7 @@ import java.util.stream.Collectors; * <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates. */ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { - private static final String TAG = "MediaBrowseAdapter"; + private static final String TAG = "BrowseAdapter"; @NonNull private final Context mContext; private final MediaBrowser mMediaBrowser; @@ -76,7 +76,21 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { private List<MediaItemMetadata> mQueue; private CharSequence mQueueTitle; private int mMaxSpanSize = 1; - private BrowseViewData.State mState = BrowseViewData.State.IDLE; + private State mState = State.IDLE; + + /** + * Possible states of the adapter + */ + public enum State { + /** Loading of this item hasn't started yet */ + IDLE, + /** There is pending information before this item can be displayed */ + LOADING, + /** It was not possible to load metadata for this item */ + ERROR, + /** Metadata for this items has been correctly loaded */ + LOADED + } /** * An {@link BrowseAdapter} observer. @@ -149,7 +163,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { */ final MediaItemMetadata mItem; /** Current loading state for this item */ - BrowseViewData.State mState = BrowseViewData.State.LOADING; + State mState = State.LOADING; /** Playable children of the given item */ List<MediaItemMetadata> mPlayableChildren = new ArrayList<>(); /** Browsable children of the given item */ @@ -265,9 +279,9 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { /** * @return the global loading state. Consumers can use this state to determine if more * information is still pending to arrive or not. This method will report - * {@link BrowseViewData.State#ERROR} only if the list of immediate children fails to load. + * {@link State#ERROR} only if the list of immediate children fails to load. */ - public BrowseViewData.State getState() { + public State getState() { return mState; } @@ -317,6 +331,8 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { if (!state.mIsSubscribed && state.mItem.isBrowsable()) { mMediaBrowser.subscribe(state.mItem.getId(), mSubscriptionCallback); state.mIsSubscribed = true; + } else { + state.mState = State.LOADED; } } @@ -380,7 +396,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { return; } itemState.setChildren(children); - itemState.mState = BrowseViewData.State.LOADED; + itemState.mState = State.LOADED; } updateGlobalState(); notify(Observer::onDirty); @@ -395,7 +411,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { private void onLoadingError(String parentId) { if (parentId.equals(mParentMediaItemId)) { - mState = BrowseViewData.State.ERROR; + mState = State.ERROR; } else { MediaItemState state = mItemStates.get(parentId); if (state == null) { @@ -403,20 +419,20 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { return; } state.setChildren(new ArrayList<>()); - state.mState = BrowseViewData.State.ERROR; + state.mState = State.ERROR; + updateGlobalState(); } - updateGlobalState(); notify(Observer::onDirty); } private void updateGlobalState() { for (MediaItemState state: mItemStates.values()) { - if (state.mState == BrowseViewData.State.LOADING) { - mState = BrowseViewData.State.LOADING; + if (state.mState == State.LOADING) { + mState = State.LOADING; return; } } - mState = BrowseViewData.State.LOADED; + mState = State.LOADED; } private DiffUtil.Callback createDiffUtil(List<BrowseViewData> oldList, @@ -463,7 +479,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { private class ItemsBuilder { private List<BrowseViewData> result = new ArrayList<>(); - void addItem(MediaItemMetadata item, BrowseViewData.State state, + void addItem(MediaItemMetadata item, State state, BrowseItemViewType viewType, Consumer<Observer> notification) { View.OnClickListener listener = notification != null ? view -> BrowseAdapter.this.notify(notification) : @@ -494,7 +510,7 @@ public class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> { } - void addBrowseBlock(MediaItemMetadata header, BrowseViewData.State state, + void addBrowseBlock(MediaItemMetadata header, State state, List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxChildren, boolean showHeader, boolean showMoreFooter) { if (showHeader) { diff --git a/src/com/android/car/media/browse/BrowseViewData.java b/src/com/android/car/media/browse/BrowseViewData.java index 31d0588..3a6cc65 100644 --- a/src/com/android/car/media/browse/BrowseViewData.java +++ b/src/com/android/car/media/browse/BrowseViewData.java @@ -33,27 +33,13 @@ class BrowseViewData { /** View type associated with this item */ @NonNull public final BrowseItemViewType mViewType; /** Current state of this item */ - public final State mState; + public final BrowseAdapter.State mState; /** Text associated with this item */ public final CharSequence mText; /** Click listener to set for this item */ public final View.OnClickListener mOnClickListener; /** - * Possible states of a view - */ - public enum State { - /** Loading of this item hasn't started yet */ - IDLE, - /** There is pending information before this item can be displayed */ - LOADING, - /** It was not possible to load metadata for this item */ - ERROR, - /** Metadata for this items has been correctly loaded */ - LOADED - } - - /** * Creates a {@link BrowseViewData} for a particular {@link MediaItemMetadata}. * * @param mediaItem {@link MediaItemMetadata} metadata @@ -62,7 +48,7 @@ class BrowseViewData { * @param onClickListener optional {@link android.view.View.OnClickListener} */ BrowseViewData(MediaItemMetadata mediaItem, @NonNull BrowseItemViewType viewType, - @NonNull State state, View.OnClickListener onClickListener) { + @NonNull BrowseAdapter.State state, View.OnClickListener onClickListener) { mMediaItem = mediaItem; mViewType = viewType; mState = state; diff --git a/src/com/android/car/media/browse/ContentForwardStrategy.java b/src/com/android/car/media/browse/ContentForwardStrategy.java index 7720366..88fdc1a 100644 --- a/src/com/android/car/media/browse/ContentForwardStrategy.java +++ b/src/com/android/car/media/browse/ContentForwardStrategy.java @@ -89,7 +89,7 @@ public interface ContentForwardStrategy { @Override public boolean shouldBeExpanded(MediaItemMetadata mediaItem) { - return true; + return false; } @Override diff --git a/src/com/android/car/media/widgets/AppBarView.java b/src/com/android/car/media/widgets/AppBarView.java index a8cc415..02a6daf 100644 --- a/src/com/android/car/media/widgets/AppBarView.java +++ b/src/com/android/car/media/widgets/AppBarView.java @@ -36,7 +36,6 @@ public class AppBarView extends RelativeLayout { /** Default number of tabs to show on this app bar */ private static int DEFAULT_MAX_TABS = 4; - private List<MediaSource> mMediaSources = new ArrayList<>(); private LinearLayout mTabsContainer; private ImageView mAppIcon; private ImageView mAppSwitchIcon; @@ -55,6 +54,8 @@ public class AppBarView extends RelativeLayout { private float mSelectedTabAlpha; private float mUnselectedTabAlpha; private MediaItemMetadata mSelectedItem; + private String mMediaAppTitle; + private Drawable mDefaultIcon; /** * Application bar listener @@ -154,6 +155,8 @@ public class AppBarView extends RelativeLayout { mSelectedTabAlpha = outValue.getFloat(); getResources().getValue(R.dimen.browse_tab_alpha_unselected, outValue, true); mUnselectedTabAlpha = outValue.getFloat(); + mMediaAppTitle = getResources().getString(R.string.media_app_title); + mDefaultIcon = getResources().getDrawable(R.drawable.ic_music); setState(State.IDLE); } @@ -231,15 +234,18 @@ public class AppBarView extends RelativeLayout { * Updates the title to display when the bar is not showing tabs. */ public void setTitle(CharSequence title) { - mTitle.setText(title); + mTitle.setText(title != null ? title : mMediaAppTitle); } /** * Updates the application icon to show next to the application switcher. */ public void setAppIcon(Bitmap icon) { - mAppIcon.setImageBitmap(icon); - mAppIcon.setVisibility(icon != null ? View.VISIBLE : View.GONE); + if (icon != null) { + mAppIcon.setImageBitmap(icon); + } else { + mAppIcon.setImageDrawable(mDefaultIcon); + } } /** @@ -266,9 +272,9 @@ public class AppBarView extends RelativeLayout { mSelectedItem.getId(), ((MediaItemMetadata) tabView.getTag()).getId()); tabView.setAlpha(match ? mSelectedTabAlpha : mUnselectedTabAlpha); - tabView.invalidate(); } } + mTabsContainer.invalidate(); } /** diff --git a/src/com/android/car/media/widgets/MediaItemTabView.java b/src/com/android/car/media/widgets/MediaItemTabView.java index 9701926..581a373 100644 --- a/src/com/android/car/media/widgets/MediaItemTabView.java +++ b/src/com/android/car/media/widgets/MediaItemTabView.java @@ -47,6 +47,7 @@ public class MediaItemTabView extends LinearLayout { setOrientation(LinearLayout.VERTICAL); setFocusable(true); setGravity(Gravity.CENTER); + setBackground(context.getDrawable(R.drawable.app_item_background)); int[] attrs = new int[]{android.R.attr.selectableItemBackground}; TypedArray typedArray = context.obtainStyledAttributes(attrs); |