diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index b26b93173e..9913494af4 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -48,6 +48,10 @@
* Muxers:
* IMA extension:
* Session:
+ * Add default implementation to `MediaSession.Callback.onAddMediaItems` to
+ allow requested `MediaItems` to be passed onto `Player` if they have
+ `LocalConfiguration` (e.g. URI)
+ ([#282](https://github.com/androidx/media/issues/282)).
* UI:
* Downloads:
* OkHttp Extension:
diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
index 66148ab6f6..166a60a6a9 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
@@ -1087,7 +1087,7 @@ public final class MediaItem implements Bundleable {
}
/** Properties for local playback. */
- public static final class LocalConfiguration {
+ public static final class LocalConfiguration implements Bundleable {
/** The {@link Uri}. */
public final Uri uri;
@@ -1183,6 +1183,82 @@ public final class MediaItem implements Bundleable {
result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result;
}
+
+ // Bundleable implementation.
+
+ private static final String FIELD_URI = Util.intToStringMaxRadix(0);
+ private static final String FIELD_MIME_TYPE = Util.intToStringMaxRadix(1);
+ private static final String FIELD_DRM_CONFIGURATION = Util.intToStringMaxRadix(2);
+ private static final String FIELD_ADS_CONFIGURATION = Util.intToStringMaxRadix(3);
+ private static final String FIELD_STREAM_KEYS = Util.intToStringMaxRadix(4);
+ private static final String FIELD_CUSTOM_CACHE_KEY = Util.intToStringMaxRadix(5);
+ private static final String FIELD_SUBTITLE_CONFIGURATION = Util.intToStringMaxRadix(6);
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #tag} field. The {@link #tag} of an instance restored from such a
+ * bundle by {@link #CREATOR} will be {@code null}.
+ */
+ @UnstableApi
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(FIELD_URI, uri);
+ if (mimeType != null) {
+ bundle.putString(FIELD_MIME_TYPE, mimeType);
+ }
+ if (drmConfiguration != null) {
+ bundle.putBundle(FIELD_DRM_CONFIGURATION, drmConfiguration.toBundle());
+ }
+ if (adsConfiguration != null) {
+ bundle.putBundle(FIELD_ADS_CONFIGURATION, adsConfiguration.toBundle());
+ }
+ if (!streamKeys.isEmpty()) {
+ bundle.putParcelableArrayList(FIELD_STREAM_KEYS, new ArrayList<>(streamKeys));
+ }
+ if (customCacheKey != null) {
+ bundle.putString(FIELD_CUSTOM_CACHE_KEY, customCacheKey);
+ }
+ if (!subtitleConfigurations.isEmpty()) {
+ bundle.putParcelableArrayList(
+ FIELD_SUBTITLE_CONFIGURATION, BundleableUtil.toBundleArrayList(subtitleConfigurations));
+ }
+ return bundle;
+ }
+
+ /** Object that can restore {@link LocalConfiguration} from a {@link Bundle}. */
+ @UnstableApi
+ public static final Creator CREATOR = LocalConfiguration::fromBundle;
+
+ @UnstableApi
+ private static LocalConfiguration fromBundle(Bundle bundle) {
+ @Nullable Bundle drmBundle = bundle.getBundle(FIELD_DRM_CONFIGURATION);
+ DrmConfiguration drmConfiguration =
+ drmBundle == null ? null : DrmConfiguration.CREATOR.fromBundle(drmBundle);
+ @Nullable Bundle adsBundle = bundle.getBundle(FIELD_ADS_CONFIGURATION);
+ AdsConfiguration adsConfiguration =
+ adsBundle == null ? null : AdsConfiguration.CREATOR.fromBundle(adsBundle);
+ @Nullable List streamKeysList = bundle.getParcelableArrayList(FIELD_STREAM_KEYS);
+ List streamKeys =
+ streamKeysList == null ? ImmutableList.of() : ImmutableList.copyOf(streamKeysList);
+ @Nullable
+ List subtitleBundles = bundle.getParcelableArrayList(FIELD_SUBTITLE_CONFIGURATION);
+ ImmutableList subtitleConfiguration =
+ subtitleBundles == null
+ ? ImmutableList.of()
+ : BundleableUtil.fromBundleList(SubtitleConfiguration.CREATOR, subtitleBundles);
+
+ return new LocalConfiguration(
+ checkNotNull(bundle.getParcelable(FIELD_URI)),
+ bundle.getString(FIELD_MIME_TYPE),
+ drmConfiguration,
+ adsConfiguration,
+ streamKeys,
+ bundle.getString(FIELD_CUSTOM_CACHE_KEY),
+ subtitleConfiguration,
+ /* tag= */ null);
+ }
}
/** Live playback configuration. */
@@ -2167,16 +2243,10 @@ public final class MediaItem implements Bundleable {
private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2);
private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4);
+ private static final String FIELD_LOCAL_CONFIGURATION = Util.intToStringMaxRadix(5);
- /**
- * {@inheritDoc}
- *
- * It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
- * instance restored by {@link #CREATOR} will always be {@code null}.
- */
@UnstableApi
- @Override
- public Bundle toBundle() {
+ private Bundle toBundle(boolean includeLocalConfiguration) {
Bundle bundle = new Bundle();
if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
bundle.putString(FIELD_MEDIA_ID, mediaId);
@@ -2193,9 +2263,33 @@ public final class MediaItem implements Bundleable {
if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
}
+ if (includeLocalConfiguration && localConfiguration != null) {
+ bundle.putBundle(FIELD_LOCAL_CONFIGURATION, localConfiguration.toBundle());
+ }
return bundle;
}
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #localConfiguration} field. The {@link #localConfiguration} of an
+ * instance restored from such a bundle by {@link #CREATOR} will be {@code null}.
+ */
+ @UnstableApi
+ @Override
+ public Bundle toBundle() {
+ return toBundle(/* includeLocalConfiguration= */ false);
+ }
+
+ /**
+ * Returns a {@link Bundle} representing the information stored in this {@link #MediaItem} object,
+ * while including the {@link #localConfiguration} field if it is not null (otherwise skips it).
+ */
+ @UnstableApi
+ public Bundle toBundleIncludeLocalConfiguration() {
+ return toBundle(/* includeLocalConfiguration= */ true);
+ }
+
/**
* An object that can restore {@link MediaItem} from a {@link Bundle}.
*
@@ -2234,10 +2328,17 @@ public final class MediaItem implements Bundleable {
} else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
}
+ @Nullable Bundle localConfigurationBundle = bundle.getBundle(FIELD_LOCAL_CONFIGURATION);
+ LocalConfiguration localConfiguration;
+ if (localConfigurationBundle == null) {
+ localConfiguration = null;
+ } else {
+ localConfiguration = LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
+ }
return new MediaItem(
mediaId,
clippingConfiguration,
- /* localConfiguration= */ null,
+ localConfiguration,
liveConfiguration,
mediaMetadata,
requestMetadata);
diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BundleableUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/BundleableUtil.java
index fe040458eb..7049542c1b 100644
--- a/libraries/common/src/main/java/androidx/media3/common/util/BundleableUtil.java
+++ b/libraries/common/src/main/java/androidx/media3/common/util/BundleableUtil.java
@@ -22,6 +22,7 @@ import android.os.Bundle;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
+import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
@@ -36,10 +37,21 @@ public final class BundleableUtil {
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static ImmutableList toBundleList(List bundleableList) {
+ return toBundleList(bundleableList, Bundleable::toBundle);
+ }
+
+ /**
+ * Converts a list of {@link Bundleable} to a list {@link Bundle}
+ *
+ * @param bundleableList list of Bundleable items to be converted
+ * @param customToBundleFunc function that specifies how to bundle up each {@link Bundleable}
+ */
+ public static ImmutableList toBundleList(
+ List bundleableList, Function customToBundleFunc) {
ImmutableList.Builder builder = ImmutableList.builder();
for (int i = 0; i < bundleableList.size(); i++) {
- Bundleable bundleable = bundleableList.get(i);
- builder.add(bundleable.toBundle());
+ T bundleable = bundleableList.get(i);
+ builder.add(customToBundleFunc.apply(bundleable));
}
return builder.build();
}
diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java
index a59a899416..99fe569c98 100644
--- a/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java
+++ b/libraries/common/src/test/java/androidx/media3/common/MediaItemTest.java
@@ -665,6 +665,68 @@ public class MediaItemTest {
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
}
+ @Test
+ public void
+ createDefaultLocalConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
+ MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
+
+ Bundle localConfigurationBundle = mediaItem.localConfiguration.toBundle();
+
+ // Check that default values are skipped when bundling, only Uri field (="0") is present
+ assertThat(localConfigurationBundle.keySet()).containsExactly("0");
+
+ MediaItem.LocalConfiguration restoredLocalConfiguration =
+ MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfigurationBundle);
+
+ assertThat(restoredLocalConfiguration).isEqualTo(mediaItem.localConfiguration);
+ assertThat(restoredLocalConfiguration.streamKeys).isEmpty();
+ assertThat(restoredLocalConfiguration.subtitleConfigurations).isEmpty();
+ }
+
+ @Test
+ public void createLocalConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
+ Map requestHeaders = new HashMap<>();
+ requestHeaders.put("Referer", "http://www.google.com");
+ MediaItem mediaItem =
+ new MediaItem.Builder()
+ .setUri(URI_STRING)
+ .setMimeType(MimeTypes.APPLICATION_MP4)
+ .setCustomCacheKey("key")
+ .setSubtitleConfigurations(
+ ImmutableList.of(
+ new MediaItem.SubtitleConfiguration.Builder(Uri.parse(URI_STRING + "/en"))
+ .setMimeType(MimeTypes.APPLICATION_TTML)
+ .setLanguage("en")
+ .setSelectionFlags(C.SELECTION_FLAG_FORCED)
+ .setRoleFlags(C.ROLE_FLAG_ALTERNATE)
+ .setLabel("label")
+ .setId("id")
+ .build()))
+ .setDrmConfiguration(
+ new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
+ .setLicenseUri(Uri.parse(URI_STRING))
+ .setLicenseRequestHeaders(requestHeaders)
+ .setMultiSession(true)
+ .setForceDefaultLicenseUri(true)
+ .setPlayClearContentWithoutKey(true)
+ .setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
+ .setKeySetId(new byte[] {1, 2, 3})
+ .build())
+ .setAdsConfiguration(
+ new MediaItem.AdsConfiguration.Builder(Uri.parse(URI_STRING)).build())
+ .build();
+
+ MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
+ MediaItem.LocalConfiguration localConfigurationFromBundle =
+ MediaItem.LocalConfiguration.CREATOR.fromBundle(localConfiguration.toBundle());
+ MediaItem.LocalConfiguration localConfigurationFromMediaItemBundle =
+ MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration())
+ .localConfiguration;
+
+ assertThat(localConfigurationFromBundle).isEqualTo(localConfiguration);
+ assertThat(localConfigurationFromMediaItemBundle).isEqualTo(localConfiguration);
+ }
+
@Test
public void builderSetLiveConfiguration() {
MediaItem mediaItem =
@@ -892,13 +954,25 @@ public class MediaItemTest {
}
@Test
- public void roundTripViaBundle_withLocalConfiguration_dropsLocalConfiguration() {
+ public void
+ roundTripViaDefaultBundle_mediaItemContainsLocalConfiguration_dropsLocalConfiguration() {
MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull();
}
+ @Test
+ public void
+ roundTripViaBundleIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_restoresLocalConfiguration() {
+ MediaItem mediaItem = new MediaItem.Builder().setUri(URI_STRING).build();
+ MediaItem restoredMediaItem =
+ MediaItem.CREATOR.fromBundle(mediaItem.toBundleIncludeLocalConfiguration());
+
+ assertThat(mediaItem.localConfiguration).isNotNull();
+ assertThat(restoredMediaItem.localConfiguration).isEqualTo(mediaItem.localConfiguration);
+ }
+
@Test
public void createDefaultMediaItemInstance_checksDefaultValues() {
MediaItem mediaItem = new MediaItem.Builder().build();
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
index a284f999a5..a19f86929a 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java
@@ -724,7 +724,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
dispatchRemoteSessionTaskWithPlayerCommand(
- (iSession, seq) -> iSession.setMediaItem(controllerStub, seq, mediaItem.toBundle()));
+ (iSession, seq) ->
+ iSession.setMediaItem(
+ controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
setMediaItemsInternal(
Collections.singletonList(mediaItem),
@@ -742,7 +744,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) ->
iSession.setMediaItemWithStartPosition(
- controllerStub, seq, mediaItem.toBundle(), startPositionMs));
+ controllerStub,
+ seq,
+ mediaItem.toBundleIncludeLocalConfiguration(),
+ startPositionMs));
setMediaItemsInternal(
Collections.singletonList(mediaItem),
@@ -760,7 +765,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) ->
iSession.setMediaItemWithResetPosition(
- controllerStub, seq, mediaItem.toBundle(), resetPosition));
+ controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration(), resetPosition));
setMediaItemsInternal(
Collections.singletonList(mediaItem),
@@ -780,7 +785,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItems(
controllerStub,
seq,
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
setMediaItemsInternal(
mediaItems,
@@ -800,7 +807,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItemsWithResetPosition(
controllerStub,
seq,
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
resetPosition));
setMediaItemsInternal(
@@ -821,7 +830,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.setMediaItemsWithStartIndex(
controllerStub,
seq,
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems)),
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration)),
startIndex,
startPositionMs));
@@ -860,7 +871,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
}
dispatchRemoteSessionTaskWithPlayerCommand(
- (iSession, seq) -> iSession.addMediaItem(controllerStub, seq, mediaItem.toBundle()));
+ (iSession, seq) ->
+ iSession.addMediaItem(
+ controllerStub, seq, mediaItem.toBundleIncludeLocalConfiguration()));
addMediaItemsInternal(
getCurrentTimeline().getWindowCount(), Collections.singletonList(mediaItem));
@@ -875,7 +888,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) ->
- iSession.addMediaItemWithIndex(controllerStub, seq, index, mediaItem.toBundle()));
+ iSession.addMediaItemWithIndex(
+ controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration()));
addMediaItemsInternal(index, Collections.singletonList(mediaItem));
}
@@ -891,7 +905,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
iSession.addMediaItems(
controllerStub,
seq,
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
addMediaItemsInternal(getCurrentTimeline().getWindowCount(), mediaItems);
}
@@ -909,7 +925,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
controllerStub,
seq,
index,
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems))));
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration))));
addMediaItemsInternal(index, mediaItems);
}
@@ -1193,9 +1211,11 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> {
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
- iSession.replaceMediaItem(controllerStub, seq, index, mediaItem.toBundle());
+ iSession.replaceMediaItem(
+ controllerStub, seq, index, mediaItem.toBundleIncludeLocalConfiguration());
} else {
- iSession.addMediaItemWithIndex(controllerStub, seq, index + 1, mediaItem.toBundle());
+ iSession.addMediaItemWithIndex(
+ controllerStub, seq, index + 1, mediaItem.toBundleIncludeLocalConfiguration());
iSession.removeMediaItem(controllerStub, seq, index);
}
});
@@ -1213,7 +1233,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
dispatchRemoteSessionTaskWithPlayerCommand(
(iSession, seq) -> {
IBinder mediaItemsBundleBinder =
- new BundleListRetriever(BundleableUtil.toBundleList(mediaItems));
+ new BundleListRetriever(
+ BundleableUtil.toBundleList(
+ mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
if (checkNotNull(connectedToken).getInterfaceVersion() >= 2) {
iSession.replaceMediaItems(
controllerStub, seq, fromIndex, toIndex, mediaItemsBundleBinder);
diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
index ca2d0231db..021a66d186 100644
--- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
+++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java
@@ -1129,10 +1129,15 @@ public class MediaSession {
* prepare or play media (for instance when browsing the catalogue and then selecting an item
* for preparation from Android Auto that is using the legacy Media1 library).
*
- * 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
+ *
By default, if and only if each of the provided {@linkplain MediaItem media items} has a
+ * set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
+ * list unaltered. Otherwise, the default implementation returns an {@link
+ * UnsupportedOperationException}.
+ *
+ *
If the requested {@linkplain MediaItem media items} don't have a {@link
+ * MediaItem.LocalConfiguration}, they need to be updated to make them playable by the
+ * underlying {@link Player}. Typically, this callback would be overridden with implementation
+ * that identifies the correct item by its {@link MediaItem#mediaId} and/or the {@link
* MediaItem#requestMetadata}.
*
*
Return a {@link ListenableFuture} with the resolved {@link MediaItem media items}. You can
@@ -1167,7 +1172,12 @@ public class MediaSession {
*/
default ListenableFuture> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List mediaItems) {
- return Futures.immediateFailedFuture(new UnsupportedOperationException());
+ for (MediaItem mediaItem : mediaItems) {
+ if (mediaItem.localConfiguration == null) {
+ return Futures.immediateFailedFuture(new UnsupportedOperationException());
+ }
+ }
+ return Futures.immediateFuture(mediaItems);
}
/**
@@ -1181,11 +1191,16 @@ public class MediaSession {
* the catalogue and then selecting an item for preparation from Android Auto that is using the
* legacy Media1 library).
*
- * Note that the requested {@linkplain MediaItem media items} in the
- * MediaItemsWithStartPosition 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}.
+ *
By default, if and only if each of the provided {@linkplain MediaItem media items} has a
+ * set {@link MediaItem.LocalConfiguration} (for example, a URI), then the callback returns the
+ * list unaltered. Otherwise, the default implementation returns an {@link
+ * UnsupportedOperationException}.
+ *
+ *
If the requested {@linkplain MediaItem media items} don't have a {@link
+ * MediaItem.LocalConfiguration}, they need to be updated to make them playable by the
+ * underlying {@link Player}. Typically, this callback would be overridden with implementation
+ * that identifies the correct item by its {@link MediaItem#mediaId} and/or the {@link
+ * MediaItem#requestMetadata}.
*
*
Return a {@link ListenableFuture} with the resolved {@linkplain
* MediaItemsWithStartPosition media items and starting index and position}. You can also return
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java
index defc3a8f84..28e1a8f129 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackTest.java
@@ -377,6 +377,85 @@ public class MediaSessionCallbackTest {
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
}
+ @Test
+ public void
+ onAddMediaItemsDefault_withSetMediaItemIncludeLocalConfiguration_mediaItemDoesntContainLocalConfiguration_noItemsSet()
+ throws Exception {
+ MediaItem mediaItemWithoutLocalConfiguration = createMediaItem("mediaId");
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.setMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
+
+ Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
+ assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
+ .isFalse();
+ assertThat(player.mediaItems).isEmpty();
+ }
+
+ @Test
+ public void
+ onAddMediaItemsDefault_withSetMediaItemsIncludeLocalConfiguration_mediaItemsDontContainLocalConfiguration_noItemsSet()
+ throws Exception {
+ MediaItem mediaItemWithoutLocalConfiguration1 = createMediaItem("mediaId1");
+ MediaItem mediaItemWithoutLocalConfiguration2 = createMediaItem("mediaId2");
+ List mediaItemsWithoutLocalConfiguration =
+ ImmutableList.of(mediaItemWithoutLocalConfiguration1, mediaItemWithoutLocalConfiguration2);
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.setMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
+
+ Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
+ assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
+ .isFalse();
+ assertThat(player.mediaItems).isEmpty();
+ }
+
+ @Test
+ public void
+ onAddMediaItemsDefault_withSetMediaItemIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_itemSet()
+ throws Exception {
+ MediaItem mediaItem = createMediaItem("mediaId");
+ MediaItem mediaItemWithLocalConfiguration = updateMediaItemWithLocalConfiguration(mediaItem);
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.setMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
+ player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
+
+ assertThat(player.mediaItems).containsExactly(mediaItemWithLocalConfiguration);
+ }
+
+ @Test
+ public void
+ onAddMediaItemsDefault_withSetMediaItemsIncludeLocalConfiguration_mediaItemsContainLocalConfiguration_itemsSet()
+ throws Exception {
+ MediaItem mediaItem1 = createMediaItem("mediaId1");
+ MediaItem mediaItem2 = createMediaItem("mediaId2");
+ List fullMediaItems =
+ updateMediaItemsWithLocalConfiguration(ImmutableList.of(mediaItem1, mediaItem2));
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.setMediaItemsIncludeLocalConfiguration(fullMediaItems);
+ player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS);
+
+ assertThat(player.mediaItems).containsExactlyElementsIn(fullMediaItems).inOrder();
+ }
+
@Test
public void onAddMediaItems_withSetMediaItemWithStartPosition() throws Exception {
MediaItem mediaItem = createMediaItem("mediaId");
@@ -551,6 +630,83 @@ public class MediaSessionCallbackTest {
assertThat(player.mediaItems).containsExactly(updateMediaItemWithLocalConfiguration(mediaItem));
}
+ @Test
+ public void
+ onAddMediaItems_withAddMediaItemIncludeLocalConfiguration_mediaItemDoesntContainLocalConfiguration_noItemsAdded()
+ throws Exception {
+ MediaItem mediaItemWithoutLocalConfiguration = createMediaItem("mediaId");
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.addMediaItemIncludeLocalConfiguration(mediaItemWithoutLocalConfiguration);
+
+ Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
+ assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isFalse();
+ assertThat(player.mediaItems).isEmpty();
+ }
+
+ @Test
+ public void
+ onAddMediaItems_withAddMediaItemsIncludeLocalConfiguration_mediaItemsDontContainLocalConfiguration_noItemsAdded()
+ throws Exception {
+ MediaItem mediaItemWithoutLocalConfiguration1 = createMediaItem("mediaId1");
+ MediaItem mediaItemWithoutLocalConfiguration2 = createMediaItem("mediaId2");
+ List mediaItemsWithoutLocalConfiguration =
+ ImmutableList.of(mediaItemWithoutLocalConfiguration1, mediaItemWithoutLocalConfiguration2);
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.addMediaItemsIncludeLocalConfiguration(mediaItemsWithoutLocalConfiguration);
+
+ Thread.sleep(NO_RESPONSE_TIMEOUT_MS);
+ assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isFalse();
+ assertThat(player.mediaItems).isEmpty();
+ }
+
+ @Test
+ public void
+ onAddMediaItems_withAddMediaItemIncludeLocalConfiguration_mediaItemContainsLocalConfiguration_itemAdded()
+ throws Exception {
+ MediaItem mediaItem = createMediaItem("mediaId");
+ MediaItem mediaItemWithLocalConfiguration = updateMediaItemWithLocalConfiguration(mediaItem);
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.addMediaItemIncludeLocalConfiguration(mediaItemWithLocalConfiguration);
+ player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
+
+ assertThat(player.mediaItems).containsExactly(mediaItemWithLocalConfiguration);
+ }
+
+ @Test
+ public void
+ onAddMediaItems_withAddMediaItemsIncludeLocalConfiguration_mediaItemsContainLocalConfiguration_itemsAdded()
+ throws Exception {
+ MediaItem mediaItem1 = createMediaItem("mediaId1");
+ MediaItem mediaItem2 = createMediaItem("mediaId2");
+ List fullMediaItems =
+ updateMediaItemsWithLocalConfiguration(ImmutableList.of(mediaItem1, mediaItem2));
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build());
+ RemoteMediaController controller =
+ controllerTestRule.createRemoteController(session.getToken());
+
+ // Default MediaSession.Callback.onAddMediaItems will be called
+ controller.addMediaItemsIncludeLocalConfiguration(fullMediaItems);
+ player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS);
+
+ assertThat(player.mediaItems).containsAtLeastElementsIn(fullMediaItems).inOrder();
+ }
+
@Test
public void onAddMediaItems_withAddMediaItemWithIndex() throws Exception {
MediaItem existingItem = createMediaItem("existingItem");
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java
index 18449ba820..56e891b4a4 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/RemoteMediaController.java
@@ -144,6 +144,10 @@ public class RemoteMediaController {
binder.setMediaItem(controllerId, mediaItem.toBundle());
}
+ public void setMediaItemIncludeLocalConfiguration(MediaItem mediaItem) throws RemoteException {
+ binder.setMediaItem(controllerId, mediaItem.toBundleIncludeLocalConfiguration());
+ }
+
public void setMediaItem(MediaItem mediaItem, long startPositionMs) throws RemoteException {
binder.setMediaItemWithStartPosition(controllerId, mediaItem.toBundle(), startPositionMs);
}
@@ -156,6 +160,13 @@ public class RemoteMediaController {
binder.setMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems));
}
+ public void setMediaItemsIncludeLocalConfiguration(List mediaItems)
+ throws RemoteException {
+ binder.setMediaItems(
+ controllerId,
+ BundleableUtil.toBundleList(mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
+ }
+
public void setMediaItems(List mediaItems, boolean resetPosition)
throws RemoteException {
binder.setMediaItemsWithResetPosition(
@@ -186,6 +197,10 @@ public class RemoteMediaController {
binder.addMediaItem(controllerId, mediaItem.toBundle());
}
+ public void addMediaItemIncludeLocalConfiguration(MediaItem mediaItem) throws RemoteException {
+ binder.addMediaItem(controllerId, mediaItem.toBundleIncludeLocalConfiguration());
+ }
+
public void addMediaItem(int index, MediaItem mediaItem) throws RemoteException {
binder.addMediaItemWithIndex(controllerId, index, mediaItem.toBundle());
}
@@ -194,6 +209,13 @@ public class RemoteMediaController {
binder.addMediaItems(controllerId, BundleableUtil.toBundleList(mediaItems));
}
+ public void addMediaItemsIncludeLocalConfiguration(List mediaItems)
+ throws RemoteException {
+ binder.addMediaItems(
+ controllerId,
+ BundleableUtil.toBundleList(mediaItems, MediaItem::toBundleIncludeLocalConfiguration));
+ }
+
public void addMediaItems(int index, List mediaItems) throws RemoteException {
binder.addMediaItemsWithIndex(controllerId, index, BundleableUtil.toBundleList(mediaItems));
}