AudioFocusHandler.java
/*
* Copyright 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.media;
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
import static androidx.media.BaseMediaPlayer.PLAYER_STATE_ERROR;
import static androidx.media.BaseMediaPlayer.PLAYER_STATE_IDLE;
import static androidx.media.BaseMediaPlayer.PLAYER_STATE_PAUSED;
import static androidx.media.BaseMediaPlayer.PLAYER_STATE_PLAYING;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.ObjectsCompat;
/**
* Handles audio focus and noisy intent depending on the {@link AudioAttributesCompat}.
* <p>
* This follows our developer's guideline, and does nothing if the audio attribute hasn't set.
*
* @see {@docRoot}guide/topics/media-apps/audio-app/mediasession-callbacks.html
* @see {@docRoot}guide/topics/media-apps/video-app/mediasession-callbacks.html
* @see {@docRoot}guide/topics/media-apps/audio-focus.html
* @see {@docRoot}guide/topics/media-apps/volume-and-earphones.html
* @hide
*/
@VisibleForTesting(otherwise = PACKAGE_PRIVATE)
@RestrictTo(Scope.LIBRARY)
public class AudioFocusHandler {
private static final String TAG = "AudioFocusHandler";
private static final boolean DEBUG = false;
interface AudioFocusHandlerImpl {
boolean onPlayRequested();
boolean onPauseRequested();
void onPlayerStateChanged(int playerState);
void close();
void sendIntent(Intent intent);
}
private final AudioFocusHandlerImpl mImpl;
AudioFocusHandler(Context context, MediaSession2 session) {
mImpl = new AudioFocusHandlerImplBase(context, session);
}
/**
* Should be called when the {@link MediaSession2#play()} is called. Returns whether the play()
* can be proceed.
* <p>
* This matches with the Session.Callback#onPlay() written in the guideline.
*
* @return {@code true} if we don't need to handle audio focus or audio focus was granted.
* {@code false} otherwise (i.e. attempt to request audio focus was failed).
*/
public boolean onPlayRequested() {
return mImpl.onPlayRequested();
}
/**
* Should be called when the {@link MediaSession2#pause()} is called. Returns whether the
* pause() can be proceed.
* <p>
* This matches with the Session.Callback#onPlay() written in the guideline.
*
* @return {@code true} if we don't need to handle audio focus or audio focus was granted.
* {@code false} otherwise (i.e. attempt to request audio focus was failed).
*/
public boolean onPauseRequested() {
return mImpl.onPauseRequested();
}
/**
* Should be called when the player state is changed.
* <p>
* This is to implement the guideline for media session callback.
*/
public void onPlayerStateChanged(int playerState) {
mImpl.onPlayerStateChanged(playerState);
}
/**
* Closes this resource, relinquishing any underlying resources.
*/
public void close() {
mImpl.close();
}
/**
* Testing purpose.
*
* @param intent
*/
public void sendIntent(Intent intent) {
mImpl.sendIntent(intent);
}
private static class AudioFocusHandlerImplBase implements AudioFocusHandlerImpl {
// Value is from the {@link AudioFocusRequest} as follows
// 'A typical attenuation by the “ducked” application is a factor of 0.2f (or -14dB), that
// can for instance be applied with MediaPlayer.setVolume(0.2f) when using this class for
// playback.'
private static final float VOLUME_DUCK_FACTOR = 0.2f;
private final BroadcastReceiver mBecomingNoisyIntentReceiver = new NoisyIntentReceiver();
private final IntentFilter mIntentFilter =
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private final OnAudioFocusChangeListener mAudioFocusListener = new AudioFocusListener();
private final Object mLock = new Object();
private final Context mContext;
private final MediaSession2 mSession;
private final AudioManager mAudioManager;
@GuardedBy("mLock")
private AudioAttributesCompat mAudioAttributes;
@GuardedBy("mLock")
private boolean mHasAudioFocus;
@GuardedBy("mLock")
private boolean mResumeWhenAudioFocusGain;
@GuardedBy("mLock")
private boolean mHasRegisteredReceiver;
AudioFocusHandlerImplBase(Context context, MediaSession2 session) {
mContext = context;
mSession = session;
// Cannot use session.getContext() because session's impl isn't initialized at this
// moment.
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
private AudioAttributesCompat getAudioAttributesNotLocked() {
if (mSession.getVolumeProvider() != null) {
// Remote session. Ignore audio attributes.
return null;
}
BaseMediaPlayer player = mSession.getPlayer();
return player == null ? null : player.getAudioAttributes();
}
@GuardedBy("mLock")
private void updateAudioAttributesIfNeededLocked(AudioAttributesCompat attributes) {
if (ObjectsCompat.equals(attributes, mAudioAttributes)) {
// It's the same.
return;
}
// Keep cache of the audio attributes. Otherwise audio attributes may be changed
// between the audio focus request and audio focus change, resulting the unexpected
// situation.
mAudioAttributes = attributes;
if (mHasAudioFocus) {
mHasAudioFocus = requestAudioFocusLocked();
if (!mHasAudioFocus) {
Log.w(TAG, "Failed to regain audio focus.");
}
}
}
@Override
public boolean onPlayRequested() {
// Instead of registering a listener for audio attribute changes, grabs the new one
// here.
final AudioAttributesCompat attr = getAudioAttributesNotLocked();
synchronized (mLock) {
updateAudioAttributesIfNeededLocked(attr);
// Try getting audio focus.
if (!requestAudioFocusLocked()) {
return false;
}
}
return true;
}
@Override
public boolean onPauseRequested() {
synchronized (mLock) {
mResumeWhenAudioFocusGain = false;
}
return true;
}
@Override
public void onPlayerStateChanged(int playerState) {
switch (playerState) {
case PLAYER_STATE_IDLE: {
synchronized (mLock) {
abandonAudioFocusLocked();
}
break;
}
case PLAYER_STATE_PAUSED: {
final AudioAttributesCompat attr = getAudioAttributesNotLocked();
synchronized (mLock) {
updateAudioAttributesIfNeededLocked(attr);
unregisterReceiverLocked();
}
break;
}
case PLAYER_STATE_PLAYING: {
final AudioAttributesCompat attr = getAudioAttributesNotLocked();
synchronized (mLock) {
updateAudioAttributesIfNeededLocked(attr);
registerReceiverLocked();
}
break;
}
case PLAYER_STATE_ERROR: {
close();
break;
}
}
}
@Override
public void close() {
synchronized (mLock) {
unregisterReceiverLocked();
abandonAudioFocusLocked();
}
}
@Override
public void sendIntent(Intent intent) {
mBecomingNoisyIntentReceiver.onReceive(mContext, intent);
}
/**
* Requests audio focus. This may regain audio focus.
*
* @return {@code true} if we don't need to handle audio focus nor audio focus was granted.
* {@code false} only when the attempt to request audio focus was failed.
*/
@GuardedBy("mLock")
private boolean requestAudioFocusLocked() {
int focusGain = convertAudioAttributesToFocusGainLocked();
if (focusGain == AudioManager.AUDIOFOCUS_NONE) {
// Developer hasn't set audio focus request. Let the developer handle by themselves.
return true;
}
// Note: This API is deprecated from the API level 26, but there's not much reason to
// use the new API for now.
int audioFocusRequestResult = mAudioManager.requestAudioFocus(mAudioFocusListener,
mAudioAttributes.getVolumeControlStream(), focusGain);
if (audioFocusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mHasAudioFocus = true;
} else {
Log.w(TAG, "requestAudioFocus(" + focusGain + ") failed (return="
+ audioFocusRequestResult + ") playback wouldn't start.");
mHasAudioFocus = false;
}
if (DEBUG) {
Log.d(TAG, "requestAudioFocus(" + focusGain + "), result=" + mHasAudioFocus);
}
mResumeWhenAudioFocusGain = false;
return mHasAudioFocus;
}
/**
* Abandons audio focus if it has granted.
*/
@GuardedBy("mLock")
private void abandonAudioFocusLocked() {
if (!mHasAudioFocus) {
return;
}
if (DEBUG) {
Log.d(TAG, "abandonAudioFocus, result=" + mHasAudioFocus);
}
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mHasAudioFocus = false;
mResumeWhenAudioFocusGain = false;
}
@GuardedBy("mLock")
private void registerReceiverLocked() {
if (mHasRegisteredReceiver) {
return;
}
if (DEBUG) {
Log.d(TAG, "registering noisy intent");
}
// Registering the receiver multiple-times may not be allowed for newer platform.
// Register only when it's not registered.
mContext.registerReceiver(mBecomingNoisyIntentReceiver, mIntentFilter);
mHasRegisteredReceiver = true;
}
@GuardedBy("mLock")
private void unregisterReceiverLocked() {
if (!mHasRegisteredReceiver) {
return;
}
if (DEBUG) {
Log.d(TAG, "unregistering noisy intent");
}
mContext.unregisterReceiver(mBecomingNoisyIntentReceiver);
mHasRegisteredReceiver = false;
}
// Converts {@link AudioAttributesCompat} to one of the audio focus request. This follows
// the class Javadoc of {@link AudioFocusRequest}.
// Note: Implementation may not be the perfect match with the Javadoc because there's NO
// clear documentation for audio focus handling with the specific usage and content type.
@GuardedBy("mLock")
private int convertAudioAttributesToFocusGainLocked() {
AudioAttributesCompat audioAttributesCompat = mAudioAttributes;
if (audioAttributesCompat == null) {
return AudioManager.AUDIOFOCUS_NONE;
}
// Javadoc here means 'The different types of focus reuqests' written in the
// {@link AudioFocusRequest}.
switch (audioAttributesCompat.getUsage()) {
// Javadoc says 'AUDIOFOCUS_GAIN: Examples of uses of this focus gain are for music
// playback, for a game or a video player'
case AudioAttributesCompat.USAGE_GAME:
case AudioAttributesCompat.USAGE_MEDIA:
return AudioManager.AUDIOFOCUS_GAIN;
// Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT: An example is for playing an alarm, or
// during a VoIP call'
case AudioAttributesCompat.USAGE_ALARM:
case AudioAttributesCompat.USAGE_VOICE_COMMUNICATION:
case AudioAttributesCompat.USAGE_VOICE_COMMUNICATION_SIGNALLING:
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
// Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing
// driving directions or notifications'
case AudioAttributesCompat.USAGE_ASSISTANCE_ACCESSIBILITY:
case AudioAttributesCompat.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
case AudioAttributesCompat.USAGE_ASSISTANCE_SONIFICATION:
case AudioAttributesCompat.USAGE_ASSISTANT:
case AudioAttributesCompat.USAGE_NOTIFICATION:
case AudioAttributesCompat.USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
case AudioAttributesCompat.USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
case AudioAttributesCompat.USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
case AudioAttributesCompat.USAGE_NOTIFICATION_EVENT:
case AudioAttributesCompat.USAGE_NOTIFICATION_RINGTONE:
case AudioAttributesCompat.USAGE_UNKNOWN:
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
}
// Javadoc also mentioned about AUDIOFOCUS_GAIN_EXCLUSIVE that 'This is typically used
// if you are doing audio recording or speech recognition', but there's no way to
// distinguish playback vs recording only with the AudioAttributesCompat, and using
// media session for recording doesn't seem like a good use case. Don't handle audio
// focus, so developer can can decide more finer grained control.
return AudioManager.AUDIOFOCUS_NONE;
}
private class NoisyIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) {
Log.d(TAG, "Received noisy intent " + intent);
}
// This is always the main thread, except for the test.
synchronized (mLock) {
if (!mHasRegisteredReceiver) {
return;
}
}
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
final int usage;
synchronized (mLock) {
if (mAudioAttributes == null) {
return;
}
usage = mAudioAttributes.getUsage();
}
switch (usage) {
case AudioAttributesCompat.USAGE_MEDIA:
// Noisy intent guide says 'In the case of music players, users
// typically expect the playback to be paused.'
mSession.pause();
break;
case AudioAttributesCompat.USAGE_GAME:
// Noisy intent guide says 'For gaming apps, you may choose to
// significantly lower the volume instead'.
BaseMediaPlayer player = mSession.getPlayer();
if (player != null) {
player.setPlayerVolume(player.getPlayerVolume()
* VOLUME_DUCK_FACTOR);
}
break;
default:
// Noisy intent guide didn't say anything more for this. No-op for now.
break;
}
}
}
}
private class AudioFocusListener implements OnAudioFocusChangeListener {
private float mPlayerVolumeBeforeDucking;
private float mPlayerDuckingVolume;
// This is the thread where the AudioManager was originally instantiated.
// see: b/78617702
@Override
public void onAudioFocusChange(int focusGain) {
switch (focusGain) {
case AudioManager.AUDIOFOCUS_GAIN:
// Regains focus after the LOSS_TRANSIENT or LOSS_TRANSIENT_CAN_DUCK.
if (mSession.getPlayerState() == PLAYER_STATE_PAUSED) {
// Note: onPlayRequested() will be called again with this.
synchronized (mLock) {
if (!mResumeWhenAudioFocusGain) {
break;
}
}
mSession.play();
} else {
BaseMediaPlayer player = mSession.getPlayer();
if (player != null) {
// Resets the volume if the user didn't change it.
final float currentVolume = player.getPlayerVolume();
final float volumeBeforeDucking;
synchronized (mLock) {
if (currentVolume != mPlayerDuckingVolume) {
// User manually changed the volume meanwhile. Don't reset.
break;
}
volumeBeforeDucking = mPlayerVolumeBeforeDucking;
}
player.setPlayerVolume(volumeBeforeDucking);
}
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Audio-focus developer guide says 'Your app should pause playback
// immediately, as it won't ever receive an AUDIOFOCUS_GAIN callback'.
mSession.pause();
// Don't resume even after you regain the audio focus.
synchronized (mLock) {
mResumeWhenAudioFocusGain = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
final boolean pause;
synchronized (mLock) {
if (mAudioAttributes == null) {
// This shouldn't happen. Just ignoring for now.
break;
}
pause = (mAudioAttributes.getContentType()
== AudioAttributesCompat.CONTENT_TYPE_SPEECH);
}
if (pause) {
mSession.pause();
} else {
BaseMediaPlayer player = mSession.getPlayer();
if (player != null) {
// Lower the volume by the factor
final float currentVolume = player.getPlayerVolume();
final float duckingVolume = currentVolume * VOLUME_DUCK_FACTOR;
synchronized (mLock) {
mPlayerVolumeBeforeDucking = currentVolume;
mPlayerDuckingVolume = duckingVolume;
}
player.setPlayerVolume(duckingVolume);
}
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
mSession.pause();
// Resume after regaining the audio focus.
synchronized (mLock) {
mResumeWhenAudioFocusGain = true;
}
break;
}
}
};
}
}