AudioFocusManager.java

/*
 * 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 androidx.media3.exoplayer;

import static androidx.media3.common.util.Assertions.checkNotNull;

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 androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.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 focusGainToRequest;
  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 =
        checkNotNull(
            (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;
      focusGainToRequest = convertAudioAttributesToFocusGain(audioAttributes);
      Assertions.checkArgument(
          focusGainToRequest == C.AUDIOFOCUS_GAIN || focusGainToRequest == 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 (shouldAbandonAudioFocusIfHeld(playbackState)) {
      abandonAudioFocusIfHeld();
      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;
    abandonAudioFocusIfHeld();
  }

  // Internal methods.

  @VisibleForTesting
  /* package */ AudioManager.OnAudioFocusChangeListener getFocusListener() {
    return focusListener;
  }

  private boolean shouldAbandonAudioFocusIfHeld(@Player.State int playbackState) {
    return playbackState == Player.STATE_IDLE || focusGainToRequest != 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 abandonAudioFocusIfHeld() {
    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(checkNotNull(audioAttributes).usage),
        focusGainToRequest);
  }

  @RequiresApi(26)
  private int requestAudioFocusV26() {
    if (audioFocusRequest == null || rebuildAudioFocusRequest) {
      AudioFocusRequest.Builder builder =
          audioFocusRequest == null
              ? new AudioFocusRequest.Builder(focusGainToRequest)
              : new AudioFocusRequest.Builder(audioFocusRequest);

      boolean willPauseWhenDucked = willPauseWhenDucked();
      audioFocusRequest =
          builder
              .setAudioAttributes(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);
        abandonAudioFocusIfHeld();
        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));
    }
  }
}