blob: 5aeca440ffeafc538c3a66a47fff18729bc62dcf [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2;
import android.content.Context;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Handler;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Manages requesting and responding to changes in audio focus. */
/* package */ final class AudioFocusManager {
/** Interface to allow AudioFocusManager to give commands to a player. */
public interface PlayerControl {
/**
* Called when the volume multiplier on the player should be changed.
*
* @param volumeMultiplier The new volume multiplier.
*/
void setVolumeMultiplier(float volumeMultiplier);
/**
* Called when a command must be executed on the player.
*
* @param playerCommand The command that must be executed.
*/
void executePlayerCommand(@PlayerCommand int playerCommand);
}
/**
* Player commands. One of {@link #PLAYER_COMMAND_DO_NOT_PLAY}, {@link
* #PLAYER_COMMAND_WAIT_FOR_CALLBACK} or {@link #PLAYER_COMMAND_PLAY_WHEN_READY}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
PLAYER_COMMAND_DO_NOT_PLAY,
PLAYER_COMMAND_WAIT_FOR_CALLBACK,
PLAYER_COMMAND_PLAY_WHEN_READY,
})
public @interface PlayerCommand {}
/** Do not play. */
public static final int PLAYER_COMMAND_DO_NOT_PLAY = -1;
/** Do not play now. Wait for callback to play. */
public static final int PLAYER_COMMAND_WAIT_FOR_CALLBACK = 0;
/** Play freely. */
public static final int PLAYER_COMMAND_PLAY_WHEN_READY = 1;
/** Audio focus state. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AUDIO_FOCUS_STATE_NO_FOCUS,
AUDIO_FOCUS_STATE_HAVE_FOCUS,
AUDIO_FOCUS_STATE_LOSS_TRANSIENT,
AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK
})
private @interface AudioFocusState {}
/** No audio focus is currently being held. */
private static final int AUDIO_FOCUS_STATE_NO_FOCUS = 0;
/** The requested audio focus is currently held. */
private static final int AUDIO_FOCUS_STATE_HAVE_FOCUS = 1;
/** Audio focus has been temporarily lost. */
private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT = 2;
/** 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 String TAG = "AudioFocusManager";
private static final float VOLUME_MULTIPLIER_DUCK = 0.2f;
private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f;
private final AudioManager audioManager;
private final AudioFocusListener focusListener;
@Nullable private PlayerControl playerControl;
@Nullable private AudioAttributes audioAttributes;
@AudioFocusState private int audioFocusState;
@C.AudioFocusGain private int focusGain;
private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT;
private @MonotonicNonNull AudioFocusRequest audioFocusRequest;
private boolean rebuildAudioFocusRequest;
/**
* Constructs an AudioFocusManager to automatically handle audio focus for a player.
*
* @param context The current context.
* @param eventHandler A {@link Handler} to for the thread on which the player is used.
* @param playerControl A {@link PlayerControl} to handle commands from this instance.
*/
public AudioFocusManager(Context context, Handler eventHandler, PlayerControl playerControl) {
this.audioManager =
(AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
this.playerControl = playerControl;
this.focusListener = new AudioFocusListener(eventHandler);
this.audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS;
}
/** Gets the current player volume multiplier. */
public float getVolumeMultiplier() {
return volumeMultiplier;
}
/**
* Sets audio attributes that should be used to manage audio focus.
*
* <p>Call {@link #updateAudioFocus(boolean, int)} to update the audio focus based on these
* attributes.
*
* @param audioAttributes The audio attributes or {@code null} if audio focus should not be
* managed automatically.
*/
public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) {
if (!Util.areEqual(this.audioAttributes, audioAttributes)) {
this.audioAttributes = audioAttributes;
focusGain = convertAudioAttributesToFocusGain(audioAttributes);
Assertions.checkArgument(
focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE,
"Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME.");
}
}
/**
* Called by the player to abandon or request audio focus based on the desired player state.
*
* @param playWhenReady The desired value of playWhenReady.
* @param playbackState The desired playback state.
* @return A {@link PlayerCommand} to execute on the player.
*/
@PlayerCommand
public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) {
if (shouldAbandonAudioFocus(playbackState)) {
abandonAudioFocus();
return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY;
}
return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY;
}
/**
* Called when the manager is no longer required. Audio focus will be released without making any
* calls to the {@link PlayerControl}.
*/
public void release() {
playerControl = null;
abandonAudioFocus();
}
// Internal methods.
@VisibleForTesting
/* package */ AudioManager.OnAudioFocusChangeListener getFocusListener() {
return focusListener;
}
private boolean shouldAbandonAudioFocus(@Player.State int playbackState) {
return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN;
}
@PlayerCommand
private int requestAudioFocus() {
if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) {
return PLAYER_COMMAND_PLAY_WHEN_READY;
}
int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault();
if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS);
return PLAYER_COMMAND_PLAY_WHEN_READY;
} else {
setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS);
return PLAYER_COMMAND_DO_NOT_PLAY;
}
}
private void abandonAudioFocus() {
if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) {
return;
}
if (Util.SDK_INT >= 26) {
abandonAudioFocusV26();
} else {
abandonAudioFocusDefault();
}
setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS);
}
private int requestAudioFocusDefault() {
return audioManager.requestAudioFocus(
focusListener,
Util.getStreamTypeForAudioUsage(Assertions.checkNotNull(audioAttributes).usage),
focusGain);
}
@RequiresApi(26)
private int requestAudioFocusV26() {
if (audioFocusRequest == null || rebuildAudioFocusRequest) {
AudioFocusRequest.Builder builder =
audioFocusRequest == null
? new AudioFocusRequest.Builder(focusGain)
: new AudioFocusRequest.Builder(audioFocusRequest);
boolean willPauseWhenDucked = willPauseWhenDucked();
audioFocusRequest =
builder
.setAudioAttributes(Assertions.checkNotNull(audioAttributes).getAudioAttributesV21())
.setWillPauseWhenDucked(willPauseWhenDucked)
.setOnAudioFocusChangeListener(focusListener)
.build();
rebuildAudioFocusRequest = false;
}
return audioManager.requestAudioFocus(audioFocusRequest);
}
private void abandonAudioFocusDefault() {
audioManager.abandonAudioFocus(focusListener);
}
@RequiresApi(26)
private void abandonAudioFocusV26() {
if (audioFocusRequest != null) {
audioManager.abandonAudioFocusRequest(audioFocusRequest);
}
}
private boolean willPauseWhenDucked() {
return audioAttributes != null && audioAttributes.contentType == C.CONTENT_TYPE_SPEECH;
}
/**
* Converts {@link AudioAttributes} to one of the audio focus request.
*
* <p>This follows the class Javadoc of {@link AudioFocusRequest}.
*
* @param audioAttributes The audio attributes associated with this focus request.
* @return The type of audio focus gain that should be requested.
*/
@C.AudioFocusGain
private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) {
if (audioAttributes == null) {
// Don't handle audio focus. It may be either video only contents or developers
// want to have more finer grained control. (e.g. adding audio focus listener)
return C.AUDIOFOCUS_NONE;
}
switch (audioAttributes.usage) {
// USAGE_VOICE_COMMUNICATION_SIGNALLING is for DTMF that may happen multiple times
// during the phone call when AUDIOFOCUS_GAIN_TRANSIENT is requested for that.
// Don't request audio focus here.
case C.USAGE_VOICE_COMMUNICATION_SIGNALLING:
return C.AUDIOFOCUS_NONE;
// Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music
// playback, for a game or a video player'
case C.USAGE_GAME:
case C.USAGE_MEDIA:
return C.AUDIOFOCUS_GAIN;
// Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent
// multiple media playback happen at the same time.
case C.USAGE_UNKNOWN:
Log.w(
TAG,
"Specify a proper usage in the audio attributes for audio focus"
+ " handling. Using AUDIOFOCUS_GAIN by default.");
return C.AUDIOFOCUS_GAIN;
// Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or
// during a VoIP call'
case C.USAGE_ALARM:
case C.USAGE_VOICE_COMMUNICATION:
return C.AUDIOFOCUS_GAIN_TRANSIENT;
// Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing
// driving directions or notifications'
case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
case C.USAGE_ASSISTANCE_SONIFICATION:
case C.USAGE_NOTIFICATION:
case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
case C.USAGE_NOTIFICATION_EVENT:
case C.USAGE_NOTIFICATION_RINGTONE:
return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
// Javadoc says 'AUDIOFOCUS_GAIN_EXCLUSIVE: This is typically used if you are doing
// audio recording or speech recognition'.
// Assistant is considered as both recording and notifying developer
case C.USAGE_ASSISTANT:
if (Util.SDK_INT >= 19) {
return C.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
} else {
return C.AUDIOFOCUS_GAIN_TRANSIENT;
}
// Special usages:
case C.USAGE_ASSISTANCE_ACCESSIBILITY:
if (audioAttributes.contentType == C.CONTENT_TYPE_SPEECH) {
// Voice shouldn't be interrupted by other playback.
return C.AUDIOFOCUS_GAIN_TRANSIENT;
}
return C.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
default:
Log.w(TAG, "Unidentified audio usage: " + audioAttributes.usage);
return C.AUDIOFOCUS_NONE;
}
}
private void setAudioFocusState(@AudioFocusState int audioFocusState) {
if (this.audioFocusState == audioFocusState) {
return;
}
this.audioFocusState = audioFocusState;
float volumeMultiplier =
(audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK)
? AudioFocusManager.VOLUME_MULTIPLIER_DUCK
: AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT;
if (this.volumeMultiplier == volumeMultiplier) {
return;
}
this.volumeMultiplier = volumeMultiplier;
if (playerControl != null) {
playerControl.setVolumeMultiplier(volumeMultiplier);
}
}
private void handlePlatformAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS);
executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY);
return;
case AudioManager.AUDIOFOCUS_LOSS:
executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY);
abandonAudioFocus();
return;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) {
executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK);
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT);
} else {
setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK);
}
return;
default:
Log.w(TAG, "Unknown focus change type: " + focusChange);
}
}
private void executePlayerCommand(@PlayerCommand int playerCommand) {
if (playerControl != null) {
playerControl.executePlayerCommand(playerCommand);
}
}
// Internal audio focus listener.
private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener {
private final Handler eventHandler;
public AudioFocusListener(Handler eventHandler) {
this.eventHandler = eventHandler;
}
@Override
public void onAudioFocusChange(int focusChange) {
eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange));
}
}
}