| /* |
| * 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)); |
| } |
| } |
| } |