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
This commit is contained in:
parent
048d71e392
commit
66c19390e2
@ -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:
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user