/*
* 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 java.lang.annotation.RetentionPolicy.SOURCE;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.util.ObjectsCompat;
import java.lang.annotation.Retention;
/** Compatibility version of an {@link AudioFocusRequest}. */
public class AudioFocusRequestCompat {
// default attributes for the request when not specified
/* package */ static final AudioAttributesCompat FOCUS_DEFAULT_ATTR =
new AudioAttributesCompat.Builder().setUsage(AudioAttributesCompat.USAGE_MEDIA).build();
@Retention(SOURCE)
@IntDef({
AudioManagerCompat.AUDIOFOCUS_GAIN,
AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT,
AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
})
private @interface FocusGainType {}
private final int mFocusGain;
private final OnAudioFocusChangeListener mOnAudioFocusChangeListener;
private final Handler mFocusChangeHandler;
private final AudioAttributesCompat mAudioAttributesCompat;
private final boolean mPauseOnDuck;
private final Object mFrameworkAudioFocusRequest;
/* package */ AudioFocusRequestCompat(
int focusGain,
OnAudioFocusChangeListener onAudioFocusChangeListener,
Handler focusChangeHandler,
AudioAttributesCompat audioFocusRequestCompat,
boolean pauseOnDuck) {
mFocusGain = focusGain;
mFocusChangeHandler = focusChangeHandler;
mAudioAttributesCompat = audioFocusRequestCompat;
mPauseOnDuck = pauseOnDuck;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
&& mFocusChangeHandler.getLooper() != Looper.getMainLooper()) {
mOnAudioFocusChangeListener =
new OnAudioFocusChangeListenerHandlerCompat(
onAudioFocusChangeListener, focusChangeHandler);
} else {
mOnAudioFocusChangeListener = onAudioFocusChangeListener;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mFrameworkAudioFocusRequest = Api26Impl.createInstance(mFocusGain, getAudioAttributes(),
mPauseOnDuck, mOnAudioFocusChangeListener, mFocusChangeHandler);
} else {
mFrameworkAudioFocusRequest = null;
}
}
/**
* Gets the type of audio focus request configured for this {@code AudioFocusRequestCompat}.
*
* @return one of {@link AudioManagerCompat#AUDIOFOCUS_GAIN}, {@link
* AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT}, {@link
* AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and {@link
* AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}.
*/
public @FocusGainType int getFocusGain() {
return mFocusGain;
}
/**
* Gets the {@link AudioAttributesCompat} set for this {@code AudioFocusRequestCompat}, or the
* default attributes if none were set.
*
* @return non-null {@link AudioAttributesCompat}.
*/
@NonNull
public AudioAttributesCompat getAudioAttributesCompat() {
return mAudioAttributesCompat;
}
/**
* Gets whether the application that would use this {@code AudioFocusRequestCompat} would pause
* when it is requested to duck. This value is only applicable on {@link
* android.os.Build.VERSION_CODES#O} and later.
*
* @return the duck/pause behavior.
*/
public boolean willPauseWhenDucked() {
return mPauseOnDuck;
}
/**
* Gets the focus change listener set for this {@code AudioFocusRequestCompat}.
*
* @return The {@link AudioManager.OnAudioFocusChangeListener} that was set.
*/
@NonNull
public OnAudioFocusChangeListener getOnAudioFocusChangeListener() {
return mOnAudioFocusChangeListener;
}
/**
* Gets the {@link Handler} to be used for the focus change listener.
*
* @return the same {@code Handler} set in. {@link
* Builder#setOnAudioFocusChangeListener(OnAudioFocusChangeListener, Handler)}.
*/
@NonNull
public Handler getFocusChangeHandler() {
return mFocusChangeHandler;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AudioFocusRequestCompat)) return false;
AudioFocusRequestCompat that = (AudioFocusRequestCompat) o;
return mFocusGain == that.mFocusGain
&& mPauseOnDuck == that.mPauseOnDuck
&& ObjectsCompat.equals(
mOnAudioFocusChangeListener, that.mOnAudioFocusChangeListener)
&& ObjectsCompat.equals(mFocusChangeHandler, that.mFocusChangeHandler)
&& ObjectsCompat.equals(mAudioAttributesCompat, that.mAudioAttributesCompat);
}
@Override
public int hashCode() {
return ObjectsCompat.hash(
mFocusGain,
mOnAudioFocusChangeListener,
mFocusChangeHandler,
mAudioAttributesCompat,
mPauseOnDuck);
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
/* package */ AudioAttributes getAudioAttributes() {
return (mAudioAttributesCompat != null)
? (AudioAttributes) mAudioAttributesCompat.unwrap()
: null;
}
@RequiresApi(Build.VERSION_CODES.O)
/* package */ AudioFocusRequest getAudioFocusRequest() {
return (AudioFocusRequest) mFrameworkAudioFocusRequest;
}
/**
* Builder class for {@link AudioFocusRequestCompat} objects.
*
* <p>See {@link AudioFocusRequestCompat} for an example of building an instance with this
* builder. <br>
* The default values for the instance to be built are:
*
* <table>
* <tr><td>focus listener and handler</td><td>none</td></tr>
* <tr><td>{@link AudioAttributesCompat}</td><td>attributes with usage set to
* {@link AudioAttributesCompat#USAGE_MEDIA}</td></tr>
* <tr><td>pauses on duck</td><td>false</td></tr>
* <tr><td>supports delayed focus grant</td><td>false</td></tr>
* </table>
*
* <p>In contrast to a {@link AudioFocusRequest}, attempting to {@link #build()} an {@link
* AudioFocusRequestCompat} without an {@link AudioManager.OnAudioFocusChangeListener} will
* throw an {@link IllegalArgumentException}, because the listener is required for all API
* levels up to API 26.
*/
public static final class Builder {
private int mFocusGain;
private OnAudioFocusChangeListener mOnAudioFocusChangeListener;
private Handler mFocusChangeHandler;
private AudioAttributesCompat mAudioAttributesCompat = FOCUS_DEFAULT_ATTR;
// Flags
private boolean mPauseOnDuck;
/**
* Constructs a new {@code Builder}, and specifies how audio focus will be requested. Valid
* values for focus requests are {@link AudioManagerCompat#AUDIOFOCUS_GAIN},
* {@link AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT}, and {@link
* AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and {@link
* AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}.
* {@link AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE} is converted to
* {@link AudioManagerCompat#AUDIOFOCUS_GAIN_TRANSIENT} on API levels previous to API 19.
*
* <p>By default there is no focus change listener, delayed focus is not supported, ducking
* is suitable for the application, and the <code>AudioAttributesCompat</code> have a usage
* of {@link AudioAttributes#USAGE_MEDIA}.
*
* @param focusGain the type of audio focus gain that will be requested
* @throws IllegalArgumentException thrown when an invalid focus gain type is used
*/
public Builder(@FocusGainType int focusGain) {
setFocusGain(focusGain);
}
/**
* Constructs a new {@code Builder} with all the properties of the {@code
* AudioFocusRequestCompat} passed as parameter. Use this method when you want a new request
* to differ only by some properties.
*
* @param requestToCopy the non-null {@code AudioFocusRequestCompat} to duplicate.
* @throws IllegalArgumentException thrown when a null {@code AudioFocusRequestCompat} is
* used.
*/
public Builder(@NonNull AudioFocusRequestCompat requestToCopy) {
if (requestToCopy == null) {
throw new IllegalArgumentException(
"AudioFocusRequestCompat to copy must not be null");
}
mFocusGain = requestToCopy.getFocusGain();
mOnAudioFocusChangeListener = requestToCopy.getOnAudioFocusChangeListener();
mFocusChangeHandler = requestToCopy.getFocusChangeHandler();
mAudioAttributesCompat = requestToCopy.getAudioAttributesCompat();
mPauseOnDuck = requestToCopy.willPauseWhenDucked();
}
/**
* Sets the type of focus gain that will be requested. Use this method to replace the focus
* gain when building a request by modifying an existing {@code AudioFocusRequestCompat}
* instance.
*
* @param focusGain the type of audio focus gain that will be requested.
* @return this {@code Builder} instance
* @throws IllegalArgumentException thrown when an invalid focus gain type is used
*/
@NonNull
public Builder setFocusGain(@FocusGainType int focusGain) {
if (!isValidFocusGain(focusGain)) {
throw new IllegalArgumentException("Illegal audio focus gain type " + focusGain);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT
&& focusGain == AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
focusGain = AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT;
}
mFocusGain = focusGain;
return this;
}
/**
* Sets the listener called when audio focus changes after being requested with {@link
* AudioManagerCompat#requestAudioFocus(AudioManager, AudioFocusRequestCompat)}, and until
* being abandoned with {@link AudioManagerCompat#abandonAudioFocusRequest(AudioManager,
* AudioFocusRequestCompat)}. Note that only focus changes (gains and losses) affecting the
* focus owner are reported, not gains and losses of other focus requesters in the system.
* <br>
* Notifications are delivered on the main thread.
*
* @param listener the listener receiving the focus change notifications.
* @return this {@code Builder} instance.
* @throws NullPointerException thrown when a null focus listener is used.
*/
@NonNull
public Builder setOnAudioFocusChangeListener(@NonNull OnAudioFocusChangeListener listener) {
return setOnAudioFocusChangeListener(listener, new Handler(Looper.getMainLooper()));
}
/**
* Sets the listener called when audio focus changes after being requested with {@link
* AudioManagerCompat#requestAudioFocus(AudioManager, AudioFocusRequestCompat)}, and until
* being abandoned with {@link AudioManagerCompat#abandonAudioFocusRequest(AudioManager,
* AudioFocusRequestCompat)}. Note that only focus changes (gains and losses) affecting the
* focus owner are reported, not gains and losses of other focus requesters in the system.
*
* @param listener the listener receiving the focus change notifications.
* @param handler the {@link Handler} for the thread on which to execute the notifications.
* @return this {@code Builder} instance.
* @throws NullPointerException thrown when a null focus listener or handler is used.
*/
@NonNull
public Builder setOnAudioFocusChangeListener(
@NonNull OnAudioFocusChangeListener listener, @NonNull Handler handler) {
if (listener == null) {
throw new IllegalArgumentException("OnAudioFocusChangeListener must not be null");
}
if (handler == null) {
throw new IllegalArgumentException("Handler must not be null");
}
mOnAudioFocusChangeListener = listener;
mFocusChangeHandler = handler;
return this;
}
/**
* Sets the {@link AudioAttributesCompat} to be associated with the focus request, and which
* describe the use case for which focus is requested. As the focus requests typically
* precede audio playback, this information is used on certain platforms to declare the
* subsequent playback use case. It is therefore good practice to use in this method the
* same {@code AudioAttributesCompat} as used for playback, see for example {@link
* MediaPlayer#setAudioAttributes(AudioAttributes)} in {@code MediaPlayer} or {@link
* android.media.AudioTrack.Builder#setAudioAttributes(AudioAttributes)} in
* {@code AudioTrack}.
*
* @param attributes the {@link AudioAttributesCompat} for the focus request.
* @return this {@code Builder} instance.
* @throws NullPointerException thrown when using null for the attributes.
*/
@NonNull
public Builder setAudioAttributes(@NonNull AudioAttributesCompat attributes) {
if (attributes == null) {
throw new NullPointerException("Illegal null AudioAttributes");
}
mAudioAttributesCompat = attributes;
return this;
}
/**
* Declare the intended behavior of the application with regards to audio ducking. See more
* details in the {@link AudioFocusRequest} class documentation. Setting pauseOnDuck to true
* will only have an effect on {@link android.os.Build.VERSION_CODES#O} and later.
*
* @param pauseOnDuck use {@code true} if the application intends to pause audio playback
* when losing focus with {@link AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}.
* @return this {@code Builder} instance.
*/
@NonNull
public Builder setWillPauseWhenDucked(boolean pauseOnDuck) {
mPauseOnDuck = pauseOnDuck;
return this;
}
/**
* Builds a new {@code AudioFocusRequestCompat} instance combining all the information
* gathered by this {@code Builder}'s configuration methods.
*
* @return the {@code AudioFocusRequestCompat} instance qualified by all the properties set
* on this {@code Builder}.
* @throws IllegalStateException thrown when attempting to build a focus request without a
* focus change listener set.
*/
public AudioFocusRequestCompat build() {
if (mOnAudioFocusChangeListener == null) {
throw new IllegalStateException(
"Can't build an AudioFocusRequestCompat instance without a listener");
}
return new AudioFocusRequestCompat(
mFocusGain,
mOnAudioFocusChangeListener,
mFocusChangeHandler,
mAudioAttributesCompat,
mPauseOnDuck);
}
/**
* Checks whether a focus gain constant is a valid value for an audio focus request.
*
* @param focusGain value to check
* @return true if focusGain is a valid value for an audio focus request.
*/
private static boolean isValidFocusGain(@FocusGainType int focusGain) {
switch (focusGain) {
case AudioManagerCompat.AUDIOFOCUS_GAIN:
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT:
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
case AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
return true;
default:
return false;
}
}
}
/**
* Class to allow {@link OnAudioFocusChangeListener#onAudioFocusChange(int)} calls on a specific
* thread prior to {@link Build.VERSION_CODES#O}.
*/
private static class OnAudioFocusChangeListenerHandlerCompat
implements Handler.Callback, OnAudioFocusChangeListener {
private static final int FOCUS_CHANGE = 0x002a74b2;
private final Handler mHandler;
private final OnAudioFocusChangeListener mListener;
/* package */ OnAudioFocusChangeListenerHandlerCompat(
@NonNull OnAudioFocusChangeListener listener, @NonNull Handler handler) {
mListener = listener;
mHandler = new Handler(handler.getLooper(), this);
}
@Override
public void onAudioFocusChange(final int focusChange) {
mHandler.sendMessage(Message.obtain(mHandler, FOCUS_CHANGE, focusChange, 0));
}
@Override
public boolean handleMessage(Message message) {
if (message.what == FOCUS_CHANGE) {
mListener.onAudioFocusChange(message.arg1);
return true;
}
return false;
}
}
@RequiresApi(26)
private static class Api26Impl {
private Api26Impl() {}
@DoNotInline
static AudioFocusRequest createInstance(
int focusGain,
AudioAttributes audioAttributes,
boolean pauseOnDuck,
OnAudioFocusChangeListener onAudioFocusChangeListener,
Handler focusChangeHandler) {
return new AudioFocusRequest.Builder(focusGain)
.setAudioAttributes(audioAttributes)
.setWillPauseWhenDucked(pauseOnDuck)
.setOnAudioFocusChangeListener(onAudioFocusChangeListener, focusChangeHandler)
.build();
}
}
}