From bd5f99cb09bec02981a8d6aa7fa345db33851091 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 18 Jun 2024 09:39:35 -0700 Subject: [PATCH] Audio focus player command clean up The 'player commands' returned to ExoPlayerImpl instruct the player on how to treat the current audio focus state. The current return value when playWhenReady==false is misleading because it implies we are definitely not allowed to play as if we've lost focus. Instead, we should return the actual player command corresponding to the focus state we are in. This has no practical effect in ExoPlayerImpl as we already ignore the 'player command' completely when playWhenReady=false. To facilitate this change, we also introduce a new internal state for FOCUS_NOT_REQUESTED to distinguish it from the state in which we lost focus. #cherrypick PiperOrigin-RevId: 644416586 (cherry picked from commit 66c19390e2b31106a2a467366f7945c37a273fc6) --- .../media3/exoplayer/AudioFocusManager.java | 47 ++++++---- .../exoplayer/AudioFocusManagerTest.java | 90 +++++++++++++++++-- 2 files changed, 116 insertions(+), 21 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java index fe7c3c2d33..8e1c1c84f1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java @@ -72,13 +72,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; }) public @interface PlayerCommand {} - /** Do not play. */ + /** Do not play, because audio focus is lost or denied. */ public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1; - /** Do not play now. Wait for callback to play. */ + /** Do not play now, because of a transient focus loss. */ public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0; - /** Play freely. */ + /** Play freely, because audio focus is granted or not applicable. */ public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1; /** Audio focus state. */ @@ -86,6 +86,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({ + AUDIO_FOCUS_STATE_NOT_REQUESTED, AUDIO_FOCUS_STATE_NO_FOCUS, AUDIO_FOCUS_STATE_HAVE_FOCUS, AUDIO_FOCUS_STATE_LOSS_TRANSIENT, @@ -93,17 +94,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; }) private @interface AudioFocusState {} + /** Audio focus has not been requested yet. */ + private static final int AUDIO_FOCUS_STATE_NOT_REQUESTED = 0; + /** No audio focus is currently being held. */ - private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0; + private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 1; /** The requested audio focus is currently held. */ - private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1; + private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 2; /** Audio focus has been temporarily lost. */ - private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2; + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 3; /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */ - private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 3; + private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 4; /** * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link @@ -181,7 +185,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE)); this.playerControl = playerControl; this.focusListener = new AudioFocusListener(eventHandler); - this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + this.audioFocusState = AUDIO_FOCUS_STATE_NOT_REQUESTED; } /** Gets the current player volume multiplier. */ @@ -217,11 +221,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ public @PlayerCommand int updateAudioFocus( boolean playWhenReady, @Player.State int playbackState) { - if (shouldAbandonAudioFocusIfHeld(playbackState)) { + if (!shouldHandleAudioFocus(playbackState)) { abandonAudioFocusIfHeld(); - return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + setAudioFocusState(AUDIO_FOCUS_STATE_NOT_REQUESTED); + return PLAYER_COMMAND_PLAY_WHEN_READY; + } + if (playWhenReady) { + return requestAudioFocus(); + } + switch (audioFocusState) { + case AUDIO_FOCUS_STATE_NO_FOCUS: + return PLAYER_COMMAND_DO_NOT_PLAY; + case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: + return PLAYER_COMMAND_WAIT_FOR_CALLBACK; + default: + return PLAYER_COMMAND_PLAY_WHEN_READY; } - return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; } /** @@ -231,6 +246,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public void release() { playerControl = null; abandonAudioFocusIfHeld(); + setAudioFocusState(AUDIO_FOCUS_STATE_NOT_REQUESTED); } // Internal methods. @@ -240,8 +256,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return focusListener; } - private boolean shouldAbandonAudioFocusIfHeld(@Player.State int playbackState) { - return playbackState == Player.STATE_IDLE || focusGainToRequest != AUDIOFOCUS_GAIN; + private boolean shouldHandleAudioFocus(@Player.State int playbackState) { + return playbackState != Player.STATE_IDLE && focusGainToRequest == AUDIOFOCUS_GAIN; } private @PlayerCommand int requestAudioFocus() { @@ -259,7 +275,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void abandonAudioFocusIfHeld() { - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS + || audioFocusState == AUDIO_FOCUS_STATE_NOT_REQUESTED) { return; } if (Util.SDK_INT >= 26) { @@ -267,7 +284,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } else { abandonAudioFocusDefault(); } - setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); } private int requestAudioFocusDefault() { @@ -417,6 +433,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; case AudioManager.AUDIOFOCUS_LOSS: executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); abandonAudioFocusIfHeld(); + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); return; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/AudioFocusManagerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/AudioFocusManagerTest.java index 712663290d..90e6aeff2d 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/AudioFocusManagerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/AudioFocusManagerTest.java @@ -69,7 +69,7 @@ public class AudioFocusManagerTest { audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = @@ -188,7 +188,7 @@ public class AudioFocusManagerTest { // Audio focus should not be requested yet, because playWhenReady is false. assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); // Audio focus should be requested now that playWhenReady is true. @@ -241,6 +241,84 @@ public class AudioFocusManagerTest { assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f); } + @Test + public void updateAudioFocus_toPausedBeforeRequestingFocus_setsPlayerCommandPlayWhenReady() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY); + + assertThat(playerCommand).isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + } + + @Test + public void updateAudioFocus_toPausedWithFocus_setsPlayerCommandPlayWhenReady() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY); + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY); + + assertThat(playerCommand).isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + } + + @Test + public void updateAudioFocus_toPausedWithFocusLoss_setsPlayerCommandDoNotPlay() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY); + audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS); + shadowOf(Looper.getMainLooper()).idle(); + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY); + + assertThat(playerCommand).isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + } + + @Test + public void updateAudioFocus_toPausedWithTransientFocusLoss_setsPlayerCommandWaitForCallback() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY); + audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); + shadowOf(Looper.getMainLooper()).idle(); + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY); + + assertThat(playerCommand).isEqualTo(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + } + + @Test + public void + updateAudioFocus_toPausedWithTransientFocusLossCanDuck_setsPlayerCommandPlayWhenReady() { + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(AudioAttributes.DEFAULT); + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY); + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + shadowOf(Looper.getMainLooper()).idle(); + + @AudioFocusManager.PlayerCommand + int playerCommand = + audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY); + + assertThat(playerCommand).isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + } + @Test public void updateAudioFocus_abandonFocusWhenDucked_restoresFullVolume() { Shadows.shadowOf(audioManager) @@ -313,14 +391,14 @@ public class AudioFocusManagerTest { audioFocusManager.setAudioAttributes(null); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(request).isNull(); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); } @@ -332,14 +410,14 @@ public class AudioFocusManagerTest { audioFocusManager.setAudioAttributes(null); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(request).isNull(); assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); }