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 index 9eb6f5b590..5a85b8b956 100644 --- a/libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java +++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioManagerCompat.java @@ -15,12 +15,18 @@ */ package androidx.media3.common.audio; +import static androidx.media3.common.util.Assertions.checkNotNull; import static java.lang.annotation.ElementType.TYPE_USE; +import android.content.Context; import android.media.AudioManager; +import android.os.Looper; import androidx.annotation.IntDef; import androidx.annotation.IntRange; +import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.util.BackgroundExecutor; +import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -28,11 +34,14 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Compatibility layer for {@link AudioManager} with fallbacks for older Android versions. */ @UnstableApi public final class AudioManagerCompat { + private static final String TAG = "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 @@ -83,6 +92,55 @@ public final class AudioManagerCompat { public static final int AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE; + @SuppressWarnings("NonFinalStaticField") // Lazily initialized under class lock + @Nullable + private static AudioManager audioManager; + + @SuppressWarnings("NonFinalStaticField") // Lazily initialized under class lock + private static @MonotonicNonNull Context applicationContext; + + /** + * Returns the {@link AudioManager}. + * + *

This method avoids potential threading issues where AudioManager keeps access to the thread + * it was created on until after this thread is stopped. + * + *

It is recommended to use this method from a background thread. + * + * @param context A {@link Context}. + * @return The {@link AudioManager}. + */ + public static synchronized AudioManager getAudioManager(Context context) { + Context applicationContext = context.getApplicationContext(); + if (AudioManagerCompat.applicationContext != applicationContext) { + // Reset cached instance if the application context changed. This should only happen in tests. + audioManager = null; + } + if (audioManager != null) { + return audioManager; + } + @Nullable Looper myLooper = Looper.myLooper(); + if (myLooper == null || myLooper == Looper.getMainLooper()) { + // The AudioManager will assume the main looper as default callback anyway, so create the + // instance here without using BackgroundExecutor. + audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); + return checkNotNull(audioManager); + } + // Create the audio manager on the BackgroundExecutor to avoid running the potentially blocking + // command on the main thread but still use a thread that is guaranteed to exist for the + // lifetime of the app. + ConditionVariable audioManagerSetCondition = new ConditionVariable(); + BackgroundExecutor.get() + .execute( + () -> { + audioManager = + (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); + audioManagerSetCondition.open(); + }); + audioManagerSetCondition.blockUninterruptible(); + return checkNotNull(audioManager); + } + /** * Requests audio focus. See the {@link AudioFocusRequestCompat} for information about the options * available to configure your request, and notification of focus gain and loss. diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java index 194805f8d0..0a7453ba9e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/BackgroundExecutor.java @@ -31,6 +31,8 @@ public final class BackgroundExecutor { * *

Must only be used for quick, high-priority tasks to ensure other background tasks are not * blocked. + * + *

The thread is guaranteed to be alive for the lifetime of the application. */ public static synchronized Executor get() { if (staticInstance == null) { @@ -42,6 +44,9 @@ public final class BackgroundExecutor { /** * Sets the {@link Executor} to be returned from {@link #get()}. * + *

Note that the thread of the provided {@link Executor} must stay alive for the lifetime of + * the application. + * * @param executor An {@link Executor} that runs tasks on background threads and should only be * used for quick, high-priority tasks to ensure other background tasks are not blocked. */ 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 1e6cd7794e..6626cff721 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 @@ -95,6 +95,7 @@ import androidx.media3.common.ParserException; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.Player.Commands; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.audio.AudioProcessor; import com.google.common.base.Ascii; import com.google.common.io.ByteStreams; @@ -2485,9 +2486,7 @@ public final class Util { */ @UnstableApi public static int generateAudioSessionIdV21(Context context) { - @Nullable - AudioManager audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); - return audioManager == null ? AudioManager.ERROR : audioManager.generateAudioSessionId(); + return AudioManagerCompat.getAudioManager(context).generateAudioSessionId(); } /** diff --git a/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/LibiamfAudioRenderer.java b/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/LibiamfAudioRenderer.java index 9365f9b4d1..78fa93972d 100644 --- a/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/LibiamfAudioRenderer.java +++ b/libraries/decoder_iamf/src/main/java/androidx/media3/decoder/iamf/LibiamfAudioRenderer.java @@ -26,6 +26,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -95,10 +96,7 @@ public class LibiamfAudioRenderer extends DecoderAudioRenderer { return false; } - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - if (audioManager == null) { - return false; - } + AudioManager audioManager = AudioManagerCompat.getAudioManager(context); AudioFormat audioFormat = new AudioFormat.Builder() .setEncoding(IamfDecoder.OUTPUT_PCM_ENCODING) 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 95902c1511..f865c72e1d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/AudioFocusManager.java @@ -142,12 +142,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param playerControl A {@link PlayerControl} to handle commands from this instance. */ public AudioFocusManager(Context context, Looper eventLooper, PlayerControl playerControl) { - this.audioManager = - Suppliers.memoize( - () -> - checkNotNull( - (AudioManager) - context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE))); + this.audioManager = Suppliers.memoize(() -> AudioManagerCompat.getAudioManager(context)); this.playerControl = playerControl; this.eventHandler = new Handler(eventLooper); this.audioFocusState = AUDIO_FOCUS_STATE_NOT_REQUESTED; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java index bf8b595841..e9e6801ed7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilities.java @@ -40,6 +40,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import com.google.common.collect.ImmutableList; @@ -142,8 +143,7 @@ public final class AudioCapabilities { @Nullable Intent intent, AudioAttributes audioAttributes, @Nullable AudioDeviceInfoApi23 routedDevice) { - AudioManager audioManager = - (AudioManager) checkNotNull(context.getSystemService(Context.AUDIO_SERVICE)); + AudioManager audioManager = AudioManagerCompat.getAudioManager(context); AudioDeviceInfoApi23 currentDevice = routedDevice != null ? routedDevice diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java index 2e0aa8b4b4..e7cf60171b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioCapabilitiesReceiver.java @@ -31,6 +31,7 @@ import android.os.Handler; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.media3.common.AudioAttributes; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import java.util.Objects; @@ -261,14 +262,12 @@ public final class AudioCapabilitiesReceiver { public static void registerAudioDeviceCallback( Context context, AudioDeviceCallback callback, Handler handler) { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - checkNotNull(audioManager).registerAudioDeviceCallback(callback, handler); + AudioManagerCompat.getAudioManager(context).registerAudioDeviceCallback(callback, handler); } public static void unregisterAudioDeviceCallback( Context context, AudioDeviceCallback callback) { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - checkNotNull(audioManager).unregisterAudioDeviceCallback(callback); + AudioManagerCompat.getAudioManager(context).unregisterAudioDeviceCallback(callback); } private Api23() {} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java index 2c0020f135..a5d285d917 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioOffloadSupportProvider.java @@ -26,6 +26,7 @@ import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -116,17 +117,13 @@ public final class DefaultAudioOffloadSupportProvider } if (context != null) { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - if (audioManager != null) { - String offloadVariableRateSupportedKeyValue = - audioManager.getParameters(/* keys= */ OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY); - isOffloadVariableRateSupported = - offloadVariableRateSupportedKeyValue != null - && offloadVariableRateSupportedKeyValue.equals( - OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY + "=1"); - } else { - isOffloadVariableRateSupported = false; - } + AudioManager audioManager = AudioManagerCompat.getAudioManager(context); + String offloadVariableRateSupportedKeyValue = + audioManager.getParameters(/* keys= */ OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY); + isOffloadVariableRateSupported = + offloadVariableRateSupportedKeyValue != null + && offloadVariableRateSupportedKeyValue.equals( + OFFLOAD_VARIABLE_RATE_SUPPORTED_KEY + "=1"); } else { isOffloadVariableRateSupported = false; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index f709f50d40..55ae6f3751 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -54,6 +54,7 @@ import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.BundleCollectionUtil; import androidx.media3.common.util.Log; @@ -4280,7 +4281,8 @@ public class DefaultTrackSelector extends MappingTrackSelector @Nullable Context context, DefaultTrackSelector defaultTrackSelector) { @Nullable AudioManager audioManager = - context == null ? null : (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + context == null ? null : AudioManagerCompat.getAudioManager(context); + ; if (audioManager == null || Util.isTv(checkNotNull(context))) { spatializer = null; spatializationSupported = false; diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java index 932060500f..2f91bedc3f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java @@ -66,6 +66,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; +import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; @@ -2335,7 +2336,7 @@ public class MediaSessionCompat { } mContext = context; mSessionInfo = sessionInfo; - mAudioManager = (AudioManager) checkNotNull(context.getSystemService(Context.AUDIO_SERVICE)); + mAudioManager = AudioManagerCompat.getAudioManager(context); mMediaButtonReceiverComponentName = mbrComponent; mMediaButtonReceiverIntent = mbrIntent; mStub = new MediaSessionStub(/* mediaSessionImpl= */ this, context.getPackageName(), tag);