Add util to obtain AudioManager

AudioManager internally assumes the thread is was created on can be
used as a callback thread. Since the AudioManager is only created
once in the lifetime of an app, we need to make sure its obtained
on a thread that stays alive.

#cherrypick

PiperOrigin-RevId: 732072255
(cherry picked from commit 2088697a19ac85feb26ba2521a70647803246571)
This commit is contained in:
tonihei 2025-02-28 03:21:38 -08:00 committed by oceanjules
parent 595fd48cd7
commit 766dbd497e
10 changed files with 86 additions and 32 deletions

View File

@ -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}.
*
* <p>This method avoids potential threading issues where AudioManager keeps access to the thread
* it was created on until after this thread is stopped.
*
* <p>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.

View File

@ -31,6 +31,8 @@ public final class BackgroundExecutor {
*
* <p>Must only be used for quick, high-priority tasks to ensure other background tasks are not
* blocked.
*
* <p>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()}.
*
* <p>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.
*/

View File

@ -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();
}
/**

View File

@ -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<IamfDecoder> {
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)

View File

@ -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;

View File

@ -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

View File

@ -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() {}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);