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
|
* 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
|
||||||
|
@ -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() {}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user