diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java new file mode 100644 index 0000000000..5806f57a08 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/AudioFocusManager.java @@ -0,0 +1,397 @@ +/* + * 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 org.mozilla.thirdparty.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 org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.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)); + } + } +} |