diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ddf2d0fc4e..4f154ce7f1 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();