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