From 7cb7636ed954f0737aaac2f9cf556715a2ee9b37 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Jul 2022 10:34:22 +0000 Subject: [PATCH] Run MediaSessionStub commands in order Some commands are run asynchronously and subsequent commands need to wait until the previous one finished. This can be supported by returning a Future for each command and using the existing command execution logic to wait for each Future to complete. As some MediaSessionStub code is now executed delayed to when it was originally created, we also need to check if the session is not released before triggering any actions or sending result codes. Issue: androidx/media#85 PiperOrigin-RevId: 462101136 --- RELEASENOTES.md | 4 + .../session/ConnectedControllersManager.java | 9 +- .../media3/session/MediaSessionStub.java | 251 +++++++++++------- .../common/IRemoteMediaController.aidl | 1 + .../session/MediaSessionPlayerTest.java | 33 ++- .../MediaControllerProviderService.java | 20 ++ .../media3/session/RemoteMediaController.java | 10 + 7 files changed, 220 insertions(+), 108 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 696e4a02ab..9d6aa87817 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,10 @@ `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, boolean)` to specify whether the renderer will output metadata early or in sync with the player position. +* Session: + * Ensure commands are always executed in the correct order even if some + require asynchronous resolution + ([#85](https://github.com/androidx/media/issues/85)). ### 1.0.0-beta02 (2022-07-15) diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java index 01f2c9a3e0..64b069effb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java @@ -24,7 +24,6 @@ import androidx.collection.ArrayMap; import androidx.media3.common.Player; import androidx.media3.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayDeque; @@ -227,15 +226,11 @@ import org.checkerframework.checker.nullness.qual.NonNull; } } - public void addToCommandQueue(ControllerInfo controllerInfo, Runnable commandRunnable) { + public void addToCommandQueue(ControllerInfo controllerInfo, AsyncCommand asyncCommand) { synchronized (lock) { @Nullable ConnectedControllerRecord info = controllerRecords.get(controllerInfo); if (info != null) { - info.commandQueue.add( - () -> { - commandRunnable.run(); - return Futures.immediateVoidFuture(); - }); + info.commandQueue.add(asyncCommand); } } } 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 5a8892df1d..d895208b0b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -80,8 +80,10 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.SessionCommand.CommandCode; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; @@ -126,52 +128,66 @@ import java.util.concurrent.ExecutionException; } } - private static SessionTask sendSessionResultSuccess( - Consumer task) { + private static + SessionTask, K> sendSessionResultSuccess( + Consumer task) { return (sessionImpl, controller, sequence) -> { + if (sessionImpl.isReleased()) { + return Futures.immediateVoidFuture(); + } task.accept(sessionImpl.getPlayerWrapper()); sendSessionResult(controller, sequence, new SessionResult(SessionResult.RESULT_SUCCESS)); - return null; + return Futures.immediateVoidFuture(); }; } - private static SessionTask sendSessionResultWhenReady( - SessionTask, K> task) { - return (sessionImpl, controller, sequence) -> { - ListenableFuture future = task.run(sessionImpl, controller, sequence); - future.addListener( - () -> { - SessionResult result; - try { - result = checkNotNull(future.get(), "SessionResult must not be null"); - } catch (CancellationException unused) { - result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED); - } catch (ExecutionException | InterruptedException exception) { - result = - new SessionResult( - exception.getCause() instanceof UnsupportedOperationException - ? SessionResult.RESULT_ERROR_NOT_SUPPORTED - : SessionResult.RESULT_ERROR_UNKNOWN); - } - sendSessionResult(controller, sequence, result); - }, - MoreExecutors.directExecutor()); - return null; - }; + private static + SessionTask, K> sendSessionResultWhenReady( + SessionTask, K> task) { + return (sessionImpl, controller, sequence) -> + handleSessionTaskWhenReady( + sessionImpl, + controller, + sequence, + task, + future -> { + SessionResult result; + try { + result = checkNotNull(future.get(), "SessionResult must not be null"); + } catch (CancellationException unused) { + result = new SessionResult(SessionResult.RESULT_INFO_SKIPPED); + } catch (ExecutionException | InterruptedException exception) { + result = + new SessionResult( + exception.getCause() instanceof UnsupportedOperationException + ? SessionResult.RESULT_ERROR_NOT_SUPPORTED + : SessionResult.RESULT_ERROR_UNKNOWN); + } + sendSessionResult(controller, sequence, result); + }); } private static SessionTask, K> handleMediaItemsWhenReady( SessionTask>, K> mediaItemsTask, MediaItemPlayerTask mediaItemPlayerTask) { - return (sessionImpl, controller, sequence) -> - transformFutureAsync( - mediaItemsTask.run(sessionImpl, controller, sequence), - mediaItems -> - postOrRunWithCompletion( - sessionImpl.getApplicationHandler(), - () -> mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems), - new SessionResult(SessionResult.RESULT_SUCCESS))); + return (sessionImpl, controller, sequence) -> { + if (sessionImpl.isReleased()) { + return Futures.immediateFuture( + new SessionResult(SessionResult.RESULT_ERROR_SESSION_DISCONNECTED)); + } + return transformFutureAsync( + mediaItemsTask.run(sessionImpl, controller, sequence), + mediaItems -> + postOrRunWithCompletion( + sessionImpl.getApplicationHandler(), + () -> { + if (!sessionImpl.isReleased()) { + mediaItemPlayerTask.run(sessionImpl.getPlayerWrapper(), mediaItems); + } + }, + new SessionResult(SessionResult.RESULT_SUCCESS))); + }; } private static void sendLibraryResult( @@ -184,29 +200,32 @@ import java.util.concurrent.ExecutionException; } private static - SessionTask sendLibraryResultWhenReady( + SessionTask, K> sendLibraryResultWhenReady( SessionTask>, K> task) { - return (sessionImpl, controller, sequence) -> { - ListenableFuture> future = task.run(sessionImpl, controller, sequence); - future.addListener( - () -> { - LibraryResult result; - try { - result = checkNotNull(future.get(), "LibraryResult must not be null"); - } catch (CancellationException unused) { - result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED); - } catch (ExecutionException | InterruptedException unused) { - result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN); - } - sendLibraryResult(controller, sequence, result); - }, - MoreExecutors.directExecutor()); - return null; - }; + return (sessionImpl, controller, sequence) -> + handleSessionTaskWhenReady( + sessionImpl, + controller, + sequence, + task, + future -> { + LibraryResult result; + try { + result = checkNotNull(future.get(), "LibraryResult must not be null"); + } catch (CancellationException unused) { + result = LibraryResult.ofError(LibraryResult.RESULT_INFO_SKIPPED); + } catch (ExecutionException | InterruptedException unused) { + result = LibraryResult.ofError(LibraryResult.RESULT_ERROR_UNKNOWN); + } + sendLibraryResult(controller, sequence, result); + }); } - private void dispatchSessionTaskWithPlayerCommand( - IMediaController caller, int seq, @Player.Command int command, SessionTask task) { + private void queueSessionTaskWithPlayerCommand( + IMediaController caller, + int seq, + @Player.Command int command, + SessionTask, K> task) { long token = Binder.clearCallingIdentity(); try { @SuppressWarnings({"unchecked", "cast.unsafe"}) @@ -248,13 +267,19 @@ import java.util.concurrent.ExecutionException; } private void dispatchSessionTaskWithSessionCommand( - IMediaController caller, int seq, @CommandCode int commandCode, SessionTask task) { + IMediaController caller, + int seq, + @CommandCode int commandCode, + SessionTask, K> task) { dispatchSessionTaskWithSessionCommand( caller, seq, /* sessionCommand= */ null, commandCode, task); } private void dispatchSessionTaskWithSessionCommand( - IMediaController caller, int seq, SessionCommand sessionCommand, SessionTask task) { + IMediaController caller, + int seq, + SessionCommand sessionCommand, + SessionTask, K> task) { dispatchSessionTaskWithSessionCommand(caller, seq, sessionCommand, COMMAND_CODE_CUSTOM, task); } @@ -263,7 +288,7 @@ import java.util.concurrent.ExecutionException; int seq, @Nullable SessionCommand sessionCommand, @CommandCode int commandCode, - SessionTask task) { + SessionTask, K> task) { long token = Binder.clearCallingIdentity(); try { @SuppressWarnings({"unchecked", "cast.unsafe"}) @@ -308,6 +333,34 @@ import java.util.concurrent.ExecutionException; } } + private static ListenableFuture handleSessionTaskWhenReady( + K sessionImpl, + ControllerInfo controller, + int sequence, + SessionTask, K> task, + Consumer> futureResultHandler) { + if (sessionImpl.isReleased()) { + return Futures.immediateVoidFuture(); + } + ListenableFuture future = task.run(sessionImpl, controller, sequence); + SettableFuture outputFuture = SettableFuture.create(); + future.addListener( + () -> { + if (sessionImpl.isReleased()) { + outputFuture.set(null); + return; + } + try { + futureResultHandler.accept(future); + outputFuture.set(null); + } catch (Throwable error) { + outputFuture.setException(error); + } + }, + MoreExecutors.directExecutor()); + return outputFuture; + } + public void connect( IMediaController caller, int controllerVersion, @@ -480,7 +533,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_STOP, sendSessionResultSuccess(Player::stop)); } @@ -529,7 +582,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::play)); } @@ -538,7 +591,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_PLAY_PAUSE, sendSessionResultSuccess(Player::pause)); } @@ -547,7 +600,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_PREPARE, sendSessionResultSuccess(Player::prepare)); } @@ -556,7 +609,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_DEFAULT_POSITION, @@ -569,7 +622,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_MEDIA_ITEM, @@ -582,7 +635,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, @@ -596,7 +649,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_MEDIA_ITEM, @@ -608,7 +661,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_BACK, sendSessionResultSuccess(Player::seekBack)); } @@ -617,7 +670,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_FORWARD, sendSessionResultSuccess(Player::seekForward)); } @@ -698,7 +751,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_SPEED_AND_PITCH, @@ -713,7 +766,7 @@ import java.util.concurrent.ExecutionException; } PlaybackParameters playbackParameters = PlaybackParameters.CREATOR.fromBundle(playbackParametersBundle); - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_SPEED_AND_PITCH, @@ -733,7 +786,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_MEDIA_ITEM, @@ -760,7 +813,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_MEDIA_ITEM, @@ -788,7 +841,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_MEDIA_ITEM, @@ -815,7 +868,7 @@ import java.util.concurrent.ExecutionException; return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -844,7 +897,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -874,7 +927,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -899,7 +952,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaMetadata", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_MEDIA_ITEMS_METADATA, @@ -918,7 +971,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -942,7 +995,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -968,7 +1021,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -997,7 +1050,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for MediaItem", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -1013,7 +1066,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -1026,7 +1079,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -1038,7 +1091,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, sendSessionResultSuccess(Player::clearMediaItems)); } @@ -1048,7 +1101,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -1061,7 +1114,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_CHANGE_MEDIA_ITEMS, @@ -1073,7 +1126,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, @@ -1085,7 +1138,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, @@ -1097,7 +1150,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_PREVIOUS, sendSessionResultSuccess(Player::seekToPrevious)); } @@ -1106,7 +1159,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SEEK_TO_NEXT, sendSessionResultSuccess(Player::seekToNext)); } @@ -1116,7 +1169,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_REPEAT_MODE, @@ -1129,7 +1182,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_SHUFFLE_MODE, @@ -1142,7 +1195,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_VIDEO_SURFACE, @@ -1154,7 +1207,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_VOLUME, @@ -1166,7 +1219,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_DEVICE_VOLUME, @@ -1178,7 +1231,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_ADJUST_DEVICE_VOLUME, @@ -1190,7 +1243,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_ADJUST_DEVICE_VOLUME, @@ -1202,7 +1255,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_DEVICE_VOLUME, @@ -1214,7 +1267,7 @@ import java.util.concurrent.ExecutionException; if (caller == null) { return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_PLAY_PAUSE, @@ -1258,7 +1311,7 @@ import java.util.concurrent.ExecutionException; Log.w(TAG, "Ignoring malformed Bundle for TrackSelectionParameters", e); return; } - dispatchSessionTaskWithPlayerCommand( + queueSessionTaskWithPlayerCommand( caller, seq, COMMAND_SET_TRACK_SELECTION_PARAMETERS, diff --git a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl index 0f611d7be5..5380bc1215 100644 --- a/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl +++ b/libraries/test_session_common/src/main/aidl/androidx/media3/test/session/common/IRemoteMediaController.aidl @@ -81,6 +81,7 @@ interface IRemoteMediaController { void release(String controllerId); void stop(String controllerId); void setTrackSelectionParameters(String controllerId, in Bundle parameters); + void setMediaItemsPreparePlayAddItemsSeek(String controllerId, in List initialMediaItems, in List addedMediaItems, int seekIndex); // MediaBrowser methods Bundle getLibraryRoot(String controllerId, in Bundle libraryParams); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java index 1d5e92344f..076643c2a2 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionPlayerTest.java @@ -19,19 +19,21 @@ import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PA import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; +import android.os.Handler; +import android.os.HandlerThread; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.util.Util; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.MainLooperTestRule; 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; @@ -58,6 +60,7 @@ public class MediaSessionPlayerTest { private MediaSession session; private MockPlayer player; private RemoteMediaController controller; + private HandlerThread asyncHandlerThread; @Before public void setUp() throws Exception { @@ -66,6 +69,9 @@ public class MediaSessionPlayerTest { .setApplicationLooper(threadTestRule.getHandler().getLooper()) .setMediaItems(/* itemCount= */ 5) .build(); + asyncHandlerThread = new HandlerThread("AsyncHandlerThread"); + asyncHandlerThread.start(); + Handler asyncHandler = new Handler(asyncHandlerThread.getLooper()); session = new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player) .setCallback( @@ -84,7 +90,9 @@ public class MediaSessionPlayerTest { MediaSession mediaSession, MediaSession.ControllerInfo controller, List mediaItems) { - return Futures.immediateFuture(mediaItems); + // Send empty message and return mediaItems once done to simulate asynchronous + // media item resolution. + return Util.postOrRunWithCompletion(asyncHandler, () -> {}, mediaItems); } }) .build(); @@ -97,6 +105,7 @@ public class MediaSessionPlayerTest { public void tearDown() throws Exception { controller.release(); session.release(); + asyncHandlerThread.quit(); } @Test @@ -544,6 +553,26 @@ public class MediaSessionPlayerTest { assertThat(player.trackSelectionParameters).isEqualTo(trackSelectionParameters); } + @Test + public void mixedAsyncAndSyncCommands_calledInCorrectOrder() throws Exception { + List initialItems = MediaTestUtils.createMediaItems(/* size= */ 2); + List addedItems = MediaTestUtils.createMediaItems(/* size= */ 3); + + controller.setMediaItemsPreparePlayAddItemsSeek(initialItems, addedItems, /* seekIndex= */ 3); + player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); + boolean setMediaItemsCalledBeforePrepare = + player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS); + player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + boolean addMediaItemsCalledBeforeSeek = + player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS); + + assertThat(setMediaItemsCalledBeforePrepare).isTrue(); + assertThat(addMediaItemsCalledBeforeSeek).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue(); + assertThat(player.mediaItems).hasSize(5); + assertThat(player.seekMediaItemIndex).isEqualTo(3); + } + private void changePlaybackTypeToRemote() throws Exception { threadTestRule .getHandler() diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java index 7d263e9c71..dffcada3d3 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaControllerProviderService.java @@ -659,6 +659,26 @@ public class MediaControllerProviderService extends Service { }); } + @Override + public void setMediaItemsPreparePlayAddItemsSeek( + String controllerId, + List initialMediaItems, + List addedMediaItems, + int seekIndex) + throws RemoteException { + runOnHandler( + () -> { + MediaController controller = mediaControllerMap.get(controllerId); + controller.setMediaItems( + BundleableUtil.fromBundleList(MediaItem.CREATOR, initialMediaItems)); + controller.prepare(); + controller.play(); + controller.addMediaItems( + BundleableUtil.fromBundleList(MediaItem.CREATOR, addedMediaItems)); + controller.seekTo(seekIndex, /* positionMs= */ 0); + }); + } + //////////////////////////////////////////////////////////////////////////////// // MediaBrowser methods //////////////////////////////////////////////////////////////////////////////// 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 6b0449ce25..bf354f1f8d 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 @@ -292,6 +292,16 @@ public class RemoteMediaController { binder.setTrackSelectionParameters(controllerId, parameters.toBundle()); } + public void setMediaItemsPreparePlayAddItemsSeek( + List initialMediaItems, List addedMediaItems, int seekIndex) + throws RemoteException { + binder.setMediaItemsPreparePlayAddItemsSeek( + controllerId, + BundleableUtil.toBundleList(initialMediaItems), + BundleableUtil.toBundleList(addedMediaItems), + seekIndex); + } + //////////////////////////////////////////////////////////////////////////////// // Non-public methods ////////////////////////////////////////////////////////////////////////////////