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 @@
`MediaLibrarySession.MediaLibrarySessionCallback` to
`MediaLibrarySession.Callback` and
`MediaSession.Builder.setSessionCallback` to `setCallback`.
* Replace `MediaSession.MediaItemFiler` with
`MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution
of requests.
* Data sources:
* Rename `DummyDataSource` to `PlaceHolderDataSource`.
* Workaround OkHttp interrupt handling.
......
......@@ -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) {
// Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched
......@@ -236,7 +246,6 @@ class PlaybackService : MediaLibraryService() {
mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(sessionActivityPendingIntent)
.build()
if (!customLayout.isEmpty()) {
......@@ -262,14 +271,4 @@ class PlaybackService : MediaLibraryService() {
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* 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 {
}
/**
* 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
* MediaLibrarySession#getToken()} session token} will have the {@link
* SessionToken#getExtras() extras}. If not set, an empty {@link Bundle} will be used.
......@@ -439,8 +428,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/
@Override
public MediaLibrarySession build() {
return new MediaLibrarySession(
context, id, player, sessionActivity, callback, mediaItemFiller, extras);
return new MediaLibrarySession(context, id, player, sessionActivity, callback, extras);
}
}
......@@ -450,9 +438,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
super(context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
super(context, id, player, sessionActivity, callback, tokenExtras);
}
@Override
......@@ -462,17 +449,9 @@ public abstract class MediaLibraryService extends MediaSessionService {
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
return new MediaLibrarySessionImpl(
this,
context,
id,
player,
sessionActivity,
(Callback) callback,
mediaItemFiller,
tokenExtras);
this, context, id, player, sessionActivity, (Callback) callback, tokenExtras);
}
@Override
......
......@@ -63,9 +63,8 @@ import java.util.concurrent.Future;
Player player,
@Nullable PendingIntent sessionActivity,
MediaLibrarySession.Callback callback,
MediaSession.MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
super(instance, context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
super(instance, context, id, player, sessionActivity, callback, tokenExtras);
this.instance = instance;
this.callback = callback;
subscriptions = new ArrayMap<>();
......
......@@ -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
* MediaSession#getToken()} session token} will have the {@link SessionToken#getExtras()
* extras}. If not set, an empty {@link Bundle} will be used.
......@@ -327,8 +315,7 @@ public class MediaSession {
*/
@Override
public MediaSession build() {
return new MediaSession(
context, id, player, sessionActivity, callback, mediaItemFiller, extras);
return new MediaSession(context, id, player, sessionActivity, callback, extras);
}
}
......@@ -484,7 +471,6 @@ public class MediaSession {
Player player,
@Nullable PendingIntent sessionActivity,
Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
synchronized (STATIC_LOCK) {
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
......@@ -492,7 +478,7 @@ public class MediaSession {
}
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(
......@@ -501,10 +487,8 @@ public class MediaSession {
Player player,
@Nullable PendingIntent sessionActivity,
Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
return new MediaSessionImpl(
this, context, id, player, sessionActivity, callback, mediaItemFiller, tokenExtras);
return new MediaSessionImpl(this, context, id, player, sessionActivity, callback, tokenExtras);
}
/* package */ MediaSessionImpl getImpl() {
......@@ -1041,23 +1025,29 @@ public class MediaSession {
Bundle args) {
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
* controllers.
* Called when a controller requested to add new {@linkplain MediaItem media items} to the
* 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 mediaItem The media item whose local configuration will be filled in.
* @return A media item with filled local configuration.
* @param mediaItems The list of requested {@link MediaItem media items}.
* @return A {@link ListenableFuture} for the list of resolved {@link MediaItem media items}
* that are playable by the underlying {@link Player}.
*/
default MediaItem fillInLocalConfiguration(
MediaSession session, MediaSession.ControllerInfo controller, MediaItem mediaItem) {
return mediaItem;
default ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
return Futures.immediateFailedFuture(new UnsupportedOperationException());
}
}
......@@ -1230,7 +1220,6 @@ public class MediaSession {
/* package */ final Player player;
/* package */ String id;
/* package */ C callback;
/* package */ MediaItemFiller mediaItemFiller;
/* package */ @Nullable PendingIntent sessionActivity;
/* package */ Bundle extras;
......@@ -1240,7 +1229,6 @@ public class MediaSession {
checkArgument(player.canAdvertiseSession());
id = "";
this.callback = callback;
this.mediaItemFiller = new MediaItemFiller() {};
extras = Bundle.EMPTY;
}
......@@ -1263,12 +1251,6 @@ public class MediaSession {
}
@SuppressWarnings("unchecked")
/* package */ U setMediaItemFiller(MediaItemFiller mediaItemFiller) {
this.mediaItemFiller = checkNotNull(mediaItemFiller);
return (U) this;
}
@SuppressWarnings("unchecked")
public U setExtras(Bundle extras) {
this.extras = new Bundle(checkNotNull(extras));
return (U) this;
......
......@@ -67,7 +67,6 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.MediaSession.MediaItemFiller;
import androidx.media3.session.SequencedFutureManager.SequencedFuture;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
......@@ -104,12 +103,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
protected final Object lock = new Object();
private final Uri sessionUri;
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;
private final MediaSession.Callback callback;
private final MediaItemFiller mediaItemFiller;
private final Context context;
private final MediaSessionStub sessionStub;
private final MediaSessionLegacyStub sessionLegacyStub;
......@@ -143,7 +138,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
Player player,
@Nullable PendingIntent sessionActivity,
MediaSession.Callback callback,
MediaItemFiller mediaItemFiller,
Bundle tokenExtras) {
this.context = context;
this.instance = instance;
......@@ -157,7 +151,6 @@ import org.checkerframework.checker.initialization.qual.Initialized;
applicationHandler = new Handler(player.getApplicationLooper());
this.callback = callback;
this.mediaItemFiller = mediaItemFiller;
playerInfo = PlayerInfo.DEFAULT;
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper());
......@@ -495,9 +488,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
return applicationHandler;
}
protected MediaItem fillInLocalConfiguration(
MediaSession.ControllerInfo controller, MediaItem mediaItem) {
return mediaItemFiller.fillInLocalConfiguration(instance, controller, mediaItem);
protected ListenableFuture<List<MediaItem>> onAddMediaItemsOnHandler(
ControllerInfo controller, List<MediaItem> mediaItems) {
return checkNotNull(
callback.onAddMediaItems(instance, controller, mediaItems),
"onAddMediaItems must return a non-null future");
}
protected boolean isReleased() {
......
......@@ -31,6 +31,8 @@ import androidx.media3.test.session.common.TestUtils;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import org.junit.After;
import org.junit.Before;
......@@ -76,6 +78,14 @@ public class MediaSessionPlayerTest {
}
return MediaSession.ConnectionResult.reject();
}
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession,
MediaSession.ControllerInfo controller,
List<MediaItem> mediaItems) {
return Futures.immediateFuture(mediaItems);
}
})
.build();
......@@ -197,7 +207,7 @@ public class MediaSessionPlayerTest {
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.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition);
......@@ -213,7 +223,7 @@ public class MediaSessionPlayerTest {
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.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition);
......@@ -229,7 +239,7 @@ public class MediaSessionPlayerTest {
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.startPositionMs).isEqualTo(startPositionMs);
assertThat(player.resetPosition).isEqualTo(resetPosition);
......@@ -317,7 +327,7 @@ public class MediaSessionPlayerTest {
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);
}
......@@ -328,7 +338,7 @@ public class MediaSessionPlayerTest {
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.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