From bfd1a2724c660de0df3c13f8394238ac6aa26e68 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 19 Oct 2023 05:36:49 -0700 Subject: [PATCH] Add missing command checks to playback resumption flow Player methods shouldn't be called if they are not available and the entry point to the playback resumption flow only checks COMMAND_PLAY_PAUSE. #minor-release PiperOrigin-RevId: 574834148 --- .../androidx/media3/session/MediaSession.java | 14 +- .../media3/session/MediaSessionImpl.java | 126 +++++++------ .../session/MediaSessionLegacyStub.java | 13 +- .../media3/session/MediaSessionStub.java | 11 +- .../session/MediaSessionCallbackTest.java | 112 ++++++++++- ...CallbackWithMediaControllerCompatTest.java | 178 +++++++++++++----- 6 files changed, 317 insertions(+), 137 deletions(-) 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 d4a8009856..ba653118d7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -1423,11 +1423,19 @@ public class MediaSession { } /** - * Returns the last recent playlist of the player with which the player should be prepared when - * playback resumption from a media button receiver or the System UI notification is requested. + * Returns the playlist with which the player should be prepared when a controller requests to + * play without a current {@link MediaItem}. + * + *

This happens, for example, if playback + * resumption is requested from a media button receiver or the System UI notification. + * + *

The method will only be called if the {@link Player} has {@link + * Player#COMMAND_GET_CURRENT_MEDIA_ITEM} and either {@link Player#COMMAND_SET_MEDIA_ITEM} or + * {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available. * * @param mediaSession The media session for which playback resumption is requested. - * @param controller The controller that requests the playback resumption. This is a short + * @param controller The controller that requests the playback resumption. This may be a short * living controller created only for issuing a play command for resuming playback. * @return The {@linkplain MediaItemsWithStartPosition playlist} to resume playback with. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 4fe78f23da..79253148b6 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -25,6 +25,8 @@ import static android.view.KeyEvent.KEYCODE_MEDIA_REWIND; import static android.view.KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD; import static android.view.KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD; import static android.view.KeyEvent.KEYCODE_MEDIA_STOP; +import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS; +import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.SDK_INT; @@ -33,7 +35,6 @@ import static androidx.media3.session.MediaSessionStub.UNKNOWN_SEQUENCE_NUMBER; import static androidx.media3.session.SessionResult.RESULT_ERROR_SESSION_DISCONNECTED; import static androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN; import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED; -import static java.lang.Math.min; import android.app.PendingIntent; import android.content.ComponentName; @@ -55,10 +56,8 @@ import androidx.annotation.CheckResult; import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import androidx.core.os.ExecutorCompat; import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.AudioAttributes; -import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; @@ -87,14 +86,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; 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.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -815,67 +812,72 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } /** - * Attempts to prepare and play for playback resumption. + * Handles a play request from a media controller. * - *

If playlist data for playback resumption can be successfully obtained, the media items are - * set and the player is prepared. {@link Player#play()} is called regardless of success or - * failure of playback resumption. + *

Attempts to prepare and play for playback resumption if the playlist is empty. {@link + * Player#play()} is called regardless of success or failure of playback resumption. * - * @param controller The controller requesting playback resumption. - * @param player The player to setup for playback resumption. + * @param controller The controller requesting to play. */ - /* package */ void prepareAndPlayForPlaybackResumption(ControllerInfo controller, Player player) { - verifyApplicationThread(); - @Nullable - ListenableFuture future = - checkNotNull( - callback.onPlaybackResumption(instance, resolveControllerInfoForCallback(controller)), - "Callback.onPlaybackResumption must return a non-null future"); - // Use a direct executor when an immediate future is returned to execute the player setup in the - // caller's looper event on the application thread. - Executor executor = - future.isDone() - ? MoreExecutors.directExecutor() - : ExecutorCompat.create(getApplicationHandler()); - Futures.addCallback( - future, - new FutureCallback() { - @Override - public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { - ImmutableList mediaItems = mediaItemsWithStartPosition.mediaItems; - player.setMediaItems( - mediaItems, - mediaItemsWithStartPosition.startIndex != C.INDEX_UNSET - ? min(mediaItems.size() - 1, mediaItemsWithStartPosition.startIndex) - : 0, - mediaItemsWithStartPosition.startPositionMs); - if (player.getPlaybackState() == Player.STATE_IDLE) { - player.prepare(); + /* package */ void handleMediaControllerPlayRequest(ControllerInfo controller) { + if (!onPlayRequested()) { + // Request denied, e.g. due to missing foreground service abilities. + return; + } + boolean hasCurrentMediaItem = + playerWrapper.isCommandAvailable(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + && playerWrapper.getCurrentMediaItem() != null; + boolean canAddMediaItems = + playerWrapper.isCommandAvailable(COMMAND_SET_MEDIA_ITEM) + || playerWrapper.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS); + if (hasCurrentMediaItem || !canAddMediaItems) { + // No playback resumption needed or possible. + if (!hasCurrentMediaItem) { + Log.w( + TAG, + "Play requested without current MediaItem, but playback resumption prevented by" + + " missing available commands"); + } + Util.handlePlayButtonAction(playerWrapper); + } else { + @Nullable + ListenableFuture future = + checkNotNull( + callback.onPlaybackResumption(instance, resolveControllerInfoForCallback(controller)), + "Callback.onPlaybackResumption must return a non-null future"); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { + MediaUtils.setMediaItemsWithStartIndexAndPosition( + playerWrapper, mediaItemsWithStartPosition); + Util.handlePlayButtonAction(playerWrapper); } - player.play(); - } - @Override - public void onFailure(Throwable t) { - if (t instanceof UnsupportedOperationException) { - Log.w( - TAG, - "UnsupportedOperationException: Make sure to implement" - + " MediaSession.Callback.onPlaybackResumption() if you add a" - + " media button receiver to your manifest or if you implement the recent" - + " media item contract with your MediaLibraryService.", - t); - } else { - Log.e( - TAG, - "Failure calling MediaSession.Callback.onPlaybackResumption(): " + t.getMessage(), - t); + @Override + public void onFailure(Throwable t) { + if (t instanceof UnsupportedOperationException) { + Log.w( + TAG, + "UnsupportedOperationException: Make sure to implement" + + " MediaSession.Callback.onPlaybackResumption() if you add a" + + " media button receiver to your manifest or if you implement the recent" + + " media item contract with your MediaLibraryService.", + t); + } else { + Log.e( + TAG, + "Failure calling MediaSession.Callback.onPlaybackResumption(): " + + t.getMessage(), + t); + } + // Play as requested even if playback resumption fails. + Util.handlePlayButtonAction(playerWrapper); } - // Play as requested either way. - Util.handlePlayButtonAction(player); - } - }, - executor); + }, + this::postOrRunOnApplicationHandler); + } } private void setAvailableFrameworkControllerCommands( @@ -1147,6 +1149,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return true; } + private void postOrRunOnApplicationHandler(Runnable runnable) { + Util.postOrRun(getApplicationHandler(), runnable); + } + /* @FunctionalInterface */ interface RemoteControllerTask { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index d9e177dc8a..b63abdad8d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -411,18 +411,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; public void onPlay() { dispatchSessionTaskWithPlayerCommand( COMMAND_PLAY_PAUSE, - controller -> { - if (sessionImpl.onPlayRequested()) { - PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper(); - if (playerWrapper.getMediaItemCount() == 0) { - // The player is in IDLE or ENDED state and has no media items in the playlist yet. - // Handle the play command as a playback resumption command to try resume playback. - sessionImpl.prepareAndPlayForPlaybackResumption(controller, playerWrapper); - } else { - Util.handlePlayButtonAction(playerWrapper); - } - } - }, + sessionImpl::handleMediaControllerPlayRequest, sessionCompat.getCurrentControllerInfo()); } 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 6f449112c2..bc6be4088b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -719,16 +719,7 @@ import java.util.concurrent.ExecutionException; if (impl == null || impl.isReleased()) { return; } - if (impl.onPlayRequested()) { - if (player.getMediaItemCount() == 0) { - // The player is in IDLE or ENDED state and has no media items in the playlist - // yet. Handle the play command as a playback resumption command to try resume - // playback. - impl.prepareAndPlayForPlaybackResumption(controller, player); - } else { - Util.handlePlayButtonAction(player); - } - } + impl.handleMediaControllerPlayRequest(controller); })); } 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 192ea93ba6..7c9446b188 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 @@ -1073,8 +1073,7 @@ public class MediaSessionCallbackTest { } @Test - public void onPlay_withEmptyTimelinePlaybackResumptionOn_callsOnGetPlaybackResumptionPlaylist() - throws Exception { + public void onPlay_withEmptyTimeline_callsOnGetPlaybackResumptionPlaylist() throws Exception { List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); MediaSession.Callback callback = new MediaSession.Callback() { @@ -1093,8 +1092,8 @@ public class MediaSessionCallbackTest { remoteControllerTestRule.createRemoteController(session.getToken()); controller.play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isTrue(); @@ -1104,18 +1103,110 @@ public class MediaSessionCallbackTest { } @Test - public void onPlay_withEmptyTimelineCallbackFailure_callsHandlePlayButtonAction() - throws Exception { - player.startMediaItemIndex = 7; - player.startPositionMs = 321L; + public void + onPlay_withEmptyTimelineWithoutCommandGetCurrentMediaItem_doesNotTriggerPlaybackResumption() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build(); MediaSession session = sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); RemoteMediaController controller = remoteControllerTestRule.createRemoteController(session.getToken()); controller.play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) + .isFalse(); + assertThat(player.mediaItems).isEmpty(); + } + + @Test + public void + onPlay_withEmptyTimelineWithoutCommandSetOrChangeMediaItems_doesNotTriggerPlaybackResumption() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .removeAll(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build(); + MediaSession session = + sessionTestRule.ensureReleaseAfterTest(new MediaSession.Builder(context, player).build()); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + controller.play(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) + .isFalse(); + assertThat(player.mediaItems).isEmpty(); + } + + @Test + public void onPlay_withEmptyTimelineWithoutCommandChangeMediaItems_setsSingleItem() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build(); + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onPlaybackResumption( + MediaSession mediaSession, ControllerInfo controller) { + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L)); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + controller.play(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEM_WITH_START_POSITION)) + .isTrue(); + assertThat(player.startMediaItemIndex).isEqualTo(0); + assertThat(player.startPositionMs).isEqualTo(123L); + assertThat(player.mediaItems).containsExactly(mediaItems.get(0)); + } + + @Test + public void + onPlay_withEmptyTimelinePlaybackResumptionCallbackFailure_callsHandlePlayButtonAction() + throws Exception { + player.startMediaItemIndex = 7; + player.startPositionMs = 321L; + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onPlaybackResumption( + MediaSession mediaSession, ControllerInfo controller) { + return Futures.immediateFailedFuture(new UnsupportedOperationException()); + } + }; + MediaSession session = + sessionTestRule.ensureReleaseAfterTest( + new MediaSession.Builder(context, player).setCallback(callback).build()); + RemoteMediaController controller = + remoteControllerTestRule.createRemoteController(session.getToken()); + + controller.play(); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isFalse(); @@ -1150,8 +1241,8 @@ public class MediaSessionCallbackTest { remoteControllerTestRule.createRemoteController(session.getToken()); controller.play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isFalse(); @@ -1188,6 +1279,7 @@ public class MediaSessionCallbackTest { remoteControllerTestRule.createRemoteController( session.getToken(), /* waitForConnection= */ false, testConnectionHints); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(TestUtils.equals(testConnectionHints, connectionHints.get())).isTrue(); } @@ -1213,7 +1305,9 @@ public class MediaSessionCallbackTest { .build()); RemoteMediaController controller = remoteControllerTestRule.createRemoteController(session.getToken()); + controller.release(); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java index 484ccb9f0a..65ebd75a8b 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionCallbackWithMediaControllerCompatTest.java @@ -155,11 +155,13 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { .build(); // Make onDisconnected() to be called immediately after the connection. session.setLegacyControllerConnectionTimeoutMs(0); + controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); // Invoke any command for session to recognize the controller compat. controller.getTransportControls().seekTo(111); + assertThat(disconnectedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } @@ -221,8 +223,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); } @@ -240,9 +242,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); } @@ -261,8 +263,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); } @@ -280,9 +282,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); } @@ -305,8 +307,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().play(); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION)).isFalse(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isFalse(); } @@ -323,7 +325,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().pause(); - player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); } @@ -339,7 +340,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().stop(); - player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); } @@ -355,7 +355,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().prepare(); - player.awaitMethodCalled(MockPlayer.METHOD_PREPARE, TIMEOUT_MS); } @@ -369,11 +368,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - long seekPosition = 12125L; - controller.getTransportControls().seekTo(seekPosition); + controller.getTransportControls().seekTo(seekPosition); player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS); + assertThat(player.seekPositionMs).isEqualTo(seekPosition); } @@ -387,11 +386,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - float testSpeed = 2.0f; - controller.getTransportControls().setPlaybackSpeed(testSpeed); + controller.getTransportControls().setPlaybackSpeed(testSpeed); player.awaitMethodCalled(MockPlayer.METHOD_SET_PLAYBACK_SPEED, TIMEOUT_MS); + assertThat(player.playbackParameters.speed).isEqualTo(testSpeed); } @@ -425,15 +424,15 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); - // Prepare an item to add. String mediaId = "newMediaItemId"; Uri mediaUri = Uri.parse("https://test.test"); MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); - controller.addQueueItem(desc); + controller.addQueueItem(desc); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); @@ -471,16 +470,16 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { player.timeline = MediaTestUtils.createTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); - // Prepare an item to add. int testIndex = 1; String mediaId = "media_id"; Uri mediaUri = Uri.parse("https://test.test"); MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(mediaId).setMediaUri(mediaUri).build(); - controller.addQueueItem(desc, testIndex); + controller.addQueueItem(desc, testIndex); player.awaitMethodCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS_WITH_INDEX, TIMEOUT_MS); + assertThat(requestedMediaItems.get()).hasSize(1); assertThat(requestedMediaItems.get().get(0).mediaId).isEqualTo(mediaId); assertThat(requestedMediaItems.get().get(0).requestMetadata.mediaUri).isEqualTo(mediaUri); @@ -507,15 +506,15 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { player.timeline = new PlaylistTimeline(mediaItems); player.notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); }); - // Select an item to remove. int targetIndex = 3; MediaItem targetItem = mediaItems.get(targetIndex); MediaDescriptionCompat desc = new MediaDescriptionCompat.Builder().setMediaId(targetItem.mediaId).build(); - controller.removeQueueItem(desc); + controller.removeQueueItem(desc); player.awaitMethodCalled(MockPlayer.METHOD_REMOVE_MEDIA_ITEM, TIMEOUT_MS); + assertThat(player.index).isEqualTo(targetIndex); } @@ -531,7 +530,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToPrevious(); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); } @@ -553,7 +551,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToPrevious(); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS_MEDIA_ITEM, TIMEOUT_MS); } @@ -569,7 +566,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToNext(); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT, TIMEOUT_MS); } @@ -588,7 +584,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); controller.getTransportControls().skipToNext(); - player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_NEXT_MEDIA_ITEM, TIMEOUT_MS); } @@ -613,9 +608,9 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { List queue = session.getSessionCompat().getController().getQueue(); int targetIndex = 3; controller.getTransportControls().skipToQueueItem(queue.get(targetIndex).getQueueId()); - player.awaitMethodCalled( MockPlayer.METHOD_SEEK_TO_DEFAULT_POSITION_WITH_MEDIA_ITEM_INDEX, TIMEOUT_MS); + assertThat(player.seekMediaItemIndex).isEqualTo(targetIndex); } @@ -643,8 +638,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); @@ -655,20 +650,121 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { @Test public void - dispatchMediaButtonEvent_playWithEmptyTimelineCallbackFailure_callsHandlePlayButtonAction() + dispatchMediaButtonEvent_playWithEmptyTimelineWithoutCommandGetCurrentMediaItem_doesNotTriggerPlaybackResumption() throws Exception { - player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); - player.startMediaItemIndex = 1; - player.startPositionMs = 321L; - session = new MediaSession.Builder(context, player).setId("sendMediaButtonEvent").build(); + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_GET_CURRENT_MEDIA_ITEM) + .build(); + session = new MediaSession.Builder(context, player).setId("dispatchMediaButtonEvent").build(); controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) + .isFalse(); + assertThat(player.mediaItems).isEmpty(); + } + + @Test + public void + dispatchMediaButtonEvent_playWithEmptyTimelineWithoutCommandSetOrChangeMediaItems_doesNotTriggerPlaybackResumption() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .removeAll(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build(); + session = new MediaSession.Builder(context, player).setId("dispatchMediaButtonEvent").build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); + + session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) + .isFalse(); + assertThat(player.mediaItems).isEmpty(); + } + + @Test + public void + dispatchMediaButtonEvent_playWithEmptyTimelineWithoutCommandChangeMediaItems_setsSingleItem() + throws Exception { + player.commands = + new Player.Commands.Builder() + .addAllCommands() + .remove(Player.COMMAND_CHANGE_MEDIA_ITEMS) + .build(); + List mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onPlaybackResumption( + MediaSession mediaSession, ControllerInfo controller) { + return Futures.immediateFuture( + new MediaSession.MediaItemsWithStartPosition( + mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 123L)); + } + }; + session = + new MediaSession.Builder(context, player) + .setCallback(callback) + .setId("dispatchMediaButtonEvent") + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); + + session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEM_WITH_START_POSITION)) + .isTrue(); + assertThat(player.startMediaItemIndex).isEqualTo(0); + assertThat(player.startPositionMs).isEqualTo(123L); + assertThat(player.mediaItems).containsExactly(mediaItems.get(0)); + } + + @Test + public void + dispatchMediaButtonEvent_playWithEmptyTimelineCallbackFailure_callsHandlePlayButtonAction() + throws Exception { + player.mediaItems = MediaTestUtils.createMediaItems(/* size= */ 3); + player.startMediaItemIndex = 1; + player.startPositionMs = 321L; + MediaSession.Callback callback = + new MediaSession.Callback() { + @Override + public ListenableFuture onPlaybackResumption( + MediaSession mediaSession, ControllerInfo controller) { + return Futures.immediateFailedFuture(new UnsupportedOperationException()); + } + }; + session = + new MediaSession.Builder(context, player) + .setCallback(callback) + .setId("sendMediaButtonEvent") + .build(); + controller = + new RemoteMediaControllerCompat( + context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); + KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY); + + session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isFalse(); @@ -707,8 +803,8 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); session.getSessionCompat().getController().dispatchMediaButtonEvent(keyEvent); - player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX)) .isFalse(); @@ -727,11 +823,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - @PlaybackStateCompat.ShuffleMode int testShuffleMode = PlaybackStateCompat.SHUFFLE_MODE_GROUP; - controller.getTransportControls().setShuffleMode(testShuffleMode); + controller.getTransportControls().setShuffleMode(testShuffleMode); player.awaitMethodCalled(MockPlayer.METHOD_SET_SHUFFLE_MODE, TIMEOUT_MS); + assertThat(player.shuffleModeEnabled).isTrue(); } @@ -745,11 +841,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { controller = new RemoteMediaControllerCompat( context, session.getSessionCompat().getSessionToken(), /* waitForConnection= */ true); - int testRepeatMode = Player.REPEAT_MODE_ALL; - controller.getTransportControls().setRepeatMode(testRepeatMode); + controller.getTransportControls().setRepeatMode(testRepeatMode); player.awaitMethodCalled(MockPlayer.METHOD_SET_REPEAT_MODE, TIMEOUT_MS); + assertThat(player.repeatMode).isEqualTo(testRepeatMode); } @@ -777,11 +873,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { remotePlayer.deviceVolume = 23; session.setPlayer(remotePlayer); }); - int targetVolume = 50; - controller.setVolumeTo(targetVolume, /* flags= */ 0); + controller.setVolumeTo(targetVolume, /* flags= */ 0); remotePlayer.awaitMethodCalled(MockPlayer.METHOD_SET_DEVICE_VOLUME, TIMEOUT_MS); + assertThat(remotePlayer.deviceVolume).isEqualTo(targetVolume); } @@ -805,11 +901,11 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { remotePlayer.deviceVolume = 23; session.setPlayer(remotePlayer); }); - int targetVolume = 50; - controller.setVolumeTo(targetVolume, /* flags= */ 0); + controller.setVolumeTo(targetVolume, /* flags= */ 0); remotePlayer.awaitMethodCalled(MockPlayer.METHOD_SET_DEVICE_VOLUME_WITH_FLAGS, TIMEOUT_MS); + assertThat(remotePlayer.deviceVolume).isEqualTo(targetVolume); } @@ -839,7 +935,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { }); controller.adjustVolume(AudioManager.ADJUST_RAISE, /* flags= */ 0); - remotePlayer.awaitMethodCalled(MockPlayer.METHOD_INCREASE_DEVICE_VOLUME, TIMEOUT_MS); } @@ -864,7 +959,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { }); controller.adjustVolume(AudioManager.ADJUST_RAISE, /* flags= */ 0); - remotePlayer.awaitMethodCalled(MockPlayer.METHOD_INCREASE_DEVICE_VOLUME_WITH_FLAGS, TIMEOUT_MS); } @@ -894,7 +988,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { }); controller.adjustVolume(AudioManager.ADJUST_LOWER, /* flags= */ 0); - remotePlayer.awaitMethodCalled(MockPlayer.METHOD_DECREASE_DEVICE_VOLUME, TIMEOUT_MS); } @@ -920,7 +1013,6 @@ public class MediaSessionCallbackWithMediaControllerCompatTest { }); controller.adjustVolume(AudioManager.ADJUST_LOWER, /* flags= */ 0); - remotePlayer.awaitMethodCalled(MockPlayer.METHOD_DECREASE_DEVICE_VOLUME_WITH_FLAGS, TIMEOUT_MS); }