mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
982403a0cc
commit
28bfb27fb5
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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 =
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user