diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 63d2cacbdd..d52ea09c2e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,6 +38,9 @@ * Keep notification visible when playback enters an error or stopped state. The notification is only removed if the playlist is cleared or the player is released. + * Improve handling of Android platform MediaSession actions ACTION_PLAY + and ACTION_PAUSE to only set one of them according to the available + commands and also accept if only one of them is set. * UI: * Downloads: * OkHttp Extension: diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index 6ca2d3a721..b5e0f034c1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -77,7 +77,6 @@ import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Log; -import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.legacy.AudioAttributesCompat; import androidx.media3.session.legacy.MediaBrowserCompat; @@ -1027,12 +1026,11 @@ import java.util.concurrent.TimeoutException; /** Converts {@link Player}' states to state of {@link PlaybackStateCompat}. */ @PlaybackStateCompat.State - public static int convertToPlaybackStateCompatState(Player player, boolean playIfSuppressed) { + public static int convertToPlaybackStateCompatState(Player player, boolean shouldShowPlayButton) { if (player.getPlayerError() != null) { return PlaybackStateCompat.STATE_ERROR; } @Player.State int playbackState = player.getPlaybackState(); - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, playIfSuppressed); switch (playbackState) { case Player.STATE_IDLE: return PlaybackStateCompat.STATE_NONE; @@ -1370,8 +1368,9 @@ import java.util.concurrent.TimeoutException; boolean isSessionReady) { Player.Commands.Builder playerCommandsBuilder = new Player.Commands.Builder(); long actions = playbackStateCompat == null ? 0 : playbackStateCompat.getActions(); - if ((hasAction(actions, PlaybackStateCompat.ACTION_PLAY) - && hasAction(actions, PlaybackStateCompat.ACTION_PAUSE)) + boolean playWhenReady = convertToPlayWhenReady(playbackStateCompat); + if ((hasAction(actions, PlaybackStateCompat.ACTION_PLAY) && !playWhenReady) + || (hasAction(actions, PlaybackStateCompat.ACTION_PAUSE) && playWhenReady) || hasAction(actions, PlaybackStateCompat.ACTION_PLAY_PAUSE)) { playerCommandsBuilder.add(COMMAND_PLAY_PAUSE); } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 1d0b4c23af..d1cc1e0ad4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -50,6 +50,7 @@ import androidx.media3.common.VideoSize; import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; import androidx.media3.common.util.Size; +import androidx.media3.common.util.Util; import androidx.media3.session.legacy.MediaSessionCompat; import androidx.media3.session.legacy.PlaybackStateCompat; import androidx.media3.session.legacy.VolumeProviderCompat; @@ -1084,13 +1085,16 @@ import java.util.List; .build(); } @Nullable PlaybackException playerError = getPlayerError(); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(/* player= */ this, playIfSuppressed); int state = - LegacyConversions.convertToPlaybackStateCompatState(/* player= */ this, playIfSuppressed); + LegacyConversions.convertToPlaybackStateCompatState( + /* player= */ this, shouldShowPlayButton); // Always advertise ACTION_SET_RATING. long actions = PlaybackStateCompat.ACTION_SET_RATING; Commands availableCommands = intersect(availablePlayerCommands, getAvailableCommands()); for (int i = 0; i < availableCommands.size(); i++) { - actions |= convertCommandToPlaybackStateActions(availableCommands.get(i)); + actions |= + convertCommandToPlaybackStateActions(availableCommands.get(i), shouldShowPlayButton); } if (!mediaButtonPreferences.isEmpty() && !legacyExtras.getBoolean(MediaConstants.EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV)) { @@ -1346,12 +1350,13 @@ import java.util.List; } @SuppressWarnings("deprecation") // Uses deprecated PlaybackStateCompat actions. - private static long convertCommandToPlaybackStateActions(@Command int command) { + private static long convertCommandToPlaybackStateActions( + @Command int command, boolean shouldShowPlayButton) { switch (command) { case Player.COMMAND_PLAY_PAUSE: - return PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PLAY_PAUSE; + return shouldShowPlayButton + ? PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE + : PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE; case Player.COMMAND_PREPARE: return PlaybackStateCompat.ACTION_PREPARE; case Player.COMMAND_SEEK_BACK: diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java index 931a23edcc..ff49a4f0e3 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java @@ -712,9 +712,31 @@ public final class LegacyConversionsTest { } @Test - public void convertToPlayerCommands_withJustPlayAction_playPauseCommandNotAvailable() { + public void convertToPlayerCommands_withJustPlayActionWhileNotReady_playPauseCommandAvailable() { PlaybackStateCompat playbackStateCompat = - new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PLAY).build(); + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_ERROR, /* position= */ 0, /* playbackSpeed= */ 1f) + .setActions(PlaybackStateCompat.ACTION_PLAY) + .build(); + + Player.Commands playerCommands = + LegacyConversions.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_PLAY_PAUSE); + } + + @Test + public void convertToPlayerCommands_withJustPlayActionWhileReady_playPauseCommandNotAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_BUFFERING, /* position= */ 0, /* playbackSpeed= */ 1f) + .setActions(PlaybackStateCompat.ACTION_PLAY) + .build(); Player.Commands playerCommands = LegacyConversions.convertToPlayerCommands( @@ -727,9 +749,13 @@ public final class LegacyConversionsTest { } @Test - public void convertToPlayerCommands_withJustPauseAction_playPauseCommandNotAvailable() { + public void + convertToPlayerCommands_withJustPauseActionWhileNotReady_playPauseCommandNotAvailable() { PlaybackStateCompat playbackStateCompat = - new PlaybackStateCompat.Builder().setActions(PlaybackStateCompat.ACTION_PAUSE).build(); + new PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_ERROR, /* position= */ 0, /* playbackSpeed= */ 1f) + .setActions(PlaybackStateCompat.ACTION_PAUSE) + .build(); Player.Commands playerCommands = LegacyConversions.convertToPlayerCommands( @@ -741,6 +767,25 @@ public final class LegacyConversionsTest { assertThat(getCommandsAsList(playerCommands)).doesNotContain(Player.COMMAND_PLAY_PAUSE); } + @Test + public void convertToPlayerCommands_withJustPauseActionWhileReady_playPauseCommandAvailable() { + PlaybackStateCompat playbackStateCompat = + new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_BUFFERING, /* position= */ 0, /* playbackSpeed= */ 1f) + .setActions(PlaybackStateCompat.ACTION_PAUSE) + .build(); + + Player.Commands playerCommands = + LegacyConversions.convertToPlayerCommands( + playbackStateCompat, + /* volumeControlType= */ VolumeProviderCompat.VOLUME_CONTROL_FIXED, + /* sessionFlags= */ 0, + /* isSessionReady= */ true); + + assertThat(getCommandsAsList(playerCommands)).contains(Player.COMMAND_PLAY_PAUSE); + } + @Test public void convertToPlayerCommands_withPlayAndPauseAction_playPauseCommandAvailable() { PlaybackStateCompat playbackStateCompat = diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java index eef19dcaf4..8b940e3976 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest.java @@ -78,10 +78,14 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest @Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG); @Test - public void playerWithCommandPlayPause_actionsPlayAndPauseAndPlayPauseAdvertised() + public void playerWithCommandPlayPauseAndShouldShowPlayButton_actionsPlayAndPlayPauseAdvertised() throws Exception { Player player = - createPlayerWithAvailableCommand(createDefaultPlayer(), Player.COMMAND_PLAY_PAUSE); + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav"))), + Player.COMMAND_PLAY_PAUSE); MediaSession mediaSession = createMediaSession(player); MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); @@ -89,7 +93,7 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest assertThat(actions & PlaybackStateCompat.ACTION_PLAY_PAUSE).isNotEqualTo(0); assertThat(actions & PlaybackStateCompat.ACTION_PLAY).isNotEqualTo(0); - assertThat(actions & PlaybackStateCompat.ACTION_PAUSE).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PAUSE).isEqualTo(0); CountDownLatch latch = new CountDownLatch(2); List receivedPlayWhenReady = new ArrayList<>(); @@ -114,6 +118,51 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest releasePlayer(player); } + @Test + public void + playerWithCommandPlayPauseAndShouldShowPauseButton_actionsPauseAndPlayPauseAdvertised() + throws Exception { + Player player = + createPlayerWithAvailableCommand( + createPlayer( + /* onPostCreationTask= */ createdPlayer -> { + createdPlayer.setMediaItem(MediaItem.fromUri("asset://media/wav/sample.wav")); + createdPlayer.prepare(); + createdPlayer.play(); + }), + Player.COMMAND_PLAY_PAUSE); + MediaSession mediaSession = createMediaSession(player); + MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession); + + long actions = controllerCompat.getPlaybackState().getActions(); + + assertThat(actions & PlaybackStateCompat.ACTION_PLAY_PAUSE).isNotEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PLAY).isEqualTo(0); + assertThat(actions & PlaybackStateCompat.ACTION_PAUSE).isNotEqualTo(0); + + CountDownLatch latch = new CountDownLatch(2); + List receivedPlayWhenReady = new ArrayList<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) { + receivedPlayWhenReady.add(playWhenReady); + latch.countDown(); + } + }; + player.addListener(listener); + + controllerCompat.getTransportControls().pause(); + controllerCompat.getTransportControls().play(); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(receivedPlayWhenReady).containsExactly(false, true).inOrder(); + + mediaSession.release(); + releasePlayer(player); + } + @Test public void playerWithoutCommandPlayPause_actionsPlayAndPauseAndPlayPauseNotAdvertised() throws Exception { diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java index 149237e3a6..3cd57ccd81 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionKeyEventTest.java @@ -136,6 +136,14 @@ public class MediaSessionKeyEventTest { @Test public void playKeyEvent() throws Exception { + handler.postAndSync( + () -> { + // Update state to allow play event to be triggered. + player.notifyPlayWhenReadyChanged( + /* playWhenReady= */ false, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + Player.PLAYBACK_SUPPRESSION_REASON_NONE); + }); dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY, false); player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); @@ -143,6 +151,15 @@ public class MediaSessionKeyEventTest { @Test public void pauseKeyEvent() throws Exception { + handler.postAndSync( + () -> { + // Update state to allow pause event to be triggered. + player.notifyPlayWhenReadyChanged( + /* playWhenReady= */ true, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + Player.PLAYBACK_SUPPRESSION_REASON_NONE); + player.notifyPlaybackStateChanged(Player.STATE_READY); + }); dispatchMediaKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE, false); player.awaitMethodCalled(MockPlayer.METHOD_PAUSE, TIMEOUT_MS); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java index a0f99bfe18..e8a60cb7cd 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java @@ -672,16 +672,29 @@ public class MediaSessionTest { }) .build())); MediaSessionImpl impl = session.get().getImpl(); + ControllerInfo controllerInfo = createMediaButtonCaller(); threadTestRule .getHandler() .postAndSync( () -> { - ControllerInfo controllerInfo = createMediaButtonCaller(); assertThat( impl.onMediaButtonEvent( controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PLAY))) .isTrue(); + }); + player.awaitMethodCalled(MockPlayer.METHOD_PLAY, TIMEOUT_MS); + threadTestRule + .getHandler() + .postAndSync( + () -> { + // Update state to allow pause event to be triggered. + player.notifyPlaybackStateChanged(Player.STATE_READY); + player.notifyPlayWhenReadyChanged( + /* playWhenReady= */ true, + Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST, + Player.PLAYBACK_SUPPRESSION_REASON_NONE); + assertThat( impl.onMediaButtonEvent( controllerInfo, getMediaButtonIntent(KEYCODE_MEDIA_PAUSE))) @@ -717,12 +730,10 @@ public class MediaSessionTest { player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO_PREVIOUS, TIMEOUT_MS); player.awaitMethodCalled(MockPlayer.METHOD_STOP, TIMEOUT_MS); assertThat(callerCollectorPlayer.callingControllers).hasSize(7); - for (ControllerInfo controllerInfo : callerCollectorPlayer.callingControllers) { - assertThat(session.get().isMediaNotificationController(controllerInfo)).isFalse(); - assertThat(controllerInfo.getControllerVersion()) - .isEqualTo(ControllerInfo.LEGACY_CONTROLLER_VERSION); - assertThat(controllerInfo.getPackageName()) - .isEqualTo(getControllerCallerPackageName(controllerInfo)); + for (ControllerInfo info : callerCollectorPlayer.callingControllers) { + assertThat(session.get().isMediaNotificationController(info)).isFalse(); + assertThat(info.getControllerVersion()).isEqualTo(ControllerInfo.LEGACY_CONTROLLER_VERSION); + assertThat(info.getPackageName()).isEqualTo(getControllerCallerPackageName(info)); } }