Commit 6b782d10 by tonihei Committed by Marc Baechinger

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
parent 342be88d
...@@ -129,6 +129,9 @@ ...@@ -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.
......
...@@ -202,6 +202,16 @@ class PlaybackService : MediaLibraryService() { ...@@ -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() { ...@@ -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() { ...@@ -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
}
}
} }
...@@ -407,17 +407,6 @@ public abstract class MediaLibraryService extends MediaSessionService { ...@@ -407,17 +407,6 @@ public abstract class MediaLibraryService extends MediaSessionService {
} }
/** /**
* 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
* SessionToken#getExtras() extras}. If not set, an empty {@link Bundle} will be used. * SessionToken#getExtras() extras}. If not set, an empty {@link Bundle} will be used.
...@@ -439,8 +428,7 @@ public abstract class MediaLibraryService extends MediaSessionService { ...@@ -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 { ...@@ -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 { ...@@ -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
......
...@@ -63,9 +63,8 @@ import java.util.concurrent.Future; ...@@ -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<>();
......
...@@ -294,18 +294,6 @@ public class MediaSession { ...@@ -294,18 +294,6 @@ public class MediaSession {
} }
/** /**
* 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()
* extras}. If not set, an empty {@link Bundle} will be used. * extras}. If not set, an empty {@link Bundle} will be used.
...@@ -327,8 +315,7 @@ public class MediaSession { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
...@@ -1263,12 +1251,6 @@ public class MediaSession { ...@@ -1263,12 +1251,6 @@ public class MediaSession {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
/* package */ U setMediaItemFiller(MediaItemFiller mediaItemFiller) {
this.mediaItemFiller = checkNotNull(mediaItemFiller);
return (U) this;
}
@SuppressWarnings("unchecked")
public U setExtras(Bundle extras) { public U setExtras(Bundle extras) {
this.extras = new Bundle(checkNotNull(extras)); this.extras = new Bundle(checkNotNull(extras));
return (U) this; return (U) this;
......
...@@ -67,7 +67,6 @@ import androidx.media3.common.util.Log; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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() {
......
...@@ -31,6 +31,8 @@ import androidx.media3.test.session.common.TestUtils; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment