From 2d11a339de8b78b4d61d196b58ab64429910c4ec Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Dec 2024 06:11:43 -0800 Subject: [PATCH] Import AudioManagerCompat and AudioFocusRequest from androidx.media Both classes provide utilities widely used by apps that are not yet available in Media3. This change imports the existing logic as it is with style adjustments to the Media3 codebase. PiperOrigin-RevId: 707067512 --- RELEASENOTES.md | 2 + .../media3/common/AudioAttributes.java | 37 ++ .../main/java/androidx/media3/common/C.java | 9 +- .../java/androidx/media3/common/Player.java | 7 +- .../common/audio/AudioFocusRequestCompat.java | 356 ++++++++++++++++++ .../common/audio/AudioManagerCompat.java | 152 ++++++++ .../androidx/media3/common/util/Util.java | 10 +- .../media3/exoplayer/AudioFocusManager.java | 129 ++----- .../media3/exoplayer/ExoPlayerImpl.java | 8 +- .../androidx/media3/exoplayer/Renderer.java | 4 - .../media3/exoplayer/StreamVolumeManager.java | 5 +- 11 files changed, 594 insertions(+), 125 deletions(-) create mode 100644 libraries/common/src/main/java/androidx/media3/common/audio/AudioFocusRequestCompat.java create mode 100644 libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java 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. */