blob: b7f0e3cfbad35823ae52bb7b6c191e04a95905be [file] [log] [blame]
/*
* Copyright 2019 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.media2.player;
import static androidx.media2.common.SessionPlayer.PLAYER_STATE_PAUSED;
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.os.Build;
import android.util.Log;
import androidx.annotation.DoNotInline;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.media.AudioAttributesCompat;
/**
* 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
*/
/* package */ class AudioFocusHandler {
private static final String TAG = "AudioFocusHandler";
private static final boolean DEBUG = true;
interface AudioFocusHandlerImpl {
boolean onPlay();
void onPause();
void onReset();
void close();
void sendIntent(Intent intent);
}
private final AudioFocusHandlerImpl mImpl;
AudioFocusHandler(Context context, MediaPlayer player) {
mImpl = new AudioFocusHandlerImplBase(context, player);
}
/**
* Should be called when the {@link MediaPlayer#play()} is called. Returns whether the play()
* can be proceed.
*
* @return {@code true} if it's OK to start playback because audio focus was successfully
* granted or audio focus isn't needed for starting playback. {@code false} otherwise.
* (i.e. Audio focus is needed for starting playback but failed)
*/
public boolean onPlay() {
return mImpl.onPlay();
}
/**
* Called when the {@link MediaPlayer#pause()} is called.
*/
public void onPause() {
mImpl.onPause();
}
/**
* Called when the {@link MediaPlayer#reset()} is called.
*/
public void onReset() {
mImpl.onReset();
}
/**
* 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 mBecomingNoisyReceiver = new BecomingNoisyReceiver();
private final IntentFilter mIntentFilter =
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private final OnAudioFocusChangeListener mAudioFocusListener = new AudioFocusListener();
final Object mLock = new Object();
private final Context mContext;
final MediaPlayer mPlayer;
private final AudioManager mAudioManager;
@GuardedBy("mLock")
AudioAttributesCompat mAudioAttributes;
@GuardedBy("mLock")
private int mCurrentFocusGainType;
@GuardedBy("mLock")
boolean mResumeWhenAudioFocusGain;
@GuardedBy("mLock")
boolean mBecomingNoisyReceiverRegistered;
AudioFocusHandlerImplBase(Context context, MediaPlayer player) {
mContext = context;
mPlayer = player;
// Cannot use session.getContext() because session's impl isn't initialized at this
// moment.
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
@Override
public boolean onPlay() {
final AudioAttributesCompat attrs = mPlayer.getAudioAttributes();
boolean result = true;
synchronized (mLock) {
mAudioAttributes = attrs;
// Checks whether the audio attributes is {@code null}, to check indirectly whether
// the media item has audio track.
if (attrs == null) {
// No sound.
abandonAudioFocusLocked();
unregisterBecomingNoisyReceiverLocked();
} else {
result = requestAudioFocusLocked();
if (result) {
registerBecomingNoisyReceiverLocked();
}
}
}
return result;
}
@Override
public void onPause() {
synchronized (mLock) {
mResumeWhenAudioFocusGain = false;
unregisterBecomingNoisyReceiverLocked();
}
}
@Override
public void onReset() {
synchronized (mLock) {
abandonAudioFocusLocked();
unregisterBecomingNoisyReceiverLocked();
}
}
@Override
public void close() {
synchronized (mLock) {
unregisterBecomingNoisyReceiverLocked();
abandonAudioFocusLocked();
}
}
@Override
public void sendIntent(Intent intent) {
mBecomingNoisyReceiver.onReceive(mContext, intent);
}
/**
* Requests audio focus. This may regain audio focus.
*
* @return {@code true} if audio focus is granted or isn't needed.
* {@code false} only when the attempt to request audio focus was failed.
*/
@GuardedBy("mLock")
private boolean requestAudioFocusLocked() {
int focusGain = convertAudioAttributesToFocusGain(mAudioAttributes);
if (focusGain == AudioManager.AUDIOFOCUS_NONE) {
if (mAudioAttributes == null && DEBUG) {
// If audio attributes is null, it should be handled outside to set volume
// to zero without holding an lock.
Log.e(TAG, "requestAudioFocusLocked() shouldn't be called when AudioAttributes"
+ " is null");
}
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) {
mCurrentFocusGainType = focusGain;
} else {
Log.w(TAG, "requestAudioFocus(" + focusGain + ") failed (return="
+ audioFocusRequestResult + ") playback wouldn't start.");
mCurrentFocusGainType = AudioManager.AUDIOFOCUS_NONE;
}
if (DEBUG) {
Log.d(TAG, "requestAudioFocus(" + focusGain + "), result="
+ (audioFocusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED));
}
mResumeWhenAudioFocusGain = false;
return mCurrentFocusGainType != AudioManager.AUDIOFOCUS_NONE;
}
/**
* Abandons audio focus if it has granted.
*/
@GuardedBy("mLock")
private void abandonAudioFocusLocked() {
if (mCurrentFocusGainType == AudioManager.AUDIOFOCUS_NONE) {
return;
}
if (DEBUG) {
Log.d(TAG, "abandoningAudioFocusLocked, currently=" + mCurrentFocusGainType);
}
mAudioManager.abandonAudioFocus(mAudioFocusListener);
mCurrentFocusGainType = AudioManager.AUDIOFOCUS_NONE;
mResumeWhenAudioFocusGain = false;
}
@GuardedBy("mLock")
private void registerBecomingNoisyReceiverLocked() {
if (mBecomingNoisyReceiverRegistered) {
return;
}
if (DEBUG) {
Log.d(TAG, "registering becoming noisy receiver");
}
// Registering the receiver multiple-times may not be allowed for newer platform.
// Register only when it's not registered.
if (Build.VERSION.SDK_INT < 33) {
mContext.registerReceiver(mBecomingNoisyReceiver, mIntentFilter);
} else {
Api33.registerReceiver(mContext, mBecomingNoisyReceiver, mIntentFilter,
Context.RECEIVER_NOT_EXPORTED);
}
mBecomingNoisyReceiverRegistered = true;
}
@GuardedBy("mLock")
private void unregisterBecomingNoisyReceiverLocked() {
if (!mBecomingNoisyReceiverRegistered) {
return;
}
if (DEBUG) {
Log.d(TAG, "unregistering becoming noisy receiver");
}
mContext.unregisterReceiver(mBecomingNoisyReceiver);
mBecomingNoisyReceiverRegistered = false;
}
// Converts {@link AudioAttributesCompat} to one of the audio focus request. This follows
// the class Javadoc of {@link AudioFocusRequest}.
// Note: Any change here should also reflects public Javadoc of {@link MediaSession}.
private static int convertAudioAttributesToFocusGain(
final AudioAttributesCompat audioAttributesCompat) {
if (audioAttributesCompat == 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 AudioManager.AUDIOFOCUS_NONE;
}
// Javadoc here means 'The different types of focus requests' written in the
// {@link AudioFocusRequest}.
switch (audioAttributesCompat.getUsage()) {
// 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 AudioAttributesCompat.USAGE_VOICE_COMMUNICATION_SIGNALLING:
return AudioManager.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 AudioAttributesCompat.USAGE_GAME:
case AudioAttributesCompat.USAGE_MEDIA:
return AudioManager.AUDIOFOCUS_GAIN;
// Special usages: USAGE_UNKNOWN shouldn't be used. Request audio focus to prevent
// multiple media playback happen at the same time.
case AudioAttributesCompat.USAGE_UNKNOWN:
Log.w(TAG, "Specify a proper usage in the audio attributes for audio focus"
+ " handling. Using AUDIOFOCUS_GAIN by default.");
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:
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
// Javadoc says 'AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: Examples are when playing
// driving directions or notifications'
case AudioAttributesCompat.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
case AudioAttributesCompat.USAGE_ASSISTANCE_SONIFICATION:
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:
return AudioManager.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 AudioAttributesCompat.USAGE_ASSISTANT:
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE;
// Special usages:
case AudioAttributesCompat.USAGE_ASSISTANCE_ACCESSIBILITY:
if (audioAttributesCompat.getContentType()
== AudioAttributesCompat.CONTENT_TYPE_SPEECH) {
// Voice shouldn't be interrupted by other playback.
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT;
}
return AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
}
Log.w(TAG, "Unidentified AudioAttribute " + audioAttributesCompat);
return AudioManager.AUDIOFOCUS_NONE;
}
private class BecomingNoisyReceiver extends BroadcastReceiver {
BecomingNoisyReceiver() {
}
// Note: This is always the main thread, except for the test.
@SuppressWarnings("FutureReturnValueIgnored")
@Override
public void onReceive(Context context, Intent intent) {
if (!AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
return;
}
final int usage;
synchronized (mLock) {
if (DEBUG) {
Log.d(TAG, "Received noisy intent, intent=" + intent + ", registered="
+ mBecomingNoisyReceiverRegistered + ", attr=" + mAudioAttributes);
}
if (!mBecomingNoisyReceiverRegistered || 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.'
mPlayer.pause();
break;
case AudioAttributesCompat.USAGE_GAME:
// Noisy intent guide says 'For gaming apps, you may choose to
// significantly lower the volume instead'.
mPlayer.setPlayerVolume(mPlayer.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;
AudioFocusListener() {
}
// This is the thread where the AudioManager was originally instantiated.
// see: b/78617702
@SuppressWarnings("FutureReturnValueIgnored")
@Override
public void onAudioFocusChange(int focusGain) {
switch (focusGain) {
case AudioManager.AUDIOFOCUS_GAIN:
// Regains focus after the LOSS_TRANSIENT or LOSS_TRANSIENT_CAN_DUCK.
if (mPlayer.getPlayerState() == PLAYER_STATE_PAUSED) {
// Note: onPlay() will be called again with this.
synchronized (mLock) {
if (!mResumeWhenAudioFocusGain) {
break;
}
}
mPlayer.play();
} else {
// Resets the volume if the user didn't change it.
final float currentVolume = mPlayer.getPlayerVolume();
final float volumeBeforeDucking;
synchronized (mLock) {
if (currentVolume != mPlayerDuckingVolume) {
// User manually changed the volume meanwhile. Don't reset.
break;
}
volumeBeforeDucking = mPlayerVolumeBeforeDucking;
}
mPlayer.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'.
mPlayer.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) {
mPlayer.pause();
} else {
// Lower the volume by the factor
final float currentVolume = mPlayer.getPlayerVolume();
final float duckingVolume = currentVolume * VOLUME_DUCK_FACTOR;
synchronized (mLock) {
mPlayerVolumeBeforeDucking = currentVolume;
mPlayerDuckingVolume = duckingVolume;
}
mPlayer.setPlayerVolume(duckingVolume);
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
mPlayer.pause();
// Resume after regaining the audio focus.
synchronized (mLock) {
mResumeWhenAudioFocusGain = true;
}
break;
}
}
}
}
@RequiresApi(33)
private static class Api33 {
@DoNotInline
static void registerReceiver(@NonNull Context context, @NonNull BroadcastReceiver receiver,
@NonNull IntentFilter filter, int flags) {
context.registerReceiver(receiver, filter, flags);
}
}
}