diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 534e9a4200..88dbca7d39 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,10 @@ * Make `DefaultRenderersFactory` add two `MetadataRenderer` instances by default to enable apps to receive two different schemes of metadata by default. + * Initialize `DeviceInfo` and device volume asynchronously (if enabled via + `setDeviceVolumeControlEnabled`). These values won't be available + instantly after the `ExoPlayer.Builder.build()` and are notified via + `Player.Listener.onDeviceInfoChanged` and `onDeviceVolumeChanged`. * Transformer: * Enable support for Android platform diagnostics via `MediaMetricsManager`. Transformer will forward editing events and 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 f465959d15..9eb6f5b590 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 @@ -21,6 +21,7 @@ import android.media.AudioManager; import androidx.annotation.IntDef; import androidx.annotation.IntRange; import androidx.media3.common.C; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import java.lang.annotation.Documented; @@ -148,5 +149,41 @@ public final class AudioManagerCompat { return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; } + /** + * Returns the current volume for a particular stream. + * + * @param audioManager The {@link AudioManager}. + * @param streamType The {@link C.StreamType} whose volume is returned. + * @return The current volume of the stream. + */ + public static int getStreamVolume(AudioManager audioManager, @C.StreamType int streamType) { + // AudioManager#getStreamVolume(int) throws an exception on some devices. See + // https://github.com/google/ExoPlayer/issues/8191. + try { + return audioManager.getStreamVolume(streamType); + } catch (RuntimeException e) { + Log.w( + "AudioManagerCompat", + "Could not retrieve stream volume for stream type " + streamType, + e); + return audioManager.getStreamMaxVolume(streamType); + } + } + + /** + * Returns whether the given stream is muted. + * + * @param audioManager The {@link AudioManager}. + * @param streamType The {@link C.StreamType} to check. + * @return Whether the stream is muted. + */ + public static boolean isStreamMute(AudioManager audioManager, @C.StreamType int streamType) { + if (Util.SDK_INT >= 23) { + return audioManager.isStreamMute(streamType); + } else { + return getStreamVolume(audioManager, streamType) == 0; + } + } + private AudioManagerCompat() {} } diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java index b19dfc243b..586ad8970d 100644 --- a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/StreamVolumeManagerTest.java @@ -21,8 +21,10 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.media.AudioManager; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; import androidx.media3.common.C; +import androidx.media3.common.util.Clock; import androidx.media3.test.utils.DummyMainThread; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -43,27 +45,39 @@ public class StreamVolumeManagerTest { private AudioManager audioManager; private TestListener testListener; private DummyMainThread testThread; + private HandlerThread backgroundThread; private StreamVolumeManager streamVolumeManager; @Before - public void setUp() { + public void setUp() throws Exception { Context context = ApplicationProvider.getApplicationContext(); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); testListener = new TestListener(); testThread = new DummyMainThread(); + backgroundThread = new HandlerThread("StreamVolumeManagerTest"); + backgroundThread.start(); + testThread.runOnMainThread( () -> streamVolumeManager = new StreamVolumeManager( - context, new Handler(Looper.myLooper()), testListener, C.STREAM_TYPE_DEFAULT)); + context, + testListener, + C.STREAM_TYPE_DEFAULT, + backgroundThread.getLooper(), + /* listenerLooper= */ Looper.myLooper(), + Clock.DEFAULT)); + idleBackgroundThread(); } @After - public void tearDown() { + public void tearDown() throws Exception { testThread.runOnMainThread(() -> streamVolumeManager.release()); + idleBackgroundThread(); testThread.release(); + backgroundThread.quit(); } @Test @@ -95,7 +109,8 @@ public class StreamVolumeManagerTest { } @Test - public void setVolume_changesStreamVolume() { + public void setVolume_changesStreamVolume() throws Exception { + AtomicInteger targetVolume = new AtomicInteger(); testThread.runOnMainThread( () -> { int minVolume = streamVolumeManager.getMinVolume(); @@ -104,15 +119,21 @@ public class StreamVolumeManagerTest { return; } int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; - int oldVolume = streamVolumeManager.getVolume(); - int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; + targetVolume.set(oldVolume == maxVolume ? minVolume : maxVolume); - streamVolumeManager.setVolume(targetVolume, volumeFlags); + streamVolumeManager.setVolume(targetVolume.get(), volumeFlags); - assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); - assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); - assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + }); + idleBackgroundThread(); + testThread.runOnMainThread( + () -> { + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)) + .isEqualTo(targetVolume.get()); }); } @@ -134,7 +155,8 @@ public class StreamVolumeManagerTest { } @Test - public void increaseVolume_increasesStreamVolumeByOne() { + public void increaseVolume_increasesStreamVolumeByOne() throws Exception { + AtomicInteger targetVolume = new AtomicInteger(); testThread.runOnMainThread( () -> { int minVolume = streamVolumeManager.getMinVolume(); @@ -145,13 +167,20 @@ public class StreamVolumeManagerTest { int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; streamVolumeManager.setVolume(minVolume, volumeFlags); - int targetVolume = minVolume + 1; + targetVolume.set(minVolume + 1); streamVolumeManager.increaseVolume(volumeFlags); - assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); - assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); - assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + }); + idleBackgroundThread(); + testThread.runOnMainThread( + () -> { + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)) + .isEqualTo(targetVolume.get()); }); } @@ -170,7 +199,8 @@ public class StreamVolumeManagerTest { } @Test - public void decreaseVolume_decreasesStreamVolumeByOne() { + public void decreaseVolume_decreasesStreamVolumeByOne() throws Exception { + AtomicInteger targetVolume = new AtomicInteger(); testThread.runOnMainThread( () -> { int minVolume = streamVolumeManager.getMinVolume(); @@ -181,13 +211,20 @@ public class StreamVolumeManagerTest { int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; streamVolumeManager.setVolume(maxVolume, volumeFlags); - int targetVolume = maxVolume - 1; + targetVolume.set(maxVolume - 1); streamVolumeManager.decreaseVolume(volumeFlags); - assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); - assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); - assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + }); + idleBackgroundThread(); + testThread.runOnMainThread( + () -> { + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get()); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get()); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)) + .isEqualTo(targetVolume.get()); }); } @@ -231,7 +268,9 @@ public class StreamVolumeManagerTest { } @Test - public void setStreamType_toNonDefaultType_notifiesStreamTypeAndVolume() { + public void setStreamType_toNonDefaultType_notifiesStreamTypeAndVolume() throws Exception { + int testStreamType = C.STREAM_TYPE_ALARM; // not STREAM_TYPE_DEFAULT, i.e. MUSIC + int testStreamVolume = audioManager.getStreamVolume(testStreamType); testThread.runOnMainThread( () -> { int minVolume = streamVolumeManager.getMinVolume(); @@ -241,22 +280,28 @@ public class StreamVolumeManagerTest { } int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; - int testStreamType = C.STREAM_TYPE_ALARM; // not STREAM_TYPE_DEFAULT, i.e. MUSIC - int testStreamVolume = audioManager.getStreamVolume(testStreamType); - int oldVolume = streamVolumeManager.getVolume(); + int differentVolume = oldVolume; if (oldVolume == testStreamVolume) { - int differentVolume = oldVolume == minVolume ? maxVolume : minVolume; + differentVolume = oldVolume == minVolume ? maxVolume : minVolume; streamVolumeManager.setVolume(differentVolume, volumeFlags); } streamVolumeManager.setStreamType(testStreamType); + assertThat(testListener.lastStreamType).isEqualTo(testStreamType); + assertThat(testListener.lastStreamVolume).isEqualTo(differentVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(differentVolume); + }); + idleBackgroundThread(); + testThread.runOnMainThread( + () -> { assertThat(testListener.lastStreamType).isEqualTo(testStreamType); assertThat(testListener.lastStreamVolume).isEqualTo(testStreamVolume); assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume); }); } + ; @Test public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception { @@ -273,6 +318,7 @@ public class StreamVolumeManagerTest { int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; targetVolumeRef.set(targetVolume); + testListener.onStreamVolumeChangedLatch = new CountDownLatch(1); audioManager.setStreamVolume(C.STREAM_TYPE_DEFAULT, targetVolume, /* flags= */ 0); }); @@ -280,16 +326,20 @@ public class StreamVolumeManagerTest { assertThat(testListener.lastStreamVolume).isEqualTo(targetVolumeRef.get()); } + private void idleBackgroundThread() throws InterruptedException { + CountDownLatch waitForPendingBackgroundThreadOperation = new CountDownLatch(1); + new Handler(backgroundThread.getLooper()) + .post(waitForPendingBackgroundThreadOperation::countDown); + assertThat(waitForPendingBackgroundThreadOperation.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + } + private static class TestListener implements StreamVolumeManager.Listener { private @C.StreamType int lastStreamType; private int lastStreamVolume; private boolean lastStreamVolumeMuted; - public final CountDownLatch onStreamVolumeChangedLatch; - public TestListener() { - onStreamVolumeChangedLatch = new CountDownLatch(1); - } + public CountDownLatch onStreamVolumeChangedLatch; @Override public void onStreamTypeChanged(@C.StreamType int streamType) { @@ -300,7 +350,9 @@ public class StreamVolumeManagerTest { public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) { lastStreamVolume = streamVolume; lastStreamVolumeMuted = streamMuted; - onStreamVolumeChangedLatch.countDown(); + if (onStreamVolumeChangedLatch != null) { + onStreamVolumeChangedLatch.countDown(); + } } } } 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 3e9411014e..3ca0adbada 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -421,7 +421,12 @@ import java.util.concurrent.CopyOnWriteArraySet; if (builder.deviceVolumeControlEnabled) { streamVolumeManager = new StreamVolumeManager( - builder.context, eventHandler, componentListener, audioAttributes.getStreamType()); + builder.context, + componentListener, + audioAttributes.getStreamType(), + playbackLooper, + applicationLooper, + clock); } else { streamVolumeManager = null; } @@ -429,7 +434,7 @@ import java.util.concurrent.CopyOnWriteArraySet; wakeLockManager.setEnabled(builder.wakeMode != C.WAKE_MODE_NONE); wifiLockManager = new WifiLockManager(builder.context, playbackLooper, clock); wifiLockManager.setEnabled(builder.wakeMode == C.WAKE_MODE_NETWORK); - deviceInfo = createDeviceInfo(streamVolumeManager); + deviceInfo = DeviceInfo.UNKNOWN; videoSize = VideoSize.UNKNOWN; surfaceSize = Size.UNKNOWN; 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 67bf73e634..cb4499f1e1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/StreamVolumeManager.java @@ -15,18 +15,24 @@ */ package androidx.media3.exoplayer; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; -import android.os.Handler; +import android.os.Looper; 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.BackgroundThreadStateHandler; +import androidx.media3.common.util.Clock; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */ /* package */ final class StreamVolumeManager { @@ -48,48 +54,71 @@ import androidx.media3.common.util.Util; private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"; private final Context applicationContext; - private final Handler eventHandler; private final Listener listener; - private final AudioManager audioManager; + private final BackgroundThreadStateHandler stateHandler; + private @MonotonicNonNull AudioManager audioManager; @Nullable private VolumeChangeReceiver receiver; - private @C.StreamType int streamType; - private int volume; - private boolean muted; - /** Creates a manager. */ + /** + * Creates a manager. + * + * @param context A {@link Context}. + * @param listener A {@link Listener} for volume changes. + * @param streamType The initial {@link C.StreamType}. + * @param audioManagerLooper The background {@link Looper} to run {@link AudioManager} calls on. + * @param listenerLooper The {@link Looper} to call {@code listener} methods on. + * @param clock The {@link Clock}. + */ + @SuppressWarnings("initialization:methodref.receiver.bound.invalid") // this::method reference public StreamVolumeManager( - Context context, Handler eventHandler, Listener listener, @C.StreamType int streamType) { - applicationContext = context.getApplicationContext(); - this.eventHandler = eventHandler; + Context context, + Listener listener, + @C.StreamType int streamType, + Looper audioManagerLooper, + Looper listenerLooper, + Clock clock) { + this.applicationContext = context.getApplicationContext(); this.listener = listener; - audioManager = - Assertions.checkStateNotNull( - (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE)); - - this.streamType = streamType; - volume = getVolumeFromManager(audioManager, streamType); - muted = getMutedFromManager(audioManager, streamType); - - VolumeChangeReceiver receiver = new VolumeChangeReceiver(); - IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION); - try { - applicationContext.registerReceiver(receiver, filter); - this.receiver = receiver; - } catch (RuntimeException e) { - Log.w(TAG, "Error registering stream volume receiver", e); - } + StreamVolumeState initialState = + new StreamVolumeState( + streamType, + /* volume= */ 0, + /* muted= */ false, + /* minVolume= */ 0, + /* maxVolume= */ 0); + stateHandler = + new BackgroundThreadStateHandler<>( + initialState, + audioManagerLooper, + listenerLooper, + clock, + this::onStreamVolumeStateChanged); + stateHandler.runInBackground( + () -> { + audioManager = + Assertions.checkStateNotNull( + (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE)); + VolumeChangeReceiver receiver = new VolumeChangeReceiver(); + IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION); + try { + applicationContext.registerReceiver(receiver, filter); + this.receiver = receiver; + } catch (RuntimeException e) { + Log.w(TAG, "Error registering stream volume receiver", e); + } + stateHandler.setStateInBackground(generateState(streamType)); + }); } /** Sets the audio stream type. */ public void setStreamType(@C.StreamType int streamType) { - if (this.streamType == streamType) { - return; - } - this.streamType = streamType; - - updateVolumeAndNotifyIfChanged(); - listener.onStreamTypeChanged(streamType); + stateHandler.updateStateAsync( + /* placeholderState= */ state -> + new StreamVolumeState( + streamType, state.volume, state.muted, state.minVolume, state.maxVolume), + /* backgroundStateUpdate= */ state -> + state.streamType == streamType ? state : generateState(streamType)); } /** @@ -97,7 +126,7 @@ import androidx.media3.common.util.Util; * #setStreamType(int)} is called. */ public int getMinVolume() { - return AudioManagerCompat.getStreamMinVolume(audioManager, streamType); + return stateHandler.get().minVolume; } /** @@ -105,17 +134,17 @@ import androidx.media3.common.util.Util; * #setStreamType(int)} is called. */ public int getMaxVolume() { - return AudioManagerCompat.getStreamMaxVolume(audioManager, streamType); + return stateHandler.get().maxVolume; } /** Gets the current volume for the current audio stream. */ public int getVolume() { - return volume; + return stateHandler.get().volume; } /** Gets whether the current audio stream is muted or not. */ public boolean isMuted() { - return muted; + return stateHandler.get().muted; } /** @@ -125,12 +154,23 @@ import androidx.media3.common.util.Util; * otherwise the volume will not be changed. * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. */ + @SuppressLint("WrongConstant") // Setting C.VolumeFlags as audio system volume flags. public void setVolume(int volume, @C.VolumeFlags int flags) { - if (volume < getMinVolume() || volume > getMaxVolume()) { - return; - } - audioManager.setStreamVolume(streamType, volume, flags); - updateVolumeAndNotifyIfChanged(); + stateHandler.updateStateAsync( + /* placeholderState= */ state -> + new StreamVolumeState( + state.streamType, + volume >= state.minVolume && volume <= state.maxVolume ? volume : state.volume, + state.muted, + state.minVolume, + state.maxVolume), + /* backgroundStateUpdate= */ state -> { + if (volume == state.volume || volume < state.minVolume || volume > state.maxVolume) { + return state; + } + checkNotNull(audioManager).setStreamVolume(state.streamType, volume, flags); + return generateState(state.streamType); + }); } /** @@ -139,12 +179,24 @@ import androidx.media3.common.util.Util; * * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. */ + @SuppressLint("WrongConstant") // Setting C.VolumeFlags as audio system volume flags. public void increaseVolume(@C.VolumeFlags int flags) { - if (volume >= getMaxVolume()) { - return; - } - audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_RAISE, flags); - updateVolumeAndNotifyIfChanged(); + stateHandler.updateStateAsync( + /* placeholderState= */ state -> + new StreamVolumeState( + state.streamType, + state.volume < state.maxVolume ? state.volume + 1 : state.maxVolume, + state.muted, + state.minVolume, + state.maxVolume), + /* backgroundStateUpdate= */ state -> { + if (state.volume >= state.maxVolume) { + return state; + } + checkNotNull(audioManager) + .adjustStreamVolume(state.streamType, AudioManager.ADJUST_RAISE, flags); + return generateState(state.streamType); + }); } /** @@ -153,12 +205,24 @@ import androidx.media3.common.util.Util; * * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. */ + @SuppressLint("WrongConstant") // Setting C.VolumeFlags as audio system volume flags. public void decreaseVolume(@C.VolumeFlags int flags) { - if (volume <= getMinVolume()) { - return; - } - audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_LOWER, flags); - updateVolumeAndNotifyIfChanged(); + stateHandler.updateStateAsync( + /* placeholderState= */ state -> + new StreamVolumeState( + state.streamType, + state.volume > state.minVolume ? state.volume - 1 : state.minVolume, + state.muted, + state.minVolume, + state.maxVolume), + /* backgroundStateUpdate= */ state -> { + if (state.volume <= state.minVolume) { + return state; + } + checkNotNull(audioManager) + .adjustStreamVolume(state.streamType, AudioManager.ADJUST_LOWER, flags); + return generateState(state.streamType); + }); } /** @@ -167,55 +231,81 @@ import androidx.media3.common.util.Util; * @param muted Whether to mute or to unmute the stream. * @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. */ + @SuppressLint("WrongConstant") // Setting C.VolumeFlags as audio system volume flags. public void setMuted(boolean muted, @C.VolumeFlags int flags) { - if (Util.SDK_INT >= 23) { - audioManager.adjustStreamVolume( - streamType, muted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, flags); - } else { - audioManager.setStreamMute(streamType, muted); - } - updateVolumeAndNotifyIfChanged(); + stateHandler.updateStateAsync( + /* placeholderState= */ state -> + new StreamVolumeState( + state.streamType, state.volume, muted, state.minVolume, state.maxVolume), + /* backgroundStateUpdate= */ state -> { + if (state.muted == muted) { + return state; + } + checkNotNull(audioManager); + if (Util.SDK_INT >= 23) { + audioManager.adjustStreamVolume( + state.streamType, + muted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, + flags); + } else { + audioManager.setStreamMute(state.streamType, muted); + } + return generateState(state.streamType); + }); } /** Releases the manager. It must be called when the manager is no longer required. */ public void release() { - if (receiver != null) { - try { - applicationContext.unregisterReceiver(receiver); - } catch (RuntimeException e) { - Log.w(TAG, "Error unregistering stream volume receiver", e); - } - receiver = null; + stateHandler.updateStateAsync( + /* placeholderState= */ state -> state, + /* backgroundStateUpdate= */ state -> { + if (receiver != null) { + try { + applicationContext.unregisterReceiver(receiver); + } catch (RuntimeException e) { + Log.w(TAG, "Error unregistering stream volume receiver", e); + } + receiver = null; + } + return state; + }); + } + + private void onStreamVolumeStateChanged(StreamVolumeState oldState, StreamVolumeState newState) { + if (oldState.volume != newState.volume || oldState.muted != newState.muted) { + listener.onStreamVolumeChanged(newState.volume, newState.muted); + } + if (oldState.streamType != newState.streamType + || oldState.minVolume != newState.minVolume + || oldState.maxVolume != newState.maxVolume) { + listener.onStreamTypeChanged(newState.streamType); } } - private void updateVolumeAndNotifyIfChanged() { - int newVolume = getVolumeFromManager(audioManager, streamType); - boolean newMuted = getMutedFromManager(audioManager, streamType); - if (volume != newVolume || muted != newMuted) { - volume = newVolume; - muted = newMuted; - listener.onStreamVolumeChanged(newVolume, newMuted); - } + private StreamVolumeState generateState(@C.StreamType int streamType) { + checkNotNull(audioManager); + int volume = AudioManagerCompat.getStreamVolume(audioManager, streamType); + boolean muted = AudioManagerCompat.isStreamMute(audioManager, streamType); + int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, streamType); + int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, streamType); + return new StreamVolumeState(streamType, volume, muted, minVolume, maxVolume); } - private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) { - // AudioManager#getStreamVolume(int) throws an exception on some devices. See - // https://github.com/google/ExoPlayer/issues/8191. - try { - return audioManager.getStreamVolume(streamType); - } catch (RuntimeException e) { - Log.w(TAG, "Could not retrieve stream volume for stream type " + streamType, e); - return audioManager.getStreamMaxVolume(streamType); - } - } + private static final class StreamVolumeState { - private static boolean getMutedFromManager( - AudioManager audioManager, @C.StreamType int streamType) { - if (Util.SDK_INT >= 23) { - return audioManager.isStreamMute(streamType); - } else { - return getVolumeFromManager(audioManager, streamType) == 0; + public final @C.StreamType int streamType; + public final int volume; + public final boolean muted; + public final int minVolume; + public final int maxVolume; + + public StreamVolumeState( + @C.StreamType int streamType, int volume, boolean muted, int minVolume, int maxVolume) { + this.streamType = streamType; + this.volume = volume; + this.muted = muted; + this.minVolume = minVolume; + this.maxVolume = maxVolume; } } @@ -223,7 +313,16 @@ import androidx.media3.common.util.Util; @Override public void onReceive(Context context, Intent intent) { - eventHandler.post(StreamVolumeManager.this::updateVolumeAndNotifyIfChanged); + // BroadcastReceivers are called on the main thread. + stateHandler.runInBackground( + () -> { + if (receiver == null) { + // Stale event. StreamVolumeManager is already released. + return; + } + int streamType = stateHandler.get().streamType; + stateHandler.setStateInBackground(generateState(streamType)); + }); } } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index 68383b5c13..670e6c6d18 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -14175,18 +14175,20 @@ public class ExoPlayerTest { } @Test - public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener() { + public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener() + throws Exception { ExoPlayer player = parameterizeTestExoPlayerBuilder( new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) .setDeviceVolumeControlEnabled(true)) .build(); Player.Listener listener = mock(Player.Listener.class); - player.addListener(listener); + run(player).untilPendingCommandsAreFullyHandled(); int deviceVolume = player.getDeviceVolume(); int noVolumeFlags = 0; int volumeFlags = C.VOLUME_FLAG_PLAY_SOUND | C.VOLUME_FLAG_VIBRATE; + player.addListener(listener); try { player.setDeviceVolume(deviceVolume + 1, noVolumeFlags); // No-op if at max volume. player.setDeviceVolume(deviceVolume - 1, noVolumeFlags); // No-op if at min volume.