Refactor media error handling and make the widget display fatal errors.
Fixes: 165084717
Test: manual
Change-Id: I5e734bcb66d84c6c46c61243bc236e4544969e1b
diff --git a/car-media-common/res/layout/playback_fragment.xml b/car-media-common/res/layout/playback_fragment.xml
index 4a33a27..bd77f74 100644
--- a/car-media-common/res/layout/playback_fragment.xml
+++ b/car-media-common/res/layout/playback_fragment.xml
@@ -97,6 +97,29 @@
app:layout_constraintEnd_toStartOf="@+id/app_selector_container"
app:layout_constraintTop_toBottomOf="@+id/title"/>
+ <com.android.car.apps.common.UxrTextView
+ android:id="@+id/error_message"
+ style="@style/FullScreenErrorMessageStyle"
+ android:layout_marginHorizontal="@dimen/playback_fragment_text_margin_x"
+ android:maxLines="@integer/widget_error_text_max_lines"
+ app:layout_constraintVertical_chainStyle="packed"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/app_name"
+ app:layout_constraintBottom_toTopOf="@+id/error_button"
+ />
+
+ <com.android.car.apps.common.UxrButton
+ android:id="@+id/error_button"
+ style="@style/FullScreenErrorButtonStyle"
+ android:layout_marginTop="@dimen/playback_fragment_error_button_margin_top"
+ android:layout_marginBottom="@dimen/playback_fragment_error_button_margin_bottom"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/error_message"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+
<FrameLayout
android:id="@+id/app_selector_container"
xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/car-media-common/res/values/dimens.xml b/car-media-common/res/values/dimens.xml
index d888e4b..058ec8e 100644
--- a/car-media-common/res/values/dimens.xml
+++ b/car-media-common/res/values/dimens.xml
@@ -24,6 +24,8 @@
<!-- playback_fragment.xml -->
<dimen name="playback_fragment_text_margin_top">@dimen/car_ui_padding_4</dimen>
<dimen name="playback_fragment_text_margin_x">@dimen/car_ui_padding_4</dimen>
+ <dimen name="playback_fragment_error_button_margin_top">@dimen/car_ui_padding_5</dimen>
+ <dimen name="playback_fragment_error_button_margin_bottom">@dimen/car_ui_padding_4</dimen>
<dimen name="playback_fragment_app_icon_margin_right">@dimen/car_ui_padding_4</dimen>
<dimen name="playback_fragment_controls_margin_bottom">@dimen/car_ui_padding_4</dimen>
diff --git a/car-media-common/res/values/integers.xml b/car-media-common/res/values/integers.xml
index 2c6177d..cda2e06 100644
--- a/car-media-common/res/values/integers.xml
+++ b/car-media-common/res/values/integers.xml
@@ -36,4 +36,7 @@
-->
<integer name="media_items_bitmap_max_size_px">256</integer>
+ <!-- The maximum number of lines for the error message in the media widget. -->
+ <integer name="widget_error_text_max_lines">3</integer>
+
</resources>
diff --git a/car-media-common/res/values/strings.xml b/car-media-common/res/values/strings.xml
index e5831bc..424e9f7 100644
--- a/car-media-common/res/values/strings.xml
+++ b/car-media-common/res/values/strings.xml
@@ -19,4 +19,29 @@
<string name="album_art">Album Art</string>
<!-- Default title of current track's metadata. [CHAR LIMIT=50] -->
<string name="metadata_default_title">No Title</string>
+
+ <!-- Default error message [CHAR LIMIT=100] -->
+ <string name="default_error_message">Something’s wrong. Try later.</string>
+ <!-- Error message set when the application state is invalid to fulfill the request. [CHAR LIMIT=100] -->
+ <string name="error_code_app_error">Can’t do that right now</string>
+ <!-- Error message set when the request is not supported by the application. [CHAR LIMIT=100] -->
+ <string name="error_code_not_supported">This app can’t do that</string>
+ <!-- Error message set when the request cannot be performed because authentication has expired. [CHAR LIMIT=100] -->
+ <string name="error_code_authentication_expired">Sign in to use this app</string>
+ <!-- Error message set when a premium account is required for the request to succeed. [CHAR LIMIT=100] -->
+ <string name="error_code_premium_account_required">Premium access required</string>
+ <!-- Error message set when too many concurrent streams are detected. [CHAR LIMIT=100] -->
+ <string name="error_code_concurrent_stream_limit">Listening on too many devices</string>
+ <!-- Error message set when the content is blocked due to parental controls. [CHAR LIMIT=100] -->
+ <string name="error_code_parental_control_restricted">That content is blocked</string>
+ <!-- Error message set when the content is blocked due to being regionally unavailable. [CHAR LIMIT=100] -->
+ <string name="error_code_not_available_in_region">Can’t get that content here</string>
+ <!-- Error message set when the requested content is already playing. [CHAR LIMIT=100] -->
+ <string name="error_code_content_already_playing">Already playing that content</string>
+ <!-- Error message set when the application cannot skip any more songs because skip limit is reached. [CHAR LIMIT=100] -->
+ <string name="error_code_skip_limit_reached">Can’t skip any more tracks</string>
+ <!-- Error message set when the action is interrupted due to some external event. [CHAR LIMIT=100] -->
+ <string name="error_code_action_aborted">Couldn’t finish. Try again.</string>
+ <!-- Error message set when the playback navigation (previous, next) is not possible because the queue was exhausted. [CHAR LIMIT=100] -->
+ <string name="error_code_end_of_queue">Nothing else is queued up</string>
</resources>
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackErrorViewController.java b/car-media-common/src/com/android/car/media/common/PlaybackErrorViewController.java
new file mode 100644
index 0000000..2d4c7c8
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlaybackErrorViewController.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 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.common;
+
+import android.app.PendingIntent;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.car.apps.common.UxrButton;
+import com.android.car.apps.common.UxrTextView;
+import com.android.car.apps.common.util.ViewUtils;
+
+
+/** Simple controller for the error message and the error button. */
+public class PlaybackErrorViewController {
+
+ private static final String TAG = "PlaybckErrViewCtrlr";
+
+ // mErrorMessageView is defined explicitly as a UxrTextView instead of a TextView to
+ // provide clarity as it may be misleading to assume that mErrorMessageView extends all TextView
+ // methods. In addition, it increases discoverability of runtime issues that may occur.
+ private final UxrTextView mErrorMessageView;
+ private final UxrButton mErrorButton;
+
+ private final int mFadeDuration;
+
+ public PlaybackErrorViewController(View content) {
+ mErrorMessageView = content.findViewById(R.id.error_message);
+ mErrorButton = content.findViewById(R.id.error_button);
+
+ Resources res = content.getContext().getResources();
+ mFadeDuration = res.getInteger(R.integer.new_album_art_fade_in_duration);
+ }
+
+ /** Animates away the error views. */
+ public void hideError() {
+ ViewUtils.hideViewAnimated(mErrorMessageView, mFadeDuration);
+ ViewUtils.hideViewAnimated(mErrorButton, mFadeDuration);
+ }
+
+ /** Hides the error views without animation. */
+ public void hideErrorNoAnim() {
+ ViewUtils.hideViewAnimated(mErrorMessageView, 0);
+ ViewUtils.hideViewAnimated(mErrorButton, 0);
+ }
+
+ /** Sets the error message and optionally the error button. */
+ public void setError(String message, @Nullable String label,
+ @Nullable PendingIntent pendingIntent, boolean isDistractionOptimized) {
+ mErrorMessageView.setText(message);
+ ViewUtils.showViewAnimated(mErrorMessageView, mFadeDuration);
+
+ // Only show the error button if the error is actionable.
+ if (label != null && pendingIntent != null) {
+ mErrorButton.setText(label);
+
+ mErrorButton.setUxRestrictions(isDistractionOptimized
+ ? CarUxRestrictions.UX_RESTRICTIONS_BASELINE
+ : CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
+
+ mErrorButton.setOnClickListener(v -> {
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ if (Log.isLoggable(TAG, Log.ERROR)) {
+ Log.e(TAG, "Pending intent canceled");
+ }
+ }
+ });
+ ViewUtils.showViewAnimated(mErrorButton, mFadeDuration);
+ } else {
+ ViewUtils.hideViewAnimated(mErrorButton, mFadeDuration);
+ }
+ }
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackErrorsHelper.java b/car-media-common/src/com/android/car/media/common/PlaybackErrorsHelper.java
new file mode 100644
index 0000000..475346d
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/PlaybackErrorsHelper.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 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.common;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Abstract class to factorize most of the error handling logic.
+ */
+public abstract class PlaybackErrorsHelper {
+
+ private static final Map<Integer, Integer> ERROR_CODE_MESSAGES_MAP;
+
+ static {
+ Map<Integer, Integer> map = new HashMap<>();
+ map.put(PlaybackStateCompat.ERROR_CODE_APP_ERROR, R.string.error_code_app_error);
+ map.put(PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED, R.string.error_code_not_supported);
+ map.put(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
+ R.string.error_code_authentication_expired);
+ map.put(PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
+ R.string.error_code_premium_account_required);
+ map.put(PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT,
+ R.string.error_code_concurrent_stream_limit);
+ map.put(PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
+ R.string.error_code_parental_control_restricted);
+ map.put(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION,
+ R.string.error_code_not_available_in_region);
+ map.put(PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING,
+ R.string.error_code_content_already_playing);
+ map.put(PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED,
+ R.string.error_code_skip_limit_reached);
+ map.put(PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED, R.string.error_code_action_aborted);
+ map.put(PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE, R.string.error_code_end_of_queue);
+ ERROR_CODE_MESSAGES_MAP = Collections.unmodifiableMap(map);
+ }
+
+ private final Context mContext;
+ private PlaybackStateWrapper mCurrentPlaybackStateWrapper;
+
+ public PlaybackErrorsHelper(Context context) {
+ mContext = context;
+ }
+
+ protected abstract void handleNewPlaybackState(String displayedMessage, PendingIntent intent,
+ String label);
+
+ /**
+ * Triggers updates of the error state.
+ * Must be called when the children list of the root of the browse tree changes AND when
+ * the playback state changes.
+ */
+ public void handlePlaybackState(@NonNull String tag, PlaybackStateWrapper state,
+ boolean ignoreSameState) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag,
+ "handlePlaybackState(); state change: " + (mCurrentPlaybackStateWrapper != null
+ ? mCurrentPlaybackStateWrapper.getState() : null) + " -> " + (
+ state != null ? state.getState() : null));
+ }
+
+ if (state == null) {
+ mCurrentPlaybackStateWrapper = null;
+ return;
+ }
+
+ String displayedMessage = getDisplayedMessage(mContext, state);
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, "Displayed error message: [" + displayedMessage + "]");
+ }
+ if (ignoreSameState && mCurrentPlaybackStateWrapper != null
+ && mCurrentPlaybackStateWrapper.getState() == state.getState()
+ && TextUtils.equals(displayedMessage,
+ getDisplayedMessage(mContext, mCurrentPlaybackStateWrapper))) {
+ if (Log.isLoggable(tag, Log.DEBUG)) {
+ Log.d(tag, "Ignore same playback state.");
+ }
+ return;
+ }
+
+ mCurrentPlaybackStateWrapper = state;
+
+ PendingIntent intent = getErrorResolutionIntent(state);
+ String label = getErrorResolutionLabel(state);
+ handleNewPlaybackState(displayedMessage, intent, label);
+ }
+
+
+ @Nullable
+ private String getDisplayedMessage(Context ctx, @Nullable PlaybackStateWrapper state) {
+ if (state == null) {
+ return null;
+ }
+ if (!TextUtils.isEmpty(state.getErrorMessage())) {
+ return state.getErrorMessage().toString();
+ }
+ // ERROR_CODE_UNKNOWN_ERROR means there is no error in PlaybackState.
+ if (state.getErrorCode() != PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR) {
+ Integer messageId = ERROR_CODE_MESSAGES_MAP.get(state.getErrorCode());
+ return messageId != null ? ctx.getString(messageId) : ctx.getString(
+ R.string.default_error_message);
+ }
+ if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
+ return ctx.getString(R.string.default_error_message);
+ }
+ return null;
+ }
+
+ @Nullable
+ private PendingIntent getErrorResolutionIntent(@NonNull PlaybackStateWrapper state) {
+ Bundle extras = state.getExtras();
+ return extras == null ? null : extras.getParcelable(
+ MediaConstants.ERROR_RESOLUTION_ACTION_INTENT);
+ }
+
+ @Nullable
+ private String getErrorResolutionLabel(@NonNull PlaybackStateWrapper state) {
+ Bundle extras = state.getExtras();
+ return extras == null ? null : extras.getString(
+ MediaConstants.ERROR_RESOLUTION_ACTION_LABEL);
+ }
+
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
index 9ddf601..60f4d18 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -21,10 +21,13 @@
import static com.android.car.arch.common.LiveDataFunctions.mapNonNull;
import android.app.Application;
+import android.app.PendingIntent;
import android.car.Car;
+import android.car.content.pm.CarPackageManager;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
+import android.text.TextUtils;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.View;
@@ -38,65 +41,103 @@
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProviders;
import com.android.car.apps.common.BitmapUtils;
import com.android.car.apps.common.CrossfadeImageView;
import com.android.car.apps.common.imaging.ImageBinder;
import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType;
+import com.android.car.apps.common.util.CarPackageManagerUtils;
import com.android.car.apps.common.util.ViewUtils;
+import com.android.car.arch.common.FutureData;
+import com.android.car.media.common.browse.BrowsedMediaItems;
+import com.android.car.media.common.browse.MediaBrowserViewModel;
import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
import com.android.car.media.common.source.MediaSource;
import com.android.car.media.common.source.MediaSourceViewModel;
+import java.util.List;
+
/**
* {@link Fragment} that can be used to display and control the currently playing media item. Its
* requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission be held by the hosting
* application.
*/
public class PlaybackFragment extends Fragment {
+ private static final String TAG = "PlaybackFragmentWidget";
+
+ private Car mCar;
+ private CarPackageManager mCarPackageManager;
private Intent mAppSelectorIntent;
private MediaSourceViewModel mMediaSourceViewModel;
+ private PlaybackViewModel mPlaybackViewModel;
private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
+ private ViewModel mInnerViewModel;
+
+ private PlaybackErrorViewController mPlaybackErrorViewController;
+ private PlaybackErrorsHelper mErrorsHelper;
+ private boolean mIsFatalError;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
FragmentActivity activity = requireActivity();
- PlaybackViewModel playbackViewModel = PlaybackViewModel.get(activity.getApplication(),
+ mCar = Car.createCar(activity);
+ mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE);
+
+ mPlaybackViewModel = PlaybackViewModel.get(activity.getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
mMediaSourceViewModel = MediaSourceViewModel.get(activity.getApplication(),
MEDIA_SOURCE_MODE_PLAYBACK);
mAppSelectorIntent = MediaSource.getSourceSelectorIntent(getContext(), true);
- ViewModel innerViewModel = ViewModelProviders.of(activity).get(ViewModel.class);
- innerViewModel.init(mMediaSourceViewModel, playbackViewModel);
+ mInnerViewModel = ViewModelProviders.of(activity).get(ViewModel.class);
+ mInnerViewModel.init(activity, mMediaSourceViewModel, mPlaybackViewModel);
View view = inflater.inflate(R.layout.playback_fragment, container, false);
+ mPlaybackErrorViewController = new PlaybackErrorViewController(view);
+
PlaybackControlsActionBar playbackControls = view.findViewById(R.id.playback_controls);
- playbackControls.setModel(playbackViewModel, getViewLifecycleOwner());
- playbackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
- state -> ViewUtils.setVisible(playbackControls,
- (state != null) && state.shouldDisplay()));
+ playbackControls.setModel(mPlaybackViewModel, getViewLifecycleOwner());
+ mPlaybackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
+ state -> {
+ ViewUtils.setVisible(playbackControls,
+ (state != null) && state.shouldDisplay());
+ if (mErrorsHelper != null) {
+ mErrorsHelper.handlePlaybackState(TAG, state, /*ignoreSameState*/ true);
+ }
+ });
TextView appName = view.findViewById(R.id.app_name);
- innerViewModel.getAppName().observe(getViewLifecycleOwner(), appName::setText);
+ mInnerViewModel.getAppName().observe(getViewLifecycleOwner(), appName::setText);
TextView title = view.findViewById(R.id.title);
- innerViewModel.getTitle().observe(getViewLifecycleOwner(), title::setText);
+ mInnerViewModel.getTitle().observe(getViewLifecycleOwner(), title::setText);
TextView subtitle = view.findViewById(R.id.subtitle);
- innerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
+ mInnerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
ImageView appIcon = view.findViewById(R.id.app_icon);
- innerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+ mInnerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+
+ mInnerViewModel.getBrowseTreeHasChildren().observe(getViewLifecycleOwner(),
+ this::onBrowseTreeHasChildrenChanged);
+
+ mMediaSourceViewModel.getPrimaryMediaSource().observe(getViewLifecycleOwner(),
+ this::onMediaSourceChanged);
View playbackScrim = view.findViewById(R.id.playback_scrim);
playbackScrim.setOnClickListener(
// Let the Media center trampoline figure out what to open.
- v -> startActivity(new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE)));
+ v -> {
+ if (!mIsFatalError) {
+ startActivity(new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE));
+ }
+ });
CrossfadeImageView albumBackground = view.findViewById(R.id.album_background);
int max = activity.getResources().getInteger(R.integer.media_items_bitmap_max_size_px);
@@ -108,16 +149,59 @@
albumBackground.setImageBitmap(bitmap, true);
});
- playbackViewModel.getMetadata().observe(getViewLifecycleOwner(),
+ mPlaybackViewModel.getMetadata().observe(getViewLifecycleOwner(),
item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
item != null ? item.getArtworkKey() : null));
View appSelector = view.findViewById(R.id.app_selector_container);
appSelector.setVisibility(mAppSelectorIntent != null ? View.VISIBLE : View.GONE);
appSelector.setOnClickListener(e -> getContext().startActivity(mAppSelectorIntent));
+
+ mErrorsHelper = new PlaybackErrorsHelper(activity) {
+
+ @Override
+ public void handleNewPlaybackState(String displayedMessage, PendingIntent intent,
+ String label) {
+ mIsFatalError = false;
+ if (!TextUtils.isEmpty(displayedMessage)) {
+ Boolean hasChildren = mInnerViewModel.getBrowseTreeHasChildren().getValue();
+ if (hasChildren != null && !hasChildren) {
+ boolean isDistractionOptimized =
+ intent != null && CarPackageManagerUtils.isDistractionOptimized(
+ mCarPackageManager, intent);
+ mPlaybackErrorViewController.setError(displayedMessage, label, intent,
+ isDistractionOptimized);
+ mIsFatalError = true;
+ }
+ }
+
+ if (!mIsFatalError) {
+ mPlaybackErrorViewController.hideError();
+ }
+ }
+ };
+
return view;
}
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mCar.disconnect();
+ mErrorsHelper = null;
+ }
+
+ private void onBrowseTreeHasChildrenChanged(@Nullable Boolean hasChildren) {
+ if (hasChildren != null && mErrorsHelper != null) {
+ PlaybackStateWrapper state = mPlaybackViewModel.getPlaybackStateWrapper().getValue();
+ mErrorsHelper.handlePlaybackState(TAG, state, /*ignoreSameState*/ false);
+ }
+ }
+
+ private void onMediaSourceChanged(MediaSource source) {
+ mPlaybackErrorViewController.hideErrorNoAnim();
+ }
+
/**
* ViewModel for the PlaybackFragment
*/
@@ -128,15 +212,18 @@
private LiveData<Bitmap> mAppIcon;
private LiveData<CharSequence> mTitle;
private LiveData<CharSequence> mSubtitle;
+ private MutableLiveData<Boolean> mBrowseTreeHasChildren = new MutableLiveData<>();
private PlaybackViewModel mPlaybackViewModel;
private MediaSourceViewModel mMediaSourceViewModel;
+ private MediaBrowserViewModel mRootMediaBrowserViewModel;
public ViewModel(Application application) {
super(application);
}
- void init(MediaSourceViewModel mediaSourceViewModel, PlaybackViewModel playbackViewModel) {
+ void init(FragmentActivity activity, MediaSourceViewModel mediaSourceViewModel,
+ PlaybackViewModel playbackViewModel) {
if (mMediaSourceViewModel == mediaSourceViewModel
&& mPlaybackViewModel == playbackViewModel) {
return;
@@ -148,6 +235,11 @@
mAppIcon = mapNonNull(mMediaSource, MediaSource::getCroppedPackageIcon);
mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist);
+
+ mRootMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(
+ mMediaSourceViewModel, ViewModelProviders.of(activity));
+ mRootMediaBrowserViewModel.getBrowsedMediaItems()
+ .observe(activity, this::onItemsUpdate);
}
LiveData<CharSequence> getAppName() {
@@ -165,5 +257,22 @@
LiveData<CharSequence> getSubtitle() {
return mSubtitle;
}
+
+ LiveData<Boolean> getBrowseTreeHasChildren() {
+ return mBrowseTreeHasChildren;
+ }
+
+ private void onItemsUpdate(FutureData<List<MediaItemMetadata>> futureData) {
+ if (futureData.isLoading()) {
+ mBrowseTreeHasChildren.setValue(null);
+ return;
+ }
+
+ List<MediaItemMetadata> items =
+ BrowsedMediaItems.filterItems(/*forRoot*/ true, futureData.getData());
+
+ boolean browseTreeHasChildren = items != null && !items.isEmpty();
+ mBrowseTreeHasChildren.setValue(browseTreeHasChildren);
+ }
}
}
diff --git a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
index f93be0a..c9a5864 100644
--- a/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
+++ b/car-media-common/src/com/android/car/media/common/browse/BrowsedMediaItems.java
@@ -21,19 +21,21 @@
import android.support.v4.media.MediaBrowserCompat;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import com.android.car.media.common.MediaItemMetadata;
import java.util.List;
import java.util.Objects;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* A LiveData that provides access the a MediaBrowser's children
*/
-class BrowsedMediaItems extends LiveData<List<MediaItemMetadata>> {
+public class BrowsedMediaItems extends LiveData<List<MediaItemMetadata>> {
/**
* Number of times we will retry obtaining the list of children of a certain node
@@ -58,6 +60,19 @@
mParentId = parentId;
}
+ /**
+ * Filters the items that are valid for the root (tabs) or the current node. Returns null when
+ * the given list is null to preserve its error signal.
+ */
+ @Nullable
+ public static List<MediaItemMetadata> filterItems(boolean forRoot,
+ @Nullable List<MediaItemMetadata> items) {
+ if (items == null) return null;
+ Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
+ : item -> (item.isPlayable() || item.isBrowsable());
+ return items.stream().filter(predicate).collect(Collectors.toList());
+ }
+
@Override
protected void onActive() {
super.onActive();