Show play button during playback suppression by default

This changes the default logic of shouldShowPlayButton to show a play
button while the playback is temporarily suppressed. This helps to
provide better UI feedback to the fact that playback stopped and
provides a quick way for users to override the suppression and attempt
to restart playback.

Some apps may want to keep the legacy behavior depending on their app's
needs. Hence, we also add a config parameter to set this behavior both
in MediaSession and our default UI components.

Issue: google/ExoPlayer#11213
PiperOrigin-RevId: 557129171
This commit is contained in:
tonihei 2023-08-15 15:40:36 +01:00 committed by oceanjules
parent 86b9fdae63
commit 597de8706d
19 changed files with 642 additions and 77 deletions

View File

@ -13,6 +13,13 @@
and nullable array element types are not detected as nullable. Examples and nullable array element types are not detected as nullable. Examples
are `TrackSelectorResult` and `SimpleDecoder` method parameters are `TrackSelectorResult` and `SimpleDecoder` method parameters
([6792](https://github.com/google/ExoPlayer/issues/6792)). ([6792](https://github.com/google/ExoPlayer/issues/6792)).
* Change default UI and notification behavior in
`Util.shouldShowPlayButton` to show a "play" button while playback is
temporarily suppressed (e.g. due to transient audio focus loss). The
legacy behavior can be maintained by using
`PlayerView.setShowPlayButtonIfPlaybackIsSuppressed(false)` or
`MediaSession.Builder.setShowPlayButtonIfPlaybackIsSuppressed(false)`
([#11213](https://github.com/google/ExoPlayer/issues/11213)).
* ExoPlayer: * ExoPlayer:
* Fix seeking issues in AC4 streams caused by not identifying decode-only * Fix seeking issues in AC4 streams caused by not identifying decode-only
samples correctly samples correctly

View File

@ -3171,23 +3171,42 @@ public final class Util {
* <p>Use {@link #handlePlayPauseButtonAction}, {@link #handlePlayButtonAction} or {@link * <p>Use {@link #handlePlayPauseButtonAction}, {@link #handlePlayButtonAction} or {@link
* #handlePauseButtonAction} to handle the interaction with the play or pause button UI element. * #handlePauseButtonAction} to handle the interaction with the play or pause button UI element.
* *
* @param player The {@link Player}. May be null. * @param player The {@link Player}. May be {@code null}.
*/ */
@EnsuresNonNullIf(result = false, expression = "#1") @EnsuresNonNullIf(result = false, expression = "#1")
public static boolean shouldShowPlayButton(@Nullable Player player) { public static boolean shouldShowPlayButton(@Nullable Player player) {
return shouldShowPlayButton(player, /* playIfSuppressed= */ true);
}
/**
* 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 {@code null}.
* @param playIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
@UnstableApi
@EnsuresNonNullIf(result = false, expression = "#1")
public static boolean shouldShowPlayButton(@Nullable Player player, boolean playIfSuppressed) {
return player == null return player == null
|| !player.getPlayWhenReady() || !player.getPlayWhenReady()
|| player.getPlaybackState() == Player.STATE_IDLE || player.getPlaybackState() == Player.STATE_IDLE
|| player.getPlaybackState() == Player.STATE_ENDED; || player.getPlaybackState() == Player.STATE_ENDED
|| (playIfSuppressed
&& player.getPlaybackSuppressionReason() != Player.PLAYBACK_SUPPRESSION_REASON_NONE);
} }
/** /**
* Updates the player to handle an interaction with a play button. * Updates the player to handle an interaction with a play button.
* *
* <p>This method assumes the play button is enabled if {@link #shouldShowPlayButton} returns * <p>This method assumes the play button is enabled if {@link #shouldShowPlayButton} returns
* true. * {@code true}.
* *
* @param player The {@link Player}. May be null. * @param player The {@link Player}. May be {@code null}.
* @return Whether a player method was triggered to handle this action. * @return Whether a player method was triggered to handle this action.
*/ */
public static boolean handlePlayButtonAction(@Nullable Player player) { public static boolean handlePlayButtonAction(@Nullable Player player) {
@ -3215,9 +3234,9 @@ public final class Util {
* Updates the player to handle an interaction with a pause button. * Updates the player to handle an interaction with a pause button.
* *
* <p>This method assumes the pause button is enabled if {@link #shouldShowPlayButton} returns * <p>This method assumes the pause button is enabled if {@link #shouldShowPlayButton} returns
* false. * {@code false}.
* *
* @param player The {@link Player}. May be null. * @param player The {@link Player}. May be {@code null}.
* @return Whether a player method was triggered to handle this action. * @return Whether a player method was triggered to handle this action.
*/ */
public static boolean handlePauseButtonAction(@Nullable Player player) { public static boolean handlePauseButtonAction(@Nullable Player player) {
@ -3232,13 +3251,30 @@ public final class Util {
* Updates the player to handle an interaction with a play or pause button. * 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 * <p>This method assumes that the UI element enables a play button if {@link
* #shouldShowPlayButton} returns true and a pause button otherwise. * #shouldShowPlayButton} returns {@code true} and a pause button otherwise.
* *
* @param player The {@link Player}. May be null. * @param player The {@link Player}. May be {@code null}.
* @return Whether a player method was triggered to handle this action. * @return Whether a player method was triggered to handle this action.
*/ */
public static boolean handlePlayPauseButtonAction(@Nullable Player player) { public static boolean handlePlayPauseButtonAction(@Nullable Player player) {
if (shouldShowPlayButton(player)) { return handlePlayPauseButtonAction(player, /* playIfSuppressed= */ true);
}
/**
* 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(Player, boolean)} returns {@code true} and a pause button otherwise.
*
* @param player The {@link Player}. May be {@code null}.
* @param playIfSuppressed Whether to trigger a play action if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
* @return Whether a player method was triggered to handle this action.
*/
@UnstableApi
public static boolean handlePlayPauseButtonAction(
@Nullable Player player, boolean playIfSuppressed) {
if (shouldShowPlayButton(player, playIfSuppressed)) {
return handlePlayButtonAction(player); return handlePlayButtonAction(player);
} else { } else {
return handlePauseButtonAction(player); return handlePauseButtonAction(player);

View File

@ -23,7 +23,6 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_STOP; import static androidx.media3.common.Player.COMMAND_STOP;
import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
@ -320,8 +319,8 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi
mediaSession, mediaSession,
player.getAvailableCommands(), player.getAvailableCommands(),
customLayoutWithEnabledCommandButtonsOnly.build(), customLayoutWithEnabledCommandButtonsOnly.build(),
/* showPauseButton= */ player.getPlayWhenReady() !Util.shouldShowPlayButton(
&& player.getPlaybackState() != STATE_ENDED), player, mediaSession.getShowPlayButtonIfPlaybackIsSuppressed())),
builder, builder,
actionFactory); actionFactory);
mediaStyle.setShowActionsInCompactView(compactViewIndices); mediaStyle.setShowActionsInCompactView(compactViewIndices);

View File

@ -461,6 +461,22 @@ public abstract class MediaLibraryService extends MediaSessionService {
return super.setCustomLayout(customLayout); return super.setCustomLayout(customLayout);
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfPlaybackIsSuppressed Whether to show a play button if playback is
* {@linkplain Player#getPlaybackSuppressionReason() suppressed}.
*/
@UnstableApi
@Override
public Builder setShowPlayButtonIfPlaybackIsSuppressed(
boolean showPlayButtonIfPlaybackIsSuppressed) {
return super.setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfPlaybackIsSuppressed);
}
/** /**
* Builds a {@link MediaLibrarySession}. * Builds a {@link MediaLibrarySession}.
* *
@ -481,7 +497,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
customLayout, customLayout,
callback, callback,
extras, extras,
checkNotNull(bitmapLoader)); checkNotNull(bitmapLoader),
playIfSuppressed);
} }
} }
@ -493,9 +510,18 @@ public abstract class MediaLibraryService extends MediaSessionService {
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
super( super(
context, id, player, sessionActivity, customLayout, callback, tokenExtras, bitmapLoader); context,
id,
player,
sessionActivity,
customLayout,
callback,
tokenExtras,
bitmapLoader,
playIfSuppressed);
} }
@Override @Override
@ -507,7 +533,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
return new MediaLibrarySessionImpl( return new MediaLibrarySessionImpl(
this, this,
context, context,
@ -517,7 +544,8 @@ public abstract class MediaLibraryService extends MediaSessionService {
customLayout, customLayout,
(Callback) callback, (Callback) callback,
tokenExtras, tokenExtras,
bitmapLoader); bitmapLoader,
playIfSuppressed);
} }
@Override @Override

View File

@ -78,7 +78,8 @@ import java.util.concurrent.Future;
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
MediaLibrarySession.Callback callback, MediaLibrarySession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
super( super(
instance, instance,
context, context,
@ -88,7 +89,8 @@ import java.util.concurrent.Future;
customLayout, customLayout,
callback, callback,
tokenExtras, tokenExtras,
bitmapLoader); bitmapLoader,
playIfSuppressed);
this.instance = instance; this.instance = instance;
this.callback = callback; this.callback = callback;
subscriptions = new ArrayMap<>(); subscriptions = new ArrayMap<>();

View File

@ -377,6 +377,22 @@ public class MediaSession {
return super.setCustomLayout(customLayout); return super.setCustomLayout(customLayout);
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfPlaybackIsSuppressed Whether to show a play button if playback is
* {@linkplain Player#getPlaybackSuppressionReason() suppressed}.
*/
@UnstableApi
@Override
public Builder setShowPlayButtonIfPlaybackIsSuppressed(
boolean showPlayButtonIfPlaybackIsSuppressed) {
return super.setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfPlaybackIsSuppressed);
}
/** /**
* Builds a {@link MediaSession}. * Builds a {@link MediaSession}.
* *
@ -397,7 +413,8 @@ public class MediaSession {
customLayout, customLayout,
callback, callback,
extras, extras,
checkNotNull(bitmapLoader)); checkNotNull(bitmapLoader),
playIfSuppressed);
} }
} }
@ -589,7 +606,8 @@ public class MediaSession {
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
Callback callback, Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
synchronized (STATIC_LOCK) { synchronized (STATIC_LOCK) {
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) { if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
throw new IllegalStateException("Session ID must be unique. ID=" + id); throw new IllegalStateException("Session ID must be unique. ID=" + id);
@ -605,7 +623,8 @@ public class MediaSession {
customLayout, customLayout,
callback, callback,
tokenExtras, tokenExtras,
bitmapLoader); bitmapLoader,
playIfSuppressed);
} }
/* package */ MediaSessionImpl createImpl( /* package */ MediaSessionImpl createImpl(
@ -616,7 +635,8 @@ public class MediaSession {
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
Callback callback, Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
return new MediaSessionImpl( return new MediaSessionImpl(
this, this,
context, context,
@ -626,7 +646,8 @@ public class MediaSession {
customLayout, customLayout,
callback, callback,
tokenExtras, tokenExtras,
bitmapLoader); bitmapLoader,
playIfSuppressed);
} }
/* package */ MediaSessionImpl getImpl() { /* package */ MediaSessionImpl getImpl() {
@ -935,6 +956,15 @@ public class MediaSession {
return impl.getBitmapLoader(); return impl.getBitmapLoader();
} }
/**
* Returns whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
@UnstableApi
public final boolean getShowPlayButtonIfPlaybackIsSuppressed() {
return impl.shouldPlayIfSuppressed();
}
/** /**
* Sends a custom command to a specific controller. * Sends a custom command to a specific controller.
* *
@ -1740,6 +1770,7 @@ public class MediaSession {
/* package */ @Nullable PendingIntent sessionActivity; /* package */ @Nullable PendingIntent sessionActivity;
/* package */ Bundle extras; /* package */ Bundle extras;
/* package */ @MonotonicNonNull BitmapLoader bitmapLoader; /* package */ @MonotonicNonNull BitmapLoader bitmapLoader;
/* package */ boolean playIfSuppressed;
/* package */ ImmutableList<CommandButton> customLayout; /* package */ ImmutableList<CommandButton> customLayout;
@ -1751,6 +1782,7 @@ public class MediaSession {
this.callback = callback; this.callback = callback;
extras = Bundle.EMPTY; extras = Bundle.EMPTY;
customLayout = ImmutableList.of(); customLayout = ImmutableList.of();
playIfSuppressed = true;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -1789,6 +1821,13 @@ public class MediaSession {
return (BuilderT) this; return (BuilderT) this;
} }
@SuppressWarnings("unchecked")
public BuilderT setShowPlayButtonIfPlaybackIsSuppressed(
boolean showPlayButtonIfPlaybackIsSuppressed) {
this.playIfSuppressed = showPlayButtonIfPlaybackIsSuppressed;
return (BuilderT) this;
}
public abstract SessionT build(); public abstract SessionT build();
} }
} }

View File

@ -112,6 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final BitmapLoader bitmapLoader; private final BitmapLoader bitmapLoader;
private final Runnable periodicSessionPositionInfoUpdateRunnable; private final Runnable periodicSessionPositionInfoUpdateRunnable;
private final Handler mainHandler; private final Handler mainHandler;
private final boolean playIfSuppressed;
private PlayerInfo playerInfo; private PlayerInfo playerInfo;
private PlayerWrapper playerWrapper; private PlayerWrapper playerWrapper;
@ -140,7 +141,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ImmutableList<CommandButton> customLayout, ImmutableList<CommandButton> customLayout,
MediaSession.Callback callback, MediaSession.Callback callback,
Bundle tokenExtras, Bundle tokenExtras,
BitmapLoader bitmapLoader) { BitmapLoader bitmapLoader,
boolean playIfSuppressed) {
this.context = context; this.context = context;
this.instance = instance; this.instance = instance;
@ -156,6 +158,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
applicationHandler = new Handler(player.getApplicationLooper()); applicationHandler = new Handler(player.getApplicationLooper());
this.callback = callback; this.callback = callback;
this.bitmapLoader = bitmapLoader; this.bitmapLoader = bitmapLoader;
this.playIfSuppressed = playIfSuppressed;
playerInfo = PlayerInfo.DEFAULT; playerInfo = PlayerInfo.DEFAULT;
onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper()); onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper());
@ -189,7 +192,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sessionLegacyStub = sessionLegacyStub =
new MediaSessionLegacyStub(/* session= */ thisRef, sessionUri, applicationHandler); new MediaSessionLegacyStub(/* session= */ thisRef, sessionUri, applicationHandler);
PlayerWrapper playerWrapper = new PlayerWrapper(player); PlayerWrapper playerWrapper = new PlayerWrapper(player, playIfSuppressed);
this.playerWrapper = playerWrapper; this.playerWrapper = playerWrapper;
this.playerWrapper.setCustomLayout(customLayout); this.playerWrapper.setCustomLayout(customLayout);
postOrRun( postOrRun(
@ -208,7 +211,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (player == playerWrapper.getWrappedPlayer()) { if (player == playerWrapper.getWrappedPlayer()) {
return; return;
} }
setPlayerInternal(/* oldPlayerWrapper= */ playerWrapper, new PlayerWrapper(player)); setPlayerInternal(
/* oldPlayerWrapper= */ playerWrapper, new PlayerWrapper(player, playIfSuppressed));
} }
private void setPlayerInternal( private void setPlayerInternal(
@ -397,6 +401,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return bitmapLoader; return bitmapLoader;
} }
public boolean shouldPlayIfSuppressed() {
return playIfSuppressed;
}
public void setAvailableCommands( public void setAvailableCommands(
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
if (sessionStub.getConnectedControllersManager().isConnected(controller)) { if (sessionStub.getConnectedControllersManager().isConnected(controller)) {

View File

@ -346,7 +346,9 @@ import org.checkerframework.checker.initialization.qual.Initialized;
mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey(); mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey();
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE, COMMAND_PLAY_PAUSE,
controller -> Util.handlePlayPauseButtonAction(sessionImpl.getPlayerWrapper()), controller ->
Util.handlePlayPauseButtonAction(
sessionImpl.getPlayerWrapper(), sessionImpl.shouldPlayIfSuppressed()),
remoteUserInfo); remoteUserInfo);
} }

View File

@ -759,24 +759,25 @@ import java.util.concurrent.TimeoutException;
/** Converts {@link Player}' states to state of {@link PlaybackStateCompat}. */ /** Converts {@link Player}' states to state of {@link PlaybackStateCompat}. */
@PlaybackStateCompat.State @PlaybackStateCompat.State
public static int convertToPlaybackStateCompatState( public static int convertToPlaybackStateCompatState(Player player, boolean playIfSuppressed) {
@Nullable PlaybackException playerError, if (player.getPlayerError() != null) {
@Player.State int playbackState,
boolean playWhenReady) {
if (playerError != null) {
return PlaybackStateCompat.STATE_ERROR; return PlaybackStateCompat.STATE_ERROR;
} }
@Player.State int playbackState = player.getPlaybackState();
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, playIfSuppressed);
switch (playbackState) { switch (playbackState) {
case Player.STATE_IDLE: case Player.STATE_IDLE:
return PlaybackStateCompat.STATE_NONE; return PlaybackStateCompat.STATE_NONE;
case Player.STATE_READY: case Player.STATE_READY:
return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; return shouldShowPlayButton
? PlaybackStateCompat.STATE_PAUSED
: PlaybackStateCompat.STATE_PLAYING;
case Player.STATE_ENDED: case Player.STATE_ENDED:
return PlaybackStateCompat.STATE_STOPPED; return PlaybackStateCompat.STATE_STOPPED;
case Player.STATE_BUFFERING: case Player.STATE_BUFFERING:
return playWhenReady return shouldShowPlayButton
? PlaybackStateCompat.STATE_BUFFERING ? PlaybackStateCompat.STATE_PAUSED
: PlaybackStateCompat.STATE_PAUSED; : PlaybackStateCompat.STATE_BUFFERING;
default: default:
throw new IllegalArgumentException("Unrecognized State: " + playbackState); throw new IllegalArgumentException("Unrecognized State: " + playbackState);
} }

View File

@ -64,13 +64,16 @@ import java.util.List;
private static final int STATUS_CODE_SUCCESS_COMPAT = -1; private static final int STATUS_CODE_SUCCESS_COMPAT = -1;
private final boolean playIfSuppressed;
private int legacyStatusCode; private int legacyStatusCode;
@Nullable private String legacyErrorMessage; @Nullable private String legacyErrorMessage;
@Nullable private Bundle legacyErrorExtras; @Nullable private Bundle legacyErrorExtras;
private ImmutableList<CommandButton> customLayout; private ImmutableList<CommandButton> customLayout;
public PlayerWrapper(Player player) { public PlayerWrapper(Player player, boolean playIfSuppressed) {
super(player); super(player);
this.playIfSuppressed = playIfSuppressed;
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
customLayout = ImmutableList.of(); customLayout = ImmutableList.of();
} }
@ -968,9 +971,7 @@ import java.util.List;
.build(); .build();
} }
@Nullable PlaybackException playerError = getPlayerError(); @Nullable PlaybackException playerError = getPlayerError();
int state = int state = MediaUtils.convertToPlaybackStateCompatState(/* player= */ this, playIfSuppressed);
MediaUtils.convertToPlaybackStateCompatState(
playerError, getPlaybackState(), getPlayWhenReady());
// Always advertise ACTION_SET_RATING. // Always advertise ACTION_SET_RATING.
long actions = PlaybackStateCompat.ACTION_SET_RATING; long actions = PlaybackStateCompat.ACTION_SET_RATING;
Commands availableCommands = getAvailableCommands(); Commands availableCommands = getAvailableCommands();

View File

@ -43,11 +43,14 @@ import androidx.media3.common.ForwardingPlayer;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.Commands; import androidx.media3.common.Player.Commands;
import androidx.media3.common.SimpleBasePlayer;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -663,6 +666,320 @@ public class DefaultMediaNotificationProviderTest {
mediaSession.release(); mediaSession.release();
} }
@Test
public void
createNotification_withStateReadyAndPlayWhenReadyTrueAndNoSuppression_showsPauseButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_READY, /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_pause_description));
}
@Test
public void
createNotification_withStateReadyAndPlayWhenReadyTrueAndPlaybackSuppression_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_READY,
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void
createNotification_withStateReadyAndPlayWhenReadyTrueAndPlaybackSuppressionWithoutShowPauseIfSuppressed_showsPauseButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_READY,
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession =
new MediaSession.Builder(context, player)
.setShowPlayButtonIfPlaybackIsSuppressed(false)
.build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_pause_description));
}
@Test
public void
createNotification_withStateBufferingAndPlayWhenReadyTrueAndNoSuppression_showsPauseButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_BUFFERING,
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_pause_description));
}
@Test
public void
createNotification_withStateBufferingAndPlayWhenReadyTrueAndPlaybackSuppression_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_BUFFERING,
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void
createNotification_withStateBufferingAndPlayWhenReadyTrueAndPlaybackSuppressionWithoutShowPauseIfSuppressed_showsPauseButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_BUFFERING,
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession =
new MediaSession.Builder(context, player)
.setShowPlayButtonIfPlaybackIsSuppressed(false)
.build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_pause_description));
}
@Test
public void createNotification_withStateReadyAndPlayWhenReadyFalse_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_READY,
/* playWhenReady= */ false,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void createNotification_withStateBufferingAndPlayWhenReadyFalse_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_BUFFERING,
/* playWhenReady= */ false,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void createNotification_withStateEndedAndPlayWhenReadyTrue_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_ENDED, /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void createNotification_withStateEndedAndPlayWhenReadyFalse_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_ENDED, /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void createNotification_withStateIdleAndPlayWhenReadyTrue_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_IDLE, /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test
public void createNotification_withStateIdleAndPlayWhenReadyFalse_showsPlayButton() {
Player player =
createPlayerWithFixedState(
Player.STATE_IDLE, /* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
DefaultActionFactory defaultActionFactory =
new DefaultActionFactory(Robolectric.setupService(TestService.class));
MediaSession mediaSession = new MediaSession.Builder(context, player).build();
DefaultMediaNotificationProvider defaultMediaNotificationProvider =
new DefaultMediaNotificationProvider.Builder(ApplicationProvider.getApplicationContext())
.build();
MediaNotification mediaNotification =
defaultMediaNotificationProvider.createNotification(
mediaSession,
/* customLayout= */ ImmutableList.of(),
defaultActionFactory,
notification -> {});
mediaSession.release();
assertThat(mediaNotification.notification.actions[0].title.toString())
.isEqualTo(context.getString(R.string.media3_controls_play_description));
}
@Test @Test
public void provider_idsNotSpecified_usesDefaultIds() { public void provider_idsNotSpecified_usesDefaultIds() {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
@ -1010,6 +1327,31 @@ public class DefaultMediaNotificationProviderTest {
}; };
} }
private static Player createPlayerWithFixedState(
@Player.State int playbackState,
boolean playWhenReady,
@Player.PlaybackSuppressionReason int suppressionReason) {
return new SimpleBasePlayer(Looper.getMainLooper()) {
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(new Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build())
.setPlaylist(
ImmutableList.of(new MediaItemData.Builder(/* uid= */ new Object()).build()))
.setPlaybackState(playbackState)
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.setPlaybackSuppressionReason(suppressionReason)
.build();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
// Do nothing.
return Futures.immediateVoidFuture();
}
};
}
/** A test service for unit tests. */ /** A test service for unit tests. */
private static final class TestService extends MediaLibraryService { private static final class TestService extends MediaLibraryService {
@Nullable @Nullable

View File

@ -42,7 +42,7 @@ public class PlayerWrapperTest {
@Before @Before
public void setUp() { public void setUp() {
playerWrapper = new PlayerWrapper(player); playerWrapper = new PlayerWrapper(player, /* playIfSuppressed= */ true);
when(player.isCommandAvailable(anyInt())).thenReturn(true); when(player.isCommandAvailable(anyInt())).thenReturn(true);
when(player.getApplicationLooper()).thenReturn(Looper.myLooper()); when(player.getApplicationLooper()).thenReturn(Looper.myLooper());
} }

View File

@ -28,6 +28,8 @@ public class MediaSessionConstants {
public static final String TEST_ON_VIDEO_SIZE_CHANGED = "onVideoSizeChanged"; public static final String TEST_ON_VIDEO_SIZE_CHANGED = "onVideoSizeChanged";
public static final String TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION = public static final String TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION =
"onTracksChanged_videoToAudioTransition"; "onTracksChanged_videoToAudioTransition";
public static final String TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE =
"testSetShowPlayButtonIfSuppressedToFalse";
// Bundle keys // Bundle keys
public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands"; public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands";

View File

@ -20,6 +20,7 @@ import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_USER_RATING; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_USER_RATING;
import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.common.Player.STATE_READY;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE;
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS; import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@ -662,12 +663,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(playbackStateCompatRef.get().getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED); assertThat(playbackStateCompatRef.get().getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED);
assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
playbackStateCompatRef
.get()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
assertThat( assertThat(
playbackStateCompatRef playbackStateCompatRef
.get() .get()
@ -713,12 +708,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(playbackStateCompatRef.get().getState()) assertThat(playbackStateCompatRef.get().getState())
.isEqualTo(PlaybackStateCompat.STATE_BUFFERING); .isEqualTo(PlaybackStateCompat.STATE_BUFFERING);
assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
playbackStateCompatRef
.get()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
assertThat( assertThat(
playbackStateCompatRef playbackStateCompatRef
.get() .get()
@ -760,12 +749,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(playbackStateCompatRef.get().getState()) assertThat(playbackStateCompatRef.get().getState())
.isEqualTo(PlaybackStateCompat.STATE_STOPPED); .isEqualTo(PlaybackStateCompat.STATE_STOPPED);
assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
playbackStateCompatRef
.get()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
assertThat( assertThat(
playbackStateCompatRef playbackStateCompatRef
.get() .get()
@ -784,8 +767,57 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
} }
@Test @Test
public void playbackStateChange_withPlaybackSuppression_notifiesPlayingWithSpeedZero() public void playbackStateChange_withPlaybackSuppression_notifiesPaused() throws Exception {
throws Exception { session.getMockPlayer().setPlaybackState(Player.STATE_READY);
session
.getMockPlayer()
.setPlayWhenReady(/* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE);
AtomicReference<PlaybackStateCompat> playbackStateCompatRef = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
MediaControllerCompat.Callback callback =
new MediaControllerCompat.Callback() {
@Override
public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) {
playbackStateCompatRef.set(playbackStateCompat);
latch.countDown();
}
};
controllerCompat.registerCallback(callback, handler);
session
.getMockPlayer()
.notifyPlayWhenReadyChanged(
/* playWhenReady= */ true,
Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(playbackStateCompatRef.get().getState()).isEqualTo(PlaybackStateCompat.STATE_PAUSED);
assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
playbackStateCompatRef
.get()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
assertThat(controllerCompat.getPlaybackState().getState())
.isEqualTo(PlaybackStateCompat.STATE_PAUSED);
assertThat(controllerCompat.getPlaybackState().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
controllerCompat
.getPlaybackState()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
}
@Test
public void
playbackStateChange_withPlaybackSuppressionWithoutShowPauseIfSuppressed_notifiesPlayingWithSpeedZero()
throws Exception {
RemoteMediaSession session =
new RemoteMediaSession(TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE, context, null);
MediaControllerCompat controllerCompat =
new MediaControllerCompat(context, session.getCompatToken());
session.getMockPlayer().setPlaybackState(Player.STATE_READY); session.getMockPlayer().setPlaybackState(Player.STATE_READY);
session session
.getMockPlayer() .getMockPlayer()
@ -812,12 +844,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
assertThat(playbackStateCompatRef.get().getState()) assertThat(playbackStateCompatRef.get().getState())
.isEqualTo(PlaybackStateCompat.STATE_PLAYING); .isEqualTo(PlaybackStateCompat.STATE_PLAYING);
assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f);
assertThat(
playbackStateCompatRef
.get()
.getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f);
assertThat( assertThat(
playbackStateCompatRef playbackStateCompatRef
.get() .get()
@ -833,6 +859,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
.getExtras() .getExtras()
.getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT))
.isEqualTo(1f); .isEqualTo(1f);
session.release();
} }
@Test @Test

View File

@ -65,6 +65,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_VIDEO_SIZE_CHANGED; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_VIDEO_SIZE_CHANGED;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE;
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS; import static androidx.media3.test.session.common.MediaSessionConstants.TEST_WITH_CUSTOM_COMMANDS;
import android.app.PendingIntent; import android.app.PendingIntent;
@ -284,6 +285,11 @@ public class MediaSessionProviderService extends Service {
mockPlayer.currentTracks = MediaTestUtils.createDefaultVideoTracks(); mockPlayer.currentTracks = MediaTestUtils.createDefaultVideoTracks();
break; break;
} }
case TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE:
{
builder.setShowPlayButtonIfPlaybackIsSuppressed(false);
break;
}
default: // fall out default: // fall out
} }

View File

@ -330,6 +330,7 @@ public class LegacyPlayerControlView extends FrameLayout {
private boolean isAttachedToWindow; private boolean isAttachedToWindow;
private boolean showMultiWindowTimeBar; private boolean showMultiWindowTimeBar;
private boolean showPlayButtonIfSuppressed;
private boolean multiWindowTimeBar; private boolean multiWindowTimeBar;
private boolean scrubbing; private boolean scrubbing;
private int showTimeoutMs; private int showTimeoutMs;
@ -373,6 +374,7 @@ public class LegacyPlayerControlView extends FrameLayout {
@Nullable AttributeSet playbackAttrs) { @Nullable AttributeSet playbackAttrs) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
int controllerLayoutId = R.layout.exo_legacy_player_control_view; int controllerLayoutId = R.layout.exo_legacy_player_control_view;
showPlayButtonIfSuppressed = true;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
@ -571,6 +573,20 @@ public class LegacyPlayerControlView extends FrameLayout {
updateTimeline(); updateTimeline();
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) {
this.showPlayButtonIfSuppressed = showPlayButtonIfSuppressed;
updatePlayPauseButton();
}
/** /**
* Sets the millisecond positions of extra ad markers relative to the start of the window (or * Sets the millisecond positions of extra ad markers relative to the start of the window (or
* timeline, if in multi-window mode) and whether each extra ad has been played or not. The * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
@ -842,7 +858,7 @@ public class LegacyPlayerControlView extends FrameLayout {
} }
boolean requestPlayPauseFocus = false; boolean requestPlayPauseFocus = false;
boolean requestPlayPauseAccessibilityFocus = false; boolean requestPlayPauseAccessibilityFocus = false;
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
if (playButton != null) { if (playButton != null) {
requestPlayPauseFocus |= !shouldShowPlayButton && playButton.isFocused(); requestPlayPauseFocus |= !shouldShowPlayButton && playButton.isFocused();
requestPlayPauseAccessibilityFocus |= requestPlayPauseAccessibilityFocus |=
@ -1083,7 +1099,7 @@ public class LegacyPlayerControlView extends FrameLayout {
} }
private void requestPlayPauseFocus() { private void requestPlayPauseFocus() {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
if (shouldShowPlayButton && playButton != null) { if (shouldShowPlayButton && playButton != null) {
playButton.requestFocus(); playButton.requestFocus();
} else if (!shouldShowPlayButton && pauseButton != null) { } else if (!shouldShowPlayButton && pauseButton != null) {
@ -1092,7 +1108,7 @@ public class LegacyPlayerControlView extends FrameLayout {
} }
private void requestPlayPauseAccessibilityFocus() { private void requestPlayPauseAccessibilityFocus() {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
if (shouldShowPlayButton && playButton != null) { if (shouldShowPlayButton && playButton != null) {
playButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); playButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
} else if (!shouldShowPlayButton && pauseButton != null) { } else if (!shouldShowPlayButton && pauseButton != null) {
@ -1202,7 +1218,7 @@ public class LegacyPlayerControlView extends FrameLayout {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_HEADSETHOOK:
Util.handlePlayPauseButtonAction(player); Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed);
break; break;
case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PLAY:
Util.handlePlayButtonAction(player); Util.handlePlayButtonAction(player);

View File

@ -335,6 +335,7 @@ public class PlayerControlView extends FrameLayout {
private boolean isFullScreen; private boolean isFullScreen;
private boolean isAttachedToWindow; private boolean isAttachedToWindow;
private boolean showMultiWindowTimeBar; private boolean showMultiWindowTimeBar;
private boolean showPlayButtonIfSuppressed;
private boolean multiWindowTimeBar; private boolean multiWindowTimeBar;
private boolean scrubbing; private boolean scrubbing;
private int showTimeoutMs; private int showTimeoutMs;
@ -373,6 +374,7 @@ public class PlayerControlView extends FrameLayout {
@Nullable AttributeSet playbackAttrs) { @Nullable AttributeSet playbackAttrs) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
int controllerLayoutId = R.layout.exo_player_control_view; int controllerLayoutId = R.layout.exo_player_control_view;
showPlayButtonIfSuppressed = true;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
@ -673,6 +675,20 @@ public class PlayerControlView extends FrameLayout {
updateTimeline(); updateTimeline();
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) {
this.showPlayButtonIfSuppressed = showPlayButtonIfSuppressed;
updatePlayPauseButton();
}
/** /**
* Sets the millisecond positions of extra ad markers relative to the start of the window (or * Sets the millisecond positions of extra ad markers relative to the start of the window (or
* timeline, if in multi-window mode) and whether each extra ad has been played or not. The * timeline, if in multi-window mode) and whether each extra ad has been played or not. The
@ -980,7 +996,7 @@ public class PlayerControlView extends FrameLayout {
return; return;
} }
if (playPauseButton != null) { if (playPauseButton != null) {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
@DrawableRes @DrawableRes
int drawableRes = int drawableRes =
shouldShowPlayButton shouldShowPlayButton
@ -1479,7 +1495,7 @@ public class PlayerControlView extends FrameLayout {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_HEADSETHOOK:
Util.handlePlayPauseButtonAction(player); Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed);
break; break;
case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PLAY:
Util.handlePlayButtonAction(player); Util.handlePlayButtonAction(player);
@ -1710,7 +1726,7 @@ public class PlayerControlView extends FrameLayout {
player.seekBack(); player.seekBack();
} }
} else if (playPauseButton == view) { } else if (playPauseButton == view) {
Util.handlePlayPauseButtonAction(player); Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed);
} else if (repeatToggleButton == view) { } else if (repeatToggleButton == view) {
if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
player.setRepeatMode( player.setRepeatMode(

View File

@ -712,6 +712,7 @@ public class PlayerNotificationManager {
private boolean useRewindActionInCompactView; private boolean useRewindActionInCompactView;
private boolean useFastForwardActionInCompactView; private boolean useFastForwardActionInCompactView;
private boolean usePlayPauseActions; private boolean usePlayPauseActions;
private boolean showPlayButtonIfSuppressed;
private boolean useStopAction; private boolean useStopAction;
private int badgeIconType; private int badgeIconType;
private boolean colorized; private boolean colorized;
@ -762,6 +763,7 @@ public class PlayerNotificationManager {
usePreviousAction = true; usePreviousAction = true;
useNextAction = true; useNextAction = true;
usePlayPauseActions = true; usePlayPauseActions = true;
showPlayButtonIfSuppressed = true;
useRewindAction = true; useRewindAction = true;
useFastForwardAction = true; useFastForwardAction = true;
colorized = true; colorized = true;
@ -971,6 +973,22 @@ public class PlayerNotificationManager {
} }
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) {
if (this.showPlayButtonIfSuppressed != showPlayButtonIfSuppressed) {
this.showPlayButtonIfSuppressed = showPlayButtonIfSuppressed;
invalidate();
}
}
/** /**
* Sets whether the stop action should be used. * Sets whether the stop action should be used.
* *
@ -1339,7 +1357,7 @@ public class PlayerNotificationManager {
stringActions.add(ACTION_REWIND); stringActions.add(ACTION_REWIND);
} }
if (usePlayPauseActions) { if (usePlayPauseActions) {
if (Util.shouldShowPlayButton(player)) { if (Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed)) {
stringActions.add(ACTION_PLAY); stringActions.add(ACTION_PLAY);
} else { } else {
stringActions.add(ACTION_PAUSE); stringActions.add(ACTION_PAUSE);
@ -1387,7 +1405,7 @@ public class PlayerNotificationManager {
if (leftSideActionIndex != -1) { if (leftSideActionIndex != -1) {
actionIndices[actionCounter++] = leftSideActionIndex; actionIndices[actionCounter++] = leftSideActionIndex;
} }
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
if (pauseActionIndex != -1 && !shouldShowPlayButton) { if (pauseActionIndex != -1 && !shouldShowPlayButton) {
actionIndices[actionCounter++] = pauseActionIndex; actionIndices[actionCounter++] = pauseActionIndex;
} else if (playActionIndex != -1 && shouldShowPlayButton) { } else if (playActionIndex != -1 && shouldShowPlayButton) {

View File

@ -1123,6 +1123,21 @@ public class PlayerView extends FrameLayout implements AdViewProvider {
controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
} }
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
@UnstableApi
public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) {
Assertions.checkStateNotNull(controller);
controller.setShowPlayButtonIfPlaybackIsSuppressed(showPlayButtonIfSuppressed);
}
/** /**
* Sets the millisecond positions of extra ad markers relative to the start of the window (or * Sets the millisecond positions of extra ad markers relative to the start of the window (or
* timeline, if in multi-window mode) and whether each extra ad has been played or not. The * timeline, if in multi-window mode) and whether each extra ad has been played or not. The