mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
c797249998
commit
190563b8eb
@ -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
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<StreamVolumeState> 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user