Move StreamVolumeManager system calls to playback thread

This requires some additional state handling to update the full
state atomically and guess placeholder states while updates are
in progress, using the newly added BackgroundThreadStateHander.

Some tests also needed to be adjusted to account for the fact
that the actual audio system change doesn't happen inline
anymore.

PiperOrigin-RevId: 716702141
This commit is contained in:
tonihei 2025-01-17 09:44:56 -08:00 committed by Copybara-Service
parent c797249998
commit 190563b8eb
6 changed files with 324 additions and 125 deletions

View File

@ -19,6 +19,10 @@
* Make `DefaultRenderersFactory` add two `MetadataRenderer` instances by * Make `DefaultRenderersFactory` add two `MetadataRenderer` instances by
default to enable apps to receive two different schemes of metadata by default to enable apps to receive two different schemes of metadata by
default. 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: * Transformer:
* Enable support for Android platform diagnostics via * Enable support for Android platform diagnostics via
`MediaMetricsManager`. Transformer will forward editing events and `MediaMetricsManager`. Transformer will forward editing events and

View File

@ -21,6 +21,7 @@ import android.media.AudioManager;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -148,5 +149,41 @@ public final class AudioManagerCompat {
return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; 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() {} private AudioManagerCompat() {}
} }

View File

@ -21,8 +21,10 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Clock;
import androidx.media3.test.utils.DummyMainThread; import androidx.media3.test.utils.DummyMainThread;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -43,27 +45,39 @@ public class StreamVolumeManagerTest {
private AudioManager audioManager; private AudioManager audioManager;
private TestListener testListener; private TestListener testListener;
private DummyMainThread testThread; private DummyMainThread testThread;
private HandlerThread backgroundThread;
private StreamVolumeManager streamVolumeManager; private StreamVolumeManager streamVolumeManager;
@Before @Before
public void setUp() { public void setUp() throws Exception {
Context context = ApplicationProvider.getApplicationContext(); Context context = ApplicationProvider.getApplicationContext();
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
testListener = new TestListener(); testListener = new TestListener();
testThread = new DummyMainThread(); testThread = new DummyMainThread();
backgroundThread = new HandlerThread("StreamVolumeManagerTest");
backgroundThread.start();
testThread.runOnMainThread( testThread.runOnMainThread(
() -> () ->
streamVolumeManager = streamVolumeManager =
new 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 @After
public void tearDown() { public void tearDown() throws Exception {
testThread.runOnMainThread(() -> streamVolumeManager.release()); testThread.runOnMainThread(() -> streamVolumeManager.release());
idleBackgroundThread();
testThread.release(); testThread.release();
backgroundThread.quit();
} }
@Test @Test
@ -95,7 +109,8 @@ public class StreamVolumeManagerTest {
} }
@Test @Test
public void setVolume_changesStreamVolume() { public void setVolume_changesStreamVolume() throws Exception {
AtomicInteger targetVolume = new AtomicInteger();
testThread.runOnMainThread( testThread.runOnMainThread(
() -> { () -> {
int minVolume = streamVolumeManager.getMinVolume(); int minVolume = streamVolumeManager.getMinVolume();
@ -104,15 +119,21 @@ public class StreamVolumeManagerTest {
return; return;
} }
int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE;
int oldVolume = streamVolumeManager.getVolume(); 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(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get());
assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get());
assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); });
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 @Test
public void increaseVolume_increasesStreamVolumeByOne() { public void increaseVolume_increasesStreamVolumeByOne() throws Exception {
AtomicInteger targetVolume = new AtomicInteger();
testThread.runOnMainThread( testThread.runOnMainThread(
() -> { () -> {
int minVolume = streamVolumeManager.getMinVolume(); int minVolume = streamVolumeManager.getMinVolume();
@ -145,13 +167,20 @@ public class StreamVolumeManagerTest {
int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE;
streamVolumeManager.setVolume(minVolume, volumeFlags); streamVolumeManager.setVolume(minVolume, volumeFlags);
int targetVolume = minVolume + 1; targetVolume.set(minVolume + 1);
streamVolumeManager.increaseVolume(volumeFlags); streamVolumeManager.increaseVolume(volumeFlags);
assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get());
assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get());
assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); });
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 @Test
public void decreaseVolume_decreasesStreamVolumeByOne() { public void decreaseVolume_decreasesStreamVolumeByOne() throws Exception {
AtomicInteger targetVolume = new AtomicInteger();
testThread.runOnMainThread( testThread.runOnMainThread(
() -> { () -> {
int minVolume = streamVolumeManager.getMinVolume(); int minVolume = streamVolumeManager.getMinVolume();
@ -181,13 +211,20 @@ public class StreamVolumeManagerTest {
int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE;
streamVolumeManager.setVolume(maxVolume, volumeFlags); streamVolumeManager.setVolume(maxVolume, volumeFlags);
int targetVolume = maxVolume - 1; targetVolume.set(maxVolume - 1);
streamVolumeManager.decreaseVolume(volumeFlags); streamVolumeManager.decreaseVolume(volumeFlags);
assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume.get());
assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume.get());
assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); });
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 @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( testThread.runOnMainThread(
() -> { () -> {
int minVolume = streamVolumeManager.getMinVolume(); int minVolume = streamVolumeManager.getMinVolume();
@ -241,22 +280,28 @@ public class StreamVolumeManagerTest {
} }
int volumeFlags = C.VOLUME_FLAG_SHOW_UI | C.VOLUME_FLAG_VIBRATE; 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 oldVolume = streamVolumeManager.getVolume();
int differentVolume = oldVolume;
if (oldVolume == testStreamVolume) { if (oldVolume == testStreamVolume) {
int differentVolume = oldVolume == minVolume ? maxVolume : minVolume; differentVolume = oldVolume == minVolume ? maxVolume : minVolume;
streamVolumeManager.setVolume(differentVolume, volumeFlags); streamVolumeManager.setVolume(differentVolume, volumeFlags);
} }
streamVolumeManager.setStreamType(testStreamType); 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.lastStreamType).isEqualTo(testStreamType);
assertThat(testListener.lastStreamVolume).isEqualTo(testStreamVolume); assertThat(testListener.lastStreamVolume).isEqualTo(testStreamVolume);
assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume); assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume);
}); });
} }
;
@Test @Test
public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception { public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception {
@ -273,6 +318,7 @@ public class StreamVolumeManagerTest {
int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume;
targetVolumeRef.set(targetVolume); targetVolumeRef.set(targetVolume);
testListener.onStreamVolumeChangedLatch = new CountDownLatch(1);
audioManager.setStreamVolume(C.STREAM_TYPE_DEFAULT, targetVolume, /* flags= */ 0); audioManager.setStreamVolume(C.STREAM_TYPE_DEFAULT, targetVolume, /* flags= */ 0);
}); });
@ -280,16 +326,20 @@ public class StreamVolumeManagerTest {
assertThat(testListener.lastStreamVolume).isEqualTo(targetVolumeRef.get()); 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 static class TestListener implements StreamVolumeManager.Listener {
private @C.StreamType int lastStreamType; private @C.StreamType int lastStreamType;
private int lastStreamVolume; private int lastStreamVolume;
private boolean lastStreamVolumeMuted; private boolean lastStreamVolumeMuted;
public final CountDownLatch onStreamVolumeChangedLatch;
public TestListener() { public CountDownLatch onStreamVolumeChangedLatch;
onStreamVolumeChangedLatch = new CountDownLatch(1);
}
@Override @Override
public void onStreamTypeChanged(@C.StreamType int streamType) { public void onStreamTypeChanged(@C.StreamType int streamType) {
@ -300,7 +350,9 @@ public class StreamVolumeManagerTest {
public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) { public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) {
lastStreamVolume = streamVolume; lastStreamVolume = streamVolume;
lastStreamVolumeMuted = streamMuted; lastStreamVolumeMuted = streamMuted;
onStreamVolumeChangedLatch.countDown(); if (onStreamVolumeChangedLatch != null) {
onStreamVolumeChangedLatch.countDown();
}
} }
} }
} }

View File

@ -421,7 +421,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (builder.deviceVolumeControlEnabled) { if (builder.deviceVolumeControlEnabled) {
streamVolumeManager = streamVolumeManager =
new StreamVolumeManager( new StreamVolumeManager(
builder.context, eventHandler, componentListener, audioAttributes.getStreamType()); builder.context,
componentListener,
audioAttributes.getStreamType(),
playbackLooper,
applicationLooper,
clock);
} else { } else {
streamVolumeManager = null; streamVolumeManager = null;
} }
@ -429,7 +434,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
wakeLockManager.setEnabled(builder.wakeMode != C.WAKE_MODE_NONE); wakeLockManager.setEnabled(builder.wakeMode != C.WAKE_MODE_NONE);
wifiLockManager = new WifiLockManager(builder.context, playbackLooper, clock); wifiLockManager = new WifiLockManager(builder.context, playbackLooper, clock);
wifiLockManager.setEnabled(builder.wakeMode == C.WAKE_MODE_NETWORK); wifiLockManager.setEnabled(builder.wakeMode == C.WAKE_MODE_NETWORK);
deviceInfo = createDeviceInfo(streamVolumeManager); deviceInfo = DeviceInfo.UNKNOWN;
videoSize = VideoSize.UNKNOWN; videoSize = VideoSize.UNKNOWN;
surfaceSize = Size.UNKNOWN; surfaceSize = Size.UNKNOWN;

View File

@ -15,18 +15,24 @@
*/ */
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Handler; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.audio.AudioManagerCompat; import androidx.media3.common.audio.AudioManagerCompat;
import androidx.media3.common.util.Assertions; 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.Log;
import androidx.media3.common.util.Util; 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. */ /** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */
/* package */ final class StreamVolumeManager { /* 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 static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION";
private final Context applicationContext; private final Context applicationContext;
private final Handler eventHandler;
private final Listener listener; private final Listener listener;
private final AudioManager audioManager; private final BackgroundThreadStateHandler<StreamVolumeState> stateHandler;
private @MonotonicNonNull AudioManager audioManager;
@Nullable private VolumeChangeReceiver receiver; @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( public StreamVolumeManager(
Context context, Handler eventHandler, Listener listener, @C.StreamType int streamType) { Context context,
applicationContext = context.getApplicationContext(); Listener listener,
this.eventHandler = eventHandler; @C.StreamType int streamType,
Looper audioManagerLooper,
Looper listenerLooper,
Clock clock) {
this.applicationContext = context.getApplicationContext();
this.listener = listener; this.listener = listener;
audioManager = StreamVolumeState initialState =
Assertions.checkStateNotNull( new StreamVolumeState(
(AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE)); streamType,
/* volume= */ 0,
this.streamType = streamType; /* muted= */ false,
volume = getVolumeFromManager(audioManager, streamType); /* minVolume= */ 0,
muted = getMutedFromManager(audioManager, streamType); /* maxVolume= */ 0);
stateHandler =
VolumeChangeReceiver receiver = new VolumeChangeReceiver(); new BackgroundThreadStateHandler<>(
IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION); initialState,
try { audioManagerLooper,
applicationContext.registerReceiver(receiver, filter); listenerLooper,
this.receiver = receiver; clock,
} catch (RuntimeException e) { this::onStreamVolumeStateChanged);
Log.w(TAG, "Error registering stream volume receiver", e); 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. */ /** Sets the audio stream type. */
public void setStreamType(@C.StreamType int streamType) { public void setStreamType(@C.StreamType int streamType) {
if (this.streamType == streamType) { stateHandler.updateStateAsync(
return; /* placeholderState= */ state ->
} new StreamVolumeState(
this.streamType = streamType; streamType, state.volume, state.muted, state.minVolume, state.maxVolume),
/* backgroundStateUpdate= */ state ->
updateVolumeAndNotifyIfChanged(); state.streamType == streamType ? state : generateState(streamType));
listener.onStreamTypeChanged(streamType);
} }
/** /**
@ -97,7 +126,7 @@ import androidx.media3.common.util.Util;
* #setStreamType(int)} is called. * #setStreamType(int)} is called.
*/ */
public int getMinVolume() { 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. * #setStreamType(int)} is called.
*/ */
public int getMaxVolume() { public int getMaxVolume() {
return AudioManagerCompat.getStreamMaxVolume(audioManager, streamType); return stateHandler.get().maxVolume;
} }
/** Gets the current volume for the current audio stream. */ /** Gets the current volume for the current audio stream. */
public int getVolume() { public int getVolume() {
return volume; return stateHandler.get().volume;
} }
/** Gets whether the current audio stream is muted or not. */ /** Gets whether the current audio stream is muted or not. */
public boolean isMuted() { 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. * otherwise the volume will not be changed.
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. * @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) { public void setVolume(int volume, @C.VolumeFlags int flags) {
if (volume < getMinVolume() || volume > getMaxVolume()) { stateHandler.updateStateAsync(
return; /* placeholderState= */ state ->
} new StreamVolumeState(
audioManager.setStreamVolume(streamType, volume, flags); state.streamType,
updateVolumeAndNotifyIfChanged(); 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}. * @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) { public void increaseVolume(@C.VolumeFlags int flags) {
if (volume >= getMaxVolume()) { stateHandler.updateStateAsync(
return; /* placeholderState= */ state ->
} new StreamVolumeState(
audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_RAISE, flags); state.streamType,
updateVolumeAndNotifyIfChanged(); 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}. * @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) { public void decreaseVolume(@C.VolumeFlags int flags) {
if (volume <= getMinVolume()) { stateHandler.updateStateAsync(
return; /* placeholderState= */ state ->
} new StreamVolumeState(
audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_LOWER, flags); state.streamType,
updateVolumeAndNotifyIfChanged(); 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 muted Whether to mute or to unmute the stream.
* @param flags Either 0 or a bitwise combination of one or more {@link C.VolumeFlags}. * @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) { public void setMuted(boolean muted, @C.VolumeFlags int flags) {
if (Util.SDK_INT >= 23) { stateHandler.updateStateAsync(
audioManager.adjustStreamVolume( /* placeholderState= */ state ->
streamType, muted ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, flags); new StreamVolumeState(
} else { state.streamType, state.volume, muted, state.minVolume, state.maxVolume),
audioManager.setStreamMute(streamType, muted); /* backgroundStateUpdate= */ state -> {
} if (state.muted == muted) {
updateVolumeAndNotifyIfChanged(); 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. */ /** Releases the manager. It must be called when the manager is no longer required. */
public void release() { public void release() {
if (receiver != null) { stateHandler.updateStateAsync(
try { /* placeholderState= */ state -> state,
applicationContext.unregisterReceiver(receiver); /* backgroundStateUpdate= */ state -> {
} catch (RuntimeException e) { if (receiver != null) {
Log.w(TAG, "Error unregistering stream volume receiver", e); try {
} applicationContext.unregisterReceiver(receiver);
receiver = null; } 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() { private StreamVolumeState generateState(@C.StreamType int streamType) {
int newVolume = getVolumeFromManager(audioManager, streamType); checkNotNull(audioManager);
boolean newMuted = getMutedFromManager(audioManager, streamType); int volume = AudioManagerCompat.getStreamVolume(audioManager, streamType);
if (volume != newVolume || muted != newMuted) { boolean muted = AudioManagerCompat.isStreamMute(audioManager, streamType);
volume = newVolume; int minVolume = AudioManagerCompat.getStreamMinVolume(audioManager, streamType);
muted = newMuted; int maxVolume = AudioManagerCompat.getStreamMaxVolume(audioManager, streamType);
listener.onStreamVolumeChanged(newVolume, newMuted); return new StreamVolumeState(streamType, volume, muted, minVolume, maxVolume);
}
} }
private static int getVolumeFromManager(AudioManager audioManager, @C.StreamType int streamType) { private static final class StreamVolumeState {
// 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 boolean getMutedFromManager( public final @C.StreamType int streamType;
AudioManager audioManager, @C.StreamType int streamType) { public final int volume;
if (Util.SDK_INT >= 23) { public final boolean muted;
return audioManager.isStreamMute(streamType); public final int minVolume;
} else { public final int maxVolume;
return getVolumeFromManager(audioManager, streamType) == 0;
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 @Override
public void onReceive(Context context, Intent intent) { 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));
});
} }
} }
} }

View File

@ -14175,18 +14175,20 @@ public class ExoPlayerTest {
} }
@Test @Test
public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener() { public void releaseAfterVolumeChanges_triggerPendingDeviceVolumeEventsInListener()
throws Exception {
ExoPlayer player = ExoPlayer player =
parameterizeTestExoPlayerBuilder( parameterizeTestExoPlayerBuilder(
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setDeviceVolumeControlEnabled(true)) .setDeviceVolumeControlEnabled(true))
.build(); .build();
Player.Listener listener = mock(Player.Listener.class); Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener); run(player).untilPendingCommandsAreFullyHandled();
int deviceVolume = player.getDeviceVolume(); int deviceVolume = player.getDeviceVolume();
int noVolumeFlags = 0; int noVolumeFlags = 0;
int volumeFlags = C.VOLUME_FLAG_PLAY_SOUND | C.VOLUME_FLAG_VIBRATE; int volumeFlags = C.VOLUME_FLAG_PLAY_SOUND | C.VOLUME_FLAG_VIBRATE;
player.addListener(listener);
try { try {
player.setDeviceVolume(deviceVolume + 1, noVolumeFlags); // No-op if at max volume. player.setDeviceVolume(deviceVolume + 1, noVolumeFlags); // No-op if at max volume.
player.setDeviceVolume(deviceVolume - 1, noVolumeFlags); // No-op if at min volume. player.setDeviceVolume(deviceVolume - 1, noVolumeFlags); // No-op if at min volume.