diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 176d11da3e..f1734ecd46 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -63,6 +63,8 @@ * Update `CachedContentIndex` to use `SecureRandom` for generating the initialization vector used to encrypt the cache contents. * Remove deprecated members in `DefaultTrackSelector`. + * Add `Player.DeviceComponent` and implement it for `SimpleExoPlayer` so + that the device volume can be controlled by player. * Text: * Parse `` and `` tags in WebVTT subtitles (rendering is coming later). diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java new file mode 100644 index 0000000000..220f3313ec --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/StreamVolumeManagerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Handler; +import android.os.Looper; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SdkSuppress; +import com.google.android.exoplayer2.testutil.DummyMainThread; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link StreamVolumeManager}. */ +@RunWith(AndroidJUnit4.class) +public class StreamVolumeManagerTest { + + private static final long TIMEOUT_MS = 1_000; + + private AudioManager audioManager; + private TestListener testListener; + private DummyMainThread testThread; + private StreamVolumeManager streamVolumeManager; + + @Before + public void setUp() { + Context context = ApplicationProvider.getApplicationContext(); + + audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + testListener = new TestListener(); + + testThread = new DummyMainThread(); + testThread.runOnMainThread( + () -> + streamVolumeManager = + new StreamVolumeManager(context, new Handler(Looper.myLooper()), testListener)); + } + + @After + public void tearDown() { + testThread.runOnMainThread(() -> streamVolumeManager.release()); + testThread.release(); + } + + @Test + @SdkSuppress(minSdkVersion = 28) + public void getMinVolume_returnsStreamMinVolume() { + testThread.runOnMainThread( + () -> { + int streamMinVolume = audioManager.getStreamMinVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getMinVolume()).isEqualTo(streamMinVolume); + }); + } + + @Test + public void getMaxVolume_returnsStreamMaxVolume() { + testThread.runOnMainThread( + () -> { + int streamMaxVolume = audioManager.getStreamMaxVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getMaxVolume()).isEqualTo(streamMaxVolume); + }); + } + + @Test + public void getVolume_returnsStreamVolume() { + testThread.runOnMainThread( + () -> { + int streamVolume = audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT); + assertThat(streamVolumeManager.getVolume()).isEqualTo(streamVolume); + }); + } + + @Test + public void setVolume_changesStreamVolume() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int oldVolume = streamVolumeManager.getVolume(); + int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; + + streamVolumeManager.setVolume(targetVolume); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void setVolume_withOutOfRange_isIgnored() { + testThread.runOnMainThread( + () -> { + int maxVolume = streamVolumeManager.getMaxVolume(); + int minVolume = streamVolumeManager.getMinVolume(); + int oldVolume = streamVolumeManager.getVolume(); + + streamVolumeManager.setVolume(maxVolume + 1); + assertThat(streamVolumeManager.getVolume()).isEqualTo(oldVolume); + + streamVolumeManager.setVolume(minVolume - 1); + assertThat(streamVolumeManager.getVolume()).isEqualTo(oldVolume); + }); + } + + @Test + public void increaseVolume_increasesStreamVolumeByOne() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + streamVolumeManager.setVolume(minVolume); + int targetVolume = minVolume + 1; + + streamVolumeManager.increaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void increaseVolume_onMaxVolume_isIgnored() { + testThread.runOnMainThread( + () -> { + int maxVolume = streamVolumeManager.getMaxVolume(); + + streamVolumeManager.setVolume(maxVolume); + streamVolumeManager.increaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(maxVolume); + }); + } + + @Test + public void decreaseVolume_decreasesStreamVolumeByOne() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + streamVolumeManager.setVolume(maxVolume); + int targetVolume = maxVolume - 1; + + streamVolumeManager.decreaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(targetVolume); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolume); + assertThat(audioManager.getStreamVolume(C.STREAM_TYPE_DEFAULT)).isEqualTo(targetVolume); + }); + } + + @Test + public void decreaseVolume_onMinVolume_isIgnored() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + + streamVolumeManager.setVolume(minVolume); + streamVolumeManager.decreaseVolume(); + + assertThat(streamVolumeManager.getVolume()).isEqualTo(minVolume); + }); + } + + @Test + public void setStreamType_notifiesStreamTypeAndVolume() { + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int testStreamType = C.STREAM_TYPE_ALARM; + int testStreamVolume = audioManager.getStreamVolume(testStreamType); + + int oldVolume = streamVolumeManager.getVolume(); + if (oldVolume == testStreamVolume) { + int differentVolume = oldVolume == minVolume ? maxVolume : minVolume; + streamVolumeManager.setVolume(differentVolume); + } + + streamVolumeManager.setStreamType(testStreamType); + + assertThat(testListener.lastStreamType).isEqualTo(testStreamType); + assertThat(testListener.lastStreamVolume).isEqualTo(testStreamVolume); + assertThat(streamVolumeManager.getVolume()).isEqualTo(testStreamVolume); + }); + } + + @Test + public void onStreamVolumeChanged_isCalled_whenAudioManagerChangesIt() throws Exception { + AtomicInteger targetVolumeRef = new AtomicInteger(); + testThread.runOnMainThread( + () -> { + int minVolume = streamVolumeManager.getMinVolume(); + int maxVolume = streamVolumeManager.getMaxVolume(); + if (minVolume == maxVolume) { + return; + } + + int oldVolume = streamVolumeManager.getVolume(); + int targetVolume = oldVolume == maxVolume ? minVolume : maxVolume; + targetVolumeRef.set(targetVolume); + + audioManager.setStreamVolume(C.STREAM_TYPE_DEFAULT, targetVolume, /* flags= */ 0); + }); + + testListener.onStreamVolumeChangedLatch.await(TIMEOUT_MS, MILLISECONDS); + assertThat(testListener.lastStreamVolume).isEqualTo(targetVolumeRef.get()); + } + + private static class TestListener implements StreamVolumeManager.Listener { + + @C.StreamType private int lastStreamType; + private int lastStreamVolume; + public final CountDownLatch onStreamVolumeChangedLatch; + + public TestListener() { + onStreamVolumeChangedLatch = new CountDownLatch(1); + } + + @Override + public void onStreamTypeChanged(@C.StreamType int streamType) { + lastStreamType = streamType; + } + + @Override + public void onStreamVolumeChanged(int streamVolume) { + lastStreamVolume = streamVolume; + onStreamVolumeChangedLatch.countDown(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index b09cdace9d..5d98e59e94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -36,6 +36,8 @@ import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.device.DeviceInfo; +import com.google.android.exoplayer2.device.DeviceListener; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -73,7 +75,8 @@ public class SimpleExoPlayer extends BasePlayer Player.AudioComponent, Player.VideoComponent, Player.TextComponent, - Player.MetadataComponent { + Player.MetadataComponent, + Player.DeviceComponent { /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ @Deprecated @@ -330,6 +333,7 @@ public class SimpleExoPlayer extends BasePlayer private final CopyOnWriteArraySet audioListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet deviceListeners; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; private final BandwidthMeter bandwidthMeter; @@ -337,6 +341,7 @@ public class SimpleExoPlayer extends BasePlayer private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioFocusManager audioFocusManager; + private final StreamVolumeManager streamVolumeManager; private final WakeLockManager wakeLockManager; private final WifiLockManager wifiLockManager; @@ -364,6 +369,7 @@ public class SimpleExoPlayer extends BasePlayer @Nullable private PriorityTaskManager priorityTaskManager; private boolean isPriorityTaskManagerRegistered; private boolean playerReleased; + private DeviceInfo deviceInfo; /** @param builder The {@link Builder} to obtain all construction parameters. */ protected SimpleExoPlayer(Builder builder) { @@ -414,6 +420,7 @@ public class SimpleExoPlayer extends BasePlayer audioListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + deviceListeners = new CopyOnWriteArraySet<>(); videoDebugListeners = new CopyOnWriteArraySet<>(); audioDebugListeners = new CopyOnWriteArraySet<>(); eventHandler = new Handler(looper); @@ -456,8 +463,10 @@ public class SimpleExoPlayer extends BasePlayer audioBecomingNoisyManager = new AudioBecomingNoisyManager(context, eventHandler, componentListener); audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); + streamVolumeManager = new StreamVolumeManager(context, eventHandler, componentListener); wakeLockManager = new WakeLockManager(context); wifiLockManager = new WifiLockManager(context); + deviceInfo = createDeviceInfo(streamVolumeManager); } @Override @@ -487,8 +496,7 @@ public class SimpleExoPlayer extends BasePlayer @Override @Nullable public DeviceComponent getDeviceComponent() { - // TODO(b/145595776): Return this after implementing DeviceComponent. - return null; + return this; } /** @@ -684,6 +692,7 @@ public class SimpleExoPlayer extends BasePlayer .send(); } } + streamVolumeManager.setStreamType(Util.getStreamTypeForAudioUsage(audioAttributes.usage)); for (AudioListener audioListener : audioListeners) { audioListener.onAudioAttributesChanged(audioAttributes); } @@ -1565,6 +1574,7 @@ public class SimpleExoPlayer extends BasePlayer public void release() { verifyApplicationThread(); audioBecomingNoisyManager.setEnabled(false); + streamVolumeManager.release(); wakeLockManager.setStayAwake(false); wifiLockManager.setStayAwake(false); audioFocusManager.release(); @@ -1743,6 +1753,46 @@ public class SimpleExoPlayer extends BasePlayer } } + @Override + public void addDeviceListener(DeviceListener listener) { + deviceListeners.add(listener); + } + + @Override + public void removeDeviceListener(DeviceListener listener) { + deviceListeners.remove(listener); + } + + @Override + public DeviceInfo getDeviceInfo() { + verifyApplicationThread(); + return deviceInfo; + } + + @Override + public int getDeviceVolume() { + verifyApplicationThread(); + return streamVolumeManager.getVolume(); + } + + @Override + public void setDeviceVolume(int volume) { + verifyApplicationThread(); + streamVolumeManager.setVolume(volume); + } + + @Override + public void increaseDeviceVolume() { + verifyApplicationThread(); + streamVolumeManager.increaseVolume(); + } + + @Override + public void decreaseDeviceVolume() { + verifyApplicationThread(); + streamVolumeManager.decreaseVolume(); + } + // Internal methods. private void removeSurfaceCallbacks() { @@ -1898,6 +1948,13 @@ public class SimpleExoPlayer extends BasePlayer } } + private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) { + return new DeviceInfo( + DeviceInfo.PLAYBACK_TYPE_LOCAL, + streamVolumeManager.getMinVolume(), + streamVolumeManager.getMaxVolume()); + } + private static int getPlayWhenReadyChangeReason(boolean playWhenReady, int playerCommand) { return playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY ? PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS @@ -1913,6 +1970,7 @@ public class SimpleExoPlayer extends BasePlayer TextureView.SurfaceTextureListener, AudioFocusManager.PlayerControl, AudioBecomingNoisyManager.EventListener, + StreamVolumeManager.Listener, Player.EventListener { // VideoRendererEventListener implementation @@ -2145,6 +2203,26 @@ public class SimpleExoPlayer extends BasePlayer Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY); } + // StreamVolumeManager.Listener implementation. + + @Override + public void onStreamTypeChanged(@C.StreamType int streamType) { + DeviceInfo deviceInfo = createDeviceInfo(streamVolumeManager); + if (!deviceInfo.equals(SimpleExoPlayer.this.deviceInfo)) { + SimpleExoPlayer.this.deviceInfo = deviceInfo; + for (DeviceListener deviceListener : deviceListeners) { + deviceListener.onDeviceInfoChanged(deviceInfo); + } + } + } + + @Override + public void onStreamVolumeChanged(int streamVolume) { + for (DeviceListener deviceListener : deviceListeners) { + deviceListener.onDeviceVolumeChanged(streamVolume); + } + } + // Player.EventListener implementation. @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java new file mode 100644 index 0000000000..28f439d94f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/StreamVolumeManager.java @@ -0,0 +1,161 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2; + +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 com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** A manager that wraps {@link AudioManager} to control/listen audio stream volume. */ +/* package */ final class StreamVolumeManager { + + /** A listener for changes in the manager. */ + public interface Listener { + + /** Called when the audio stream type is changed. */ + void onStreamTypeChanged(@C.StreamType int streamType); + + /** Called when the audio stream volume is changed. */ + void onStreamVolumeChanged(int streamVolume); + } + + // TODO(b/151280453): Replace the hidden intent action with an official one. + // Copied from AudioManager#VOLUME_CHANGED_ACTION + private static final String VOLUME_CHANGED_ACTION = "android.media.VOLUME_CHANGED_ACTION"; + + // TODO(b/153317944): Allow users to override these flags. + private static final int VOLUME_FLAGS = AudioManager.FLAG_SHOW_UI; + + private final Context applicationContext; + private final Handler eventHandler; + private final Listener listener; + private final AudioManager audioManager; + private final VolumeChangeReceiver receiver; + + @C.StreamType private int streamType; + private int volume; + + /** Creates a manager. */ + public StreamVolumeManager(Context context, Handler eventHandler, Listener listener) { + applicationContext = context.getApplicationContext(); + this.eventHandler = eventHandler; + this.listener = listener; + audioManager = + Assertions.checkStateNotNull( + (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE)); + + streamType = C.STREAM_TYPE_DEFAULT; + volume = audioManager.getStreamVolume(streamType); + + receiver = new VolumeChangeReceiver(); + IntentFilter filter = new IntentFilter(VOLUME_CHANGED_ACTION); + applicationContext.registerReceiver(receiver, filter); + } + + /** Sets the audio stream type. */ + public void setStreamType(@C.StreamType int streamType) { + if (this.streamType == streamType) { + return; + } + this.streamType = streamType; + + updateVolumeAndNotifyIfChanged(); + listener.onStreamTypeChanged(streamType); + } + + /** + * Gets the minimum volume for the current audio stream. It can be changed if {@link + * #setStreamType(int)} is called. + */ + public int getMinVolume() { + return Util.SDK_INT >= 28 ? audioManager.getStreamMinVolume(streamType) : 0; + } + + /** + * Gets the maximum volume for the current audio stream. It can be changed if {@link + * #setStreamType(int)} is called. + */ + public int getMaxVolume() { + return audioManager.getStreamMaxVolume(streamType); + } + + /** Gets the current volume for the current audio stream. */ + public int getVolume() { + return volume; + } + + /** + * Sets the volume with the given value for the current audio stream. The value should be between + * {@link #getMinVolume()} and {@link #getMaxVolume()}, otherwise it will be ignored. + */ + public void setVolume(int volume) { + if (volume < getMinVolume() || volume > getMaxVolume()) { + return; + } + audioManager.setStreamVolume(streamType, volume, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** + * Increases the volume by one for the current audio stream. It will be ignored if the current + * volume is equal to {@link #getMaxVolume()}. + */ + public void increaseVolume() { + if (volume >= getMaxVolume()) { + return; + } + audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_RAISE, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** + * Decreases the volume by one for the current audio stream. It will be ignored if the current + * volume is equal to {@link #getMinVolume()}. + */ + public void decreaseVolume() { + if (volume <= getMinVolume()) { + return; + } + audioManager.adjustStreamVolume(streamType, AudioManager.ADJUST_LOWER, VOLUME_FLAGS); + updateVolumeAndNotifyIfChanged(); + } + + /** Releases the manager. It must be called when the manager is no longer required. */ + public void release() { + applicationContext.unregisterReceiver(receiver); + } + + private void updateVolumeAndNotifyIfChanged() { + int newVolume = audioManager.getStreamVolume(streamType); + if (volume != newVolume) { + volume = newVolume; + listener.onStreamVolumeChanged(newVolume); + } + } + + private final class VolumeChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + eventHandler.post(StreamVolumeManager.this::updateVolumeAndNotifyIfChanged); + } + } +}