diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d5b511d218..137ef765c6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ * Common Library: * Remove `Format.toBundle(boolean excludeMetadata)` method, use `Format.toBundle()` instead. + * Add `AudioManagerCompat` and `AudioFocusRequestCompat` to replace the + equivalent classes in `androidx.media`. * ExoPlayer: * Consider language when selecting a video track. By default select a 'main' video track that matches the language of the selected audio diff --git a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java index 5dc4e59c17..49d7b6fd35 100644 --- a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java +++ b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java @@ -170,6 +170,43 @@ public final class AudioAttributes { return audioAttributesV21; } + /** Returns the {@link C.StreamType} corresponding to these audio attributes. */ + @UnstableApi + public @C.StreamType int getStreamType() { + // Flags to stream type mapping + if ((flags & C.FLAG_AUDIBILITY_ENFORCED) == C.FLAG_AUDIBILITY_ENFORCED) { + return C.STREAM_TYPE_SYSTEM; + } + // Usage to stream type mapping + switch (usage) { + case C.USAGE_ASSISTANCE_SONIFICATION: + return C.STREAM_TYPE_SYSTEM; + case C.USAGE_VOICE_COMMUNICATION: + return C.STREAM_TYPE_VOICE_CALL; + case C.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return C.STREAM_TYPE_DTMF; + case C.USAGE_ALARM: + return C.STREAM_TYPE_ALARM; + case C.USAGE_NOTIFICATION_RINGTONE: + return C.STREAM_TYPE_RING; + case C.USAGE_NOTIFICATION: + case C.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case C.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case C.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case C.USAGE_NOTIFICATION_EVENT: + return C.STREAM_TYPE_NOTIFICATION; + case C.USAGE_ASSISTANCE_ACCESSIBILITY: + return C.STREAM_TYPE_ACCESSIBILITY; + case C.USAGE_MEDIA: + case C.USAGE_GAME: + case C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case C.USAGE_ASSISTANT: + case C.USAGE_UNKNOWN: + default: + return C.STREAM_TYPE_MUSIC; + } + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { diff --git a/libraries/common/src/main/java/androidx/media3/common/C.java b/libraries/common/src/main/java/androidx/media3/common/C.java index 5e773c24fd..046f6a4168 100644 --- a/libraries/common/src/main/java/androidx/media3/common/C.java +++ b/libraries/common/src/main/java/androidx/media3/common/C.java @@ -327,8 +327,8 @@ public final class C { /** * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link * #STREAM_TYPE_DTMF}, {@link #STREAM_TYPE_MUSIC}, {@link #STREAM_TYPE_NOTIFICATION}, {@link - * #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL} or {@link - * #STREAM_TYPE_DEFAULT}. + * #STREAM_TYPE_RING}, {@link #STREAM_TYPE_SYSTEM}, {@link #STREAM_TYPE_VOICE_CALL}, {@link + * #STREAM_TYPE_ACCESSIBILITY} or {@link #STREAM_TYPE_DEFAULT}. */ // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // with Kotlin usages from before TYPE_USE was added. @@ -345,6 +345,7 @@ public final class C { STREAM_TYPE_RING, STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL, + STREAM_TYPE_ACCESSIBILITY, STREAM_TYPE_DEFAULT }) public @interface StreamType {} @@ -370,6 +371,10 @@ public final class C { /** See {@link AudioManager#STREAM_VOICE_CALL}. */ @UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; + /** See {@link AudioManager#STREAM_ACCESSIBILITY}. */ + @UnstableApi + public static final int STREAM_TYPE_ACCESSIBILITY = AudioManager.STREAM_ACCESSIBILITY; + /** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */ @UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index da8930e07c..7b967a4b6a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -3384,8 +3384,7 @@ public interface Player { * *

For devices with {@link DeviceInfo#PLAYBACK_TYPE_LOCAL local playback}, the volume returned * by this method varies according to the current {@link C.StreamType stream type}. The stream - * type is determined by {@link AudioAttributes#usage} which can be converted to stream type with - * {@link Util#getStreamTypeForAudioUsage(int)}. + * type is determined by {@link AudioAttributes#getStreamType()}. * *

For devices with {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote playback}, the volume of the * remote device is returned. @@ -3508,10 +3507,6 @@ public interface Player { *

If tunneling is enabled by the track selector, the specified audio attributes will be * ignored, but they will take effect if audio is later played without tunneling. * - *

If the device is running a build before platform API version 21, audio attributes cannot be - * set directly on the underlying audio track. In this case, the usage will be mapped onto an - * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. - * *

If audio focus should be handled, the {@link AudioAttributes#usage} must be {@link * C#USAGE_MEDIA} or {@link C#USAGE_GAME}. Other usages will throw an {@link * IllegalArgumentException}. diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/AudioFocusRequestCompat.java b/libraries/common/src/main/java/androidx/media3/common/audio/AudioFocusRequestCompat.java new file mode 100644 index 0000000000..712108a9b3 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioFocusRequestCompat.java @@ -0,0 +1,356 @@ +/* + * Copyright 2024 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.media3.common.audio; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.media.AudioFocusRequest; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.Objects; + +/** + * Compatibility version of an {@link AudioFocusRequest} with fallbacks for older Android versions. + */ +@UnstableApi +public final class AudioFocusRequestCompat { + + private final @AudioManagerCompat.AudioFocusGain int focusGain; + private final AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener; + private final Handler focusChangeHandler; + private final AudioAttributes audioAttributes; + private final boolean pauseOnDuck; + + @Nullable private final Object frameworkAudioFocusRequest; + + /* package */ AudioFocusRequestCompat( + int focusGain, + AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener, + Handler focusChangeHandler, + AudioAttributes audioFocusRequestCompat, + boolean pauseOnDuck) { + this.focusGain = focusGain; + this.focusChangeHandler = focusChangeHandler; + this.audioAttributes = audioFocusRequestCompat; + this.pauseOnDuck = pauseOnDuck; + + if (Util.SDK_INT < 26 && focusChangeHandler.getLooper() != Looper.getMainLooper()) { + this.onAudioFocusChangeListener = + new OnAudioFocusChangeListenerHandlerCompat( + onAudioFocusChangeListener, focusChangeHandler); + } else { + this.onAudioFocusChangeListener = onAudioFocusChangeListener; + } + + if (Util.SDK_INT >= 26) { + this.frameworkAudioFocusRequest = + new AudioFocusRequest.Builder(focusGain) + .setAudioAttributes(audioAttributes.getAudioAttributesV21().audioAttributes) + .setWillPauseWhenDucked(pauseOnDuck) + .setOnAudioFocusChangeListener(onAudioFocusChangeListener, focusChangeHandler) + .build(); + } else { + this.frameworkAudioFocusRequest = null; + } + } + + /** + * Returns the type of {@link AudioManagerCompat.AudioFocusGain} configured for this {@code + * AudioFocusRequestCompat}. + */ + public @AudioManagerCompat.AudioFocusGain int getFocusGain() { + return focusGain; + } + + /** + * Returns the {@link AudioAttributes} set for this {@code AudioFocusRequestCompat}, or the + * default attributes if none were set. + */ + public AudioAttributes getAudioAttributes() { + return audioAttributes; + } + + /** + * Returns 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. + */ + public boolean willPauseWhenDucked() { + return pauseOnDuck; + } + + /** + * Returns the {@link AudioManager.OnAudioFocusChangeListener} set for this {@code + * AudioFocusRequestCompat}. + * + * @return The {@link AudioManager.OnAudioFocusChangeListener} that was set. + */ + public AudioManager.OnAudioFocusChangeListener getOnAudioFocusChangeListener() { + return onAudioFocusChangeListener; + } + + /** + * Returns the {@link Handler} to be used for the {@link AudioManager.OnAudioFocusChangeListener}. + */ + public Handler getFocusChangeHandler() { + return focusChangeHandler; + } + + /** Returns new {@link Builder} with all values of this instance pre-populated. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AudioFocusRequestCompat)) { + return false; + } + AudioFocusRequestCompat that = (AudioFocusRequestCompat) o; + return focusGain == that.focusGain + && pauseOnDuck == that.pauseOnDuck + && Objects.equals(onAudioFocusChangeListener, that.onAudioFocusChangeListener) + && Objects.equals(focusChangeHandler, that.focusChangeHandler) + && Objects.equals(audioAttributes, that.audioAttributes); + } + + @Override + public int hashCode() { + return Objects.hash( + focusGain, onAudioFocusChangeListener, focusChangeHandler, audioAttributes, pauseOnDuck); + } + + @RequiresApi(26) + /* package */ AudioFocusRequest getAudioFocusRequest() { + return (AudioFocusRequest) checkNotNull(frameworkAudioFocusRequest); + } + + /** + * Builder class for {@link AudioFocusRequestCompat} objects. + * + *

The default values are: + * + *

+ * + *

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 @AudioManagerCompat.AudioFocusGain int focusGain; + @Nullable private AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener; + @Nullable private Handler focusChangeHandler; + private AudioAttributes audioAttributes; + private boolean pauseOnDuck; + + /** + * Constructs a new {@code Builder}, and specifies how audio focus will be requested. + * + *

By default there is no focus change listener, delayed focus is not supported, ducking is + * suitable for the application, and the {@link AudioAttributes} are set to {@link + * AudioAttributes#DEFAULT}. + * + * @param focusGain The type of {@link AudioManagerCompat.AudioFocusGain} that will be + * requested. + */ + public Builder(@AudioManagerCompat.AudioFocusGain int focusGain) { + this.audioAttributes = AudioAttributes.DEFAULT; + this.focusGain = focusGain; + } + + private Builder(AudioFocusRequestCompat other) { + focusGain = other.getFocusGain(); + onAudioFocusChangeListener = other.getOnAudioFocusChangeListener(); + focusChangeHandler = other.getFocusChangeHandler(); + audioAttributes = other.getAudioAttributes(); + pauseOnDuck = other.willPauseWhenDucked(); + } + + /** + * Sets the type of {@link AudioManagerCompat.AudioFocusGain} that will be requested. + * + * @param focusGain The type of {@link AudioManagerCompat.AudioFocusGain} that will be + * requested. + * @return This {@code Builder} instance. + */ + @CanIgnoreReturnValue + public Builder setFocusGain(@AudioManagerCompat.AudioFocusGain int focusGain) { + checkArgument(isValidFocusGain(focusGain)); + this.focusGain = 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.
+ * Notifications are delivered on the main thread. + * + * @param listener The {@link AudioManager.OnAudioFocusChangeListener} receiving the focus + * change notifications. + * @return This {@code Builder} instance. + */ + @CanIgnoreReturnValue + public Builder setOnAudioFocusChangeListener(AudioManager.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 {@link AudioManager.OnAudioFocusChangeListener} receiving the focus + * change notifications. + * @param handler The {@link Handler} for the thread on which to execute the notifications. + * @return This {@code Builder} instance. + */ + @CanIgnoreReturnValue + public Builder setOnAudioFocusChangeListener( + AudioManager.OnAudioFocusChangeListener listener, Handler handler) { + checkNotNull(listener); + checkNotNull(handler); + onAudioFocusChangeListener = listener; + focusChangeHandler = handler; + return this; + } + + /** + * Sets the {@link AudioAttributes} 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 + * AudioAttributes} as used for playback, see for example {@code + * ExoPlayer.Builder.setAudioAttributes()}. + * + * @param attributes The {@link AudioAttributes} for the focus request. + * @return This {@code Builder} instance. + */ + @CanIgnoreReturnValue + public Builder setAudioAttributes(AudioAttributes attributes) { + checkNotNull(attributes); + audioAttributes = attributes; + return this; + } + + /** + * Declares the intended behavior of the application with regards to audio ducking. See more + * details in the {@link AudioFocusRequest} class documentation. Setting {@code 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. + */ + @CanIgnoreReturnValue + public Builder setWillPauseWhenDucked(boolean pauseOnDuck) { + this.pauseOnDuck = pauseOnDuck; + return this; + } + + /** + * Builds a new {@code AudioFocusRequestCompat} instance combining all the information gathered + * by this builder's configuration methods. + * + * @return The {@code AudioFocusRequestCompat}. + */ + public AudioFocusRequestCompat build() { + if (onAudioFocusChangeListener == null) { + throw new IllegalStateException( + "Can't build an AudioFocusRequestCompat instance without a listener"); + } + return new AudioFocusRequestCompat( + focusGain, + onAudioFocusChangeListener, + checkNotNull(focusChangeHandler), + audioAttributes, + pauseOnDuck); + } + + /** + * 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(@AudioManagerCompat.AudioFocusGain 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 AudioManager.OnAudioFocusChangeListener#onAudioFocusChange(int)} calls on + * a specific thread prior to API 26. + */ + private static class OnAudioFocusChangeListenerHandlerCompat + implements Handler.Callback, AudioManager.OnAudioFocusChangeListener { + + private static final int FOCUS_CHANGE = 0x002a74b2; + + private final Handler handler; + private final AudioManager.OnAudioFocusChangeListener listener; + + /* package */ OnAudioFocusChangeListenerHandlerCompat( + AudioManager.OnAudioFocusChangeListener listener, Handler handler) { + this.listener = listener; + this.handler = Util.createHandler(handler.getLooper(), /* callback= */ this); + } + + @Override + public void onAudioFocusChange(int focusChange) { + handler.sendMessage(Message.obtain(handler, FOCUS_CHANGE, focusChange, 0)); + } + + @Override + public boolean handleMessage(Message message) { + if (message.what == FOCUS_CHANGE) { + listener.onAudioFocusChange(message.arg1); + return true; + } + return false; + } + } +} diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java b/libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java new file mode 100644 index 0000000000..f465959d15 --- /dev/null +++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024 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.media3.common.audio; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.media.AudioManager; +import androidx.annotation.IntDef; +import androidx.annotation.IntRange; +import androidx.media3.common.C; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Compatibility layer for {@link AudioManager} with fallbacks for older Android versions. */ +@UnstableApi +public final class AudioManagerCompat { + + /** + * Audio focus gain types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link + * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link + * #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + AUDIOFOCUS_NONE, + AUDIOFOCUS_GAIN, + AUDIOFOCUS_GAIN_TRANSIENT, + AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, + AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + }) + public @interface AudioFocusGain {} + + /** Used to indicate no audio focus has been gained or lost, or requested. */ + @SuppressWarnings("InlinedApi") + public static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE; + + /** Used to indicate a gain of audio focus, or a request of audio focus, of unknown duration. */ + public static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN; + + /** + * Used to indicate a temporary gain or request of audio focus, anticipated to last a short amount + * of time. Examples of temporary changes are the playback of driving directions, or an event + * notification. + */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; + + /** + * Used to indicate a temporary request of audio focus, anticipated to last a short amount of + * time, and where it is acceptable for other audio applications to keep playing after having + * lowered their output level (also referred to as "ducking"). Examples of temporary changes are + * the playback of driving directions where playback of music in the background is acceptable. + */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + /** + * Used to indicate a temporary request of audio focus, anticipated to last a short amount of + * time, during which no other applications, or system components, should play anything. Examples + * of exclusive and transient audio focus requests are voice memo recording and speech + * recognition, during which the system shouldn't play any notifications, and media playback + * should have paused. + */ + public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + + /** + * Requests audio focus. See the {@link AudioFocusRequestCompat} for information about the options + * available to configure your request, and notification of focus gain and loss. + * + * @param audioManager The {@link AudioManager}. + * @param focusRequest An {@link AudioFocusRequestCompat} instance used to configure how focus is + * requested. + * @return {@link AudioManager#AUDIOFOCUS_REQUEST_FAILED} or {@link + * AudioManager#AUDIOFOCUS_REQUEST_GRANTED}. + */ + @SuppressWarnings("deprecation") + public static int requestAudioFocus( + AudioManager audioManager, AudioFocusRequestCompat focusRequest) { + if (Util.SDK_INT >= 26) { + return audioManager.requestAudioFocus(focusRequest.getAudioFocusRequest()); + } else { + return audioManager.requestAudioFocus( + focusRequest.getOnAudioFocusChangeListener(), + focusRequest.getAudioAttributes().getStreamType(), + focusRequest.getFocusGain()); + } + } + + /** + * Abandon audio focus. Causes the previous focus owner, if any, to receive focus. + * + * @param audioManager The {@link AudioManager}. + * @param focusRequest The {@link AudioFocusRequestCompat} that was used when requesting focus + * with {@link #requestAudioFocus(AudioManager, AudioFocusRequestCompat)}. + * @return {@link AudioManager#AUDIOFOCUS_REQUEST_FAILED} or {@link + * AudioManager#AUDIOFOCUS_REQUEST_GRANTED} + */ + @SuppressWarnings("deprecation") + public static int abandonAudioFocusRequest( + AudioManager audioManager, AudioFocusRequestCompat focusRequest) { + if (Util.SDK_INT >= 26) { + return audioManager.abandonAudioFocusRequest(focusRequest.getAudioFocusRequest()); + } else { + return audioManager.abandonAudioFocus(focusRequest.getOnAudioFocusChangeListener()); + } + } + + /** + * Returns the maximum volume index for a particular stream. + * + * @param audioManager The {@link AudioManager}. + * @param streamType The {@link C.StreamType} whose maximum volume index is returned. + * @return The maximum valid volume index for the stream. + */ + @IntRange(from = 0) + public static int getStreamMaxVolume(AudioManager audioManager, @C.StreamType int streamType) { + return audioManager.getStreamMaxVolume(streamType); + } + + /** + * Returns the minimum volume index for a particular stream. + * + * @param audioManager The {@link AudioManager}. + * @param streamType The {@link C.StreamType} whose minimum volume index is returned. + * @return The minimum valid volume index for the stream. + */ + @IntRange(from = 0) + public static int getStreamMinVolume(AudioManager audioManager, @C.StreamType int streamType) { + return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; + } + + private AudioManagerCompat() {} +} diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 274067327d..0c0da3e741 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -84,6 +84,7 @@ import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.C.ContentType; import androidx.media3.common.Format; @@ -2382,6 +2383,8 @@ public final class Util { return C.USAGE_ASSISTANCE_SONIFICATION; case C.STREAM_TYPE_VOICE_CALL: return C.USAGE_VOICE_COMMUNICATION; + case C.STREAM_TYPE_ACCESSIBILITY: + return C.USAGE_ASSISTANCE_ACCESSIBILITY; case C.STREAM_TYPE_MUSIC: default: return C.USAGE_MEDIA; @@ -2404,6 +2407,7 @@ public final class Util { case C.STREAM_TYPE_SYSTEM: return C.AUDIO_CONTENT_TYPE_SONIFICATION; case C.STREAM_TYPE_VOICE_CALL: + case C.STREAM_TYPE_ACCESSIBILITY: return C.AUDIO_CONTENT_TYPE_SPEECH; case C.STREAM_TYPE_MUSIC: default: @@ -2411,7 +2415,10 @@ public final class Util { } } - /** Returns the {@link C.StreamType} corresponding to the specified {@link C.AudioUsage}. */ + /** + * @deprecated Use {@link AudioAttributes#getStreamType()} instead. + */ + @Deprecated @UnstableApi public static @C.StreamType int getStreamTypeForAudioUsage(@C.AudioUsage int usage) { switch (usage) { @@ -2436,6 +2443,7 @@ public final class Util { case C.USAGE_NOTIFICATION_EVENT: return C.STREAM_TYPE_NOTIFICATION; case C.USAGE_ASSISTANCE_ACCESSIBILITY: + return C.STREAM_TYPE_ACCESSIBILITY; case C.USAGE_ASSISTANT: case C.USAGE_UNKNOWN: default: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java index c82281cae4..267cdfc562 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java @@ -15,6 +15,11 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.common.audio.AudioManagerCompat.AUDIOFOCUS_GAIN; +import static androidx.media3.common.audio.AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT; +import static androidx.media3.common.audio.AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; +import static androidx.media3.common.audio.AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; +import static androidx.media3.common.audio.AudioManagerCompat.AUDIOFOCUS_NONE; import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.annotation.ElementType.TYPE_USE; @@ -24,14 +29,15 @@ import android.media.AudioManager; import android.os.Handler; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Player; +import androidx.media3.common.audio.AudioFocusRequestCompat; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; -import androidx.media3.common.util.Util; +import com.google.common.base.Objects; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import java.lang.annotation.Documented; @@ -111,66 +117,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Audio focus has been temporarily lost, but playback may continue with reduced volume. */ private static final int AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK = 4; - /** - * Audio focus types. One of {@link #AUDIOFOCUS_NONE}, {@link #AUDIOFOCUS_GAIN}, {@link - * #AUDIOFOCUS_GAIN_TRANSIENT}, {@link #AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK} or {@link - * #AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. - */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - AUDIOFOCUS_NONE, - AUDIOFOCUS_GAIN, - AUDIOFOCUS_GAIN_TRANSIENT, - AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK, - AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - }) - private @interface AudioFocusGain {} - - /** - * @see AudioManager#AUDIOFOCUS_NONE - */ - @SuppressWarnings("InlinedApi") - private static final int AUDIOFOCUS_NONE = AudioManager.AUDIOFOCUS_NONE; - - /** - * @see AudioManager#AUDIOFOCUS_GAIN - */ - private static final int AUDIOFOCUS_GAIN = AudioManager.AUDIOFOCUS_GAIN; - - /** - * @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT - */ - private static final int AUDIOFOCUS_GAIN_TRANSIENT = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; - - /** - * @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK - */ - private static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; - - /** - * @see AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - */ - @SuppressWarnings("InlinedApi") - private static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; - private static final String TAG = "AudioFocusManager"; private static final float VOLUME_MULTIPLIER_DUCK = 0.2f; private static final float VOLUME_MULTIPLIER_DEFAULT = 1.0f; private final Supplier audioManager; - private final AudioFocusListener focusListener; + private final Handler eventHandler; @Nullable private PlayerControl playerControl; @Nullable private AudioAttributes audioAttributes; private @AudioFocusState int audioFocusState; - private @AudioFocusGain int focusGainToRequest; + private @AudioManagerCompat.AudioFocusGain int focusGainToRequest; private float volumeMultiplier = VOLUME_MULTIPLIER_DEFAULT; - private @MonotonicNonNull AudioFocusRequest audioFocusRequest; + private @MonotonicNonNull AudioFocusRequestCompat audioFocusRequest; private boolean rebuildAudioFocusRequest; /** @@ -188,7 +148,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; (AudioManager) context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE))); this.playerControl = playerControl; - this.focusListener = new AudioFocusListener(eventHandler); + this.eventHandler = eventHandler; this.audioFocusState = AUDIO_FOCUS_STATE_NOT_REQUESTED; } @@ -207,7 +167,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * managed automatically. */ public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { - if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + if (!Objects.equal(this.audioAttributes, audioAttributes)) { this.audioAttributes = audioAttributes; focusGainToRequest = convertAudioAttributesToFocusGain(audioAttributes); Assertions.checkArgument( @@ -257,7 +217,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @VisibleForTesting /* package */ AudioManager.OnAudioFocusChangeListener getFocusListener() { - return focusListener; + return this::handlePlatformAudioFocusChange; } private boolean shouldHandleAudioFocus(@Player.State int playbackState) { @@ -268,7 +228,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { return PLAYER_COMMAND_PLAY_WHEN_READY; } - int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + int requestResult = requestAudioFocusInternal(); if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); return PLAYER_COMMAND_PLAY_WHEN_READY; @@ -283,53 +243,29 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; || audioFocusState == AUDIO_FOCUS_STATE_NOT_REQUESTED) { return; } - if (Util.SDK_INT >= 26) { - abandonAudioFocusV26(); - } else { - abandonAudioFocusDefault(); + if (audioFocusRequest != null) { + AudioManagerCompat.abandonAudioFocusRequest(audioManager.get(), audioFocusRequest); } } - private int requestAudioFocusDefault() { - return audioManager - .get() - .requestAudioFocus( - focusListener, - Util.getStreamTypeForAudioUsage(checkNotNull(audioAttributes).usage), - focusGainToRequest); - } - - @RequiresApi(26) - private int requestAudioFocusV26() { + private int requestAudioFocusInternal() { if (audioFocusRequest == null || rebuildAudioFocusRequest) { - AudioFocusRequest.Builder builder = + AudioFocusRequestCompat.Builder builder = audioFocusRequest == null - ? new AudioFocusRequest.Builder(focusGainToRequest) - : new AudioFocusRequest.Builder(audioFocusRequest); + ? new AudioFocusRequestCompat.Builder(focusGainToRequest) + : audioFocusRequest.buildUpon(); boolean willPauseWhenDucked = willPauseWhenDucked(); audioFocusRequest = builder - .setAudioAttributes( - checkNotNull(audioAttributes).getAudioAttributesV21().audioAttributes) + .setAudioAttributes(checkNotNull(audioAttributes)) .setWillPauseWhenDucked(willPauseWhenDucked) - .setOnAudioFocusChangeListener(focusListener) + .setOnAudioFocusChangeListener(this::handlePlatformAudioFocusChange, eventHandler) .build(); rebuildAudioFocusRequest = false; } - return audioManager.get().requestAudioFocus(audioFocusRequest); - } - - private void abandonAudioFocusDefault() { - audioManager.get().abandonAudioFocus(focusListener); - } - - @RequiresApi(26) - private void abandonAudioFocusV26() { - if (audioFocusRequest != null) { - audioManager.get().abandonAudioFocusRequest(audioFocusRequest); - } + return AudioManagerCompat.requestAudioFocus(audioManager.get(), audioFocusRequest); } private boolean willPauseWhenDucked() { @@ -344,7 +280,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param audioAttributes The audio attributes associated with this focus request. * @return The type of audio focus gain that should be requested. */ - private static @AudioFocusGain int convertAudioAttributesToFocusGain( + private static @AudioManagerCompat.AudioFocusGain int convertAudioAttributesToFocusGain( @Nullable AudioAttributes audioAttributes) { if (audioAttributes == null) { // Don't handle audio focus. It may be either video only contents or developers @@ -460,19 +396,4 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; playerControl.executePlayerCommand(playerCommand); } } - - // Internal audio focus listener. - - private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { - private final Handler eventHandler; - - public AudioFocusListener(Handler eventHandler) { - this.eventHandler = eventHandler; - } - - @Override - public void onAudioFocusChange(int focusChange) { - eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange)); - } - } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index e070c47484..ad6d69c2b1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -426,10 +426,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (builder.deviceVolumeControlEnabled) { streamVolumeManager = new StreamVolumeManager( - builder.context, - eventHandler, - componentListener, - Util.getStreamTypeForAudioUsage(audioAttributes.usage)); + builder.context, eventHandler, componentListener, audioAttributes.getStreamType()); } else { streamVolumeManager = null; } @@ -1502,8 +1499,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.audioAttributes = newAudioAttributes; sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, newAudioAttributes); if (streamVolumeManager != null) { - streamVolumeManager.setStreamType( - Util.getStreamTypeForAudioUsage(newAudioAttributes.usage)); + streamVolumeManager.setStreamType(newAudioAttributes.getStreamType()); } // Queue event only and flush after updating playWhenReady in case both events are triggered. listeners.queueEvent( diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index 388fe1ad98..a0b9b696c9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -243,10 +243,6 @@ public interface Renderer extends PlayerMessage.Target { *

If tunneling is enabled by the track selector, the specified audio attributes will be * ignored, but they will take effect if audio is later played without tunneling. * - *

If the device is running a build before platform API version 21, audio attributes cannot be - * set directly on the underlying audio track. In this case, the usage will be mapped onto an - * equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}. - * *

To get audio attributes that are equivalent to a legacy stream type, pass the stream type to * {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build * an audio attributes instance. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java index 82157c1d91..67bf73e634 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java @@ -23,6 +23,7 @@ import android.media.AudioManager; import android.os.Handler; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; @@ -96,7 +97,7 @@ import androidx.media3.common.util.Util; * #setStreamType(int)} is called. */ public int getMinVolume() { - return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; + return AudioManagerCompat.getStreamMinVolume(audioManager, streamType); } /** @@ -104,7 +105,7 @@ import androidx.media3.common.util.Util; * #setStreamType(int)} is called. */ public int getMaxVolume() { - return audioManager.getStreamMaxVolume(streamType); + return AudioManagerCompat.getStreamMaxVolume(audioManager, streamType); } /** Gets the current volume for the current audio stream. */