From bd126ec5c5497615572bcdf259278a899b4574f8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 8 Jun 2022 08:49:40 +0000 Subject: [PATCH] Forward legacy controller onPlay/PrepareFromXY calls to onAddMediaItems These legacy callbacks are currently forwarded to onSetMediaUri which will be removed in the future. Also make sure to only call player.prepare/play after the items have been set. The calls to onAddQueueItem are also forwarded to onAddMediaItems to actually allow a session to resolve these items to playable media, which wasn't possible so far. PiperOrigin-RevId: 453625204 --- RELEASENOTES.md | 2 + .../media3/demo/session/PlaybackService.kt | 32 +-- .../androidx/media3/session/MediaSession.java | 68 ++---- .../session/MediaSessionLegacyStub.java | 210 ++++++++++-------- ...CallbackWithMediaControllerCompatTest.java | 210 ++++++++++++------ 5 files changed, 295 insertions(+), 227 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1c4dbbc843..290a9d4bca 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -143,6 +143,8 @@ * Replace `MediaSession.MediaItemFiler` with `MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution of requests. + * Forward legacy `MediaController` calls to play media to + `MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`. * Data sources: * Rename `DummyDataSource` to `PlaceholderDataSource`. * Workaround OkHttp interrupt handling. diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 25c012275a..cc8291c27d 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -19,7 +19,6 @@ import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import androidx.media3.common.AudioAttributes @@ -182,37 +181,21 @@ class PlaybackService : MediaLibraryService() { return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) } - override fun onSetMediaUri( - session: MediaSession, - controller: ControllerInfo, - uri: Uri, - extras: Bundle - ): Int { - - if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) || - uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT) - ) { - val searchQuery = - uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED - setMediaItemFromSearchQuery(searchQuery) - - return SessionResult.RESULT_SUCCESS - } else { - return SessionResult.RESULT_ERROR_NOT_SUPPORTED - } - } - override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: List ): ListenableFuture> { val updatedMediaItems: List = - mediaItems.map { mediaItem -> MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem } + mediaItems.map { mediaItem -> + if (mediaItem.requestMetadata.searchQuery != null) + getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!) + else MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem + } return Futures.immediateFuture(updatedMediaItems) } - private fun setMediaItemFromSearchQuery(query: String) { + private fun getMediaItemFromSearchQuery(query: String): MediaItem { // Only accept query with pattern "play [Title]" or "[Title]" // Where [Title]: must be exactly matched // If no media with exact name found, play a random media instead @@ -223,8 +206,7 @@ class PlaybackService : MediaLibraryService() { query } - val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() - player.setMediaItem(item) + return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() } } 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 23db7affef..93816b33fd 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -973,48 +973,6 @@ public class MediaSession { *

The implementation should create proper {@link MediaItem media item(s)} for the given * {@code uri} and call {@link Player#setMediaItems}. * - *

When {@link MediaControllerCompat} is connected and sends commands with following methods, - * the {@code uri} will have the following patterns: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Uri patterns corresponding to MediaControllerCompat command methods
MethodUri pattern
{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}The {@code uri} passed as argument
- * {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} - * {@code androidx://media3-session/prepareFromMediaId?id=[mediaId]}
- * {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} - * {@code androidx://media3-session/prepareFromSearch?query=[query]}
{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}The {@code uri} passed as argument
{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}{@code androidx://media3-session/playFromMediaId?id=[mediaId]}
{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}{@code androidx://media3-session/playFromSearch?query=[query]}
- * - *

{@link Player#prepare()} or {@link Player#play()} should follow if this is called by above - * methods. - * * @param session The session for this event. * @param controller The controller information. * @param uri The uri. @@ -1057,7 +1015,8 @@ public class MediaSession { /** * Called when a controller requested to add new {@linkplain MediaItem media items} to the - * playlist. + * playlist via one of the {@code Player.addMediaItem(s)} or {@code Player.setMediaItem(s)} + * methods. * *

Note that the requested {@linkplain MediaItem media items} don't have a {@link * MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them @@ -1066,7 +1025,28 @@ public class MediaSession { * MediaItem#requestMetadata}. * *

Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can - * also return the items directly by using Guava's {@link Futures#immediateFuture(Object)}. + * also return the items directly by using Guava's {@link Futures#immediateFuture(Object)}. Once + * the {@link MediaItem media items} have been resolved, the session will call {@link + * Player#setMediaItems} or {@link Player#addMediaItems} as requested. + * + *

Interoperability: This method will be called in response to the following {@link + * MediaControllerCompat} methods: + * + *

    + *
  • {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri} + *
  • {@link MediaControllerCompat.TransportControls#playFromUri playFromUri} + *
  • {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId} + *
  • {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId} + *
  • {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch} + *
  • {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch} + *
  • {@link MediaControllerCompat.TransportControls#addQueueItem addQueueItem} + *
+ * + * The values of {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri}, {@link + * MediaItem.RequestMetadata#searchQuery} and {@link MediaItem.RequestMetadata#extras} will be + * set to match the legacy method call. The session will call {@link Player#setMediaItems} or + * {@link Player#addMediaItems}, followed by {@link Player#prepare()} and {@link Player#play()} + * as appropriate once the {@link MediaItem} has been resolved. * * @param mediaSession The session for this event. * @param controller The controller information. diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 088aed877b..9dcd3bcfa9 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -82,6 +82,9 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.SessionCommand.CommandCode; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.List; @@ -270,39 +273,25 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId) - .build(); - onPrepareFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest( + mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), + /* play= */ false); } @Override public void onPrepareFromSearch(String query, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_SEARCH) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query) - .build(); - onPrepareFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), + /* play= */ false); } @Override public void onPrepareFromUri(Uri mediaUri, @Nullable Bundle extras) { - dispatchSessionTaskWithSessionCommand( - SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, - controller -> { - if (sessionImpl.onSetMediaUriOnHandler( - controller, mediaUri, extras == null ? Bundle.EMPTY : extras) - == RESULT_SUCCESS) { - sessionImpl.getPlayerWrapper().prepare(); - } - }); + handleMediaRequest( + createMediaItemForMediaRequest( + /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), + /* play= */ false); } @Override @@ -325,47 +314,25 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId) - .build(); - onPlayFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest( + mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), + /* play= */ true); } @Override public void onPlayFromSearch(String query, @Nullable Bundle extras) { - Uri mediaUri = - new Uri.Builder() - .scheme(MediaConstants.MEDIA_URI_SCHEME) - .authority(MediaConstants.MEDIA_URI_AUTHORITY) - .path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_SEARCH) - .appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query) - .build(); - onPlayFromUri(mediaUri, extras); + handleMediaRequest( + createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), + /* play= */ true); } @Override public void onPlayFromUri(Uri mediaUri, @Nullable Bundle extras) { - dispatchSessionTaskWithSessionCommand( - SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, - controller -> { - if (sessionImpl.onSetMediaUriOnHandler( - controller, mediaUri, extras == null ? Bundle.EMPTY : extras) - == RESULT_SUCCESS) { - PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); - @Player.State int playbackState = playerWrapper.getPlaybackState(); - if (playbackState == Player.STATE_IDLE) { - playerWrapper.prepare(); - } else if (playbackState == STATE_ENDED) { - playerWrapper.seekTo( - playerWrapper.getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET); - } - playerWrapper.play(); - } - }); + handleMediaRequest( + createMediaItemForMediaRequest( + /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), + /* play= */ true); } @Override @@ -498,40 +465,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; @Override public void onAddQueueItem(@Nullable MediaDescriptionCompat description) { - if (description == null) { - return; - } - dispatchSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, - controller -> { - @Nullable String mediaId = description.getMediaId(); - if (TextUtils.isEmpty(mediaId)) { - Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); - return; - } - MediaItem mediaItem = MediaUtils.convertToMediaItem(description); - sessionImpl.getPlayerWrapper().addMediaItem(mediaItem); - }, - sessionCompat.getCurrentControllerInfo()); + handleOnAddQueueItem(description, /* index= */ C.INDEX_UNSET); } @Override public void onAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { - if (description == null) { - return; - } - dispatchSessionTaskWithPlayerCommand( - COMMAND_CHANGE_MEDIA_ITEMS, - controller -> { - @Nullable String mediaId = description.getMediaId(); - if (TextUtils.isEmpty(mediaId)) { - Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); - return; - } - MediaItem mediaItem = MediaUtils.convertToMediaItem(description); - sessionImpl.getPlayerWrapper().addMediaItem(index, mediaItem); - }, - sessionCompat.getCurrentControllerInfo()); + handleOnAddQueueItem(description, index); } @Override @@ -738,6 +677,85 @@ import org.checkerframework.checker.initialization.qual.Initialized; connectionTimeoutMs = timeoutMs; } + private void handleMediaRequest(MediaItem mediaItem, boolean play) { + dispatchSessionTaskWithPlayerCommand( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> { + ListenableFuture> mediaItemsFuture = + sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); + Futures.addCallback( + mediaItemsFuture, + new FutureCallback>() { + @Override + public void onSuccess(List mediaItems) { + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + Player player = sessionImpl.getPlayerWrapper(); + player.setMediaItems(mediaItems); + @Player.State int playbackState = player.getPlaybackState(); + if (playbackState == Player.STATE_IDLE) { + player.prepare(); + } else if (playbackState == Player.STATE_ENDED) { + player.seekTo(/* positionMs= */ C.TIME_UNSET); + } + if (play) { + player.play(); + } + }); + } + + @Override + public void onFailure(Throwable t) { + // Do nothing, the session is free to ignore these requests. + } + }, + MoreExecutors.directExecutor()); + }, + sessionCompat.getCurrentControllerInfo()); + } + + private void handleOnAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { + if (description == null) { + return; + } + dispatchSessionTaskWithPlayerCommand( + COMMAND_CHANGE_MEDIA_ITEMS, + controller -> { + @Nullable String mediaId = description.getMediaId(); + if (TextUtils.isEmpty(mediaId)) { + Log.w(TAG, "onAddQueueItem(): Media ID shouldn't be empty"); + return; + } + MediaItem mediaItem = MediaUtils.convertToMediaItem(description); + ListenableFuture> mediaItemsFuture = + sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)); + Futures.addCallback( + mediaItemsFuture, + new FutureCallback>() { + @Override + public void onSuccess(List mediaItems) { + postOrRun( + sessionImpl.getApplicationHandler(), + () -> { + if (index == C.INDEX_UNSET) { + sessionImpl.getPlayerWrapper().addMediaItems(mediaItems); + } else { + sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems); + } + }); + } + + @Override + public void onFailure(Throwable t) { + // Do nothing, the session is free to ignore these requests. + } + }, + MoreExecutors.directExecutor()); + }, + sessionCompat.getCurrentControllerInfo()); + } + private static void sendCustomCommandResultWhenReady( ResultReceiver receiver, ListenableFuture future) { future.addListener( @@ -776,6 +794,22 @@ import org.checkerframework.checker.initialization.qual.Initialized; sessionCompat.setQueueTitle(title); } + private static MediaItem createMediaItemForMediaRequest( + @Nullable String mediaId, + @Nullable Uri mediaUri, + @Nullable String searchQuery, + @Nullable Bundle extras) { + return new MediaItem.Builder() + .setMediaId(mediaId == null ? MediaItem.DEFAULT_MEDIA_ID : mediaId) + .setRequestMetadata( + new MediaItem.RequestMetadata.Builder() + .setMediaUri(mediaUri) + .setSearchQuery(searchQuery) + .setExtras(extras) + .build()) + .build(); + } + /* @FunctionalInterface */ private interface SessionTask { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index fd7bd48720..6fd12bc37e 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -56,11 +56,16 @@ import androidx.media3.test.session.common.TestUtils; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -75,6 +80,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { private static final String TAG = "MSCallbackWithMCCTest"; + private static final String TEST_URI = "http://test.test"; private static final String EXPECTED_CONTROLLER_PACKAGE_NAME = (Util.SDK_INT < 21 || Util.SDK_INT >= 24) ? SUPPORT_APP_PACKAGE_NAME : LEGACY_CONTROLLER; @@ -88,6 +94,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { private RemoteMediaControllerCompat controller; private MockPlayer player; private AudioManager audioManager; + private ListeningExecutorService executorService; @Before public void setUp() { @@ -95,6 +102,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { handler = threadTestRule.getHandler(); player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + // Intentionally use an Executor with another thread to test asynchronous workflows involving + // background tasks. + executorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); } @After @@ -107,6 +117,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.cleanUp(); controller = null; } + executorService.shutdownNow(); } @Test @@ -294,10 +305,22 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { @Test public void addQueueItem() throws Exception { + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); + } + }; session = new MediaSession.Builder(context, player) .setId("addQueueItem") - .setCallback(new TestSessionCallback()) + .setCallback(callback) .build(); controller = new RemoteMediaControllerCompat( @@ -305,49 +328,73 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { handler.postAndSync( () -> { - player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); + player.setMediaItems(mediaItems); + player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); // Prepare an item to add. String mediaId = "newMediaItemId"; - MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + Uri mediaUri = Uri.parse("https://test.test"); + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); controller.addQueueItem(desc); - player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); - assertThat(player.mediaItems).hasSize(1); - assertThat(player.mediaItems.get(0).mediaId).isEqualTo(mediaId); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); + assertThat(player.mediaItems).hasSize(11); + assertThat(player.mediaItems.get(10)).isEqualTo(resolvedMediaItem); } @Test public void addQueueItemWithIndex() throws Exception { + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); + } + }; session = new MediaSession.Builder(context, player) .setId("addQueueItemWithIndex") - .setCallback(new TestSessionCallback()) + .setCallback(callback) .build(); controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); handler.postAndSync( () -> { + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10); player.setMediaItems(mediaItems); - player.timeline = new PlaylistTimeline(mediaItems); + player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); // Prepare an item to add. int testIndex = 1; String mediaId = "media_id"; - MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).build(); + Uri mediaUri = Uri.parse("https://test.test"); + MediaDescriptionCompat desc = + new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); controller.addQueueItem(desc, testIndex); - player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); assertThat(player.index).isEqualTo(testIndex); assertThat(player.mediaItems).hasSize(11); - assertThat(player.mediaItems.get(1).mediaId).isEqualTo(mediaId); + assertThat(player.mediaItems.get(1)).isEqualTo(resolvedMediaItem); } @Test @@ -788,16 +835,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { Uri mediaUri = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri).isEqualTo(mediaUri); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -811,8 +858,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromUri(mediaUri, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -820,16 +871,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { Uri request = Uri.parse("foo://bar"); Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri).isEqualTo(request); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -843,8 +894,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromUri(request, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(request); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -852,17 +908,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String request = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/prepareFromMediaId?id=" + request); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -876,8 +931,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromMediaId(request, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(request); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -885,17 +944,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String mediaId = "media_id"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/playFromMediaId?id=" + mediaId); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -909,8 +967,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromMediaId(mediaId, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -918,17 +981,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/prepareFromSearch?query=" + query); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -942,8 +1004,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().prepareFromSearch(query, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.searchQuery).isEqualTo(query); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test @@ -951,17 +1017,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { String query = "test_query"; Bundle bundle = new Bundle(); bundle.putString("key", "value"); - CountDownLatch latch = new CountDownLatch(1); + AtomicReference> requestedMediaItems = new AtomicReference<>(); + MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI); MediaSession.Callback callback = - new TestSessionCallback() { + new MediaSession.Callback() { @Override - public int onSetMediaUri( - MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { - assertThat(uri.toString()) - .isEqualTo("androidx://media3-session/playFromSearch?query=" + query); - assertThat(TestUtils.equals(bundle, extras)).isTrue(); - latch.countDown(); - return RESULT_SUCCESS; + public ListenableFuture> onAddMediaItems( + MediaSession mediaSession, ControllerInfo controller, List mediaItems) { + requestedMediaItems.set(mediaItems); + // Resolve MediaItem asynchronously to test correct threading logic. + return executorService.submit(() -> ImmutableList.of(resolvedMediaItem)); } }; session = @@ -975,8 +1040,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller.getTransportControls().playFromSearch(query, bundle); - assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); + assertThat(requestedMediaItems.get().get(0).requestMetadata.searchQuery).isEqualTo(query); + TestUtils.equals(requestedMediaItems.get().get(0).requestMetadata.extras, bundle); + assertThat(player.mediaItems).containsExactly(resolvedMediaItem); } @Test