Skip to content

Instantly share code, notes, and snippets.

@nic0lette
Last active November 15, 2022 09:39
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save nic0lette/c360dd353c451d727ea017890cbaa521 to your computer and use it in GitHub Desktop.
Save nic0lette/c360dd353c451d727ea017890cbaa521 to your computer and use it in GitHub Desktop.
A couple of classes (and an interface!) to make it easier to adapt to the audio focus changes that were introduced in Android Oreo.
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* 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 com.example.android.mediasession.lib;
/**
* {@code AudioFocusAwarePlayer} defines an interface for players
* to respond to audio focus changes.
*/
public interface AudioFocusAwarePlayer {
boolean isPlaying();
void play();
void pause();
void stop();
void setVolume(float volume);
}
/*
* Copyright 2017 Google Inc. All rights reserved.
*
* 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 com.example.android.mediasession.lib;
import android.content.Context;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.v4.media.AudioAttributesCompat;
import android.util.Log;
/**
* A class to help request and abandon audio focus, with proper handling of API 26+
* audio focus changes.
*/
public class AudioFocusHelper {
private static final String TAG = "AudioFocusHelper";
private final AudioFocusHelperImpl mImpl;
private DefaultAudioFocusListener mDefaultChangeListener;
/**
* Creates an AudioFocusHelper given a {@see Context}.
* <p>
* This does not request audio focus.
*
* @param context The current context.
*/
public AudioFocusHelper(@NonNull final Context context) {
final AudioManager audioManager = (AudioManager) context
.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mImpl = new AudioFocusHelperImplApi26(audioManager);
} else {
mImpl = new AudioFocusHelperImplBase(audioManager);
}
}
/**
* Builds an {@link OnAudioFocusChangeListener} to control an
* {@link AudioFocusAwarePlayer} in response to audio focus changes.
* <p>
* This function is intended to be used in conjuction with an {@link AudioFocusRequestCompat}
* as follows:
* <code>
* AudioFocusRequestCompat focusRequest =
* new AudioFocusRequestCompat.Builder(AudioManager.AUDIOFOCUS_GAIN)
* .setOnAudioFocusChangeListener(audioFocusHelper.getListenerForPlayer(player))
* // etc...
* .build();
* </code>
*
* @param player The player that will respond to audio focus changes.
* @return An {@link OnAudioFocusChangeListener} to control the player.
*/
public OnAudioFocusChangeListener getListenerForPlayer(@NonNull AudioFocusAwarePlayer player) {
if (mDefaultChangeListener != null && mDefaultChangeListener.getPlayer().equals(player)) {
return mDefaultChangeListener;
}
mDefaultChangeListener = new DefaultAudioFocusListener(mImpl, player);
return mDefaultChangeListener;
}
/**
* Requests audio focus for the player.
*
* @param audioFocusRequestCompat The audio focus request to perform.
* @return {@code true} if audio focus was granted, {@code false} otherwise.
*/
public boolean requestAudioFocus(AudioFocusRequestCompat audioFocusRequestCompat) {
return mImpl.requestAudioFocus(audioFocusRequestCompat);
}
/**
* Abandons audio focus.
*
* @param audioFocusRequestCompat The audio focus request to abandon.
*/
public void abandonAudioFocus(AudioFocusRequestCompat audioFocusRequestCompat) {
mImpl.abandonAudioFocus();
}
interface AudioFocusHelperImpl {
boolean requestAudioFocus(AudioFocusRequestCompat audioFocusRequestCompat);
void abandonAudioFocus();
boolean willPauseWhenDucked();
}
private static class AudioFocusHelperImplBase implements AudioFocusHelperImpl {
final AudioManager mAudioManager;
AudioFocusRequestCompat mAudioFocusRequestCompat;
AudioFocusHelperImplBase(AudioManager audioManager) {
mAudioManager = audioManager;
}
@Override
public boolean requestAudioFocus(AudioFocusRequestCompat audioFocusRequestCompat) {
// Save the focus request.
mAudioFocusRequestCompat = audioFocusRequestCompat;
// Check for possible problems...
if (audioFocusRequestCompat.acceptsDelayedFocusGain()) {
final String message = "Cannot request delayed focus on API " +
Build.VERSION.SDK_INT;
// Make an exception to allow the developer to more easily find this code path.
@SuppressWarnings("ThrowableNotThrown")
final Throwable stackTrace = new UnsupportedOperationException(message)
.fillInStackTrace();
Log.w(TAG, "Cannot request delayed focus", stackTrace);
}
final OnAudioFocusChangeListener listener =
mAudioFocusRequestCompat.getOnAudioFocusChangeListener();
final int streamType =
mAudioFocusRequestCompat.getAudioAttributesCompat().getLegacyStreamType();
final int focusGain = mAudioFocusRequestCompat.getFocusGain();
return mAudioManager.requestAudioFocus(listener, streamType, focusGain) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
@Override
public void abandonAudioFocus() {
if (mAudioFocusRequestCompat == null) {
return;
}
mAudioManager.abandonAudioFocus(
mAudioFocusRequestCompat.getOnAudioFocusChangeListener());
}
@Override
public boolean willPauseWhenDucked() {
if (mAudioFocusRequestCompat == null) {
return false;
}
final AudioAttributesCompat audioAttributes =
mAudioFocusRequestCompat.getAudioAttributesCompat();
final boolean pauseWhenDucked = mAudioFocusRequestCompat.willPauseWhenDucked();
final boolean isSpeech = (audioAttributes != null) &&
audioAttributes.getContentType() == AudioAttributesCompat.CONTENT_TYPE_SPEECH;
return pauseWhenDucked || isSpeech;
}
}
@RequiresApi(Build.VERSION_CODES.O)
private static class AudioFocusHelperImplApi26 extends AudioFocusHelperImplBase {
private AudioFocusRequest mAudioFocusRequest;
AudioFocusHelperImplApi26(AudioManager audioManager) {
super(audioManager);
}
@Override
public boolean requestAudioFocus(AudioFocusRequestCompat audioFocusRequestCompat) {
// Save and unwrap the compat object.
mAudioFocusRequestCompat = audioFocusRequestCompat;
mAudioFocusRequest = audioFocusRequestCompat.getAudioFocusRequest();
return mAudioManager.requestAudioFocus(mAudioFocusRequest) ==
AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
@Override
public void abandonAudioFocus() {
mAudioManager.abandonAudioFocusRequest(mAudioFocusRequest);
}
}
/**
* Implementation of an Android Oreo inspired {@link OnAudioFocusChangeListener}.
*/
private static class DefaultAudioFocusListener
implements OnAudioFocusChangeListener {
private static final float MEDIA_VOLUME_DEFAULT = 1.0f;
private static final float MEDIA_VOLUME_DUCK = 0.2f;
private final AudioFocusHelperImpl mImpl;
private final AudioFocusAwarePlayer mPlayer;
private boolean mResumeOnFocusGain = false;
private DefaultAudioFocusListener(AudioFocusHelperImpl impl, AudioFocusAwarePlayer player) {
mImpl = impl;
mPlayer = player;
}
private AudioFocusAwarePlayer getPlayer() {
return mPlayer;
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
if (mResumeOnFocusGain) {
mPlayer.play();
mResumeOnFocusGain = false;
} else if (mPlayer.isPlaying()) {
mPlayer.setVolume(MEDIA_VOLUME_DEFAULT);
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
if (!mImpl.willPauseWhenDucked()) {
mPlayer.setVolume(MEDIA_VOLUME_DUCK);
break;
}
// This stream doesn't duck, so fall through and handle it the
// same as if it were an AUDIOFOCUS_LOSS_TRANSIENT.
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
mResumeOnFocusGain = mPlayer.isPlaying();
mPlayer.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS:
mResumeOnFocusGain = false;
mPlayer.stop();
mImpl.abandonAudioFocus();
break;
}
}
}
}
package com.example.android.mediasession.lib;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.v4.media.AudioAttributesCompat;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;
/**
* Compatibility version of an {@link AudioFocusRequest}.
*/
public class AudioFocusRequestCompat {
@Retention(SOURCE)
@IntDef({
AudioManager.AUDIOFOCUS_GAIN,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
})
public @interface FocusGain {}
private final int mFocusGain;
private final OnAudioFocusChangeListener mOnAudioFocusChangeListener;
private final Handler mFocusChangeHandler;
private final AudioAttributesCompat mAudioAttributesCompat;
private final boolean mPauseOnDuck;
private final boolean mAcceptsDelayedFocusGain;
private AudioFocusRequestCompat(int focusGain,
OnAudioFocusChangeListener onAudioFocusChangeListener,
Handler focusChangeHandler,
AudioAttributesCompat audioFocusRequestCompat,
boolean pauseOnDuck,
boolean acceptsDelayedFocusGain) {
mFocusGain = focusGain;
mOnAudioFocusChangeListener = onAudioFocusChangeListener;
mFocusChangeHandler = focusChangeHandler;
mAudioAttributesCompat = audioFocusRequestCompat;
mPauseOnDuck = pauseOnDuck;
mAcceptsDelayedFocusGain = acceptsDelayedFocusGain;
}
public int getFocusGain() {
return mFocusGain;
}
public AudioAttributesCompat getAudioAttributesCompat() {
return mAudioAttributesCompat;
}
public boolean willPauseWhenDucked() {
return mPauseOnDuck;
}
public boolean acceptsDelayedFocusGain() {
return mAcceptsDelayedFocusGain;
}
/* package */ OnAudioFocusChangeListener getOnAudioFocusChangeListener() {
return mOnAudioFocusChangeListener;
}
/* package */ Handler getFocusChangeHandler() {
return mFocusChangeHandler;
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
/* package */ AudioAttributes getAudioAttributes() {
return (mAudioAttributesCompat != null)
? (AudioAttributes) (mAudioAttributesCompat.unwrap())
: null;
}
@RequiresApi(Build.VERSION_CODES.O)
/* package */ AudioFocusRequest getAudioFocusRequest() {
return new AudioFocusRequest.Builder(mFocusGain)
.setAudioAttributes(getAudioAttributes())
.setAcceptsDelayedFocusGain(mAcceptsDelayedFocusGain)
.setWillPauseWhenDucked(mPauseOnDuck)
.setOnAudioFocusChangeListener(mOnAudioFocusChangeListener, mFocusChangeHandler)
.build();
}
/**
* Builder for an {@link AudioFocusRequestCompat}.
*/
public static final class Builder {
private int mFocusGain;
private OnAudioFocusChangeListener mOnAudioFocusChangeListener;
private Handler mFocusChangeHandler;
private AudioAttributesCompat mAudioAttributesCompat;
// Flags
private boolean mPauseOnDuck;
private boolean mAcceptsDelayedFocusGain;
public Builder(@FocusGain int focusGain) {
mFocusGain = focusGain;
}
public Builder(@NonNull AudioFocusRequestCompat requestToCopy) {
mFocusGain = requestToCopy.mFocusGain;
mOnAudioFocusChangeListener = requestToCopy.mOnAudioFocusChangeListener;
mFocusChangeHandler = requestToCopy.mFocusChangeHandler;
mAudioAttributesCompat = requestToCopy.mAudioAttributesCompat;
mPauseOnDuck = requestToCopy.mPauseOnDuck;
mAcceptsDelayedFocusGain = requestToCopy.mAcceptsDelayedFocusGain;
}
@NonNull
public Builder setFocusGain(@FocusGain int focusGain) {
mFocusGain = focusGain;
return this;
}
@NonNull
public Builder setOnAudioFocusChangeListener(@NonNull OnAudioFocusChangeListener listener) {
return setOnAudioFocusChangeListener(listener, new Handler(Looper.getMainLooper()));
}
@NonNull
public Builder setOnAudioFocusChangeListener(@NonNull OnAudioFocusChangeListener listener,
@NonNull Handler handler) {
mOnAudioFocusChangeListener = listener;
mFocusChangeHandler = handler;
return this;
}
@NonNull
public Builder setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
mAudioAttributesCompat = attributes;
return this;
}
@NonNull
public Builder setWillPauseWhenDucked(boolean pauseOnDuck) {
mPauseOnDuck = pauseOnDuck;
return this;
}
@NonNull
public Builder setAcceptsDelayedFocusGain(boolean acceptsDelayedFocusGain) {
mAcceptsDelayedFocusGain = acceptsDelayedFocusGain;
return this;
}
public AudioFocusRequestCompat build() {
return new AudioFocusRequestCompat(mFocusGain,
mOnAudioFocusChangeListener,
mFocusChangeHandler,
mAudioAttributesCompat,
mPauseOnDuck,
mAcceptsDelayedFocusGain);
}
}
}
@julioz
Copy link

julioz commented Mar 8, 2018

On AudioFocusHelper::abandonAudioFocus, we receive a AudioFocusRequestCompat parameter but never pass it down to mImpl. Why is that?

@julioz
Copy link

julioz commented Mar 8, 2018

Another point is that if AudioFocusHelperImplApi26::abandonAudioFocus is called before requestAudioFocus, that will cause a crash with a NullPointerException since the AudioFocusRequestCompat parameter is null.

@kagile
Copy link

kagile commented Nov 29, 2018

usage?

@Shrikant-B
Copy link

How should I use this in my app?

@nic0lette
Copy link
Author

You should probably not use these any longer, but use the AndroidX version of AudioFocusRequestCompat with AudioManagerCompat now.

@GauravCreed
Copy link

Hi @nic0lette

Thanks for the wonderful snippet, can u please tell me how can i integrate in my Song Service class?

Thank (_).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment