Replace MediaItemFiller by asynchronous callback.

The MediaItemFiller is not flexible enough for most realworld usages
because:
 - it doesn't allow asynchronous resolution of MediaItems (e.g. to
   look up URIs from a database)
 - it doesn't allow to batch updates for multiple items or do more
   advanced customizations (e.g. expanding a mediaId representing
   a playlist to multiple items).

Both issues can be solved by passing in a list of items and
returning a ListenableFuture. The callback itself can also move
into MediaSession.Callback for consistency with the other
callbacks.

PiperOrigin-RevId: 451857319
This commit is contained in:
tonihei 2022-05-30 12:31:46 +00:00 committed by Marc Baechinger
parent 342be88d81
commit 6b782d1011
9 changed files with 399 additions and 293 deletions

View File

@ -129,6 +129,9 @@
`MediaLibrarySession.MediaLibrarySessionCallback` to `MediaLibrarySession.MediaLibrarySessionCallback` to
`MediaLibrarySession.Callback` and `MediaLibrarySession.Callback` and
`MediaSession.Builder.setSessionCallback` to `setCallback`. `MediaSession.Builder.setSessionCallback` to `setCallback`.
* Replace `MediaSession.MediaItemFiler` with
`MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution
of requests.
* Data sources: * Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`. * Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Workaround OkHttp interrupt handling. * Workaround OkHttp interrupt handling.

View File

@ -202,6 +202,16 @@ class PlaybackService : MediaLibraryService() {
} }
} }
override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems: List<MediaItem> =
mediaItems.map { mediaItem -> MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem }
return Futures.immediateFuture(updatedMediaItems)
}
private fun setMediaItemFromSearchQuery(query: String) { private fun setMediaItemFromSearchQuery(query: String) {
// 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
@ -236,7 +246,6 @@ class PlaybackService : MediaLibraryService() {
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback) MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(sessionActivityPendingIntent) .setSessionActivity(sessionActivityPendingIntent)
.build() .build()
if (!customLayout.isEmpty()) { if (!customLayout.isEmpty()) {
@ -262,14 +271,4 @@ class PlaybackService : MediaLibraryService() {
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) { private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */ /* Do nothing. */
} }
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
override fun fillInLocalConfiguration(
session: MediaSession,
controller: ControllerInfo,
mediaItem: MediaItem
): MediaItem {
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
}
} }

View File

@ -406,17 +406,6 @@ public abstract class MediaLibraryService extends MediaSessionService {
return super.setId(id); return super.setId(id);
} }
/**
* Sets the logic used to fill in the fields of a {@link MediaItem}.
*
* @param mediaItemFiller The filler.
* @return The builder to allow chaining.
*/
@Override
public Builder setMediaItemFiller(MediaItemFiller mediaItemFiller) {
return super.setMediaItemFiller(mediaItemFiller);
}
/** /**
* Sets an extra {@link Bundle} for the {@link MediaLibrarySession}. The {@link * Sets an extra {@link Bundle} for the {@link MediaLibrarySession}. The {@link
* MediaLibrarySession#getToken()} session token} will have the {@link * MediaLibrarySession#getToken()} session token} will have the {@link
@ -439,8 +428,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/ */
@Override @Override
public MediaLibrarySession build() { public MediaLibrarySession build() {
return new MediaLibrarySession( return new MediaLibrarySession(context, id, player, sessionActivity, callback, extras);
context, id, player, sessionActivity, callback, mediaItemFiller, extras);
} }
} }
@ -450,9 +438,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
MediaSession.Callback callback, MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
super(context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras); super(context, id, player, sessionActivity, callback, tokenExtras);
} }
@Override @Override
@ -462,17 +449,9 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
MediaSession.Callback callback, MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
return new MediaLibrarySessionImpl( return new MediaLibrarySessionImpl(
this, this, context, id, player, sessionActivity, (Callback) callback, tokenExtras);
context,
id,
player,
sessionActivity,
(Callback) callback,
mediaItemFiller,
tokenExtras);
} }
@Override @Override

View File

@ -63,9 +63,8 @@ import java.util.concurrent.Future;
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
MediaLibrarySession.Callback callback, MediaLibrarySession.Callback callback,
MediaSession.MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
super(instance, context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras); super(instance, context, id, player, sessionActivity, callback, tokenExtras);
this.instance = instance; this.instance = instance;
this.callback = callback; this.callback = callback;
subscriptions = new ArrayMap<>(); subscriptions = new ArrayMap<>();

View File

@ -293,18 +293,6 @@ public class MediaSession {
return super.setCallback(callback); return super.setCallback(callback);
} }
/**
* Sets the logic used to fill in the fields of a {@link MediaItem} from {@link
* MediaController}.
*
* @param mediaItemFiller The filler.
* @return The builder to allow chaining.
*/
@Override
public Builder setMediaItemFiller(MediaItemFiller mediaItemFiller) {
return super.setMediaItemFiller(mediaItemFiller);
}
/** /**
* Sets an extra {@link Bundle} for the {@link MediaSession}. The {@link * Sets an extra {@link Bundle} for the {@link MediaSession}. The {@link
* MediaSession#getToken()} session token} will have the {@link SessionToken#getExtras() * MediaSession#getToken()} session token} will have the {@link SessionToken#getExtras()
@ -327,8 +315,7 @@ public class MediaSession {
*/ */
@Override @Override
public MediaSession build() { public MediaSession build() {
return new MediaSession( return new MediaSession(context, id, player, sessionActivity, callback, extras);
context, id, player, sessionActivity, callback, mediaItemFiller, extras);
} }
} }
@ -484,7 +471,6 @@ public class MediaSession {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
Callback callback, Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
synchronized (STATIC_LOCK) { synchronized (STATIC_LOCK) {
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) { if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
@ -492,7 +478,7 @@ public class MediaSession {
} }
SESSION_ID_TO_SESSION_MAP.put(id, this); SESSION_ID_TO_SESSION_MAP.put(id, this);
} }
impl = createImpl(context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras); impl = createImpl(context, id, player, sessionActivity, callback, tokenExtras);
} }
/* package */ MediaSessionImpl createImpl( /* package */ MediaSessionImpl createImpl(
@ -501,10 +487,8 @@ public class MediaSession {
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
Callback callback, Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
return new MediaSessionImpl( return new MediaSessionImpl(this, context, id, player, sessionActivity, callback, tokenExtras);
this, context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
} }
/* package */ MediaSessionImpl getImpl() { /* package */ MediaSessionImpl getImpl() {
@ -1041,23 +1025,29 @@ public class MediaSession {
Bundle args) { Bundle args) {
return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED)); return Futures.immediateFuture(new SessionResult(RESULT_ERROR_NOT_SUPPORTED));
} }
}
/** An object which fills in the fields of a {@link MediaItem} from {@link MediaController}. */
public interface MediaItemFiller {
/** /**
* Called to fill in the {@link MediaItem#localConfiguration} of the media item from * Called when a controller requested to add new {@linkplain MediaItem media items} to the
* controllers. * playlist.
* *
* @param session The session for this event. * <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
* playable by the underlying {@link Player}. Typically, this implementation should be able to
* identify the correct item by its {@link MediaItem#mediaId} and/or the {@link
* MediaItem#requestMetadata}.
*
* <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)}.
*
* @param mediaSession The session for this event.
* @param controller The controller information. * @param controller The controller information.
* @param mediaItem The media item whose local configuration will be filled in. * @param mediaItems The list of requested {@link MediaItem media items}.
* @return A media item with filled local configuration. * @return A {@link ListenableFuture} for the list of resolved {@link MediaItem media items}
* that are playable by the underlying {@link Player}.
*/ */
default MediaItem fillInLocalConfiguration( default ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession session, MediaSession.ControllerInfo controller, MediaItem mediaItem) { MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
return mediaItem; return Futures.immediateFailedFuture(new UnsupportedOperationException());
} }
} }
@ -1230,7 +1220,6 @@ public class MediaSession {
/* package */ final Player player; /* package */ final Player player;
/* package */ String id; /* package */ String id;
/* package */ C callback; /* package */ C callback;
/* package */ MediaItemFiller mediaItemFiller;
/* package */ @Nullable PendingIntent sessionActivity; /* package */ @Nullable PendingIntent sessionActivity;
/* package */ Bundle extras; /* package */ Bundle extras;
@ -1240,7 +1229,6 @@ public class MediaSession {
checkArgument(player.canAdvertiseSession()); checkArgument(player.canAdvertiseSession());
id = ""; id = "";
this.callback = callback; this.callback = callback;
this.mediaItemFiller = new MediaItemFiller() {};
extras = Bundle.EMPTY; extras = Bundle.EMPTY;
} }
@ -1262,12 +1250,6 @@ public class MediaSession {
return (U) this; return (U) this;
} }
@SuppressWarnings("unchecked")
/* package */ U setMediaItemFiller(MediaItemFiller mediaItemFiller) {
this.mediaItemFiller = checkNotNull(mediaItemFiller);
return (U) this;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public U setExtras(Bundle extras) { public U setExtras(Bundle extras) {
this.extras = new Bundle(checkNotNull(extras)); this.extras = new Bundle(checkNotNull(extras));

View File

@ -67,7 +67,6 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; 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.MediaSession.MediaItemFiller;
import androidx.media3.session.SequencedFutureManager.SequencedFuture; import androidx.media3.session.SequencedFutureManager.SequencedFuture;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -104,12 +103,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
protected final Object lock = new Object(); protected final Object lock = new Object();
private final Uri sessionUri; private final Uri sessionUri;
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler; private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;
private final MediaSession.Callback callback; private final MediaSession.Callback callback;
private final MediaItemFiller mediaItemFiller;
private final Context context; private final Context context;
private final MediaSessionStub sessionStub; private final MediaSessionStub sessionStub;
private final MediaSessionLegacyStub sessionLegacyStub; private final MediaSessionLegacyStub sessionLegacyStub;
@ -143,7 +138,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
Player player, Player player,
@Nullable PendingIntent sessionActivity, @Nullable PendingIntent sessionActivity,
MediaSession.Callback callback, MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) { Bundle tokenExtras) {
this.context = context; this.context = context;
this.instance = instance; this.instance = instance;
@ -157,7 +151,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
applicationHandler = new Handler(player.getApplicationLooper()); applicationHandler = new Handler(player.getApplicationLooper());
this.callback = callback; this.callback = callback;
this.mediaItemFiller = mediaItemFiller;
playerInfo = PlayerInfo.DEFAULT; playerInfo = PlayerInfo.DEFAULT;
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper()); onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper());
@ -495,9 +488,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return applicationHandler; return applicationHandler;
} }
protected MediaItem fillInLocalConfiguration( protected ListenableFuture<List<MediaItem>> onAddMediaItemsOnHandler(
MediaSession.ControllerInfo controller, MediaItem mediaItem) { ControllerInfo controller, List<MediaItem> mediaItems) {
return mediaItemFiller.fillInLocalConfiguration(instance, controller, mediaItem); return checkNotNull(
callback.onAddMediaItems(instance, controller, mediaItems),
"onAddMediaItems must return a non-null future");
} }
protected boolean isReleased() { protected boolean isReleased() {

View File

@ -147,6 +147,36 @@ import java.util.concurrent.ExecutionException;
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} }
private static <K extends MediaSessionImpl> void handleMediaItemsWhenReady(
K sessionImpl,
ControllerInfo controller,
int seq,
ListenableFuture<List<MediaItem>> future,
MediaItemPlayerTask mediaItemPlayerTask) {
future.addListener(
() -> {
SessionResult result;
try {
List<MediaItem> mediaItems =
checkNotNull(future.get(), "MediaItem list must not be null");
postOrRun(
sessionImpl.getApplicationHandler(),
() -> mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems));
result = new SessionResult(SessionResult.RESULT_SUCCESS);
} catch (CancellationException unused) {
result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED);
} catch (ExecutionException | InterruptedException exception) {
result =
new SessionResult(
exception.getCause() instanceof UnsupportedOperationException
? SessionResult.RESULT_ERROR_NOT_SUPPORTED
: SessionResult.RESULT_ERROR_UNKNOWN);
}
sendSessionResult(sessionImpl, controller, seq, result);
},
MoreExecutors.directExecutor());
}
private static void sendLibraryResult( private static void sendLibraryResult(
ControllerInfo controller, int seq, LibraryResult<?> result) { ControllerInfo controller, int seq, LibraryResult<?> result) {
try { try {
@ -816,13 +846,11 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
MediaItem mediaItemWithPlaybackProperties = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
sessionImpl.fillInLocalConfiguration(controller, mediaItem); (sessionImpl, controller, sequence, future) ->
sessionImpl.getPlayerWrapper().setMediaItem(mediaItemWithPlaybackProperties); handleMediaItemsWhenReady(
return SessionResult.RESULT_SUCCESS; sessionImpl, controller, sequence, future, Player::setMediaItems));
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -845,15 +873,16 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
MediaItem mediaItemWithPlaybackProperties = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
sessionImpl.fillInLocalConfiguration(controller, mediaItem); (sessionImpl, controller, sequence, future) ->
sessionImpl handleMediaItemsWhenReady(
.getPlayerWrapper() sessionImpl,
.setMediaItem(mediaItemWithPlaybackProperties, startPositionMs); controller,
return SessionResult.RESULT_SUCCESS; sequence,
}, future,
MediaSessionStub::sendSessionResult); (player, mediaItems) ->
player.setMediaItems(mediaItems, /* startIndex= */ 0, startPositionMs)));
} }
@Override @Override
@ -876,15 +905,15 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
MediaItem mediaItemWithPlaybackProperties = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
sessionImpl.fillInLocalConfiguration(controller, mediaItem); (sessionImpl, controller, sequence, future) ->
sessionImpl handleMediaItemsWhenReady(
.getPlayerWrapper() sessionImpl,
.setMediaItem(mediaItemWithPlaybackProperties, resetPosition); controller,
return SessionResult.RESULT_SUCCESS; sequence,
}, future,
MediaSessionStub::sendSessionResult); (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition)));
} }
@Override @Override
@ -907,20 +936,11 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
ImmutableList.Builder<MediaItem> mediaItemWithPlaybackPropertiesListBuilder = sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
ImmutableList.builder(); (sessionImpl, controller, sequence, future) ->
for (MediaItem mediaItem : mediaItemList) { handleMediaItemsWhenReady(
MediaItem mediaItemWithPlaybackProperties = sessionImpl, controller, sequence, future, Player::setMediaItems));
sessionImpl.fillInLocalConfiguration(controller, mediaItem);
mediaItemWithPlaybackPropertiesListBuilder.add(mediaItemWithPlaybackProperties);
}
sessionImpl
.getPlayerWrapper()
.setMediaItems(mediaItemWithPlaybackPropertiesListBuilder.build());
return SessionResult.RESULT_SUCCESS;
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -945,20 +965,15 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
ImmutableList.Builder<MediaItem> mediaItemWithPlaybackPropertiesListBuilder = sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
ImmutableList.builder(); (sessionImpl, controller, sequence, future) ->
for (MediaItem mediaItem : mediaItemList) { handleMediaItemsWhenReady(
MediaItem mediaItemWithPlaybackProperties = sessionImpl,
sessionImpl.fillInLocalConfiguration(controller, mediaItem); controller,
mediaItemWithPlaybackPropertiesListBuilder.add(mediaItemWithPlaybackProperties); sequence,
} future,
sessionImpl (player, mediaItems) -> player.setMediaItems(mediaItems, resetPosition)));
.getPlayerWrapper()
.setMediaItems(mediaItemWithPlaybackPropertiesListBuilder.build(), resetPosition);
return SessionResult.RESULT_SUCCESS;
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -984,22 +999,16 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
ImmutableList.Builder<MediaItem> mediaItemWithPlaybackPropertiesListBuilder = sessionImpl.onAddMediaItemsOnHandler(controller, mediaItemList),
ImmutableList.builder(); (sessionImpl, controller, sequence, future) ->
for (MediaItem mediaItem : mediaItemList) { handleMediaItemsWhenReady(
MediaItem mediaItemWithPlaybackProperties = sessionImpl,
sessionImpl.fillInLocalConfiguration(controller, mediaItem); controller,
mediaItemWithPlaybackPropertiesListBuilder.add(mediaItemWithPlaybackProperties); sequence,
} future,
(player, mediaItems) ->
sessionImpl player.setMediaItems(mediaItems, startIndex, startPositionMs)));
.getPlayerWrapper()
.setMediaItems(
mediaItemWithPlaybackPropertiesListBuilder.build(), startIndex, startPositionMs);
return SessionResult.RESULT_SUCCESS;
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -1057,13 +1066,11 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
MediaItem mediaItemWithPlaybackProperties = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
sessionImpl.fillInLocalConfiguration(controller, mediaItem); (sessionImpl, controller, sequence, future) ->
sessionImpl.getPlayerWrapper().addMediaItem(mediaItemWithPlaybackProperties); handleMediaItemsWhenReady(
return SessionResult.RESULT_SUCCESS; sessionImpl, controller, sequence, future, Player::addMediaItems));
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -1083,13 +1090,15 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) ->
MediaItem mediaItemWithPlaybackProperties = sessionImpl.onAddMediaItemsOnHandler(controller, ImmutableList.of(mediaItem)),
sessionImpl.fillInLocalConfiguration(controller, mediaItem); (sessionImpl, controller, sequence, future) ->
sessionImpl.getPlayerWrapper().addMediaItem(index, mediaItemWithPlaybackProperties); handleMediaItemsWhenReady(
return SessionResult.RESULT_SUCCESS; sessionImpl,
}, controller,
MediaSessionStub::sendSessionResult); sequence,
future,
(player, mediaItems) -> player.addMediaItems(index, mediaItems)));
} }
@Override @Override
@ -1111,21 +1120,10 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
ImmutableList.Builder<MediaItem> mediaItemsWithPlaybackPropertiesBuilder = (sessionImpl, controller, sequence, future) ->
ImmutableList.builder(); handleMediaItemsWhenReady(
for (MediaItem mediaItem : mediaItems) { sessionImpl, controller, sequence, future, Player::addMediaItems));
MediaItem mediaItemWithPlaybackProperties =
sessionImpl.fillInLocalConfiguration(controller, mediaItem);
mediaItemsWithPlaybackPropertiesBuilder.add(mediaItemWithPlaybackProperties);
}
sessionImpl
.getPlayerWrapper()
.addMediaItems(mediaItemsWithPlaybackPropertiesBuilder.build());
return SessionResult.RESULT_SUCCESS;
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -1150,21 +1148,14 @@ import java.util.concurrent.ExecutionException;
caller, caller,
seq, seq,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
(sessionImpl, controller) -> { (sessionImpl, controller) -> sessionImpl.onAddMediaItemsOnHandler(controller, mediaItems),
ImmutableList.Builder<MediaItem> mediaItemsWithPlaybackPropertiesBuilder = (sessionImpl, controller, sequence, future) ->
ImmutableList.builder(); handleMediaItemsWhenReady(
for (MediaItem mediaItem : mediaItems) { sessionImpl,
MediaItem mediaItemWithPlaybackProperties = controller,
sessionImpl.fillInLocalConfiguration(controller, mediaItem); sequence,
mediaItemsWithPlaybackPropertiesBuilder.add(mediaItemWithPlaybackProperties); future,
} (player, items) -> player.addMediaItems(index, items)));
sessionImpl
.getPlayerWrapper()
.addMediaItems(index, mediaItemsWithPlaybackPropertiesBuilder.build());
return SessionResult.RESULT_SUCCESS;
},
MediaSessionStub::sendSessionResult);
} }
@Override @Override
@ -1709,6 +1700,10 @@ import java.util.concurrent.ExecutionException;
void run(K sessionImpl, ControllerInfo controller, int seq, T result); void run(K sessionImpl, ControllerInfo controller, int seq, T result);
} }
private interface MediaItemPlayerTask {
void run(PlayerWrapper player, List<MediaItem> mediaItems);
}
/* package */ static final class Controller2Cb implements ControllerCb { /* package */ static final class Controller2Cb implements ControllerCb {
private final IMediaController iController; private final IMediaController iController;

View File

@ -15,9 +15,11 @@
*/ */
package androidx.media3.session; package androidx.media3.session;
import static androidx.media3.session.MediaTestUtils.createMediaItem;
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS; import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static androidx.media3.test.session.common.CommonConstants.METADATA_MEDIA_URI;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; 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.NO_RESPONSE_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
@ -39,12 +41,18 @@ 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.collect.Iterables;
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 java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
@ -68,6 +76,7 @@ public class MediaSessionCallbackTest {
private Context context; private Context context;
private MockPlayer player; private MockPlayer player;
private ListeningExecutorService executorService;
@Before @Before
public void setUp() { public void setUp() {
@ -76,6 +85,14 @@ public class MediaSessionCallbackTest {
new MockPlayer.Builder() new MockPlayer.Builder()
.setApplicationLooper(threadTestRule.getHandler().getLooper()) .setApplicationLooper(threadTestRule.getHandler().getLooper())
.build(); .build();
// Intentionally use an Executor with another thread to test asynchronous workflows involving
// background tasks.
executorService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
}
@After
public void tearDown() {
executorService.shutdownNow();
} }
@Test @Test
@ -335,168 +352,301 @@ public class MediaSessionCallbackTest {
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItem() throws Exception { public void onAddMediaItems_withSetMediaItem() throws Exception {
MediaItem mediaItem = MediaTestUtils.createMediaItem("mediaId"); MediaItem mediaItem = createMediaItem("mediaId");
MockFillInLocalConfigurationCallback callback = AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
new MockFillInLocalConfigurationCallback(/* latchCount= */ 1); MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem); controller.setMediaItem(mediaItem);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactly(mediaItem);
assertThat(requestedMediaItems.get()).containsExactly(mediaItem);
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItemWithIndex() throws Exception { public void onAddMediaItems_withSetMediaItemWithIndex() throws Exception {
MediaItem mediaItem = MediaTestUtils.createMediaItem("mediaId"); MediaItem mediaItem = createMediaItem("mediaId");
MockFillInLocalConfigurationCallback callback = AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
new MockFillInLocalConfigurationCallback(/* latchCount= */ 1); MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem, /* startPositionMs= */ 0); controller.setMediaItem(mediaItem, /* startPositionMs= */ 1234);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactly(mediaItem);
assertThat(requestedMediaItems.get()).containsExactly(mediaItem);
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
assertThat(player.startMediaItemIndex).isEqualTo(0);
assertThat(player.startPositionMs).isEqualTo(1234);
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItemWithResetPosition() throws Exception { public void onAddMediaItems_withSetMediaItemWithResetPosition() throws Exception {
MediaItem mediaItem = MediaTestUtils.createMediaItem("mediaId"); MediaItem mediaItem = createMediaItem("mediaId");
MockFillInLocalConfigurationCallback callback = AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
new MockFillInLocalConfigurationCallback(/* latchCount= */ 1); MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(mediaItem, /* resetPosition= */ true); controller.setMediaItem(mediaItem, /* resetPosition= */ true);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactly(mediaItem);
assertThat(requestedMediaItems.get()).containsExactly(mediaItem);
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
assertThat(player.resetPosition).isEqualTo(true);
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItems() throws Exception { public void onAddMediaItems_withSetMediaItems() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MockFillInLocalConfigurationCallback callback = MediaSession.Callback callback =
new MockFillInLocalConfigurationCallback(/* latchCount= */ 3); new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems); controller.setMediaItems(mediaItems);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactlyElementsIn(mediaItems);
assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder();
assertThat(player.mediaItems)
.containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems))
.inOrder();
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItemsWithStartPosition() throws Exception { public void onAddMediaItems_withSetMediaItemsWithStartPosition() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MockFillInLocalConfigurationCallback callback = MediaSession.Callback callback =
new MockFillInLocalConfigurationCallback(/* latchCount= */ 3); new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* startWindowIndex= */ 0, /* startPositionMs= */ 0); controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 1234);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactlyElementsIn(mediaItems);
assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder();
assertThat(player.mediaItems)
.containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems))
.inOrder();
assertThat(player.startMediaItemIndex).isEqualTo(1);
assertThat(player.startPositionMs).isEqualTo(1234);
} }
@Test @Test
public void onFillInPlaybackProperties_setMediaItemsWithResetPosition() throws Exception { public void onAddMediaItems_withSetMediaItemsWithResetPosition() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MockFillInLocalConfigurationCallback callback = MediaSession.Callback callback =
new MockFillInLocalConfigurationCallback(/* latchCount= */ 3); new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItems(mediaItems, /* resetPosition= */ true); controller.setMediaItems(mediaItems, /* resetPosition= */ true);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactlyElementsIn(mediaItems);
assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder();
assertThat(player.mediaItems)
.containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems))
.inOrder();
assertThat(player.resetPosition).isEqualTo(true);
} }
@Test @Test
public void onFillInPlaybackProperties_addMediaItem() throws Exception { public void onAddMediaItems_withAddMediaItem() throws Exception {
MediaItem mediaItem = MediaTestUtils.createMediaItem("mediaId"); MediaItem mediaItem = createMediaItem("mediaId");
MockFillInLocalConfigurationCallback callback = AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
new MockFillInLocalConfigurationCallback(/* latchCount= */ 1); MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.addMediaItem(mediaItem); controller.addMediaItem(mediaItem);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactly(mediaItem);
assertThat(requestedMediaItems.get()).containsExactly(mediaItem);
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
} }
@Test @Test
public void onFillInPlaybackProperties_addMediaItemWithIndex() throws Exception { public void onAddMediaItems_withAddMediaItemWithIndex() throws Exception {
MediaItem mediaItem = MediaTestUtils.createMediaItem("mediaId"); MediaItem existingItem = createMediaItem("existingItem");
MockFillInLocalConfigurationCallback callback = MediaItem mediaItem = createMediaItem("mediaId");
new MockFillInLocalConfigurationCallback(/* latchCount= */ 1); AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(existingItem);
controller.addMediaItem(/* index= */ 0, mediaItem); controller.addMediaItem(/* index= */ 1, mediaItem);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactly(mediaItem);
assertThat(requestedMediaItems.get()).containsExactly(mediaItem);
assertThat(player.mediaItems)
.containsExactly(
updateMediaItemWithLocalConfiguration(existingItem),
updateMediaItemWithLocalConfiguration(mediaItem));
assertThat(player.index).isEqualTo(1);
} }
@Test @Test
public void onFillInPlaybackProperties_addMediaItems() throws Exception { public void onAddMediaItems_withAddMediaItems() throws Exception {
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MockFillInLocalConfigurationCallback callback = MediaSession.Callback callback =
new MockFillInLocalConfigurationCallback(/* latchCount= */ 3); new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.addMediaItems(mediaItems); controller.addMediaItems(mediaItems);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactlyElementsIn(mediaItems);
assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder();
assertThat(player.mediaItems)
.containsExactlyElementsIn(updateMediaItemsWithLocalConfiguration(mediaItems))
.inOrder();
} }
@Test @Test
public void onFillInPlaybackProperties_addMediaItemsWithIndex() throws Exception { public void onAddMediaItems_withAddMediaItemsWithIndex() throws Exception {
MediaItem existingItem = createMediaItem("existingItem");
List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); List<MediaItem> mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3);
AtomicReference<List<MediaItem>> requestedMediaItems = new AtomicReference<>();
MockFillInLocalConfigurationCallback callback = MediaSession.Callback callback =
new MockFillInLocalConfigurationCallback(/* latchCount= */ 3); new MediaSession.Callback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
requestedMediaItems.set(mediaItems);
// Resolve MediaItems asynchronously to test correct threading logic.
return executorService.submit(() -> updateMediaItemsWithLocalConfiguration(mediaItems));
}
};
MediaSession session = MediaSession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setMediaItemFiller(callback).build()); new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller = RemoteMediaController controller =
controllerTestRule.createRemoteController(session.getToken()); controllerTestRule.createRemoteController(session.getToken());
controller.setMediaItem(existingItem);
controller.addMediaItems(/* index= */ 0, mediaItems); controller.addMediaItems(/* index= */ 1, mediaItems);
assertThat(callback.latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS);
assertThat(callback.mediaItemsFromParam).containsExactlyElementsIn(mediaItems);
assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder();
assertThat(player.mediaItems)
.containsExactlyElementsIn(
Iterables.concat(
ImmutableList.of(updateMediaItemWithLocalConfiguration(existingItem)),
updateMediaItemsWithLocalConfiguration(mediaItems)))
.inOrder();
assertThat(player.index).isEqualTo(1);
} }
@Test @Test
@ -556,22 +706,16 @@ public class MediaSessionCallbackTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
} }
private static class MockFillInLocalConfigurationCallback private static MediaItem updateMediaItemWithLocalConfiguration(MediaItem mediaItem) {
implements MediaSession.MediaItemFiller { return mediaItem.buildUpon().setUri(METADATA_MEDIA_URI).build();
}
public final List<MediaItem> mediaItemsFromParam = new ArrayList<>(); private static List<MediaItem> updateMediaItemsWithLocalConfiguration(
public CountDownLatch latch; List<MediaItem> mediaItems) {
ImmutableList.Builder<MediaItem> listBuilder = ImmutableList.builder();
public MockFillInLocalConfigurationCallback(int latchCount) { for (int i = 0; i < mediaItems.size(); i++) {
this.latch = new CountDownLatch(latchCount); listBuilder.add(updateMediaItemWithLocalConfiguration(mediaItems.get(i)));
}
@Override
public MediaItem fillInLocalConfiguration(
MediaSession session, ControllerInfo controllerInfo, MediaItem mediaItem) {
mediaItemsFromParam.add(mediaItem);
latch.countDown();
return mediaItem;
} }
return listBuilder.build();
} }
} }

View File

@ -31,6 +31,8 @@ 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.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List; import java.util.List;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -76,6 +78,14 @@ public class MediaSessionPlayerTest {
} }
return MediaSession.ConnectionResult.reject(); return MediaSession.ConnectionResult.reject();
} }
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
return Futures.immediateFuture(mediaItems);
}
}) })
.build(); .build();
@ -197,7 +207,7 @@ public class MediaSessionPlayerTest {
controller.setMediaItem(item); controller.setMediaItem(item);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactly(item); assertThat(player.mediaItems).containsExactly(item);
assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition); assertThat(player.resetPosition).isEqualTo(resetPosition);
@ -213,7 +223,7 @@ public class MediaSessionPlayerTest {
controller.setMediaItem(item); controller.setMediaItem(item);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactly(item); assertThat(player.mediaItems).containsExactly(item);
assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition); assertThat(player.resetPosition).isEqualTo(resetPosition);
@ -229,7 +239,7 @@ public class MediaSessionPlayerTest {
controller.setMediaItem(item); controller.setMediaItem(item);
player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEM, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).containsExactly(item); assertThat(player.mediaItems).containsExactly(item);
assertThat(player.startPositionMs).isEqualTo(startPositionMs); assertThat(player.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition); assertThat(player.resetPosition).isEqualTo(resetPosition);
@ -317,7 +327,7 @@ public class MediaSessionPlayerTest {
controller.addMediaItem(mediaItem); controller.addMediaItem(mediaItem);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
assertThat(player.mediaItems).hasSize(6); assertThat(player.mediaItems).hasSize(6);
} }
@ -328,7 +338,7 @@ public class MediaSessionPlayerTest {
controller.addMediaItem(index, mediaItem); controller.addMediaItem(index, mediaItem);
player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEM_WITH_INDEX, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS);
assertThat(player.index).isEqualTo(index); assertThat(player.index).isEqualTo(index);
assertThat(player.mediaItems).hasSize(6); assertThat(player.mediaItems).hasSize(6);
} }