diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2baa657f2fa..6adcd72bc20 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,13 @@ and nullable array element types are not detected as nullable. Examples are `TrackSelectorResult` and `SimpleDecoder` method parameters ([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: * Fix seeking issues in AC4 streams caused by not identifying decode-only samples correctly diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index dcd7b2e8ad5..08dd27056ee 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -3171,23 +3171,42 @@ public static String intToStringMaxRadix(int i) { *

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. + * @param player The {@link Player}. May be {@code null}. */ @EnsuresNonNullIf(result = false, expression = "#1") 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. + * + *

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 || !player.getPlayWhenReady() || 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. * *

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. */ public static boolean handlePlayButtonAction(@Nullable Player player) { @@ -3215,9 +3234,9 @@ public static boolean handlePlayButtonAction(@Nullable Player player) { * Updates the player to handle an interaction with a pause button. * *

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. */ public static boolean handlePauseButtonAction(@Nullable Player player) { @@ -3232,13 +3251,30 @@ public static boolean handlePauseButtonAction(@Nullable Player player) { * Updates the player to handle an interaction with a play or pause button. * *

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. */ 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. + * + *

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); } else { return handlePauseButtonAction(player); diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 8fa6c236126..b9f7b3debb7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -23,7 +23,6 @@ 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_STOP; -import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; @@ -320,8 +319,8 @@ public final MediaNotification createNotification( mediaSession, player.getAvailableCommands(), customLayoutWithEnabledCommandButtonsOnly.build(), - /* showPauseButton= */ player.getPlayWhenReady() - && player.getPlaybackState() != STATE_ENDED), + !Util.shouldShowPlayButton( + player, mediaSession.getShowPlayButtonIfPlaybackIsSuppressed())), builder, actionFactory); mediaStyle.setShowActionsInCompactView(compactViewIndices); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java index e393330ec67..2f6d536cdca 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryService.java @@ -461,6 +461,22 @@ public Builder setCustomLayout(List customLayout) { return super.setCustomLayout(customLayout); } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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}. * @@ -481,7 +497,8 @@ public MediaLibrarySession build() { customLayout, callback, extras, - checkNotNull(bitmapLoader)); + checkNotNull(bitmapLoader), + playIfSuppressed); } } @@ -493,9 +510,18 @@ public MediaLibrarySession build() { ImmutableList customLayout, MediaSession.Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { super( - context, id, player, sessionActivity, customLayout, callback, tokenExtras, bitmapLoader); + context, + id, + player, + sessionActivity, + customLayout, + callback, + tokenExtras, + bitmapLoader, + playIfSuppressed); } @Override @@ -507,7 +533,8 @@ public MediaLibrarySession build() { ImmutableList customLayout, MediaSession.Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { return new MediaLibrarySessionImpl( this, context, @@ -517,7 +544,8 @@ public MediaLibrarySession build() { customLayout, (Callback) callback, tokenExtras, - bitmapLoader); + bitmapLoader, + playIfSuppressed); } @Override diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index c31bc3737ff..5dc6271b354 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -78,7 +78,8 @@ public MediaLibrarySessionImpl( ImmutableList customLayout, MediaLibrarySession.Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { super( instance, context, @@ -88,7 +89,8 @@ public MediaLibrarySessionImpl( customLayout, callback, tokenExtras, - bitmapLoader); + bitmapLoader, + playIfSuppressed); this.instance = instance; this.callback = callback; subscriptions = new ArrayMap<>(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index c1f0a7507bc..ad5ad4e8bec 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -377,6 +377,22 @@ public Builder setCustomLayout(List customLayout) { return super.setCustomLayout(customLayout); } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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}. * @@ -397,7 +413,8 @@ public MediaSession build() { customLayout, callback, extras, - checkNotNull(bitmapLoader)); + checkNotNull(bitmapLoader), + playIfSuppressed); } } @@ -589,7 +606,8 @@ public static ControllerInfo createTestOnlyControllerInfo( ImmutableList customLayout, Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { synchronized (STATIC_LOCK) { if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) { throw new IllegalStateException("Session ID must be unique. ID=" + id); @@ -605,7 +623,8 @@ public static ControllerInfo createTestOnlyControllerInfo( customLayout, callback, tokenExtras, - bitmapLoader); + bitmapLoader, + playIfSuppressed); } /* package */ MediaSessionImpl createImpl( @@ -616,7 +635,8 @@ public static ControllerInfo createTestOnlyControllerInfo( ImmutableList customLayout, Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { return new MediaSessionImpl( this, context, @@ -626,7 +646,8 @@ public static ControllerInfo createTestOnlyControllerInfo( customLayout, callback, tokenExtras, - bitmapLoader); + bitmapLoader, + playIfSuppressed); } /* package */ MediaSessionImpl getImpl() { @@ -935,6 +956,15 @@ public final BitmapLoader 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. * @@ -1740,6 +1770,7 @@ default void onRenderedFirstFrame(int seq) throws RemoteException {} /* package */ @Nullable PendingIntent sessionActivity; /* package */ Bundle extras; /* package */ @MonotonicNonNull BitmapLoader bitmapLoader; + /* package */ boolean playIfSuppressed; /* package */ ImmutableList customLayout; @@ -1751,6 +1782,7 @@ public BuilderBase(Context context, Player player, CallbackT callback) { this.callback = callback; extras = Bundle.EMPTY; customLayout = ImmutableList.of(); + playIfSuppressed = true; } @SuppressWarnings("unchecked") @@ -1789,6 +1821,13 @@ public BuilderT setCustomLayout(List customLayout) { return (BuilderT) this; } + @SuppressWarnings("unchecked") + public BuilderT setShowPlayButtonIfPlaybackIsSuppressed( + boolean showPlayButtonIfPlaybackIsSuppressed) { + this.playIfSuppressed = showPlayButtonIfPlaybackIsSuppressed; + return (BuilderT) this; + } + public abstract SessionT build(); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index b100682b24e..41fb3126afb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -112,6 +112,7 @@ private final BitmapLoader bitmapLoader; private final Runnable periodicSessionPositionInfoUpdateRunnable; private final Handler mainHandler; + private final boolean playIfSuppressed; private PlayerInfo playerInfo; private PlayerWrapper playerWrapper; @@ -140,7 +141,8 @@ public MediaSessionImpl( ImmutableList customLayout, MediaSession.Callback callback, Bundle tokenExtras, - BitmapLoader bitmapLoader) { + BitmapLoader bitmapLoader, + boolean playIfSuppressed) { this.context = context; this.instance = instance; @@ -156,6 +158,7 @@ public MediaSessionImpl( applicationHandler = new Handler(player.getApplicationLooper()); this.callback = callback; this.bitmapLoader = bitmapLoader; + this.playIfSuppressed = playIfSuppressed; playerInfo = PlayerInfo.DEFAULT; onPlayerInfoChangedHandler = new PlayerInfoChangedHandler(player.getApplicationLooper()); @@ -189,7 +192,7 @@ public MediaSessionImpl( sessionLegacyStub = new MediaSessionLegacyStub(/* session= */ thisRef, sessionUri, applicationHandler); - PlayerWrapper playerWrapper = new PlayerWrapper(player); + PlayerWrapper playerWrapper = new PlayerWrapper(player, playIfSuppressed); this.playerWrapper = playerWrapper; this.playerWrapper.setCustomLayout(customLayout); postOrRun( @@ -208,7 +211,8 @@ public void setPlayer(Player player) { if (player == playerWrapper.getWrappedPlayer()) { return; } - setPlayerInternal(/* oldPlayerWrapper= */ playerWrapper, new PlayerWrapper(player)); + setPlayerInternal( + /* oldPlayerWrapper= */ playerWrapper, new PlayerWrapper(player, playIfSuppressed)); } private void setPlayerInternal( @@ -397,6 +401,10 @@ public BitmapLoader getBitmapLoader() { return bitmapLoader; } + public boolean shouldPlayIfSuppressed() { + return playIfSuppressed; + } + public void setAvailableCommands( ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) { if (sessionStub.getConnectedControllersManager().isConnected(controller)) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 8a56dc6b0d5..887b36715b7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -346,7 +346,9 @@ private void handleMediaPlayPauseOnHandler(RemoteUserInfo remoteUserInfo) { mediaPlayPauseKeyHandler.clearPendingMediaPlayPauseKey(); dispatchSessionTaskWithPlayerCommand( COMMAND_PLAY_PAUSE, - controller -> Util.handlePlayPauseButtonAction(sessionImpl.getPlayerWrapper()), + controller -> + Util.handlePlayPauseButtonAction( + sessionImpl.getPlayerWrapper(), sessionImpl.shouldPlayIfSuppressed()), remoteUserInfo); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 8cb439a0c4d..3959820cbcf 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -759,24 +759,25 @@ public static RatingCompat convertToRatingCompat(@Nullable Rating rating) { /** Converts {@link Player}' states to state of {@link PlaybackStateCompat}. */ @PlaybackStateCompat.State - public static int convertToPlaybackStateCompatState( - @Nullable PlaybackException playerError, - @Player.State int playbackState, - boolean playWhenReady) { - if (playerError != null) { + public static int convertToPlaybackStateCompatState(Player player, boolean playIfSuppressed) { + 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; case Player.STATE_READY: - return playWhenReady ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED; + return shouldShowPlayButton + ? PlaybackStateCompat.STATE_PAUSED + : PlaybackStateCompat.STATE_PLAYING; case Player.STATE_ENDED: return PlaybackStateCompat.STATE_STOPPED; case Player.STATE_BUFFERING: - return playWhenReady - ? PlaybackStateCompat.STATE_BUFFERING - : PlaybackStateCompat.STATE_PAUSED; + return shouldShowPlayButton + ? PlaybackStateCompat.STATE_PAUSED + : PlaybackStateCompat.STATE_BUFFERING; default: throw new IllegalArgumentException("Unrecognized State: " + playbackState); } diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index 24c7cb019c6..a2b40c48591 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -64,13 +64,16 @@ private static final int STATUS_CODE_SUCCESS_COMPAT = -1; + private final boolean playIfSuppressed; + private int legacyStatusCode; @Nullable private String legacyErrorMessage; @Nullable private Bundle legacyErrorExtras; private ImmutableList customLayout; - public PlayerWrapper(Player player) { + public PlayerWrapper(Player player, boolean playIfSuppressed) { super(player); + this.playIfSuppressed = playIfSuppressed; legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; customLayout = ImmutableList.of(); } @@ -968,9 +971,7 @@ public PlaybackStateCompat createPlaybackStateCompat() { .build(); } @Nullable PlaybackException playerError = getPlayerError(); - int state = - MediaUtils.convertToPlaybackStateCompatState( - playerError, getPlaybackState(), getPlayWhenReady()); + int state = MediaUtils.convertToPlaybackStateCompatState(/* player= */ this, playIfSuppressed); // Always advertise ACTION_SET_RATING. long actions = PlaybackStateCompat.ACTION_SET_RATING; Commands availableCommands = getAvailableCommands(); diff --git a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java index e89365818aa..8b0a6177d1a 100644 --- a/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/DefaultMediaNotificationProviderTest.java @@ -43,11 +43,14 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; import androidx.media3.common.Player.Commands; +import androidx.media3.common.SimpleBasePlayer; import androidx.media3.common.util.BitmapLoader; import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; 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 java.util.ArrayList; import java.util.List; @@ -663,6 +666,320 @@ protected ImmutableList getMediaButtons( 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 public void provider_idsNotSpecified_usesDefaultIds() { Context context = ApplicationProvider.getApplicationContext(); @@ -1010,6 +1327,31 @@ public MediaMetadata getMediaMetadata() { }; } + 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. */ private static final class TestService extends MediaLibraryService { @Nullable diff --git a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java index 430e14c61cc..fad36651e68 100644 --- a/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/PlayerWrapperTest.java @@ -42,7 +42,7 @@ public class PlayerWrapperTest { @Before public void setUp() { - playerWrapper = new PlayerWrapper(player); + playerWrapper = new PlayerWrapper(player, /* playIfSuppressed= */ true); when(player.isCommandAvailable(anyInt())).thenReturn(true); when(player.getApplicationLooper()).thenReturn(Looper.myLooper()); } diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java index aa9f9b3eaea..b0c2601a320 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaSessionConstants.java @@ -28,6 +28,8 @@ public class MediaSessionConstants { public static final String TEST_ON_VIDEO_SIZE_CHANGED = "onVideoSizeChanged"; public static final String TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION = "onTracksChanged_videoToAudioTransition"; + public static final String TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE = + "testSetShowPlayButtonIfSuppressedToFalse"; // Bundle keys public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands"; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java index d2a08c7465d..a7c54412f56 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerCompatCallbackWithMediaSessionTest.java @@ -20,6 +20,7 @@ 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_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.TIMEOUT_MS; import static com.google.common.truth.Truth.assertThat; @@ -662,12 +663,6 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { 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( playbackStateCompatRef .get() @@ -713,12 +708,6 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { assertThat(playbackStateCompatRef.get().getState()) .isEqualTo(PlaybackStateCompat.STATE_BUFFERING); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); - assertThat( - playbackStateCompatRef - .get() - .getExtras() - .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) - .isEqualTo(1f); assertThat( playbackStateCompatRef .get() @@ -766,6 +755,44 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { .getExtras() .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) .isEqualTo(1f); + assertThat(controllerCompat.getPlaybackState().getState()) + .isEqualTo(PlaybackStateCompat.STATE_STOPPED); + assertThat(controllerCompat.getPlaybackState().getPlaybackSpeed()).isEqualTo(0f); + assertThat( + controllerCompat + .getPlaybackState() + .getExtras() + .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) + .isEqualTo(1f); + } + + @Test + public void playbackStateChange_withPlaybackSuppression_notifiesPaused() throws Exception { + session.getMockPlayer().setPlaybackState(Player.STATE_READY); + session + .getMockPlayer() + .setPlayWhenReady(/* playWhenReady= */ true, Player.PLAYBACK_SUPPRESSION_REASON_NONE); + AtomicReference 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() @@ -773,7 +800,7 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) .isEqualTo(1f); assertThat(controllerCompat.getPlaybackState().getState()) - .isEqualTo(PlaybackStateCompat.STATE_STOPPED); + .isEqualTo(PlaybackStateCompat.STATE_PAUSED); assertThat(controllerCompat.getPlaybackState().getPlaybackSpeed()).isEqualTo(0f); assertThat( controllerCompat @@ -784,8 +811,13 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { } @Test - public void playbackStateChange_withPlaybackSuppression_notifiesPlayingWithSpeedZero() - throws Exception { + 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() @@ -812,12 +844,6 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { assertThat(playbackStateCompatRef.get().getState()) .isEqualTo(PlaybackStateCompat.STATE_PLAYING); assertThat(playbackStateCompatRef.get().getPlaybackSpeed()).isEqualTo(0f); - assertThat( - playbackStateCompatRef - .get() - .getExtras() - .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) - .isEqualTo(1f); assertThat( playbackStateCompatRef .get() @@ -833,6 +859,7 @@ public void onPlaybackStateChanged(PlaybackStateCompat playbackStateCompat) { .getExtras() .getFloat(MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT)) .isEqualTo(1f); + session.release(); } @Test diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java index 38cd4d04e3e..49850464f31 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaSessionProviderService.java @@ -65,6 +65,7 @@ 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_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 android.app.PendingIntent; @@ -284,6 +285,11 @@ public MediaSession.ConnectionResult onConnect( mockPlayer.currentTracks = MediaTestUtils.createDefaultVideoTracks(); break; } + case TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE: + { + builder.setShowPlayButtonIfPlaybackIsSuppressed(false); + break; + } default: // fall out } diff --git a/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java index 1e14fa5afa6..ab954343cc8 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/LegacyPlayerControlView.java @@ -330,6 +330,7 @@ public interface ProgressUpdateListener { private boolean isAttachedToWindow; private boolean showMultiWindowTimeBar; + private boolean showPlayButtonIfSuppressed; private boolean multiWindowTimeBar; private boolean scrubbing; private int showTimeoutMs; @@ -373,6 +374,7 @@ public LegacyPlayerControlView( @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_legacy_player_control_view; + showPlayButtonIfSuppressed = true; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; @@ -571,6 +573,20 @@ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { updateTimeline(); } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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 * timeline, if in multi-window mode) and whether each extra ad has been played or not. The @@ -842,7 +858,7 @@ private void updatePlayPauseButton() { } boolean requestPlayPauseFocus = false; boolean requestPlayPauseAccessibilityFocus = false; - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); if (playButton != null) { requestPlayPauseFocus |= !shouldShowPlayButton && playButton.isFocused(); requestPlayPauseAccessibilityFocus |= @@ -1083,7 +1099,7 @@ private void updateProgress() { } private void requestPlayPauseFocus() { - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); if (shouldShowPlayButton && playButton != null) { playButton.requestFocus(); } else if (!shouldShowPlayButton && pauseButton != null) { @@ -1092,7 +1108,7 @@ private void requestPlayPauseFocus() { } private void requestPlayPauseAccessibilityFocus() { - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); if (shouldShowPlayButton && playButton != null) { playButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } else if (!shouldShowPlayButton && pauseButton != null) { @@ -1202,7 +1218,7 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - Util.handlePlayPauseButtonAction(player); + Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); break; case KeyEvent.KEYCODE_MEDIA_PLAY: Util.handlePlayButtonAction(player); diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java index eb13af7cf6e..c8424ca82e6 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerControlView.java @@ -335,6 +335,7 @@ public interface OnFullScreenModeChangedListener { private boolean isFullScreen; private boolean isAttachedToWindow; private boolean showMultiWindowTimeBar; + private boolean showPlayButtonIfSuppressed; private boolean multiWindowTimeBar; private boolean scrubbing; private int showTimeoutMs; @@ -373,6 +374,7 @@ public PlayerControlView( @Nullable AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_player_control_view; + showPlayButtonIfSuppressed = true; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS; @@ -673,6 +675,20 @@ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { updateTimeline(); } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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 * timeline, if in multi-window mode) and whether each extra ad has been played or not. The @@ -980,7 +996,7 @@ private void updatePlayPauseButton() { return; } if (playPauseButton != null) { - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); @DrawableRes int drawableRes = shouldShowPlayButton @@ -1479,7 +1495,7 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: - Util.handlePlayPauseButtonAction(player); + Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); break; case KeyEvent.KEYCODE_MEDIA_PLAY: Util.handlePlayButtonAction(player); @@ -1710,7 +1726,7 @@ public void onClick(View view) { player.seekBack(); } } else if (playPauseButton == view) { - Util.handlePlayPauseButtonAction(player); + Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed); } else if (repeatToggleButton == view) { if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) { player.setRepeatMode( diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java index fb99b3d88bf..973320e2804 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java @@ -712,6 +712,7 @@ public void onBitmap(final Bitmap bitmap) { private boolean useRewindActionInCompactView; private boolean useFastForwardActionInCompactView; private boolean usePlayPauseActions; + private boolean showPlayButtonIfSuppressed; private boolean useStopAction; private int badgeIconType; private boolean colorized; @@ -762,6 +763,7 @@ protected PlayerNotificationManager( usePreviousAction = true; useNextAction = true; usePlayPauseActions = true; + showPlayButtonIfSuppressed = true; useRewindAction = true; useFastForwardAction = true; colorized = true; @@ -971,6 +973,22 @@ public final void setUsePlayPauseActions(boolean usePlayPauseActions) { } } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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. * @@ -1339,7 +1357,7 @@ protected List getActions(Player player) { stringActions.add(ACTION_REWIND); } if (usePlayPauseActions) { - if (Util.shouldShowPlayButton(player)) { + if (Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed)) { stringActions.add(ACTION_PLAY); } else { stringActions.add(ACTION_PAUSE); @@ -1387,7 +1405,7 @@ protected int[] getActionIndicesForCompactView(List actionNames, Player if (leftSideActionIndex != -1) { actionIndices[actionCounter++] = leftSideActionIndex; } - boolean shouldShowPlayButton = Util.shouldShowPlayButton(player); + boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed); if (pauseActionIndex != -1 && !shouldShowPlayButton) { actionIndices[actionCounter++] = pauseActionIndex; } else if (playActionIndex != -1 && shouldShowPlayButton) { diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index 198a2c1fe34..0c726e4eaaf 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -1123,6 +1123,21 @@ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); } + /** + * Sets whether a play button is shown if playback is {@linkplain + * Player#getPlaybackSuppressionReason() suppressed}. + * + *

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 * timeline, if in multi-window mode) and whether each extra ad has been played or not. The