From 6ed3e40681656c1a0ce90b3bea0a43feb43be773 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 15 Jun 2022 13:01:33 +0000 Subject: [PATCH] Support setMediaItem(s) in MediaControllerImplLegacy These calls were not implemented so far as they require a mix of initial prepareFrom/playFrom calls and addQueueItem. We can also support clients without queue handling to set single MediaItems. To make the calls consistent and predictable in the session, we need to ensure that none of the play/pause/addQueueItem/ removeQueueItem/prepare/playFromXYZ/prepareFromXYZ are called before the controller is prepared and has media. #minor-release PiperOrigin-RevId: 455110246 (cherry picked from commit b475f1f2daba8e0ed2497cbf17f4b834e58c59a4) --- RELEASENOTES.md | 2 + .../media3/session/MediaController.java | 92 ++--- .../session/MediaControllerImplLegacy.java | 323 ++++++++++-------- .../androidx/media3/session/MediaSession.java | 10 +- .../androidx/media3/session/MediaUtils.java | 4 +- .../media3/session/QueueTimeline.java | 6 +- ...aControllerWithMediaSessionCompatTest.java | 76 ----- ...CompatCallbackWithMediaControllerTest.java | 296 ++-------------- 8 files changed, 252 insertions(+), 557 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 59e3b6653f..959fdba3e1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -179,6 +179,8 @@ of requests. * Forward legacy `MediaController` calls to play media to `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. + * Support `setMediaItems(s)` methods when `MediaController` connects to a + legacy media session. * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 348750b813..624a0faa7a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -81,13 +81,14 @@ import org.checkerframework.checker.initialization.qual.Initialized; *
  • Controller Lifecycle *
  • Threading Model *
  • Package Visibility Filter + *
  • Backward Compatibility with legacy media sessions * * *

    Controller Lifecycle

    * *

    When a controller is created with the {@link SessionToken} for a {@link MediaSession} (i.e. * session token type is {@link SessionToken#TYPE_SESSION}), the controller will connect to the - * specific session. + * specific session.F * *

    When a controller is created with the {@link SessionToken} for a {@link MediaSessionService} * (i.e. session token type is {@link SessionToken#TYPE_SESSION_SERVICE} or {@link @@ -127,6 +128,34 @@ import org.checkerframework.checker.initialization.qual.Initialized; * * * } + * + *

    Backward Compatibility with legacy media sessions

    + * + *

    In addition to {@link MediaSession}, the controller also supports connecting to a legacy media + * session - {@linkplain android.media.session.MediaSession framework session} and {@linkplain + * MediaSessionCompat AndroidX session compat}. + * + *

    To request legacy sessions to play media, use one of the {@link #setMediaItem} methods and set + * either {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri} or {@link + * MediaItem.RequestMetadata#searchQuery}. Once the controller is {@linkplain #prepare() prepared}, + * the controller triggers one of the following callbacks depending on the provided information and + * the value of {@link #getPlayWhenReady()}: + * + *

    + * + * Other playlist change methods, like {@link #addMediaItem} or {@link #removeMediaItem}, trigger + * the {@link MediaSessionCompat.Callback#onAddQueueItem onAddQueueItem} and {@link + * MediaSessionCompat.Callback#onRemoveQueueItem} onRemoveQueueItem} callbacks. Check {@link + * #getAvailableCommands()} to see if playlist modifications are {@linkplain + * androidx.media3.common.Player.Command#COMMAND_CHANGE_MEDIA_ITEMS supported} by the legacy + * session. */ public class MediaController implements Player { @@ -478,13 +507,6 @@ public class MediaController implements Player { return impl.isConnected(); } - /** - * {@inheritDoc} - * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with - * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. - */ @Override public void play() { verifyApplicationThread(); @@ -505,13 +527,6 @@ public class MediaController implements Player { impl.pause(); } - /** - * {@inheritDoc} - * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with - * previously called {@link #setMediaUri}. See {@link #setMediaUri} for details. - */ @Override public void prepare() { verifyApplicationThread(); @@ -980,44 +995,6 @@ public class MediaController implements Player { *

    The {@link Player.Listener#onTimelineChanged} and/or {@link * Player.Listener#onMediaItemTransition} would be called when it's completed. * - *

    Interoperability: When connected to {@link - * android.support.v4.media.session.MediaSessionCompat}, this call will be grouped together with - * later {@link #prepare} or {@link #play}, depending on the uri pattern as follows: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
    Uri patterns and following API calls for MediaControllerCompat methods
    Uri patternsFollowing API callsMethod
    {@code androidx://media3-session/setMediaUri?uri=[uri]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} - *
    {@code androidx://media3-session/setMediaUri?id=[mediaId]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} - *
    {@code androidx://media3-session/setMediaUri?query=[query]}{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} - *
    Does not match with any pattern above{@link #prepare}{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} - *
    {@link #play}{@link MediaControllerCompat.TransportControls#playFromUri playFromUri} - *
    - * *

    Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's * handled together with {@link #prepare} or {@link #play}. If this API is called multiple times * without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for @@ -1027,15 +1004,6 @@ public class MediaController implements Player { * @param extras A {@link Bundle} to send extra information. May be empty. * @return A {@link ListenableFuture} of {@link SessionResult} representing the pending * completion. - * @see MediaConstants#MEDIA_URI_AUTHORITY - * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID - * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID - * @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH - * @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH - * @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI - * @see MediaConstants#MEDIA_URI_QUERY_ID - * @see MediaConstants#MEDIA_URI_QUERY_QUERY - * @see MediaConstants#MEDIA_URI_QUERY_URI */ public ListenableFuture setMediaUri(Uri uri, Bundle extras) { verifyApplicationThread(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index 3855969830..05fb4335cf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -31,16 +31,12 @@ import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_R import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_SET_MEDIA_URI_PREFIX; import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED; import static androidx.media3.session.MediaUtils.POSITION_DIFF_TOLERANCE_MS; import static androidx.media3.session.MediaUtils.calculateBufferedPercentage; -import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static java.lang.Math.max; import static java.lang.Math.min; @@ -60,7 +56,6 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat.QueueItem; import android.support.v4.media.session.PlaybackStateCompat; -import android.text.TextUtils; import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; @@ -112,30 +107,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final long AGGREGATES_CALLBACKS_WITHIN_TIMEOUT_MS = 500L; private static final int VOLUME_FLAGS = AudioManager.FLAG_SHOW_UI; - final Context context; + /* package */ final Context context; + /* package */ final MediaController instance; private final SessionToken token; - - final MediaController instance; - private final ListenerSet listeners; - private final ControllerCompatCallback controllerCompatCallback; @Nullable private MediaControllerCompat controllerCompat; - @Nullable private MediaBrowserCompat browserCompat; - private boolean released; - private boolean connected; - - @Nullable private SetMediaUriRequest pendingSetMediaUriRequest; - private LegacyPlayerInfo legacyPlayerInfo; - private LegacyPlayerInfo pendingLegacyPlayerInfo; - private ControllerInfo controllerInfo; public MediaControllerImplLegacy(Context context, MediaController instance, SessionToken token) { @@ -177,6 +161,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void stop() { + if (controllerInfo.playerInfo.playbackState == STATE_IDLE) { + return; + } PlayerInfo maskedPlayerInfo = controllerInfo.playerInfo.copyWithSessionPositionInfo( createSessionPositionInfo( @@ -244,6 +231,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public void play() { + if (controllerInfo.playerInfo.playWhenReady) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlayWhenReady( @@ -258,36 +248,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (pendingSetMediaUriRequest == null) { + if (isPrepared() && hasMedia()) { controllerCompat.getTransportControls().play(); - } else { - switch (pendingSetMediaUriRequest.type) { - case MEDIA_URI_QUERY_ID: - controllerCompat - .getTransportControls() - .playFromMediaId(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_QUERY: - controllerCompat - .getTransportControls() - .playFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_URI: - controllerCompat - .getTransportControls() - .playFromUri( - Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras); - break; - default: - throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type); - } - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS)); - pendingSetMediaUriRequest = null; } } @Override public void pause() { + if (!controllerInfo.playerInfo.playWhenReady) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlayWhenReady( @@ -302,11 +272,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - controllerCompat.getTransportControls().pause(); + if (isPrepared() && hasMedia()) { + controllerCompat.getTransportControls().pause(); + } } @Override public void prepare() { + if (controllerInfo.playerInfo.playbackState != STATE_IDLE) { + return; + } ControllerInfo maskedControllerInfo = new ControllerInfo( controllerInfo.playerInfo.copyWithPlaybackState( @@ -322,32 +297,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - if (pendingSetMediaUriRequest == null) { - controllerCompat.getTransportControls().prepare(); - } else { - switch (pendingSetMediaUriRequest.type) { - case MEDIA_URI_QUERY_ID: - controllerCompat - .getTransportControls() - .prepareFromMediaId( - pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_QUERY: - controllerCompat - .getTransportControls() - .prepareFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras); - break; - case MEDIA_URI_QUERY_URI: - controllerCompat - .getTransportControls() - .prepareFromUri( - Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras); - break; - default: - throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type); - } - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS)); - pendingSetMediaUriRequest = null; + if (hasMedia()) { + initializeLegacyPlaylist(); } } @@ -655,63 +606,71 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void setMediaItem(MediaItem unusedMediaItem) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem) { + setMediaItem(mediaItem, /* startPositionMs= */ C.TIME_UNSET); } @Override - public void setMediaItem(MediaItem unusedMediaItem, long unusedStartPositionMs) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs); } @Override - public void setMediaItem(MediaItem unusedMediaItem, boolean unusedResetPosition) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + setMediaItem(mediaItem); } @Override - public void setMediaItems(List unusedMediaItems) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems) { + setMediaItems(mediaItems, /* startIndex= */ 0, /* startPositionMs= */ C.TIME_UNSET); } @Override - public void setMediaItems(List unusedMediaItems, boolean unusedResetPosition) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaItems(mediaItems); } @Override - public void setMediaItems( - List unusedMediaItems, int unusedStartIndex, long unusedStartPositionMs) { - Log.w(TAG, "Session doesn't support setting media items"); + public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { + if (mediaItems.isEmpty()) { + clearMediaItems(); + return; + } + QueueTimeline newQueueTimeline = + QueueTimeline.DEFAULT.copyWithNewMediaItems(/* index= */ 0, mediaItems); + if (startPositionMs == C.TIME_UNSET) { + // Assume a default start position of 0 until we know more. + startPositionMs = 0; + } + PlayerInfo maskedPlayerInfo = + controllerInfo.playerInfo.copyWithTimelineAndSessionPositionInfo( + newQueueTimeline, + createSessionPositionInfo( + createPositionInfo(startIndex, mediaItems.get(startIndex), startPositionMs), + /* isPlayingAd= */ false, + /* durationMs= */ C.TIME_UNSET, + /* bufferedPositionMs= */ 0, + /* bufferedPercentage= */ 0, + /* totalBufferedDurationMs= */ 0)); + ControllerInfo maskedControllerInfo = + new ControllerInfo( + maskedPlayerInfo, + controllerInfo.availableSessionCommands, + controllerInfo.availablePlayerCommands, + controllerInfo.customLayout); + updateStateMaskedControllerInfo( + maskedControllerInfo, + /* discontinuityReason= */ null, + /* mediaItemTransitionReason= */ null); + if (isPrepared()) { + initializeLegacyPlaylist(); + } } @Override public ListenableFuture setMediaUri(Uri uri, Bundle extras) { - if (pendingSetMediaUriRequest != null) { - Log.w( - TAG, - "SetMediaUri() is called multiple times without prepare() nor play()." - + " Previous call will be skipped."); - pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_INFO_SKIPPED)); - pendingSetMediaUriRequest = null; - } - SettableFuture result = SettableFuture.create(); - if (uri.toString().startsWith(MEDIA_URI_SET_MEDIA_URI_PREFIX) - && uri.getQueryParameterNames().size() == 1) { - String queryParameterName = uri.getQueryParameterNames().iterator().next(); - if (TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_ID) - || TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_QUERY) - || TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_URI)) { - pendingSetMediaUriRequest = - new SetMediaUriRequest( - queryParameterName, uri.getQueryParameter(queryParameterName), extras, result); - } - } - if (pendingSetMediaUriRequest == null) { - pendingSetMediaUriRequest = - new SetMediaUriRequest(MEDIA_URI_QUERY_URI, uri.toString(), extras, result); - } - return result; + Log.w(TAG, "Session doesn't support setMediaUri"); + return Futures.immediateCancelledFuture(); } @Override @@ -744,9 +703,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (mediaItems.isEmpty()) { return; } - index = min(index, getCurrentTimeline().getWindowCount()); - QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; + if (queueTimeline.isEmpty()) { + // Handle initial items in setMediaItems to ensure initial legacy session commands are called. + setMediaItems(mediaItems); + return; + } + + index = min(index, getCurrentTimeline().getWindowCount()); QueueTimeline newQueueTimeline = queueTimeline.copyWithNewMediaItems(index, mediaItems); int currentMediaItemIndex = getCurrentMediaItemIndex(); int newCurrentMediaItemIndex = @@ -765,10 +729,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - for (int i = 0; i < mediaItems.size(); i++) { - MediaItem mediaItem = mediaItems.get(i); - controllerCompat.addQueueItem( - MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i); + if (isPrepared()) { + for (int i = 0; i < mediaItems.size(); i++) { + MediaItem mediaItem = mediaItems.get(i); + controllerCompat.addQueueItem( + MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i); + } } } @@ -815,8 +781,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) { - controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription()); + if (isPrepared()) { + for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) { + controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription()); + } } } @@ -876,14 +844,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* discontinuityReason= */ null, /* mediaItemTransitionReason= */ null); - ArrayList moveItems = new ArrayList<>(); - for (int i = 0; i < (toIndex - fromIndex); i++) { - moveItems.add(legacyPlayerInfo.queue.get(fromIndex)); - controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription()); - } - for (int i = 0; i < moveItems.size(); i++) { - QueueItem item = moveItems.get(i); - controllerCompat.addQueueItem(item.getDescription(), i + newIndex); + if (isPrepared()) { + ArrayList moveItems = new ArrayList<>(); + for (int i = 0; i < (toIndex - fromIndex); i++) { + moveItems.add(legacyPlayerInfo.queue.get(fromIndex)); + controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription()); + } + for (int i = 0; i < moveItems.size(); i++) { + QueueItem item = moveItems.get(i); + controllerCompat.addQueueItem(item.getDescription(), i + newIndex); + } } } @@ -1294,6 +1264,91 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; }); } + private boolean isPrepared() { + return controllerInfo.playerInfo.playbackState != STATE_IDLE; + } + + private boolean hasMedia() { + return !controllerInfo.playerInfo.timeline.isEmpty(); + } + + private void initializeLegacyPlaylist() { + Window window = new Window(); + checkState(isPrepared() && hasMedia()); + QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline; + // Set the current item first as these calls are expected to replace the current playlist. + int currentIndex = controllerInfo.playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex; + MediaItem currentMediaItem = queueTimeline.getWindow(currentIndex, window).mediaItem; + if (queueTimeline.getQueueId(currentIndex) != QueueItem.UNKNOWN_ID) { + // Current item is already known to the session. Just prepare or play. + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat.getTransportControls().play(); + } else { + controllerCompat.getTransportControls().prepare(); + } + } else if (currentMediaItem.requestMetadata.mediaUri != null) { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromUri( + currentMediaItem.requestMetadata.mediaUri, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromUri( + currentMediaItem.requestMetadata.mediaUri, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } else if (currentMediaItem.requestMetadata.searchQuery != null) { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromSearch( + currentMediaItem.requestMetadata.searchQuery, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromSearch( + currentMediaItem.requestMetadata.searchQuery, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } else { + if (controllerInfo.playerInfo.playWhenReady) { + controllerCompat + .getTransportControls() + .playFromMediaId( + currentMediaItem.mediaId, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } else { + controllerCompat + .getTransportControls() + .prepareFromMediaId( + currentMediaItem.mediaId, + getOrEmptyBundle(currentMediaItem.requestMetadata.extras)); + } + } + // Seek to non-zero start positon if needed. + if (controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs != 0) { + controllerCompat + .getTransportControls() + .seekTo(controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs); + } + // Add all other items to the playlist if supported. + if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) { + for (int i = 0; i < queueTimeline.getWindowCount(); i++) { + if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) { + // Skip the current item (added above) and all items already known to the session. + continue; + } + MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem; + controllerCompat.addQueueItem( + MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i); + } + } + } + private void handleNewLegacyParameters( boolean notifyConnected, LegacyPlayerInfo newLegacyPlayerInfo) { if (released || !connected) { @@ -1938,6 +1993,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return state; } + private static Bundle getOrEmptyBundle(@Nullable Bundle bundle) { + return bundle == null ? Bundle.EMPTY : bundle; + } + private static long getActiveQueueId(@Nullable PlaybackStateCompat playbackStateCompat) { return playbackStateCompat == null ? QueueItem.UNKNOWN_ID @@ -2088,22 +2147,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* contentBufferedPositionMs= */ bufferedPositionMs); } - private static final class SetMediaUriRequest { - - public final String type; - public final String value; - public final Bundle extras; - public final SettableFuture result; - - public SetMediaUriRequest( - String type, String value, Bundle extras, SettableFuture result) { - this.type = type; - this.value = value; - this.extras = extras; - this.result = result; - } - } - // Media 1.0 variables private static final class LegacyPlayerInfo { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 194f074264..71edff1179 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -88,7 +88,7 @@ import java.util.List; *

  • Threading Model *
  • Media Key Events Mapping *
  • Supporting Multiple Sessions - *
  • Backward Compatibility with Legacy Session APIs + *
  • Backward Compatibility with Legacy Session APIs *
  • Backward Compatibility with Legacy Controller APIs * * @@ -201,10 +201,10 @@ import java.util.List; * *

    Backward Compatibility with Legacy Controller APIs

    * - *

    In addition to {@link MediaController}, session also supports connection from the legacy - * controller APIs - {@link android.media.session.MediaController framework controller} and {@link - * MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} may not be - * precise for legacy controllers. See {@link ControllerInfo} for the details. + *

    In addition to {@link MediaController}, the session also supports connections from the legacy + * controller APIs - {@linkplain android.media.session.MediaController framework controller} and + * {@linkplain MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} + * may not be precise for legacy controllers. See {@link ControllerInfo} for the details. * *

    Unknown package name nor UID doesn't mean that you should disallow connection nor commands. * For SDK levels where such issues happen, session tokens could only be obtained by trusted diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index f3459fccca..d85bc1194b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -30,6 +30,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH; @@ -1058,7 +1059,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_GET_MEDIA_ITEMS_METADATA, - COMMAND_GET_CURRENT_MEDIA_ITEM); + COMMAND_GET_CURRENT_MEDIA_ITEM, + COMMAND_SET_MEDIA_ITEM); boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0; if (includePlaylistCommands) { playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS); diff --git a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java index be92deea32..adaf65d707 100644 --- a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java +++ b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java @@ -79,11 +79,11 @@ import java.util.Map; newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); } - public QueueTimeline copyWithNewMediaItems(int addToIndex, List newMediaItems) { + public QueueTimeline copyWithNewMediaItems(int index, List newMediaItems) { ImmutableList.Builder newMediaItemsBuilder = new ImmutableList.Builder<>(); - newMediaItemsBuilder.addAll(mediaItems.subList(0, addToIndex)); + newMediaItemsBuilder.addAll(mediaItems.subList(0, index)); newMediaItemsBuilder.addAll(newMediaItems); - newMediaItemsBuilder.addAll(mediaItems.subList(addToIndex, mediaItems.size())); + newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size())); return new QueueTimeline( newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 882dc54dff..1ea60474e6 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -29,7 +29,6 @@ import static androidx.media3.common.Player.STATE_BUFFERING; import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED; import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED; -import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME; import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE; @@ -37,10 +36,8 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIS import static androidx.media3.test.session.common.CommonConstants.METADATA_DESCRIPTION; import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; -import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.app.PendingIntent; @@ -85,8 +82,6 @@ import com.google.common.util.concurrent.ListenableFuture; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -567,77 +562,6 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(isPlayingAdRef.get()).isTrue(); } - @Test - public void setMediaUri_resultSetAfterPrepare() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri = Uri.parse("androidx://test"); - ListenableFuture future = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY)); - - SessionResult result; - try { - result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertWithMessage("TimeoutException is expected").fail(); - } catch (TimeoutException e) { - // expected. - } - - threadTestRule.getHandler().postAndSync(controller::prepare); - - result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); - } - - @Test - public void setMediaUri_resultSetAfterPlay() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri = Uri.parse("androidx://test"); - ListenableFuture future = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY)); - - SessionResult result; - try { - result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertWithMessage("TimeoutException is expected").fail(); - } catch (TimeoutException e) { - // expected. - } - - threadTestRule.getHandler().postAndSync(controller::play); - - result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); - } - - @Test - public void setMediaUris_multipleCalls_previousCallReturnsResultInfoSkipped() throws Exception { - MediaController controller = controllerTestRule.createController(session.getSessionToken()); - - Uri testUri1 = Uri.parse("androidx://test1"); - Uri testUri2 = Uri.parse("androidx://test2"); - ListenableFuture future1 = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY)); - ListenableFuture future2 = - threadTestRule - .getHandler() - .postAndSync(() -> controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY)); - - threadTestRule.getHandler().postAndSync(controller::prepare); - - SessionResult result1 = future1.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - SessionResult result2 = future2.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - assertThat(result1.resultCode).isEqualTo(RESULT_INFO_SKIPPED); - assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS); - } - @Test public void seekToDefaultPosition_withMediaItemIndex_updatesExpectedMediaItemIndex() throws Exception { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java index 57996844a2..ca25291cf4 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCompatCallbackWithMediaControllerTest.java @@ -16,13 +16,6 @@ package androidx.media3.session; import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; -import static androidx.media3.session.MediaConstants.MEDIA_URI_AUTHORITY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_PATH_SET_MEDIA_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY; -import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI; -import static androidx.media3.session.MediaConstants.MEDIA_URI_SCHEME; -import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.VOLUME_CHANGE_TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -78,6 +71,9 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { // The maximum time to wait for an operation. private static final long TIMEOUT_MS = 3000L; + // Timeout used where the test expects no operation. + private static final long NOOP_TIMEOUT_MS = 500L; + @ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule(); @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); @@ -122,6 +118,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void play() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PAUSED); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -132,6 +133,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void pause() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); @@ -142,20 +148,31 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { @Test public void prepare() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(1); controller.prepare(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); assertThat(sessionCallback.onPrepareCalled).isEqualTo(true); } @Test public void stop() throws Exception { + List testList = MediaTestUtils.createMediaItems(/* size= */ 2); + List testQueue = MediaUtils.convertToQueueItemList(testList); + session.setQueue(testQueue); + session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); RemoteMediaController controller = createControllerAndWaitConnection(); + controller.prepare(); sessionCallback.reset(1); controller.stop(); + assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); assertThat(sessionCallback.onStopCalled).isEqualTo(true); } @@ -314,6 +331,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { session.setQueue(testQueue); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(size); @@ -338,6 +356,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { session.setQueue(MediaUtils.convertToQueueItemList(testList)); session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS); + setPlaybackState(PlaybackStateCompat.STATE_BUFFERING); RemoteMediaController controller = createControllerAndWaitConnection(); sessionCallback.reset(count); @@ -634,269 +653,6 @@ public class MediaSessionCompatCallbackWithMediaControllerTest { assertThat(MediaUtils.convertToRating(sessionCallback.rating)).isEqualTo(rating); } - @Test - public void setMediaUri_ignored() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(Uri.parse("androidx://test?test=xx"), /* extras= */ Bundle.EMPTY); - - assertThat(sessionCallback.await(NO_RESPONSE_TIMEOUT_MS)).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromMediaId() throws Exception { - String testMediaId = "anyMediaId"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue(); - assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromSearch() throws Exception { - String testSearchQuery = "anyQuery"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromSearchCalled).isTrue(); - assertThat(sessionCallback.query).isEqualTo(testSearchQuery); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPrepare_callsPrepareFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) - .build(), - testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_withoutFormattingFollowedByPrepare_callsPrepareFromUri() - throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(testMediaUri, testExtras); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPrepareCalled).isFalse(); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromMediaId() throws Exception { - String testMediaId = "anyMediaId"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue(); - assertThat(sessionCallback.mediaId).isEqualTo(testMediaId); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromSearch() throws Exception { - String testSearchQuery = "anyQuery"; - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromSearchCalled).isTrue(); - assertThat(sessionCallback.query).isEqualTo(testSearchQuery); - assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue(); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.uri).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPlay_callsPlayFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri( - new Uri.Builder() - .scheme(MEDIA_URI_SCHEME) - .authority(MEDIA_URI_AUTHORITY) - .path(MEDIA_URI_PATH_SET_MEDIA_URI) - .appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString()) - .build(), - testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_withoutFormattingFollowedByPlay_callsPlayFromUri() throws Exception { - Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media"); - Bundle testExtras = new Bundle(); - testExtras.putString("testKey", "testValue"); - - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(1); - - controller.setMediaUri(testMediaUri, testExtras); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testMediaUri); - assertThat(sessionCallback.mediaId).isNull(); - assertThat(sessionCallback.query).isNull(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0); - } - - @Test - public void setMediaUri_followedByPrepareTwice_callsPrepareFromUriAndPrepare() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY); - - controller.prepare(); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.onPrepareCalled).isTrue(); - } - - @Test - public void setMediaUri_followedByPlayTwice_callsPlayFromUriAndPlay() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY); - - controller.play(); - controller.play(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue(); - assertThat(sessionCallback.onPlayFromUriCalled).isTrue(); - assertThat(sessionCallback.onPlayCalledCount).isEqualTo(1); - } - - @Test - public void setMediaUri_multipleCalls_skipped() throws Exception { - RemoteMediaController controller = createControllerAndWaitConnection(); - sessionCallback.reset(2); - - Uri testUri1 = Uri.parse("androidx://test1"); - Uri testUri2 = Uri.parse("androidx://test2"); - controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY); - controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY); - controller.prepare(); - - assertThat(sessionCallback.await(TIMEOUT_MS)).isFalse(); - assertThat(sessionCallback.onPrepareFromUriCalled).isTrue(); - assertThat(sessionCallback.uri).isEqualTo(testUri2); - } - @Test public void seekToNext_callsOnSkipToNext() throws Exception { RemoteMediaController controller = createControllerAndWaitConnection();