From d5f093f43cc2bda763436d4ecf32c38c76b9418e Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 16 Oct 2023 10:13:16 -0700 Subject: [PATCH] Send `ConnectionState` as in-process bundle if possible #minor-release PiperOrigin-RevId: 573849858 --- .../media3/session/ConnectionState.java | 29 +++++++- .../media3/session/MediaSessionStub.java | 5 +- .../session/MediaSessionCallbackTest.java | 73 +++++++++++++++++-- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java index 6205047a4d4..dca592fc2cc 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectionState.java @@ -18,12 +18,14 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import android.app.PendingIntent; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import androidx.annotation.Nullable; import androidx.core.app.BundleCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.Player; +import androidx.media3.common.util.BundleUtil; import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; @@ -69,13 +71,13 @@ public ConnectionState( this.libraryVersion = libraryVersion; this.sessionInterfaceVersion = sessionInterfaceVersion; this.sessionBinder = sessionBinder; + this.sessionActivity = sessionActivity; + this.customLayout = customLayout; this.sessionCommands = sessionCommands; this.playerCommandsFromSession = playerCommandsFromSession; this.playerCommandsFromPlayer = playerCommandsFromPlayer; - this.sessionActivity = sessionActivity; this.tokenExtras = tokenExtras; this.playerInfo = playerInfo; - this.customLayout = customLayout; } // Bundleable implementation. @@ -90,8 +92,9 @@ public ConnectionState( private static final String FIELD_TOKEN_EXTRAS = Util.intToStringMaxRadix(6); private static final String FIELD_PLAYER_INFO = Util.intToStringMaxRadix(7); private static final String FIELD_SESSION_INTERFACE_VERSION = Util.intToStringMaxRadix(8); + private static final String FIELD_IN_PROCESS_BINDER = Util.intToStringMaxRadix(10); - // Next field key = 10 + // Next field key = 11 @Override public Bundle toBundle() { @@ -119,10 +122,24 @@ public Bundle toBundle() { return bundle; } + /** + * Returns a {@link Bundle} that stores a direct object reference to this class for in-process + * sharing. + */ + public Bundle toBundleInProcess() { + Bundle bundle = new Bundle(); + BundleUtil.putBinder(bundle, FIELD_IN_PROCESS_BINDER, new InProcessBinder()); + return bundle; + } + /** Object that can restore a {@link ConnectionState} from a {@link Bundle}. */ public static final Creator CREATOR = ConnectionState::fromBundle; private static ConnectionState fromBundle(Bundle bundle) { + @Nullable IBinder inProcessBinder = BundleUtil.getBinder(bundle, FIELD_IN_PROCESS_BINDER); + if (inProcessBinder instanceof InProcessBinder) { + return ((InProcessBinder) inProcessBinder).getConnectionState(); + } int libraryVersion = bundle.getInt(FIELD_LIBRARY_VERSION, /* defaultValue= */ 0); int sessionInterfaceVersion = bundle.getInt(FIELD_SESSION_INTERFACE_VERSION, /* defaultValue= */ 0); @@ -169,4 +186,10 @@ private static ConnectionState fromBundle(Bundle bundle) { tokenExtras == null ? Bundle.EMPTY : tokenExtras, playerInfo); } + + private final class InProcessBinder extends Binder { + public ConnectionState getConnectionState() { + return ConnectionState.this; + } + } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index f9648eee6d3..f2c2c3797ee 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -524,7 +524,10 @@ public void connect(IMediaController caller, ControllerInfo controllerInfo) { } try { caller.onConnected( - sequencedFutureManager.obtainNextSequenceNumber(), state.toBundle()); + sequencedFutureManager.obtainNextSequenceNumber(), + caller instanceof MediaControllerStub + ? state.toBundleInProcess() + : state.toBundle()); connected = true; } catch (RemoteException e) { // Controller may be died prematurely. 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 3f46d97b391..7a013f98b9f 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 @@ -30,18 +30,21 @@ import android.content.Context; import android.os.Bundle; import android.text.TextUtils; +import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.Player; import androidx.media3.common.Rating; import androidx.media3.common.StarRating; +import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.test.session.R; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; import androidx.media3.test.session.common.TestUtils; +import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; @@ -650,7 +653,7 @@ public ListenableFuture> onAddMediaItems( RemoteMediaController controller = controllerTestRule.createRemoteController(session.getToken()); - controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 1234); + controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 1234); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); @@ -950,7 +953,7 @@ public ListenableFuture onSetMediaItem new MediaSession.MediaItemsWithStartPosition( updateMediaItemsWithLocalConfiguration(mediaItems), startIndex, - /* startPosition= */ 200)); + /* startPositionMs= */ 200)); } }; MediaSession session = @@ -959,7 +962,7 @@ public ListenableFuture onSetMediaItem RemoteMediaController controller = controllerTestRule.createRemoteController(session.getToken()); - controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 100); + controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); @@ -999,7 +1002,7 @@ public ListenableFuture onSetMediaItem RemoteMediaController controller = controllerTestRule.createRemoteController(session.getToken()); - controller.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 100); + controller.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 100); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); @@ -1045,7 +1048,7 @@ public ListenableFuture onSetMediaItem player.currentPosition = 200; // Re-set media items with start index and position as current index and position - controller.setMediaItems(mediaItems, C.INDEX_UNSET, /* startPosition= */ 0); + controller.setMediaItems(mediaItems, C.INDEX_UNSET, /* startPositionMs= */ 0); player.awaitMethodCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX, TIMEOUT_MS); assertThat(requestedMediaItems.get()).containsExactlyElementsIn(mediaItems).inOrder(); @@ -1201,6 +1204,66 @@ public void onDisconnected(MediaSession session, ControllerInfo controller) { assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + @Test + public void seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents() + throws Exception { + MediaItem mediaItem1 = + new MediaItem.Builder().setMediaId("id1").setUri("http://www.example.com/1").build(); + MediaItem mediaItem2 = + new MediaItem.Builder().setMediaId("id2").setUri("http://www.example.com/2").build(); + ExoPlayer testPlayer = + threadTestRule + .getHandler() + .postAndSync( + () -> { + ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build(); + exoPlayer.setMediaItems(ImmutableList.of(mediaItem1, mediaItem2)); + return exoPlayer; + }); + List capturedMediaItemIds = new ArrayList<>(); + List capturedEvents = new ArrayList<>(); + List eventOrder = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, testPlayer) + .setId("seekToNextMediaItem_inProcessController_correctMediaItemTransitionsEvents") + .build()); + MediaController controller = + new MediaController.Builder(ApplicationProvider.getApplicationContext(), session.getToken()) + .setApplicationLooper(threadTestRule.getHandler().getLooper()) + .buildAsync() + .get(); + controller.addListener( + new Player.Listener() { + @Override + public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { + capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId); + eventOrder.add("onMediaItemTransition"); + } + + @Override + public void onEvents(Player player, Player.Events events) { + if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { + capturedMediaItemIds.add(controller.getCurrentMediaItem().mediaId); + capturedEvents.add(events); + eventOrder.add("onEvents"); + latch.countDown(); + } + } + }); + + threadTestRule.getHandler().postAndSync(testPlayer::seekToNextMediaItem); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(capturedMediaItemIds).containsExactly("id2", "id2").inOrder(); + assertThat(eventOrder).containsExactly("onMediaItemTransition", "onEvents").inOrder(); + assertThat(capturedEvents).hasSize(1); + assertThat(capturedEvents.get(0).size()).isEqualTo(2); + assertThat(capturedEvents.get(0).contains(Player.EVENT_MEDIA_ITEM_TRANSITION)).isTrue(); + assertThat(capturedEvents.get(0).contains(Player.EVENT_POSITION_DISCONTINUITY)).isTrue(); + } + private static MediaItem updateMediaItemWithLocalConfiguration(MediaItem mediaItem) { return mediaItem.buildUpon().setUri(METADATA_MEDIA_URI).build(); }