DO NOT MERGE Use PlaybackViewModel in place of PlaybackModel
Color code that was moved from MediaSource into MediaSourceColors is now removed since PlaybackModel no longer needs it.
Test: Manual smoke test
Change-Id: I7722321e9e4e9dfcac668702012838c7d5e0130d
diff --git a/car-arch-common/src/com/android/car/arch/common/LiveDataFunctions.java b/car-arch-common/src/com/android/car/arch/common/LiveDataFunctions.java
index e46b283..7d60f88 100644
--- a/car-arch-common/src/com/android/car/arch/common/LiveDataFunctions.java
+++ b/car-arch-common/src/com/android/car/arch/common/LiveDataFunctions.java
@@ -21,6 +21,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import androidx.arch.core.util.Function;
import androidx.core.util.Pair;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
@@ -104,6 +105,43 @@
}
/**
+ * Returns a LiveData that emits the same value as {@code data}, but only notifies its observers
+ * when the new value is distinct ({@link Objects#equals(Object, Object)}
+ */
+ public static <T> LiveData<T> distinct(@NonNull LiveData<T> data) {
+ return new MediatorLiveData<T>() {
+ private boolean mInitialized = false;
+
+ {
+ addSource(data, value -> {
+ if (!mInitialized || !Objects.equals(value, getValue())) {
+ mInitialized = true;
+ setValue(value);
+ }
+ });
+ }
+ };
+ }
+
+ /**
+ * Similar to {@link Transformations#map(LiveData, Function)}, but emits {@code null} when
+ * {@code source} emits {@code null}. The input to {@code func} may be treated as not nullable.
+ */
+ public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source,
+ @NonNull Function<T, R> func) {
+ return mapNonNull(source, null, func);
+ }
+
+ /**
+ * Similar to {@link Transformations#map(LiveData, Function)}, but emits {@code nullValue} when
+ * {@code source} emits {@code null}. The input to {@code func} may be treated as not nullable.
+ */
+ public static <T, R> LiveData<R> mapNonNull(@NonNull LiveData<T> source, @Nullable R nullValue,
+ @NonNull Function<T, R> func) {
+ return Transformations.map(source, value -> value == null ? nullValue : func.apply(value));
+ }
+
+ /**
* Returns a LiveData that emits the logical AND of the two arguments. Also deals with {@code
* null} and uninitalized values as follows:
* <table>
diff --git a/car-arch-common/tests/robotests/src/com/android/car/arch/common/LiveDataFunctionsTest.java b/car-arch-common/tests/robotests/src/com/android/car/arch/common/LiveDataFunctionsTest.java
index df47259..bd2a0e3 100644
--- a/car-arch-common/tests/robotests/src/com/android/car/arch/common/LiveDataFunctionsTest.java
+++ b/car-arch-common/tests/robotests/src/com/android/car/arch/common/LiveDataFunctionsTest.java
@@ -17,6 +17,7 @@
package com.android.car.arch.common;
import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+import static com.android.car.arch.common.LiveDataFunctions.distinct;
import static com.android.car.arch.common.LiveDataFunctions.emitsNull;
import static com.android.car.arch.common.LiveDataFunctions.falseLiveData;
import static com.android.car.arch.common.LiveDataFunctions.ifThenElse;
@@ -105,6 +106,27 @@
}
@Test
+ public void testDistinct() {
+ CaptureObserver<Integer> observer = new CaptureObserver<>();
+ MutableLiveData<Integer> source = dataOf(0);
+ LiveData<Integer> distinct = distinct(source);
+ distinct.observe(mLifecycleOwner, observer);
+ observer.reset();
+
+ source.setValue(1);
+ assertThat(observer.hasBeenNotified()).isTrue();
+ assertThat(observer.getObservedValue()).isEqualTo(1);
+ observer.reset();
+
+ source.setValue(1);
+ assertThat(observer.hasBeenNotified()).isFalse();
+
+ source.setValue(2);
+ assertThat(observer.hasBeenNotified()).isTrue();
+ assertThat(observer.getObservedValue()).isEqualTo(2);
+ }
+
+ @Test
public void testAnd_truthTable() {
testBinaryOperator(
LiveDataFunctions::and,
diff --git a/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java b/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java
index 6806748..2d6d98f 100644
--- a/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java
+++ b/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java
@@ -25,6 +25,8 @@
import android.os.Handler;
import android.util.Log;
+import com.android.car.media.common.playback.PlaybackStateAnnotations;
+
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@@ -140,7 +142,7 @@
}
}
- private String getStateName(@PlaybackState.State int state) {
+ private String getStateName(@PlaybackStateAnnotations.Actions int state) {
switch (state) {
case PlaybackState.STATE_NONE:
return "NONE";
@@ -180,7 +182,7 @@
private MediaController getTopMostController(List<MediaController> controllers) {
if (controllers != null && controllers.size() > 0) {
for (MediaController candidate : controllers) {
- @PlaybackState.State int state = candidate.getPlaybackState() != null
+ @PlaybackStateAnnotations.Actions int state = candidate.getPlaybackState() != null
? candidate.getPlaybackState().getState()
: PlaybackState.STATE_NONE;
if (state == PlaybackState.STATE_BUFFERING
@@ -241,7 +243,7 @@
/**
* Stops following changes on the list of active media sources. This method could cause an
- * immediate {@link PlaybackModel.PlaybackObserver#onSourceChanged()} event if a media source
+ * immediate {@link Observer#onActiveSourceChanged()} event if a media source
* was already connected.
*/
private void stop() {
diff --git a/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
index 2e05d80..5391322 100644
--- a/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
+++ b/car-media-common/src/com/android/car/media/common/CustomPlaybackAction.java
@@ -21,12 +21,13 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+
/**
* Abstract representation of a custom playback action. A custom playback action represents a
* visual element that can be used to trigger playback actions not included in the standard
* {@link PlaybackControls} class.
* Custom actions for the current media source are exposed through
- * {@link PlaybackModel#getCustomActions()}
+ * {@link com.android.car.media.common.playback.PlaybackViewModel.PlaybackInfo#getCustomActions()}
*/
public class CustomPlaybackAction {
/** Icon to display for this custom action */
diff --git a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
index 639f6bf..71dd7a2 100644
--- a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
+++ b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
@@ -124,8 +124,6 @@
}
/**
- * An id that can be used on {@link PlaybackModel#onSkipToQueueItem(long)}
- *
* @return the id of this item in the session queue, or NULL if this is not a session queue
* item.
*/
@@ -336,4 +334,4 @@
+ (mMediaDescription != null ? mMediaDescription.getIconUri() : "-")
+ "]";
}
-}
\ No newline at end of file
+}
diff --git a/car-media-common/src/com/android/car/media/common/MediaSource.java b/car-media-common/src/com/android/car/media/common/MediaSource.java
index 32e5582..a0e977b 100644
--- a/car-media-common/src/com/android/car/media/common/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/MediaSource.java
@@ -25,8 +25,6 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
@@ -43,8 +41,6 @@
import android.service.media.MediaBrowserService;
import android.util.Log;
-import androidx.annotation.ColorInt;
-
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -65,11 +61,6 @@
public class MediaSource {
private static final String TAG = "MediaSource";
- /** Third-party defined application theme to use **/
- private static final String THEME_META_DATA_NAME =
- "com.google.android.gms.car.application.theme";
- /** Mark used to indicate that we couldn't find a color and the default one should be used */
- private static final int DEFAULT_COLOR = 0;
/** Number of times we will retry obtaining the list of children of a certain node */
private static final int CHILDREN_SUBSCRIPTION_RETRIES = 3;
/** Time between retries while trying to obtain the list of children of a certain node */
@@ -85,9 +76,6 @@
private List<Observer> mObservers = new ArrayList<>();
private CharSequence mName;
private String mRootNode;
- private @ColorInt int mPrimaryColor;
- private @ColorInt int mAccentColor;
- private @ColorInt int mPrimaryColorDark;
/**
* Custom media sources which should not be templatized.
@@ -443,7 +431,6 @@
private void extractComponentInfo(@NonNull String packageName,
@Nullable String browseServiceClassName) {
- TypedArray ta = null;
try {
ApplicationInfo applicationInfo =
mContext.getPackageManager().getApplicationInfo(packageName,
@@ -462,61 +449,11 @@
} else {
mName = null;
}
-
- // Get the proper theme, check theme for service, then application.
- Context packageContext = mContext.createPackageContext(packageName, 0);
- int appTheme = applicationInfo.metaData != null
- ? applicationInfo.metaData.getInt(THEME_META_DATA_NAME)
- : 0;
- appTheme = appTheme == 0
- ? applicationInfo.theme
- : appTheme;
- packageContext.setTheme(appTheme);
- Resources.Theme theme = packageContext.getTheme();
- ta = theme.obtainStyledAttributes(new int[] {
- android.R.attr.colorPrimary,
- android.R.attr.colorAccent,
- android.R.attr.colorPrimaryDark
- });
- mPrimaryColor = ta.getColor(0, DEFAULT_COLOR);
- mAccentColor = ta.getColor(1, DEFAULT_COLOR);
- mPrimaryColorDark = ta.getColor(2, DEFAULT_COLOR);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Unable to update media client package attributes.", e);
- mPrimaryColor = DEFAULT_COLOR;
- mAccentColor = DEFAULT_COLOR;
- mPrimaryColorDark = DEFAULT_COLOR;
- } finally {
- if (ta != null) {
- ta.recycle();
- }
}
}
- /**
- * @return media source primary color, or the given default color if the source metadata
- * is not available.
- */
- public @ColorInt int getPrimaryColor(@ColorInt int defaultColor) {
- return mPrimaryColor != DEFAULT_COLOR ? mPrimaryColor : defaultColor;
- }
-
- /**
- * @return media source accent color, or the given default color if the source metadata
- * is not available.
- */
- public @ColorInt int getAccentColor(@ColorInt int defaultColor) {
- return mAccentColor != DEFAULT_COLOR ? mAccentColor : defaultColor;
- }
-
- /**
- * @return media source primary dark color, or the given default color if the source metadata
- * is not available.
- */
- public @ColorInt int getPrimaryColorDark(@ColorInt int defaultColor) {
- return mPrimaryColorDark != DEFAULT_COLOR ? mPrimaryColorDark : defaultColor;
- }
-
private void notify(Consumer<Observer> notification) {
mHandler.post(() -> {
List<Observer> observers = new ArrayList<>(mObservers);
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControls.java b/car-media-common/src/com/android/car/media/common/PlaybackControls.java
index 11fa15d..156119a 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackControls.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackControls.java
@@ -2,15 +2,20 @@
import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.car.media.common.playback.PlaybackViewModel;
+
/**
- * Custom view that can be used to display playback controls. It accepts a {@link PlaybackModel}
+ * Custom view that can be used to display playback controls. It accepts a {@link PlaybackViewModel}
* as its data source, automatically reacting to changes in playback state.
*/
public interface PlaybackControls {
/**
- * Sets the {@link PlaybackModel} to use as the view model for this view.
+ * Sets the {@link PlaybackViewModel} to use as the view model for this view.
*/
- void setModel(PlaybackModel model);
+ void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner lifecycleOwner);
/**
* Collapses the playback controls if they were expanded.
@@ -45,4 +50,4 @@
*/
void onToggleQueue();
}
-}
\ No newline at end of file
+}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
index 0a7de10..2b93ed8 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
@@ -16,6 +16,9 @@
package com.android.car.media.common;
+import static com.android.car.arch.common.LiveDataFunctions.pair;
+import static com.android.car.arch.common.LiveDataFunctions.split;
+
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
@@ -27,8 +30,14 @@
import android.widget.ImageButton;
import android.widget.ProgressBar;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.car.widget.ActionBar;
import androidx.cardview.widget.CardView;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.car.media.common.playback.PlaybackViewModel;
+import com.android.car.media.common.source.MediaSourceColors;
import java.util.ArrayList;
import java.util.List;
@@ -41,34 +50,16 @@
private static final String TAG = "PlaybackView";
private PlayPauseStopImageView mPlayPauseStopImageView;
- private View mPlayPauseStopImageContainer;
private ProgressBar mSpinner;
private Context mContext;
private ImageButton mSkipPrevButton;
private ImageButton mSkipNextButton;
private ImageButton mTrackListButton;
- private Listener mListener;
- private PlaybackModel mModel;
- private PlaybackModel.PlaybackObserver mObserver = new PlaybackModel.PlaybackObserver() {
- @Override
- public void onPlaybackStateChanged() {
- updateState();
- updateCustomActions();
- }
-
- @Override
- public void onSourceChanged() {
- updateState();
- updateCustomActions();
- updateAccentColor();
- }
-
- @Override
- public void onMetadataChanged() {
- updateCustomActions(); // rating might have changed
- }
- };
private ColorStateList mIconsColor;
+ private Listener mListener;
+
+ private PlaybackViewModel mModel;
+ private PlaybackViewModel.PlaybackController mController;
/** Creates a {@link PlaybackControlsActionBar} view */
public PlaybackControlsActionBar(Context context) {
@@ -92,52 +83,43 @@
init(context);
}
- @Override
- public void setModel(PlaybackModel model) {
- if (mModel != null) {
- mModel.unregisterObserver(mObserver);
- }
- mModel = model;
- if (mModel != null) {
- mModel.registerObserver(mObserver);
- }
- updateAccentColor();
- }
-
private void init(Context context) {
mContext = context;
CardView actionBarWrapper = findViewById(androidx.car.R.id.action_bar_wrapper);
actionBarWrapper.setCardBackgroundColor(context.getColor(androidx.car.R.color.car_card));
- mPlayPauseStopImageContainer = inflate(context, R.layout.car_play_pause_stop_button_layout,
+ View mPlayPauseStopImageContainer = inflate(context,
+ R.layout.car_play_pause_stop_button_layout,
null);
mPlayPauseStopImageContainer.setOnClickListener(this::onPlayPauseStopClicked);
mPlayPauseStopImageView = mPlayPauseStopImageContainer.findViewById(R.id.play_pause_stop);
+ mPlayPauseStopImageView.setVisibility(View.INVISIBLE);
mSpinner = mPlayPauseStopImageContainer.findViewById(R.id.spinner);
+ mSpinner.setVisibility(View.INVISIBLE);
mPlayPauseStopImageView.setAction(PlayPauseStopImageView.ACTION_DISABLED);
mPlayPauseStopImageView.setOnClickListener(this::onPlayPauseStopClicked);
mIconsColor = context.getResources().getColorStateList(R.color.playback_control_color,
null);
- mSkipPrevButton = createIconButton(mContext, mIconsColor,
+ mSkipPrevButton = createIconButton(mContext,
context.getDrawable(R.drawable.ic_skip_previous));
mSkipPrevButton.setVisibility(INVISIBLE);
mSkipPrevButton.setOnClickListener(v -> {
- if (mModel != null) {
- mModel.onSkipPreviews();
+ if (mController != null) {
+ mController.skipToPrevious();
}
});
- mSkipNextButton = createIconButton(mContext, mIconsColor,
+ mSkipNextButton = createIconButton(mContext,
context.getDrawable(R.drawable.ic_skip_next));
mSkipNextButton.setVisibility(INVISIBLE);
mSkipNextButton.setOnClickListener(v -> {
- if (mModel != null) {
- mModel.onSkipNext();
+ if (mController != null) {
+ mController.skipToNext();
}
});
- mTrackListButton = createIconButton(mContext, mIconsColor,
+ mTrackListButton = createIconButton(mContext,
context.getDrawable(R.drawable.ic_tracklist));
mTrackListButton.setOnClickListener(v -> {
if (mListener != null) {
@@ -145,7 +127,7 @@
}
});
- ImageButton overflowButton = createIconButton(context, mIconsColor,
+ ImageButton overflowButton = createIconButton(context,
context.getDrawable(androidx.car.R.drawable.ic_overflow));
setView(mPlayPauseStopImageContainer, ActionBar.SLOT_MAIN);
@@ -154,98 +136,97 @@
setExpandCollapseView(overflowButton);
}
- private ImageButton createIconButton(Context context, ColorStateList csl, Drawable icon) {
+ private ImageButton createIconButton(Context context, Drawable icon) {
ImageButton button = new ImageButton(context, null, 0, R.style.PlaybackControl);
- button.setImageTintList(csl);
+ button.setImageTintList(mIconsColor);
button.setImageTintMode(PorterDuff.Mode.SRC_ATOP);
button.setImageDrawable(icon);
return button;
}
- private void updateState() {
+ @Override
+ public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner) {
if (mModel != null) {
- mPlayPauseStopImageView.setVisibility(View.VISIBLE);
- mPlayPauseStopImageView.setAction(convertMainAction(mModel.getMainAction()));
- } else {
- mPlayPauseStopImageView.setVisibility(View.INVISIBLE);
+ Log.w(TAG, "PlaybackViewModel set more than once. Ignoring subsequent call.");
}
- mSpinner.setVisibility(mModel != null && mModel.isLoading()
- ? View.VISIBLE : View.INVISIBLE);
- mSkipPrevButton.setVisibility(mModel != null && mModel.isSkipPreviewsEnabled()
- ? View.VISIBLE : View.INVISIBLE);
- mSkipNextButton.setVisibility(mModel != null && mModel.isSkipNextEnabled()
- ? View.VISIBLE : View.INVISIBLE);
+ mModel = model;
+ PlaybackViewModel.PlaybackInfo playbackInfo = model.getPlaybackInfo();
+
+ model.getPlaybackController().observe(owner, controller -> mController = controller);
+ mPlayPauseStopImageView.setVisibility(View.VISIBLE);
+ playbackInfo.getMainAction().observe(owner,
+ action -> mPlayPauseStopImageView.setAction(convertMainAction(action)));
+ playbackInfo.isLoading().observe(owner,
+ isLoading -> mSpinner.setVisibility(isLoading ? View.VISIBLE : View.INVISIBLE));
+ playbackInfo.isSkipPreviousEnabled().observe(owner,
+ enabled -> mSkipPrevButton.setVisibility(enabled ? VISIBLE : INVISIBLE));
+ playbackInfo.isSkipNextEnabled().observe(owner,
+ enabled -> mSkipNextButton.setVisibility(enabled ? VISIBLE : INVISIBLE));
+ model.getMediaSourceColors().observe(owner, this::applyColors);
+ pair(model.hasQueue(), playbackInfo.getCustomActions()).observe(owner,
+ split(this::updateCustomActions));
}
@PlayPauseStopImageView.Action
- private int convertMainAction(@PlaybackModel.Action int action) {
+ private int convertMainAction(@PlaybackViewModel.Action int action) {
switch (action) {
- case PlaybackModel.ACTION_DISABLED:
+ case PlaybackViewModel.ACTION_DISABLED:
return PlayPauseStopImageView.ACTION_DISABLED;
- case PlaybackModel.ACTION_PLAY:
+ case PlaybackViewModel.ACTION_PLAY:
return PlayPauseStopImageView.ACTION_PLAY;
- case PlaybackModel.ACTION_PAUSE:
+ case PlaybackViewModel.ACTION_PAUSE:
return PlayPauseStopImageView.ACTION_PAUSE;
- case PlaybackModel.ACTION_STOP:
+ case PlaybackViewModel.ACTION_STOP:
return PlayPauseStopImageView.ACTION_STOP;
}
Log.w(TAG, "Unknown action: " + action);
return PlayPauseStopImageView.ACTION_DISABLED;
}
- private void updateAccentColor() {
- int color = getMediaSourceColor();
+ private void applyColors(MediaSourceColors colors) {
+ int color = getMediaSourceColor(colors);
int tintColor = ColorChecker.getTintColor(mContext, color);
mPlayPauseStopImageView.setPrimaryActionColor(color, tintColor);
mSpinner.setIndeterminateTintList(ColorStateList.valueOf(color));
}
- private int getMediaSourceColor() {
+ private int getMediaSourceColor(@Nullable MediaSourceColors colors) {
int defaultColor = mContext.getResources().getColor(android.R.color.background_dark, null);
- MediaSource mediaSource = mModel != null ? mModel.getMediaSource() : null;
- return mediaSource != null ? mediaSource.getAccentColor(defaultColor) : defaultColor;
+ return colors != null ? colors.getAccentColor(defaultColor) : defaultColor;
}
- private List<ImageButton> getExtraActions() {
- List<ImageButton> extraActions = new ArrayList<>();
- if (mModel != null && mModel.hasQueue()) {
- extraActions.add(mTrackListButton);
- }
- return extraActions;
- }
-
- private void updateCustomActions() {
- if (mModel == null) {
- setViews(new ImageButton[0]);
- return;
- }
+ private void updateCustomActions(boolean hasQueue,
+ List<PlaybackViewModel.RawCustomPlaybackAction> customActions) {
List<ImageButton> combinedActions = new ArrayList<>();
- combinedActions.addAll(getExtraActions());
- combinedActions.addAll(mModel.getCustomActions()
+ if (hasQueue) {
+ combinedActions.add(mTrackListButton);
+ }
+ combinedActions.addAll(customActions
.stream()
+ .map(rawAction -> rawAction.fetchDrawable(getContext()))
.map(action -> {
- ImageButton button = createIconButton(getContext(), mIconsColor, action.mIcon);
+ ImageButton button = createIconButton(getContext(), action.mIcon);
button.setOnClickListener(view ->
- mModel.onCustomAction(action.mAction, action.mExtras));
+ mController.doCustomAction(action.mAction, action.mExtras));
return button;
})
.collect(Collectors.toList()));
- setViews(combinedActions.toArray(new ImageButton[combinedActions.size()]));
+ setViews(combinedActions.toArray(new ImageButton[0]));
}
private void onPlayPauseStopClicked(View view) {
- if (mModel == null) {
+ if (mController == null) {
return;
}
switch (mPlayPauseStopImageView.getAction()) {
case PlayPauseStopImageView.ACTION_PLAY:
- mModel.onPlay();
+ mController.play();
break;
case PlayPauseStopImageView.ACTION_PAUSE:
- mModel.onPause();
+ mController.pause();
break;
case PlayPauseStopImageView.ACTION_STOP:
- mModel.onStop();
+ mController.stop();
break;
default:
Log.i(TAG, "Play/Pause/Stop clicked on invalid state");
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 dd8f4f1..f70e9ff 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
@@ -16,8 +16,13 @@
package com.android.car.media.common;
+import static com.android.car.arch.common.LiveDataFunctions.mapNonNull;
+
+import android.app.Application;
import android.car.Car;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.session.MediaController;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,80 +33,72 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModelProviders;
+
+import com.android.car.media.common.playback.AlbumArtLiveData;
+import com.android.car.media.common.playback.PlaybackViewModel;
import com.bumptech.glide.request.target.Target;
-import java.util.Objects;
-
/**
- * {@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.
+ * {@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 {
+ // TODO(keyboardr): replace with MediaSourceViewModel when available
private ActiveMediaSourceManager mActiveMediaSourceManager;
- private PlaybackModel mModel;
- private CrossfadeImageView mAlbumBackground;
- private PlaybackControls mPlaybackControls;
- private ImageView mAppIcon;
- private TextView mAppName;
- private TextView mTitle;
- private TextView mSubtitle;
- private MediaItemMetadata mCurrentMetadata;
- private PlaybackModel.PlaybackObserver mPlaybackObserver =
- new PlaybackModel.PlaybackObserver() {
- @Override
- public void onSourceChanged() {
- updateMetadata();
- }
+ private MutableLiveData<MediaController> mMediaController = new MutableLiveData<>();
- @Override
- public void onMetadataChanged() {
- updateMetadata();
- }
- };
+
private ActiveMediaSourceManager.Observer mActiveSourceObserver =
new ActiveMediaSourceManager.Observer() {
- @Override
- public void onActiveSourceChanged() {
- mModel.setMediaController(mActiveMediaSourceManager.getMediaController());
- }
- };
+ @Override
+ public void onActiveSourceChanged() {
+ mMediaController.setValue(mActiveMediaSourceManager.getMediaController());
+ }
+ };
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
+ PlaybackViewModel playbackViewModel = ViewModelProviders.of(getActivity())
+ .get(PlaybackViewModel.class);
+ playbackViewModel.setMediaController(mMediaController);
+ ViewModel innerViewModel = ViewModelProviders.of(getActivity()).get(ViewModel.class);
+ innerViewModel.init(playbackViewModel);
+
View view = inflater.inflate(R.layout.car_playback_fragment, container, false);
mActiveMediaSourceManager = new ActiveMediaSourceManager(getContext());
- mModel = new PlaybackModel(getContext());
- mAlbumBackground = view.findViewById(R.id.album_background);
- mPlaybackControls = view.findViewById(R.id.playback_controls);
- mPlaybackControls.setModel(mModel);
- mAppIcon = view.findViewById(R.id.app_icon);
- mAppName = view.findViewById(R.id.app_name);
- mTitle = view.findViewById(R.id.title);
- mSubtitle = view.findViewById(R.id.subtitle);
- mAlbumBackground.setOnClickListener(v -> {
- MediaSource mediaSource = mModel.getMediaSource();
- Intent intent;
- if (mediaSource != null && mediaSource.isCustom()) {
- // We are playing a custom app. Jump to it, not to the template
- intent = getContext().getPackageManager()
- .getLaunchIntentForPackage(mediaSource.getPackageName());
- } else if (mediaSource != null) {
- // We are playing a standard app. Open the template to browse it.
- intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
- intent.putExtra(Car.CAR_EXTRA_MEDIA_PACKAGE, mediaSource.getPackageName());
- } else {
- // We are not playing. Open the template to start playing something.
- intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
- }
- startActivity(intent);
+ PlaybackControls playbackControls = view.findViewById(R.id.playback_controls);
+ playbackControls.setModel(playbackViewModel, getViewLifecycleOwner());
+
+ ImageView appIcon = view.findViewById(R.id.app_icon);
+ innerViewModel.getAppIcon().observe(getViewLifecycleOwner(), appIcon::setImageBitmap);
+
+ TextView appName = view.findViewById(R.id.app_name);
+ innerViewModel.getAppName().observe(getViewLifecycleOwner(), appName::setText);
+
+ TextView title = view.findViewById(R.id.title);
+ innerViewModel.getTitle().observe(getViewLifecycleOwner(), title::setText);
+
+ TextView subtitle = view.findViewById(R.id.subtitle);
+ innerViewModel.getSubtitle().observe(getViewLifecycleOwner(), subtitle::setText);
+
+ CrossfadeImageView albumBackground = view.findViewById(R.id.album_background);
+ innerViewModel.getAlbumArt().observe(getViewLifecycleOwner(),
+ albumArt -> albumBackground.setImageBitmap(albumArt, true));
+ LiveData<Intent> openIntent = innerViewModel.getOpenIntent();
+ openIntent.observe(getViewLifecycleOwner(), intent -> {
+ // Ensure open intent data stays fresh while view is clickable.
});
-
+ albumBackground.setOnClickListener(v -> startActivity(openIntent.getValue()));
return view;
}
@@ -109,47 +106,86 @@
public void onStart() {
super.onStart();
mActiveMediaSourceManager.registerObserver(mActiveSourceObserver);
- mModel.registerObserver(mPlaybackObserver);
}
@Override
public void onStop() {
super.onStop();
mActiveMediaSourceManager.unregisterObserver(mActiveSourceObserver);
- mModel.unregisterObserver(mPlaybackObserver);
}
- private void updateMetadata() {
- MediaSource mediaSource = mModel.getMediaSource();
+ /**
+ * ViewModel for the PlaybackFragment
+ */
+ public static class ViewModel extends AndroidViewModel {
- if (mediaSource == null) {
- mTitle.setText(null);
- mSubtitle.setText(null);
- mAppName.setText(null);
- mAlbumBackground.setImageBitmap(null, true);
- return;
+ private static final Intent MEDIA_TEMPLATE_INTENT =
+ new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
+
+ private LiveData<MediaSource> mMediaSource;
+ private LiveData<CharSequence> mAppName;
+ private LiveData<Bitmap> mAppIcon;
+ private LiveData<Intent> mOpenIntent;
+ private LiveData<CharSequence> mTitle;
+ private LiveData<CharSequence> mSubtitle;
+ private LiveData<Bitmap> mAlbumArt;
+
+ private PlaybackViewModel mPlaybackViewModel;
+
+ public ViewModel(Application application) {
+ super(application);
}
- MediaItemMetadata metadata = mModel.getMetadata();
- if (Objects.equals(mCurrentMetadata, metadata)) {
- return;
+ void init(PlaybackViewModel playbackViewModel) {
+ if (mPlaybackViewModel == playbackViewModel) {
+ return;
+ }
+ mPlaybackViewModel = playbackViewModel;
+ mMediaSource = mapNonNull(playbackViewModel.getMediaController(),
+ controller -> new MediaSource(getApplication(), controller.getPackageName()));
+ mAppName = mapNonNull(mMediaSource, MediaSource::getName);
+ mAppIcon = mapNonNull(mMediaSource, MediaSource::getRoundPackageIcon);
+ mOpenIntent = mapNonNull(mMediaSource, MEDIA_TEMPLATE_INTENT, source -> {
+ if (source.isCustom()) {
+ // We are playing a custom app. Jump to it, not to the template
+ return getApplication().getPackageManager()
+ .getLaunchIntentForPackage(source.getPackageName());
+ } else {
+ // We are playing a standard app. Open the template to browse it.
+ Intent intent = new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE);
+ intent.putExtra(Car.CAR_EXTRA_MEDIA_PACKAGE, source.getPackageName());
+ return intent;
+ }
+ });
+ mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle);
+ mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getSubtitle);
+ mAlbumArt = AlbumArtLiveData.getAlbumArt(getApplication(),
+ Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL, false,
+ playbackViewModel.getMetadata());
}
- mCurrentMetadata = metadata;
- mTitle.setText(metadata != null ? metadata.getTitle() : null);
- mSubtitle.setText(metadata != null ? metadata.getSubtitle() : null);
- if (metadata != null) {
- metadata.getAlbumArt(getContext(),
- Target.SIZE_ORIGINAL,
- Target.SIZE_ORIGINAL,
- false)
- .thenAccept(bitmap -> {
- //bitmap = ImageUtils.blur(getContext(), bitmap, 1f, 10f);
- mAlbumBackground.setImageBitmap(bitmap, true);
- });
- } else {
- mAlbumBackground.setImageBitmap(null, true);
+
+ LiveData<CharSequence> getAppName() {
+ return mAppName;
}
- mAppName.setText(mediaSource.getName());
- mAppIcon.setImageBitmap(mediaSource.getRoundPackageIcon());
+
+ LiveData<Bitmap> getAppIcon() {
+ return mAppIcon;
+ }
+
+ LiveData<Intent> getOpenIntent() {
+ return mOpenIntent;
+ }
+
+ LiveData<CharSequence> getTitle() {
+ return mTitle;
+ }
+
+ LiveData<CharSequence> getSubtitle() {
+ return mSubtitle;
+ }
+
+ LiveData<Bitmap> getAlbumArt() {
+ return mAlbumArt;
+ }
}
}
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackModel.java b/car-media-common/src/com/android/car/media/common/PlaybackModel.java
deleted file mode 100644
index e7942fb..0000000
--- a/car-media-common/src/com/android/car/media/common/PlaybackModel.java
+++ /dev/null
@@ -1,623 +0,0 @@
-/*
- * 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.common;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.media.MediaMetadata;
-import android.media.Rating;
-import android.media.session.MediaController;
-import android.media.session.MediaController.TransportControls;
-import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.media.session.PlaybackState.Actions;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.SystemClock;
-import android.util.Log;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-
-/**
- * Wrapper of {@link MediaSession}. It provides access to media session events and extended
- * information on the currently playing item metadata.
- */
-public class PlaybackModel {
- private static final String TAG = "PlaybackModel";
-
- private static final String ACTION_SET_RATING =
- "com.android.car.media.common.ACTION_SET_RATING";
- private static final String EXTRA_SET_HEART = "com.android.car.media.common.EXTRA_SET_HEART";
-
- private final Handler mHandler = new Handler();
- @Nullable
- private final Context mContext;
- private final List<PlaybackObserver> mObservers = new ArrayList<>();
- private MediaController mMediaController;
- private MediaSource mMediaSource;
- private boolean mIsStarted;
-
- /**
- * An observer of this model
- */
- public abstract static class PlaybackObserver {
- /**
- * Called whenever the playback state of the current media item changes.
- */
- protected void onPlaybackStateChanged() {};
-
- /**
- * Called when the top source media app changes.
- */
- protected void onSourceChanged() {};
-
- /**
- * Called when the media item being played changes.
- */
- protected void onMetadataChanged() {};
- }
-
- private MediaController.Callback mCallback = new MediaController.Callback() {
- @Override
- public void onPlaybackStateChanged(PlaybackState state) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onPlaybackStateChanged: " + state);
- }
- PlaybackModel.this.notify(PlaybackObserver::onPlaybackStateChanged);
- }
-
- @Override
- public void onMetadataChanged(MediaMetadata metadata) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onMetadataChanged: " + metadata);
- }
- PlaybackModel.this.notify(PlaybackObserver::onMetadataChanged);
- }
- };
-
- /**
- * Creates a {@link PlaybackModel}
- */
- public PlaybackModel(@NonNull Context context) {
- this(context, null);
- }
-
- /**
- * Creates a {@link PlaybackModel} wrapping to the given media controller
- */
- public PlaybackModel(@NonNull Context context, @Nullable MediaController controller) {
- mContext = context;
- changeMediaController(controller);
- }
-
- /**
- * Sets the {@link MediaController} wrapped by this model.
- */
- public void setMediaController(@Nullable MediaController mediaController) {
- changeMediaController(mediaController);
- }
-
- private void changeMediaController(@Nullable MediaController mediaController) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "New media controller: " + (mediaController != null
- ? mediaController.getPackageName() : null));
- }
- if ((mediaController == null && mMediaController == null)
- || (mediaController != null && mMediaController != null
- && mediaController.getPackageName().equals(mMediaController.getPackageName()))) {
- // If no change, do nothing.
- return;
- }
- if (mMediaController != null) {
- mMediaController.unregisterCallback(mCallback);
- }
- mMediaController = mediaController;
- mMediaSource = mMediaController != null
- ? new MediaSource(mContext, mMediaController.getPackageName()) : null;
- if (mMediaController != null && mIsStarted) {
- mMediaController.registerCallback(mCallback);
- }
- if (mIsStarted) {
- notify(PlaybackObserver::onSourceChanged);
- }
- }
-
- /**
- * Starts following changes on the playback state of the given source. If any changes happen,
- * all observers registered through {@link #registerObserver(PlaybackObserver)} will be
- * notified.
- */
- private void start() {
- if (mMediaController != null) {
- mMediaController.registerCallback(mCallback);
- }
- mIsStarted = true;
- }
-
- /**
- * Stops following changes on the list of active media sources.
- */
- private void stop() {
- if (mMediaController != null) {
- mMediaController.unregisterCallback(mCallback);
- }
- mIsStarted = false;
- }
-
- private void notify(Consumer<PlaybackObserver> notification) {
- mHandler.post(() -> {
- List<PlaybackObserver> observers = new ArrayList<>(mObservers);
- for (PlaybackObserver observer : observers) {
- notification.accept(observer);
- }
- });
- }
-
- /**
- * @return a {@link MediaSource} providing access to metadata of the currently playing media
- * source, or NULL if the media source has no active session.
- */
- @Nullable
- public MediaSource getMediaSource() {
- return mMediaSource;
- }
-
- /**
- * @return a {@link MediaController} that can be used to control this media source, or NULL
- * if the media source has no active session.
- */
- @Nullable
- public MediaController getMediaController() {
- return mMediaController;
- }
-
- /**
- * @return {@link Action} selected as the main action for the current media item, based on the
- * current playback state and the available actions reported by the media source.
- * Changes on this value will be notified through
- * {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- @Action
- public int getMainAction() {
- return getMainAction(mMediaController != null ? mMediaController.getPlaybackState() : null);
- }
-
- /**
- * @return {@link MediaItemMetadata} of the currently selected media item in the media source.
- * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
- */
- @Nullable
- public MediaItemMetadata getMetadata() {
- if (mMediaController == null) {
- return null;
- }
- MediaMetadata metadata = mMediaController.getMetadata();
- if (metadata == null) {
- return null;
- }
- return new MediaItemMetadata(metadata);
- }
-
- /**
- * @return duration of the media item, in milliseconds. The current position in this duration
- * can be obtained by calling {@link #getProgress()}.
- * Changes on this value will be notified through {@link PlaybackObserver#onMetadataChanged()}
- */
- public long getMaxProgress() {
- if (mMediaController == null || mMediaController.getMetadata() == null) {
- return 0;
- } else {
- return mMediaController.getMetadata()
- .getLong(MediaMetadata.METADATA_KEY_DURATION);
- }
- }
-
- /**
- * Sends a 'play' command to the media source
- */
- public void onPlay() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().play();
- }
- }
-
- /**
- * Sends a 'skip previews' command to the media source
- */
- public void onSkipPreviews() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().skipToPrevious();
- }
- }
-
- /**
- * Sends a 'skip next' command to the media source
- */
- public void onSkipNext() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().skipToNext();
- }
- }
-
- /**
- * Sends a 'pause' command to the media source
- */
- public void onPause() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().pause();
- }
- }
-
- /**
- * Sends a 'stop' command to the media source
- */
- public void onStop() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().stop();
- }
- }
-
- /**
- * Sends a custom action to the media source
- * @param action identifier of the custom action
- * @param extras additional data to send to the media source.
- */
- public void onCustomAction(String action, Bundle extras) {
- if (mMediaController == null) return;
- TransportControls cntrl = mMediaController.getTransportControls();
-
- if (ACTION_SET_RATING.equals(action)) {
- boolean setHeart = extras != null && extras.getBoolean(EXTRA_SET_HEART, false);
- cntrl.setRating(Rating.newHeartRating(setHeart));
- } else {
- cntrl.sendCustomAction(action, extras);
- }
-
- mMediaController.getTransportControls().sendCustomAction(action, extras);
- }
-
- /**
- * Starts playing a given media item. This id corresponds to {@link MediaItemMetadata#getId()}.
- */
- public void onPlayItem(String mediaItemId) {
- if (mMediaController != null) {
- mMediaController.getTransportControls().playFromMediaId(mediaItemId, null);
- }
- }
-
- /**
- * Skips to a particular item in the media queue. This id is {@link MediaItemMetadata#mQueueId}
- * of the items obtained through {@link #getQueue()}.
- */
- public void onSkipToQueueItem(long queueId) {
- if (mMediaController != null) {
- mMediaController.getTransportControls().skipToQueueItem(queueId);
- }
- }
-
- /**
- * Prepares the current media source for playback.
- */
- public void onPrepare() {
- if (mMediaController != null) {
- mMediaController.getTransportControls().prepare();
- }
- }
-
- /**
- * Possible main actions.
- */
- @IntDef({ACTION_PLAY, ACTION_STOP, ACTION_PAUSE, ACTION_DISABLED})
- @Retention(RetentionPolicy.SOURCE)
- public @interface Action {}
-
- /** Main action is disabled. The source can't play media at this time */
- public static final int ACTION_DISABLED = 0;
- /** Start playing */
- public static final int ACTION_PLAY = 1;
- /** Stop playing */
- public static final int ACTION_STOP = 2;
- /** Pause playing */
- public static final int ACTION_PAUSE = 3;
-
- @Action
- private static int getMainAction(PlaybackState state) {
- if (state == null) {
- return ACTION_DISABLED;
- }
-
- @Actions long actions = state.getActions();
- int stopAction = ACTION_DISABLED;
- if ((actions & (PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE)) != 0) {
- stopAction = ACTION_PAUSE;
- } else if ((actions & PlaybackState.ACTION_STOP) != 0) {
- stopAction = ACTION_STOP;
- }
-
- switch (state.getState()) {
- case PlaybackState.STATE_PLAYING:
- case PlaybackState.STATE_BUFFERING:
- case PlaybackState.STATE_CONNECTING:
- case PlaybackState.STATE_FAST_FORWARDING:
- case PlaybackState.STATE_REWINDING:
- case PlaybackState.STATE_SKIPPING_TO_NEXT:
- case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
- case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
- return stopAction;
- case PlaybackState.STATE_STOPPED:
- case PlaybackState.STATE_PAUSED:
- case PlaybackState.STATE_NONE:
- case PlaybackState.STATE_ERROR:
- return (actions & PlaybackState.ACTION_PLAY) != 0 ? ACTION_PLAY : ACTION_DISABLED;
- default:
- Log.w(TAG, String.format("Unknown PlaybackState: %d", state.getState()));
- return ACTION_DISABLED;
- }
- }
-
- /**
- * @return the current playback progress, in milliseconds. This is a value between 0 and
- * {@link #getMaxProgress()} or PROGRESS_UNKNOWN of the current position is unknown.
- */
- public long getProgress() {
- if (mMediaController == null) {
- return 0;
- }
- PlaybackState state = mMediaController.getPlaybackState();
- if (state == null) {
- return 0;
- }
- if (state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
- return PlaybackState.PLAYBACK_POSITION_UNKNOWN;
- }
- long timeDiff = SystemClock.elapsedRealtime() - state.getLastPositionUpdateTime();
- float speed = state.getPlaybackSpeed();
- if (state.getState() == PlaybackState.STATE_PAUSED
- || state.getState() == PlaybackState.STATE_STOPPED) {
- // This guards against apps who don't keep their playbackSpeed to spec (b/62375164)
- speed = 0f;
- }
- long posDiff = (long) (timeDiff * speed);
- return Math.min(posDiff + state.getPosition(), getMaxProgress());
- }
-
- /**
- * @return true if the current media source is playing a media item. Changes on this value
- * would be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- public boolean isPlaying() {
- return mMediaController != null
- && mMediaController.getPlaybackState() != null
- && mMediaController.getPlaybackState().getState() == PlaybackState.STATE_PLAYING;
- }
-
- /**
- * Registers an observer to be notified of media events. If the model is not started yet it
- * will start right away. If the model was already started, the observer will receive an
- * immediate {@link PlaybackObserver#onSourceChanged()} event.
- */
- public void registerObserver(PlaybackObserver observer) {
- mObservers.add(observer);
- if (!mIsStarted) {
- start();
- } else {
- observer.onSourceChanged();
- }
- }
-
- /**
- * Unregisters an observer previously registered using
- * {@link #registerObserver(PlaybackObserver)}. There are no other observers the model will
- * stop tracking changes right away.
- */
- public void unregisterObserver(PlaybackObserver observer) {
- mObservers.remove(observer);
- if (mObservers.isEmpty() && mIsStarted) {
- stop();
- }
- }
-
- /**
- * @return true if the media source supports skipping to next item. Changes on this value
- * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- public boolean isSkipNextEnabled() {
- return mMediaController != null
- && mMediaController.getPlaybackState() != null
- && (mMediaController.getPlaybackState().getActions()
- & PlaybackState.ACTION_SKIP_TO_NEXT) != 0;
- }
-
- /**
- * @return true if the media source supports skipping to previous item. Changes on this value
- * will be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- public boolean isSkipPreviewsEnabled() {
- return mMediaController != null
- && mMediaController.getPlaybackState() != null
- && (mMediaController.getPlaybackState().getActions()
- & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0;
- }
-
- /**
- * @return true if the media source is loading (e.g.: buffering, connecting, etc.). Changes on
- * this value would be notified through {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- public boolean isLoading() {
- if (mMediaController == null) {
- return false;
- }
-
- PlaybackState playbackState = mMediaController.getPlaybackState();
-
- if (playbackState == null) {
- return false;
- }
-
- int state = playbackState.getState();
-
- return state == PlaybackState.STATE_BUFFERING
- || state == PlaybackState.STATE_CONNECTING
- || state == PlaybackState.STATE_FAST_FORWARDING
- || state == PlaybackState.STATE_REWINDING
- || state == PlaybackState.STATE_SKIPPING_TO_NEXT
- || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
- || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM;
- }
-
- /**
- * @return a human readable description of the error that cause the media source to be in a
- * non-playable state, or null if there is no error. Changes on this value will be notified
- * through {@link PlaybackObserver#onPlaybackStateChanged()}
- */
- @Nullable
- public CharSequence getErrorMessage() {
- return mMediaController != null && mMediaController.getPlaybackState() != null
- ? mMediaController.getPlaybackState().getErrorMessage()
- : null;
- }
-
- /**
- * @return a sorted list of {@link MediaItemMetadata} corresponding to the queue of media items
- * as reported by the media source. Changes on this value will be notified through
- * {@link PlaybackObserver#onPlaybackStateChanged()}.
- */
- @NonNull
- public List<MediaItemMetadata> getQueue() {
- if (mMediaController == null) {
- return new ArrayList<>();
- }
- List<MediaSession.QueueItem> items = mMediaController.getQueue();
- if (items != null) {
- return items.stream()
- .filter(item -> item.getDescription() != null
- && item.getDescription().getTitle() != null)
- .map(MediaItemMetadata::new)
- .collect(Collectors.toList());
- } else {
- return new ArrayList<>();
- }
- }
-
- /**
- * @return the title of the queue or NULL if not available.
- */
- @Nullable
- public CharSequence getQueueTitle() {
- if (mMediaController == null) {
- return null;
- }
- return mMediaController.getQueueTitle();
- }
-
- /**
- * @return queue id of the currently playing queue item, or
- * {@link MediaSession.QueueItem#UNKNOWN_ID} if none of the items is currently playing.
- */
- public long getActiveQueueItemId() {
- PlaybackState playbackState = mMediaController.getPlaybackState();
- if (playbackState == null) return MediaSession.QueueItem.UNKNOWN_ID;
- return playbackState.getActiveQueueItemId();
- }
-
- /**
- * @return true if the media queue is not empty. Detailed information can be obtained by
- * calling to {@link #getQueue()}. Changes on this value will be notified through
- * {@link PlaybackObserver#onPlaybackStateChanged()}.
- */
- public boolean hasQueue() {
- if (mMediaController == null) {
- return false;
- }
- List<MediaSession.QueueItem> items = mMediaController.getQueue();
- return items != null && !items.isEmpty();
- }
-
- private @Nullable CustomPlaybackAction getRatingAction() {
- PlaybackState playbackState = mMediaController.getPlaybackState();
- if (playbackState == null) return null;
-
- long stdActions = playbackState.getActions();
- if ((stdActions & PlaybackState.ACTION_SET_RATING) == 0) return null;
-
- int ratingType = mMediaController.getRatingType();
- if (ratingType != Rating.RATING_HEART) return null;
-
- MediaMetadata metadata = mMediaController.getMetadata();
- boolean hasHeart = false;
- if (metadata != null) {
- Rating rating = metadata.getRating(MediaMetadata.METADATA_KEY_USER_RATING);
- hasHeart = rating != null && rating.hasHeart();
- }
-
- int iconResource = hasHeart ? R.drawable.ic_star_filled : R.drawable.ic_star_empty;
- Drawable icon = mContext.getResources().getDrawable(iconResource, null);
- Bundle extras = new Bundle();
- extras.putBoolean(EXTRA_SET_HEART, !hasHeart);
- return new CustomPlaybackAction(icon, ACTION_SET_RATING, extras);
- }
-
- /**
- * @return a sorted list of custom actions, as reported by the media source. Changes on this
- * value will be notified through
- * {@link PlaybackObserver#onPlaybackStateChanged()}.
- */
- public List<CustomPlaybackAction> getCustomActions() {
- List<CustomPlaybackAction> actions = new ArrayList<>();
- if (mMediaController == null) return actions;
- PlaybackState playbackState = mMediaController.getPlaybackState();
- if (playbackState == null) return actions;
-
- CustomPlaybackAction ratingAction = getRatingAction();
- if (ratingAction != null) actions.add(ratingAction);
-
- for (PlaybackState.CustomAction action : playbackState.getCustomActions()) {
- Resources resources = getResourcesForPackage(mMediaController.getPackageName());
- if (resources == null) {
- actions.add(null);
- } else {
- // the resources may be from another package. we need to update the configuration
- // using the context from the activity so we get the drawable from the correct DPI
- // bucket.
- resources.updateConfiguration(mContext.getResources().getConfiguration(),
- mContext.getResources().getDisplayMetrics());
- Drawable icon = resources.getDrawable(action.getIcon(), null);
- actions.add(new CustomPlaybackAction(icon, action.getAction(), action.getExtras()));
- }
- }
- return actions;
- }
-
- private Resources getResourcesForPackage(String packageName) {
- try {
- return mContext.getPackageManager().getResourcesForApplication(packageName);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to get resources for " + packageName);
- return null;
- }
- }
-}
diff --git a/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java b/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java
new file mode 100644
index 0000000..9a3589c
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/playback/AlbumArtLiveData.java
@@ -0,0 +1,82 @@
+/*
+ * 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.common.playback;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+
+import com.android.car.media.common.MediaItemMetadata;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * LiveData class for loading album art from a MediaItemMetadata. This type should not be used
+ * directly; instances should be created via {@link #getAlbumArt(Context, int, int, boolean,
+ * LiveData)}.
+ */
+public class AlbumArtLiveData extends MediatorLiveData<Bitmap> {
+
+ /**
+ * Returns a new LiveData that emits a Bitmap representation of the {@link MediaItemMetadata}'s
+ * album art. While the MediaItemMetadata returns a {@link CompletableFuture}, this LiveData
+ * only updates once the future has completed. If it completes exceptionally or the source emits
+ * {@code null}, the LiveData's value is set to {@code null}.
+ *
+ * @see MediaItemMetadata#getAlbumArt(Context, int, int, boolean)
+ */
+ public static LiveData<Bitmap> getAlbumArt(Context context, int width, int height, boolean fit,
+ LiveData<MediaItemMetadata> source) {
+ return new AlbumArtLiveData(context, width, height, fit, source);
+ }
+
+ private final Context mContext;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mFit;
+ private CompletableFuture<Bitmap> mFuture;
+
+ private AlbumArtLiveData(Context context, int width, int height, boolean fit,
+ LiveData<MediaItemMetadata> source) {
+ mContext = context.getApplicationContext();
+ mWidth = width;
+ mHeight = height;
+ mFit = fit;
+ addSource(source, this::update);
+ }
+
+ private void update(MediaItemMetadata metadata) {
+ if (mFuture != null && !mFuture.isDone()) {
+ mFuture.cancel(true);
+ }
+ if (metadata == null) {
+ setValue(null);
+ mFuture = null;
+ } else {
+ mFuture = metadata.getAlbumArt(mContext, mWidth, mHeight, mFit);
+ mFuture.whenComplete((result, throwable) -> {
+ if (throwable != null) {
+ postValue(null);
+ } else {
+ postValue(result);
+ }
+ });
+ }
+ }
+}
diff --git a/car-media-common/src/com/android/car/media/common/playback/MediaMetadataLiveData.java b/car-media-common/src/com/android/car/media/common/playback/MediaMetadataLiveData.java
index 4d77e65..98f1f00 100644
--- a/car-media-common/src/com/android/car/media/common/playback/MediaMetadataLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/playback/MediaMetadataLiveData.java
@@ -44,6 +44,7 @@
@Override
protected void onActive() {
+ setValue(mMediaController.getMetadata());
mMediaController.registerCallback(mCallback);
}
diff --git a/car-media-common/src/com/android/car/media/common/playback/PlaybackStateLiveData.java b/car-media-common/src/com/android/car/media/common/playback/PlaybackStateLiveData.java
index 3ee0400..41fc685 100644
--- a/car-media-common/src/com/android/car/media/common/playback/PlaybackStateLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/playback/PlaybackStateLiveData.java
@@ -43,6 +43,7 @@
@Override
protected void onActive() {
+ setValue(mMediaController.getPlaybackState());
mMediaController.registerCallback(mCallback);
}
diff --git a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
index a49589d..ac67ee1 100644
--- a/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
+++ b/car-media-common/src/com/android/car/media/common/playback/PlaybackViewModel.java
@@ -20,6 +20,7 @@
import static androidx.lifecycle.Transformations.switchMap;
import static com.android.car.arch.common.LiveDataFunctions.dataOf;
+import static com.android.car.arch.common.LiveDataFunctions.distinct;
import static com.android.car.arch.common.LiveDataFunctions.nullLiveData;
import static com.android.car.arch.common.LiveDataFunctions.pair;
import static com.android.car.media.common.playback.PlaybackStateAnnotations.Actions;
@@ -55,6 +56,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -105,8 +107,8 @@
private final LiveData<MediaMetadata> mMetadata = switchMap(mMediaController,
mediaController -> mediaController == null ? nullLiveData()
: new MediaMetadataLiveData(mediaController));
- private final LiveData<MediaItemMetadata> mWrappedMetadata = map(mMetadata,
- MediaItemMetadata::new);
+ private final LiveData<MediaItemMetadata> mWrappedMetadata = distinct(map(mMetadata,
+ metadata -> metadata == null ? null : new MediaItemMetadata(metadata)));
private final LiveData<PlaybackState> mPlaybackState = switchMap(mMediaController,
mediaController -> mediaController == null ? nullLiveData()
@@ -117,25 +119,24 @@
: new QueueLiveData(mediaController));
// Filters out queue items with no description or title and converts them to MediaItemMetadatas
- private final LiveData<List<MediaItemMetadata>> mSanitizedQueue = map(mQueue,
+ private final LiveData<List<MediaItemMetadata>> mSanitizedQueue = distinct(map(mQueue,
queue -> queue == null ? Collections.emptyList()
: queue.stream()
.filter(item -> item.getDescription() != null
&& item.getDescription().getTitle() != null)
.map(MediaItemMetadata::new)
- .collect(Collectors.toList()));
+ .collect(Collectors.toList())));
- private final LiveData<Boolean> mHasQueue = map(mQueue,
- queue -> queue != null && !queue.isEmpty());
+ private final LiveData<Boolean> mHasQueue = distinct(map(mQueue,
+ queue -> queue != null && !queue.isEmpty()));
private final LiveData<PlaybackController> mPlaybackControls = map(mMediaController,
PlaybackController::new);
- private final LiveData<CombinedInfo> mCombinedInfo =
- map(
- pair(mMediaController, pair(mMetadata, mPlaybackState)),
- input ->
- new CombinedInfo(input.first, input.second.first, input.second.second));
+ private final LiveData<CombinedInfo> mCombinedInfo = map(
+ pair(mMediaController, pair(mMetadata, mPlaybackState)),
+ input -> input.first == null ? null
+ : new CombinedInfo(input.first, input.second.first, input.second.second));
private final PlaybackInfo mPlaybackInfo = new PlaybackInfo();
@@ -217,8 +218,8 @@
}
/**
- * Contains LiveDatas related to the current PlaybackState. A single instance of this object
- * is created for each PlaybackViewModel.
+ * Contains LiveDatas related to the current PlaybackState. A single instance of this object is
+ * created for each PlaybackViewModel.
*/
public class PlaybackInfo {
private LiveData<Integer> mMainAction = map(mPlaybackState, state -> {
@@ -247,9 +248,9 @@
case PlaybackState.STATE_STOPPED:
case PlaybackState.STATE_PAUSED:
case PlaybackState.STATE_NONE:
- return ACTION_PLAY;
case PlaybackState.STATE_ERROR:
- return ACTION_DISABLED;
+ return (actions & PlaybackState.ACTION_PLAY) != 0 ? ACTION_PLAY
+ : ACTION_DISABLED;
default:
Log.w(TAG, String.format("Unknown PlaybackState: %d", state.getState()));
return ACTION_DISABLED;
@@ -276,8 +277,22 @@
state -> state != null
&& (state.getActions() & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0);
- private final LiveData<Boolean> mIsBuffering = map(mPlaybackState,
- state -> state != null && state.getState() == PlaybackState.STATE_BUFFERING);
+ // true if the media source is loading (e.g.: buffering, connecting, etc.)
+ private final LiveData<Boolean> mIsLoading = map(mPlaybackState,
+ playbackState -> {
+ if (playbackState == null) {
+ return false;
+ }
+
+ int state = playbackState.getState();
+ return state == PlaybackState.STATE_BUFFERING
+ || state == PlaybackState.STATE_CONNECTING
+ || state == PlaybackState.STATE_FAST_FORWARDING
+ || state == PlaybackState.STATE_REWINDING
+ || state == PlaybackState.STATE_SKIPPING_TO_NEXT
+ || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
+ || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM;
+ });
private final LiveData<CharSequence> mErrorMessage = map(mPlaybackState,
state -> state == null ? null : state.getErrorMessage());
@@ -286,8 +301,9 @@
state -> state == null ? MediaSession.QueueItem.UNKNOWN_ID
: state.getActiveQueueItemId());
- private final LiveData<List<RawCustomPlaybackAction>> mCustomActions = map(mCombinedInfo,
- this::getCustomActions);
+ private final LiveData<List<RawCustomPlaybackAction>> mCustomActions = distinct(
+ map(mCombinedInfo,
+ this::getCustomActions));
private PlaybackInfo() {
}
@@ -298,45 +314,14 @@
}
/**
- * Returns a sorted list of custom actions available. Call {@link #fetchDrawable(Context,
- * RawCustomPlaybackAction)} to get the appropriate icon Drawable.
+ * Returns a sorted list of custom actions available. Call {@link
+ * RawCustomPlaybackAction#fetchDrawable(Context)} to get the appropriate icon Drawable.
*/
public LiveData<List<RawCustomPlaybackAction>> getCustomActions() {
return mCustomActions;
}
/**
- * Converts a {@link RawCustomPlaybackAction} into a {@link CustomPlaybackAction} by
- * fetching the appropriate drawable for the icon.
- *
- * @param context Context into which the icon will be drawn
- * @param action RawCustomPlaybackAction that contains info to convert
- * @return the converted CustomPlaybackAction
- */
- public CustomPlaybackAction fetchDrawable(@NonNull Context context,
- @NonNull RawCustomPlaybackAction action) {
- Drawable icon;
- if (action.mPackageName == null) {
- icon = context.getDrawable(action.mIcon);
- } else {
- Resources resources = getResourcesForPackage(context, action.mPackageName);
- if (resources == null) {
- icon = null;
- } else {
- // the resources may be from another package. we need to update the
- // configuration
- // using the context from the activity so we get the drawable from the
- // correct DPI
- // bucket.
- resources.updateConfiguration(context.getResources().getConfiguration(),
- context.getResources().getDisplayMetrics());
- icon = resources.getDrawable(action.mIcon, null);
- }
- }
- return new CustomPlaybackAction(icon, action.mAction, action.mExtras);
- }
-
- /**
* Returns a LiveData that emits the duration of the media item, in milliseconds. The
* current position in this duration can be obtained by calling {@link #getProgress()}.
*/
@@ -380,10 +365,11 @@
}
/**
- * Returns a LiveData that emits {@code true} iff the media source is buffering
+ * Returns a LiveData that emits {@code true} iff the media source is loading (e.g.:
+ * buffering, connecting, etc.)
*/
- public LiveData<Boolean> isBuffering() {
- return mIsBuffering;
+ public LiveData<Boolean> isLoading() {
+ return mIsLoading;
}
/**
@@ -395,8 +381,8 @@
}
/**
- * Returns a LiveData that emits the queue id of the currently playing queue item, or
- * {@link MediaSession.QueueItem#UNKNOWN_ID} if none of the items is currently playing.
+ * Returns a LiveData that emits the queue id of the currently playing queue item, or {@link
+ * MediaSession.QueueItem#UNKNOWN_ID} if none of the items is currently playing.
*/
public LiveData<Long> getActiveQueueItemId() {
return mActiveQueueItemId;
@@ -404,8 +390,9 @@
private List<RawCustomPlaybackAction> getCustomActions(
@Nullable CombinedInfo info) {
- PlaybackState playbackState = info.mPlaybackState;
List<RawCustomPlaybackAction> actions = new ArrayList<>();
+ if (info == null) return actions;
+ PlaybackState playbackState = info.mPlaybackState;
if (playbackState == null) return actions;
RawCustomPlaybackAction ratingAction = getRatingAction(info);
@@ -423,6 +410,7 @@
@Nullable
private RawCustomPlaybackAction getRatingAction(@Nullable CombinedInfo info) {
+ if (info == null) return null;
PlaybackState playbackState = info.mPlaybackState;
if (playbackState == null) return null;
@@ -445,15 +433,6 @@
return new RawCustomPlaybackAction(iconResource, null, ACTION_SET_RATING,
extras);
}
-
- private Resources getResourcesForPackage(Context context, String packageName) {
- try {
- return context.getPackageManager().getResourcesForApplication(packageName);
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to get resources for " + packageName);
- return null;
- }
- }
}
/**
@@ -567,9 +546,9 @@
* {@link PlaybackController} class. Custom actions for the current media source are exposed
* through {@link PlaybackInfo#getCustomActions()}
* <p>
- * Does not contain a {@link Drawable} representation of the icon. Instances of
- * this object should be converted to a {@link CustomPlaybackAction} via {@link
- * PlaybackInfo#fetchDrawable(Context, RawCustomPlaybackAction)} for display.
+ * Does not contain a {@link Drawable} representation of the icon. Instances of this object
+ * should be converted to a {@link CustomPlaybackAction} via {@link
+ * RawCustomPlaybackAction#fetchDrawable(Context)} for display.
*/
public static class RawCustomPlaybackAction {
// TODO (keyboardr): This class (and associtated translation code) will be merged with
@@ -606,6 +585,64 @@
mAction = action;
mExtras = extras;
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RawCustomPlaybackAction that = (RawCustomPlaybackAction) o;
+
+ return mIcon == that.mIcon
+ && Objects.equals(mPackageName, that.mPackageName)
+ && Objects.equals(mAction, that.mAction)
+ && Objects.equals(mExtras, that.mExtras);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIcon, mPackageName, mAction, mExtras);
+ }
+
+ /**
+ * Converts this {@link RawCustomPlaybackAction} into a {@link CustomPlaybackAction} by
+ * fetching the appropriate drawable for the icon.
+ *
+ * @param context Context into which the icon will be drawn
+ * @return the converted CustomPlaybackAction or null if appropriate {@link Resources}
+ * cannot be obtained
+ */
+ @Nullable
+ public CustomPlaybackAction fetchDrawable(@NonNull Context context) {
+ Drawable icon;
+ if (mPackageName == null) {
+ icon = context.getDrawable(mIcon);
+ } else {
+ Resources resources = getResourcesForPackage(context, mPackageName);
+ if (resources == null) {
+ return null;
+ } else {
+ // the resources may be from another package. we need to update the
+ // configuration
+ // using the context from the activity so we get the drawable from the
+ // correct DPI
+ // bucket.
+ resources.updateConfiguration(context.getResources().getConfiguration(),
+ context.getResources().getDisplayMetrics());
+ icon = resources.getDrawable(mIcon, null);
+ }
+ }
+ return new CustomPlaybackAction(icon, mAction, mExtras);
+ }
+
+ private Resources getResourcesForPackage(Context context, String packageName) {
+ try {
+ return context.getPackageManager().getResourcesForApplication(packageName);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to get resources for " + packageName);
+ return null;
+ }
+ }
}
static class CombinedInfo {
diff --git a/car-media-common/src/com/android/car/media/common/playback/ProgressLiveData.java b/car-media-common/src/com/android/car/media/common/playback/ProgressLiveData.java
index ed7385c..1ae61c8 100644
--- a/car-media-common/src/com/android/car/media/common/playback/ProgressLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/playback/ProgressLiveData.java
@@ -56,7 +56,11 @@
private void updateProgress() {
setValue(getProgress());
- mTimerHandler.postDelayed(this::updateProgress, UPDATE_INTERVAL_MS);
+ if (mPlaybackState.getState() != PlaybackState.STATE_PAUSED
+ && mPlaybackState.getState() != PlaybackState.STATE_STOPPED
+ && mPlaybackState.getPlaybackSpeed() != 0) {
+ mTimerHandler.postDelayed(this::updateProgress, UPDATE_INTERVAL_MS);
+ }
}
private long getProgress() {
diff --git a/car-media-common/src/com/android/car/media/common/playback/QueueLiveData.java b/car-media-common/src/com/android/car/media/common/playback/QueueLiveData.java
index 99855a4..ad0d41d 100644
--- a/car-media-common/src/com/android/car/media/common/playback/QueueLiveData.java
+++ b/car-media-common/src/com/android/car/media/common/playback/QueueLiveData.java
@@ -47,6 +47,7 @@
@Override
protected void onActive() {
+ setValue(mMediaController.getQueue());
mMediaController.registerCallback(mCallback);
}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/MediaMetadataLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/MediaMetadataLiveDataTest.java
index bae29bd..802aeb9 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/MediaMetadataLiveDataTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/MediaMetadataLiveDataTest.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.media.MediaMetadata;
import android.media.session.MediaController;
@@ -77,6 +78,15 @@
}
@Test
+ public void testGetValueOnActive() {
+ when(mMediaController.getMetadata()).thenReturn(mMetadata);
+ CaptureObserver<MediaMetadata> observer = new CaptureObserver<>();
+ mMediaMetadataLiveData.observe(mLifecycleOwner, observer);
+
+ assertThat(observer.getObservedValue()).isEqualTo(mMetadata);
+ }
+
+ @Test
public void testDeliversValueToObserver() {
CaptureObserver<MediaMetadata> observer = new CaptureObserver<>();
mMediaMetadataLiveData.observe(mLifecycleOwner, observer);
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackStateLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackStateLiveDataTest.java
index 9a03c01..9537e56 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackStateLiveDataTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackStateLiveDataTest.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.media.session.MediaController;
import android.media.session.PlaybackState;
@@ -77,6 +78,15 @@
}
@Test
+ public void testGetValueOnActive() {
+ when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
+ CaptureObserver<PlaybackState> observer = new CaptureObserver<>();
+ mPlaybackStateLiveData.observe(mLifecycleOwner, observer);
+
+ assertThat(observer.getObservedValue()).isEqualTo(mPlaybackState);
+ }
+
+ @Test
public void testDeliversValueToObserver() {
CaptureObserver<PlaybackState> observer = new CaptureObserver<>();
mPlaybackStateLiveData.observe(mLifecycleOwner, observer);
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/QueueLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/QueueLiveDataTest.java
index 47159ce..bad5073 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/QueueLiveDataTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/QueueLiveDataTest.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import android.media.session.MediaController;
import android.media.session.MediaSession;
@@ -78,6 +79,16 @@
}
@Test
+ public void testGetValueOnActive() {
+ List<MediaSession.QueueItem> queue = Collections.emptyList();
+ when(mMediaController.getQueue()).thenReturn(queue);
+ CaptureObserver<List<MediaSession.QueueItem>> observer = new CaptureObserver<>();
+ mQueueLiveData.observe(mLifecycleOwner, observer);
+
+ assertThat(observer.getObservedValue()).isEqualTo(queue);
+ }
+
+ @Test
public void testDeliversValueToObserver() {
CaptureObserver<List<MediaSession.QueueItem>> observer = new CaptureObserver<>();
mQueueLiveData.observe(mLifecycleOwner, observer);