Use Util method for common UI play/pause button logic.

This ensures the logic is consistent and can also be easily
used from custom UIs.

PiperOrigin-RevId: 527249127
This commit is contained in:
tonihei 2023-04-26 14:35:55 +01:00 committed by Ian Baker
parent 97272c139c
commit 9128244dc3
8 changed files with 135 additions and 166 deletions

View File

@ -49,6 +49,10 @@
* Fix issue where `MediaController` doesn't update its available commands
when connected to a legacy `MediaSessionCompat` that updates its
actions.
* UI:
* Add Util methods `shouldShowPlayButton` and
`handlePlayPauseButtonAction` to write custom UI elements with a
play/pause button.
* Audio:
* Fix bug where some playbacks fail when tunneling is enabled and
`AudioProcessors` are active, e.g. for gapless trimming

View File

@ -1233,11 +1233,15 @@ package androidx.media3.common.util {
method public static boolean checkCleartextTrafficPermitted(androidx.media3.common.MediaItem...);
method @Nullable public static String getAdaptiveMimeTypeForContentType(@androidx.media3.common.C.ContentType int);
method @Nullable public static java.util.UUID getDrmUuid(String);
method public static boolean handlePauseButtonAction(@Nullable androidx.media3.common.Player);
method public static boolean handlePlayButtonAction(@Nullable androidx.media3.common.Player);
method public static boolean handlePlayPauseButtonAction(@Nullable androidx.media3.common.Player);
method @androidx.media3.common.C.ContentType public static int inferContentType(android.net.Uri);
method @androidx.media3.common.C.ContentType public static int inferContentTypeForExtension(String);
method @androidx.media3.common.C.ContentType public static int inferContentTypeForUriAndMimeType(android.net.Uri, @Nullable String);
method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, android.net.Uri...);
method public static boolean maybeRequestReadExternalStoragePermission(android.app.Activity, androidx.media3.common.MediaItem...);
method @org.checkerframework.checker.nullness.qual.EnsuresNonNullIf(result=false, expression="#1") public static boolean shouldShowPlayButton(@Nullable androidx.media3.common.Player);
}
}

View File

@ -17,6 +17,8 @@ package androidx.media3.common.util;
import static android.content.Context.UI_MODE_SERVICE;
import static androidx.media3.common.C.UNLIMITED_PENDING_FRAME_COUNT;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
@ -124,6 +126,7 @@ import java.util.zip.Inflater;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
import org.checkerframework.checker.nullness.qual.PolyNull;
/** Miscellaneous utility methods. */
@ -2933,6 +2936,87 @@ public final class Util {
return Integer.toString(i, Character.MAX_RADIX);
}
/**
* Returns whether a play button should be presented on a UI element for playback control. If
* {@code false}, a pause button should be shown instead.
*
* <p>Use {@link #handlePlayPauseButtonAction}, {@link #handlePlayButtonAction} or {@link
* #handlePauseButtonAction} to handle the interaction with the play or pause button UI element.
*
* @param player The {@link Player}. May be null.
*/
@EnsuresNonNullIf(result = false, expression = "#1")
public static boolean shouldShowPlayButton(@Nullable Player player) {
return player == null
|| !player.getPlayWhenReady()
|| player.getPlaybackState() == Player.STATE_IDLE
|| player.getPlaybackState() == Player.STATE_ENDED;
}
/**
* Updates the player to handle an interaction with a play button.
*
* <p>This method assumes the play button is enabled if {@link #shouldShowPlayButton} returns
* true.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePlayButtonAction(@Nullable Player player) {
if (player == null) {
return false;
}
@Player.State int state = player.getPlaybackState();
boolean methodTriggered = false;
if (state == Player.STATE_IDLE && player.isCommandAvailable(COMMAND_PREPARE)) {
player.prepare();
methodTriggered = true;
} else if (state == Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
player.seekToDefaultPosition();
methodTriggered = true;
}
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.play();
methodTriggered = true;
}
return methodTriggered;
}
/**
* Updates the player to handle an interaction with a pause button.
*
* <p>This method assumes the pause button is enabled if {@link #shouldShowPlayButton} returns
* false.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePauseButtonAction(@Nullable Player player) {
if (player != null && player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.pause();
return true;
}
return false;
}
/**
* Updates the player to handle an interaction with a play or pause button.
*
* <p>This method assumes that the UI element enables a play button if {@link
* #shouldShowPlayButton} returns true and a pause button otherwise.
*
* @param player The {@link Player}. May be null.
* @return Whether a player method was triggered to handle this action.
*/
public static boolean handlePlayPauseButtonAction(@Nullable Player player) {
if (shouldShowPlayButton(player)) {
return handlePlayButtonAction(player);
} else {
return handlePauseButtonAction(player);
}
}
@Nullable
private static String getSystemProperty(String name) {
try {

View File

@ -31,8 +31,6 @@ import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
@ -342,22 +340,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
(controller) -> {
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
@Player.State int playbackState = playerWrapper.getPlaybackState();
if (!playerWrapper.getPlayWhenReady()
|| playbackState == STATE_ENDED
|| playbackState == STATE_IDLE) {
if (playbackState == STATE_IDLE) {
playerWrapper.prepareIfCommandAvailable();
} else if (playbackState == STATE_ENDED) {
playerWrapper.seekToDefaultPositionIfCommandAvailable();
}
playerWrapper.play();
} else {
playerWrapper.pause();
}
},
controller -> Util.handlePlayPauseButtonAction(sessionImpl.getPlayerWrapper()),
remoteUserInfo);
}
@ -397,15 +380,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
controller -> {
PlayerWrapper playerWrapper = sessionImpl.getPlayerWrapper();
@Player.State int playbackState = playerWrapper.getPlaybackState();
if (playbackState == Player.STATE_IDLE) {
playerWrapper.prepareIfCommandAvailable();
} else if (playbackState == Player.STATE_ENDED) {
playerWrapper.seekToDefaultPositionIfCommandAvailable();
}
if (sessionImpl.onPlayRequested()) {
playerWrapper.play();
Util.handlePlayButtonAction(sessionImpl.getPlayerWrapper());
}
},
sessionCompat.getCurrentControllerInfo());
@ -438,7 +414,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public void onPause() {
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
controller -> sessionImpl.getPlayerWrapper().pause(),
controller -> Util.handlePauseButtonAction(sessionImpl.getPlayerWrapper()),
sessionCompat.getCurrentControllerInfo());
}

View File

@ -54,7 +54,6 @@ import androidx.media3.common.C;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Events;
import androidx.media3.common.Player.State;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.RepeatModeUtil;
@ -840,22 +839,22 @@ public class LegacyPlayerControlView extends FrameLayout {
}
boolean requestPlayPauseFocus = false;
boolean requestPlayPauseAccessibilityFocus = false;
boolean shouldShowPauseButton = shouldShowPauseButton();
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
if (playButton != null) {
requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused();
requestPlayPauseFocus |= !shouldShowPlayButton && playButton.isFocused();
requestPlayPauseAccessibilityFocus |=
Util.SDK_INT < 21
? requestPlayPauseFocus
: (shouldShowPauseButton && Api21.isAccessibilityFocused(playButton));
playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE);
: (!shouldShowPlayButton && Api21.isAccessibilityFocused(playButton));
playButton.setVisibility(shouldShowPlayButton ? VISIBLE : GONE);
}
if (pauseButton != null) {
requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused();
requestPlayPauseFocus |= shouldShowPlayButton && pauseButton.isFocused();
requestPlayPauseAccessibilityFocus |=
Util.SDK_INT < 21
? requestPlayPauseFocus
: (!shouldShowPauseButton && Api21.isAccessibilityFocused(pauseButton));
pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE);
: (shouldShowPlayButton && Api21.isAccessibilityFocused(pauseButton));
pauseButton.setVisibility(shouldShowPlayButton ? GONE : VISIBLE);
}
if (requestPlayPauseFocus) {
requestPlayPauseFocus();
@ -1081,19 +1080,19 @@ public class LegacyPlayerControlView extends FrameLayout {
}
private void requestPlayPauseFocus() {
boolean shouldShowPauseButton = shouldShowPauseButton();
if (!shouldShowPauseButton && playButton != null) {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
if (shouldShowPlayButton && playButton != null) {
playButton.requestFocus();
} else if (shouldShowPauseButton && pauseButton != null) {
} else if (!shouldShowPlayButton && pauseButton != null) {
pauseButton.requestFocus();
}
}
private void requestPlayPauseAccessibilityFocus() {
boolean shouldShowPauseButton = shouldShowPauseButton();
if (!shouldShowPauseButton && playButton != null) {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
if (shouldShowPlayButton && playButton != null) {
playButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
} else if (shouldShowPauseButton && pauseButton != null) {
} else if (!shouldShowPlayButton && pauseButton != null) {
pauseButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}
@ -1200,13 +1199,13 @@ public class LegacyPlayerControlView extends FrameLayout {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
Util.handlePlayPauseButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
dispatchPlay(player);
Util.handlePlayButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
dispatchPause(player);
Util.handlePauseButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
player.seekToNext();
@ -1222,36 +1221,6 @@ public class LegacyPlayerControlView extends FrameLayout {
return true;
}
private boolean shouldShowPauseButton() {
return player != null
&& player.getPlaybackState() != Player.STATE_ENDED
&& player.getPlaybackState() != Player.STATE_IDLE
&& player.getPlayWhenReady();
}
private void dispatchPlayPause(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) {
dispatchPlay(player);
} else {
dispatchPause(player);
}
}
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE) {
player.prepare();
} else if (state == Player.STATE_ENDED) {
seekTo(player, player.getCurrentMediaItemIndex(), C.TIME_UNSET);
}
player.play();
}
private void dispatchPause(Player player) {
player.pause();
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
@ -1361,9 +1330,9 @@ public class LegacyPlayerControlView extends FrameLayout {
} else if (rewindButton == view) {
player.seekBack();
} else if (playButton == view) {
dispatchPlay(player);
Util.handlePlayButtonAction(player);
} else if (pauseButton == view) {
dispatchPause(player);
Util.handlePauseButtonAction(player);
} else if (repeatToggleButton == view) {
player.setRepeatMode(
RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));

View File

@ -19,11 +19,9 @@ import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
@ -75,7 +73,6 @@ import androidx.media3.common.Format;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Events;
import androidx.media3.common.Player.State;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
@ -978,17 +975,17 @@ public class PlayerControlView extends FrameLayout {
return;
}
if (playPauseButton != null) {
boolean shouldShowPauseButton = shouldShowPauseButton();
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
@DrawableRes
int drawableRes =
shouldShowPauseButton
? R.drawable.exo_styled_controls_pause
: R.drawable.exo_styled_controls_play;
shouldShowPlayButton
? R.drawable.exo_styled_controls_play
: R.drawable.exo_styled_controls_pause;
@StringRes
int stringRes =
shouldShowPauseButton
? R.string.exo_controls_pause_description
: R.string.exo_controls_play_description;
shouldShowPlayButton
? R.string.exo_controls_play_description
: R.string.exo_controls_pause_description;
((ImageView) playPauseButton)
.setImageDrawable(getDrawable(getContext(), resources, drawableRes));
playPauseButton.setContentDescription(resources.getString(stringRes));
@ -1477,13 +1474,13 @@ public class PlayerControlView extends FrameLayout {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
dispatchPlayPause(player);
Util.handlePlayPauseButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
dispatchPlay(player);
Util.handlePlayButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
dispatchPause(player);
Util.handlePauseButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
@ -1539,41 +1536,6 @@ public class PlayerControlView extends FrameLayout {
|| !player.getCurrentTimeline().isEmpty());
}
private boolean shouldShowPauseButton() {
return player != null
&& player.getPlaybackState() != Player.STATE_ENDED
&& player.getPlaybackState() != Player.STATE_IDLE
&& player.getPlayWhenReady();
}
private void dispatchPlayPause(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE || state == Player.STATE_ENDED || !player.getPlayWhenReady()) {
dispatchPlay(player);
} else {
dispatchPause(player);
}
}
private void dispatchPlay(Player player) {
@State int state = player.getPlaybackState();
if (state == Player.STATE_IDLE && player.isCommandAvailable(COMMAND_PREPARE)) {
player.prepare();
} else if (state == Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
player.seekToDefaultPosition();
}
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.play();
}
}
private void dispatchPause(Player player) {
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.pause();
}
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
@ -1743,7 +1705,7 @@ public class PlayerControlView extends FrameLayout {
player.seekBack();
}
} else if (playPauseButton == view) {
dispatchPlayPause(player);
Util.handlePlayPauseButtonAction(player);
} else if (repeatToggleButton == view) {
if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
player.setRepeatMode(

View File

@ -18,11 +18,8 @@ package androidx.media3.ui;
import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS;
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_STOP;
@ -1334,10 +1331,10 @@ public class PlayerNotificationManager {
stringActions.add(ACTION_REWIND);
}
if (usePlayPauseActions) {
if (shouldShowPauseButton(player)) {
stringActions.add(ACTION_PAUSE);
} else {
if (Util.shouldShowPlayButton(player)) {
stringActions.add(ACTION_PLAY);
} else {
stringActions.add(ACTION_PAUSE);
}
}
if (useFastForwardAction && enableFastForward) {
@ -1382,10 +1379,10 @@ public class PlayerNotificationManager {
if (leftSideActionIndex != -1) {
actionIndices[actionCounter++] = leftSideActionIndex;
}
boolean shouldShowPauseButton = shouldShowPauseButton(player);
if (pauseActionIndex != -1 && shouldShowPauseButton) {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
if (pauseActionIndex != -1 && !shouldShowPlayButton) {
actionIndices[actionCounter++] = pauseActionIndex;
} else if (playActionIndex != -1 && !shouldShowPauseButton) {
} else if (playActionIndex != -1 && shouldShowPlayButton) {
actionIndices[actionCounter++] = playActionIndex;
}
if (rightSideActionIndex != -1) {
@ -1401,12 +1398,6 @@ public class PlayerNotificationManager {
&& player.getPlayWhenReady();
}
private boolean shouldShowPauseButton(Player player) {
return player.getPlaybackState() != Player.STATE_ENDED
&& player.getPlaybackState() != Player.STATE_IDLE
&& player.getPlayWhenReady();
}
private void postStartOrUpdateNotification() {
if (!mainHandler.hasMessages(MSG_START_OR_UPDATE_NOTIFICATION)) {
mainHandler.sendEmptyMessage(MSG_START_OR_UPDATE_NOTIFICATION);
@ -1547,20 +1538,9 @@ public class PlayerNotificationManager {
}
String action = intent.getAction();
if (ACTION_PLAY.equals(action)) {
if (player.getPlaybackState() == Player.STATE_IDLE
&& player.isCommandAvailable(COMMAND_PREPARE)) {
player.prepare();
} else if (player.getPlaybackState() == Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_TO_DEFAULT_POSITION)) {
player.seekToDefaultPosition();
}
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.play();
}
Util.handlePlayButtonAction(player);
} else if (ACTION_PAUSE.equals(action)) {
if (player.isCommandAvailable(COMMAND_PLAY_PAUSE)) {
player.pause();
}
Util.handlePauseButtonAction(player);
} else if (ACTION_PREVIOUS.equals(action)) {
if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
player.seekToPrevious();

View File

@ -121,10 +121,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
@Override
public boolean isPlaying() {
int playbackState = player.getPlaybackState();
return playbackState != Player.STATE_IDLE
&& playbackState != Player.STATE_ENDED
&& player.getPlayWhenReady();
return !Util.shouldShowPlayButton(player);
}
@Override
@ -142,13 +139,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
@SuppressWarnings("nullness:dereference.of.nullable")
@Override
public void play() {
if (player.getPlaybackState() == Player.STATE_IDLE) {
player.prepare();
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
player.seekToDefaultPosition(player.getCurrentMediaItemIndex());
}
if (player.isCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
player.play();
if (Util.handlePlayButtonAction(player)) {
getCallback().onPlayStateChanged(this);
}
}
@ -157,8 +148,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
@SuppressWarnings("nullness:dereference.of.nullable")
@Override
public void pause() {
if (player.isCommandAvailable(Player.COMMAND_PLAY_PAUSE)) {
player.pause();
if (Util.handlePauseButtonAction(player)) {
getCallback().onPlayStateChanged(this);
}
}