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
This commit is contained in:
tonihei 2022-06-08 08:49:40 +00:00 committed by Marc Baechinger
parent 4a6f431f01
commit bd126ec5c5
5 changed files with 295 additions and 227 deletions

View File

@ -143,6 +143,8 @@
* Replace `MediaSession.MediaItemFiler` with * Replace `MediaSession.MediaItemFiler` with
`MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution `MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution
of requests. of requests.
* Forward legacy `MediaController` calls to play media to
`MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`.
* Data sources: * Data sources:
* Rename `DummyDataSource` to `PlaceholderDataSource`. * Rename `DummyDataSource` to `PlaceholderDataSource`.
* Workaround OkHttp interrupt handling. * Workaround OkHttp interrupt handling.

View File

@ -19,7 +19,6 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
@ -182,37 +181,21 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) 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( override fun onAddMediaItems(
mediaSession: MediaSession, mediaSession: MediaSession,
controller: MediaSession.ControllerInfo, controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem> mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> { ): ListenableFuture<List<MediaItem>> {
val updatedMediaItems: List<MediaItem> = val updatedMediaItems: List<MediaItem> =
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) return Futures.immediateFuture(updatedMediaItems)
} }
private fun setMediaItemFromSearchQuery(query: String) { private fun getMediaItemFromSearchQuery(query: String): MediaItem {
// Only accept query with pattern "play [Title]" or "[Title]" // Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched // Where [Title]: must be exactly matched
// If no media with exact name found, play a random media instead // If no media with exact name found, play a random media instead
@ -223,8 +206,7 @@ class PlaybackService : MediaLibraryService() {
query query
} }
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
player.setMediaItem(item)
} }
} }

View File

@ -973,48 +973,6 @@ public class MediaSession {
* <p>The implementation should create proper {@link MediaItem media item(s)} for the given * <p>The implementation should create proper {@link MediaItem media item(s)} for the given
* {@code uri} and call {@link Player#setMediaItems}. * {@code uri} and call {@link Player#setMediaItems}.
* *
* <p>When {@link MediaControllerCompat} is connected and sends commands with following methods,
* the {@code uri} will have the following patterns:
*
* <table>
* <caption>Uri patterns corresponding to MediaControllerCompat command methods</caption>
* <tr>
* <th>Method</th>
* <th>Uri pattern</th>
* </tr>
* <tr>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}</td>
* <td>The {@code uri} passed as argument</td>
* </tr>
* <tr>
* <td>
* {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* </td>
* <td>{@code androidx://media3-session/prepareFromMediaId?id=[mediaId]}</td>
* </tr>
* <tr>
* <td>
* {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* </td>
* <td>{@code androidx://media3-session/prepareFromSearch?query=[query]}</td>
* </tr>
* <tr>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}</td>
* <td>The {@code uri} passed as argument</td>
* </tr>
* <tr>
* <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}</td>
* <td>{@code androidx://media3-session/playFromMediaId?id=[mediaId]}</td>
* </tr>
* <tr>
* <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}</td>
* <td>{@code androidx://media3-session/playFromSearch?query=[query]}</td>
* </tr>
* </table>
*
* <p>{@link Player#prepare()} or {@link Player#play()} should follow if this is called by above
* methods.
*
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The controller information.
* @param uri The uri. * @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 * 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.
* *
* <p>Note that the requested {@linkplain MediaItem media items} don't have a {@link * <p>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 * MediaItem.LocalConfiguration} (for example, a URI) and need to be updated to make them
@ -1066,7 +1025,28 @@ public class MediaSession {
* MediaItem#requestMetadata}. * MediaItem#requestMetadata}.
* *
* <p>Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can * <p>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.
*
* <p>Interoperability: This method will be called in response to the following {@link
* MediaControllerCompat} methods:
*
* <ul>
* <li>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* <li>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* <li>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* <li>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* <li>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* <li>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* <li>{@link MediaControllerCompat.TransportControls#addQueueItem addQueueItem}
* </ul>
*
* 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 mediaSession The session for this event.
* @param controller The controller information. * @param controller The controller information.

View File

@ -82,6 +82,9 @@ import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.SessionCommand.CommandCode; 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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.util.List; import java.util.List;
@ -270,39 +273,25 @@ import org.checkerframework.checker.initialization.qual.Initialized;
@Override @Override
public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) {
Uri mediaUri = handleMediaRequest(
new Uri.Builder() createMediaItemForMediaRequest(
.scheme(MediaConstants.MEDIA_URI_SCHEME) mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras),
.authority(MediaConstants.MEDIA_URI_AUTHORITY) /* play= */ false);
.path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID)
.appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId)
.build();
onPrepareFromUri(mediaUri, extras);
} }
@Override @Override
public void onPrepareFromSearch(String query, @Nullable Bundle extras) { public void onPrepareFromSearch(String query, @Nullable Bundle extras) {
Uri mediaUri = handleMediaRequest(
new Uri.Builder() createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras),
.scheme(MediaConstants.MEDIA_URI_SCHEME) /* play= */ false);
.authority(MediaConstants.MEDIA_URI_AUTHORITY)
.path(MediaConstants.MEDIA_URI_PATH_PREPARE_FROM_SEARCH)
.appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query)
.build();
onPrepareFromUri(mediaUri, extras);
} }
@Override @Override
public void onPrepareFromUri(Uri mediaUri, @Nullable Bundle extras) { public void onPrepareFromUri(Uri mediaUri, @Nullable Bundle extras) {
dispatchSessionTaskWithSessionCommand( handleMediaRequest(
SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, createMediaItemForMediaRequest(
controller -> { /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras),
if (sessionImpl.onSetMediaUriOnHandler( /* play= */ false);
controller, mediaUri, extras == null ? Bundle.EMPTY : extras)
== RESULT_SUCCESS) {
sessionImpl.getPlayerWrapper().prepare();
}
});
} }
@Override @Override
@ -325,47 +314,25 @@ import org.checkerframework.checker.initialization.qual.Initialized;
@Override @Override
public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) {
Uri mediaUri = handleMediaRequest(
new Uri.Builder() createMediaItemForMediaRequest(
.scheme(MediaConstants.MEDIA_URI_SCHEME) mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras),
.authority(MediaConstants.MEDIA_URI_AUTHORITY) /* play= */ true);
.path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID)
.appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_ID, mediaId)
.build();
onPlayFromUri(mediaUri, extras);
} }
@Override @Override
public void onPlayFromSearch(String query, @Nullable Bundle extras) { public void onPlayFromSearch(String query, @Nullable Bundle extras) {
Uri mediaUri = handleMediaRequest(
new Uri.Builder() createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras),
.scheme(MediaConstants.MEDIA_URI_SCHEME) /* play= */ true);
.authority(MediaConstants.MEDIA_URI_AUTHORITY)
.path(MediaConstants.MEDIA_URI_PATH_PLAY_FROM_SEARCH)
.appendQueryParameter(MediaConstants.MEDIA_URI_QUERY_QUERY, query)
.build();
onPlayFromUri(mediaUri, extras);
} }
@Override @Override
public void onPlayFromUri(Uri mediaUri, @Nullable Bundle extras) { public void onPlayFromUri(Uri mediaUri, @Nullable Bundle extras) {
dispatchSessionTaskWithSessionCommand( handleMediaRequest(
SessionCommand.COMMAND_CODE_SESSION_SET_MEDIA_URI, createMediaItemForMediaRequest(
controller -> { /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras),
if (sessionImpl.onSetMediaUriOnHandler( /* play= */ true);
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();
}
});
} }
@Override @Override
@ -498,40 +465,12 @@ import org.checkerframework.checker.initialization.qual.Initialized;
@Override @Override
public void onAddQueueItem(@Nullable MediaDescriptionCompat description) { public void onAddQueueItem(@Nullable MediaDescriptionCompat description) {
if (description == null) { handleOnAddQueueItem(description, /* index= */ C.INDEX_UNSET);
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());
} }
@Override @Override
public void onAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { public void onAddQueueItem(@Nullable MediaDescriptionCompat description, int index) {
if (description == null) { handleOnAddQueueItem(description, index);
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());
} }
@Override @Override
@ -738,6 +677,85 @@ import org.checkerframework.checker.initialization.qual.Initialized;
connectionTimeoutMs = timeoutMs; connectionTimeoutMs = timeoutMs;
} }
private void handleMediaRequest(MediaItem mediaItem, boolean play) {
dispatchSessionTaskWithPlayerCommand(
COMMAND_CHANGE_MEDIA_ITEMS,
controller -> {
ListenableFuture<List<MediaItem>> mediaItemsFuture =
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem));
Futures.addCallback(
mediaItemsFuture,
new FutureCallback<List<MediaItem>>() {
@Override
public void onSuccess(List<MediaItem> 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<List<MediaItem>> mediaItemsFuture =
sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem));
Futures.addCallback(
mediaItemsFuture,
new FutureCallback<List<MediaItem>>() {
@Override
public void onSuccess(List<MediaItem> 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( private static void sendCustomCommandResultWhenReady(
ResultReceiver receiver, ListenableFuture<SessionResult> future) { ResultReceiver receiver, ListenableFuture<SessionResult> future) {
future.addListener( future.addListener(
@ -776,6 +794,22 @@ import org.checkerframework.checker.initialization.qual.Initialized;
sessionCompat.setQueueTitle(title); 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 */ /* @FunctionalInterface */
private interface SessionTask { private interface SessionTask {

View File

@ -56,11 +56,16 @@ import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
@ -75,6 +80,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
private static final String TAG = "MSCallbackWithMCCTest"; private static final String TAG = "MSCallbackWithMCCTest";
private static final String TEST_URI = "http://test.test";
private static final String EXPECTED_CONTROLLER_PACKAGE_NAME = private static final String EXPECTED_CONTROLLER_PACKAGE_NAME =
(Util.SDK_INT < 21 || Util.SDK_INT >= 24) ? SUPPORT_APP_PACKAGE_NAME : LEGACY_CONTROLLER; (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 RemoteMediaControllerCompat controller;
private MockPlayer player; private MockPlayer player;
private AudioManager audioManager; private AudioManager audioManager;
private ListeningExecutorService executorService;
@Before @Before
public void setUp() { public void setUp() {
@ -95,6 +102,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
handler = threadTestRule.getHandler(); handler = threadTestRule.getHandler();
player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build(); player = new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 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 @After
@ -107,6 +117,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.cleanUp(); controller.cleanUp();
controller = null; controller = null;
} }
executorService.shutdownNow();
} }
@Test @Test
@ -294,10 +305,22 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
@Test @Test
public void addQueueItem() throws Exception { public void addQueueItem() throws Exception {
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItem asynchronously to test correct threading logic.
return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
}
};
session = session =
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player)
.setId("addQueueItem") .setId("addQueueItem")
.setCallback(new TestSessionCallback()) .setCallback(callback)
.build(); .build();
controller = controller =
new RemoteMediaControllerCompat( new RemoteMediaControllerCompat(
@ -305,49 +328,73 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
handler.postAndSync( handler.postAndSync(
() -> { () -> {
player.timeline = MediaTestUtils.createTimeline(/* windowCount= */ 10); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10);
player.setMediaItems(mediaItems);
player.timeline = MediaTestUtils.createTimeline(mediaItems);
player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}); });
// Prepare an item to add. // Prepare an item to add.
String mediaId = "newMediaItemId"; 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); controller.addQueueItem(desc);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).hasSize(1); assertThat(requestedMediaItems.get()).hasSize(1);
assertThat(player.mediaItems.get(0).mediaId).isEqualTo(mediaId); 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 @Test
public void addQueueItemWithIndex() throws Exception { public void addQueueItemWithIndex() throws Exception {
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItem asynchronously to test correct threading logic.
return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
}
};
session = session =
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player)
.setId("addQueueItemWithIndex") .setId("addQueueItemWithIndex")
.setCallback(new TestSessionCallback()) .setCallback(callback)
.build(); .build();
controller = controller =
new RemoteMediaControllerCompat( new RemoteMediaControllerCompat(
context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true);
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10);
handler.postAndSync( handler.postAndSync(
() -> { () -> {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 10);
player.setMediaItems(mediaItems); player.setMediaItems(mediaItems);
player.timeline = new PlaylistTimeline(mediaItems); player.timeline = MediaTestUtils.createTimeline(mediaItems);
player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED);
}); });
// Prepare an item to add. // Prepare an item to add.
int testIndex = 1; int testIndex = 1;
String mediaId = "media_id"; 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); 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.index).isEqualTo(testIndex);
assertThat(player.mediaItems).hasSize(11); assertThat(player.mediaItems).hasSize(11);
assertThat(player.mediaItems.get(1).mediaId).isEqualTo(mediaId); assertThat(player.mediaItems.get(1)).isEqualTo(resolvedMediaItem);
} }
@Test @Test
@ -788,16 +835,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
Uri mediaUri = Uri.parse("foo://bar"); Uri mediaUri = Uri.parse("foo://bar");
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri).isEqualTo(mediaUri); requestedMediaItems.set(mediaItems);
assertThat(TestUtils.equals(bundle, extras)).isTrue(); // Resolve MediaItem asynchronously to test correct threading logic.
latch.countDown(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -811,8 +858,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().prepareFromUri(mediaUri, bundle); 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); 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 @Test
@ -820,16 +871,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
Uri request = Uri.parse("foo://bar"); Uri request = Uri.parse("foo://bar");
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri).isEqualTo(request); requestedMediaItems.set(mediaItems);
assertThat(TestUtils.equals(bundle, extras)).isTrue(); // Resolve MediaItem asynchronously to test correct threading logic.
latch.countDown(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -843,8 +894,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().playFromUri(request, bundle); 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); 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 @Test
@ -852,17 +908,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
String request = "media_id"; String request = "media_id";
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri.toString()) requestedMediaItems.set(mediaItems);
.isEqualTo("androidx://media3-session/prepareFromMediaId?id=" + request); // Resolve MediaItem asynchronously to test correct threading logic.
assertThat(TestUtils.equals(bundle, extras)).isTrue(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
latch.countDown();
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -876,8 +931,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().prepareFromMediaId(request, bundle); 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); 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 @Test
@ -885,17 +944,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
String mediaId = "media_id"; String mediaId = "media_id";
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri.toString()) requestedMediaItems.set(mediaItems);
.isEqualTo("androidx://media3-session/playFromMediaId?id=" + mediaId); // Resolve MediaItem asynchronously to test correct threading logic.
assertThat(TestUtils.equals(bundle, extras)).isTrue(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
latch.countDown();
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -909,8 +967,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().playFromMediaId(mediaId, bundle); 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); 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 @Test
@ -918,17 +981,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
String query = "test_query"; String query = "test_query";
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri.toString()) requestedMediaItems.set(mediaItems);
.isEqualTo("androidx://media3-session/prepareFromSearch?query=" + query); // Resolve MediaItem asynchronously to test correct threading logic.
assertThat(TestUtils.equals(bundle, extras)).isTrue(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
latch.countDown();
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -942,8 +1004,12 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().prepareFromSearch(query, bundle); 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); 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 @Test
@ -951,17 +1017,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
String query = "test_query"; String query = "test_query";
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("key", "value"); bundle.putString("key", "value");
CountDownLatch latch = new CountDownLatch(1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaItem resolvedMediaItem = MediaItem.fromUri(TEST_URI);
MediaSession.Callback callback = MediaSession.Callback callback =
new TestSessionCallback() { new MediaSession.Callback() {
@Override @Override
public int onSetMediaUri( public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, ControllerInfo controller, Uri uri, Bundle extras) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
assertThat(uri.toString()) requestedMediaItems.set(mediaItems);
.isEqualTo("androidx://media3-session/playFromSearch?query=" + query); // Resolve MediaItem asynchronously to test correct threading logic.
assertThat(TestUtils.equals(bundle, extras)).isTrue(); return executorService.submit(() -> ImmutableList.of(resolvedMediaItem));
latch.countDown();
return RESULT_SUCCESS;
} }
}; };
session = session =
@ -975,8 +1040,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
controller.getTransportControls().playFromSearch(query, bundle); 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); 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 @Test