Refine logic to set and interpret ACTION_PAUSE/ACTION_PLAY

We currently set both actions depending on the playWhenReady state
and we require both actions when converting a platform session to
a Media3 session (if ACTION_PLAY_PAUSE isn't set anyway).

This causes problems in two situations:
 - A controller using the platform ACTION_PAUSE/ACTION_PLAY to
   determine which button to show in a UI. This needs to be aligned
   to the existing Util.shouldShowPlayButton we already use when
   setting the PlaybackStateCompat state.
 - A session only setting either ACTION_PAUSE or ACTION_PLAY
   depending on its state. We should check if the action triggered
   by setPlayWhenReady(...) is possible and allow COMMAND_PLAY_PAUSE
   accordingly.

PiperOrigin-RevId: 726916720
This commit is contained in:
tonihei 2025-02-14 07:12:12 -08:00 committed by Copybara-Service
parent 982403a0cc
commit 28bfb27fb5
7 changed files with 154 additions and 25 deletions

View File

@ -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:

View File

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

View File

@ -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:

View File

@ -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 =

View File

@ -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<Boolean> 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<Boolean> 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 {

View File

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

View File

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