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)); }