Resume/Pause on playback suppression changes with timeout.

Auto-resume playback when the removal of playback suppression due to unsuitable output is conveyed via change in playback suppression to Player.PLAYBACK_SUPPRESSION_REASON_NONE within a configurable timeout defaulting to 5 minutes.

PiperOrigin-RevId: 544411987
This commit is contained in:
Googler 2023-06-29 18:22:13 +00:00 committed by Tianyi Feng
parent 832d5b5f98
commit 6732c0e286
3 changed files with 276 additions and 17 deletions

View File

@ -79,7 +79,9 @@
playback suppression due to playback suppression due to
`Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` by `Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT` by
launching a system dialog to allow a user to connect a suitable audio launching a system dialog to allow a user to connect a suitable audio
output (e.g. bluetooth headphones). output (e.g. bluetooth headphones). The listener will auto-resume
playback if a suitable device is connected within a configurable timeout
(default is 5 minutes).
* Downloads: * Downloads:
* OkHttp Extension: * OkHttp Extension:
* Cronet Extension: * Cronet Extension:

View File

@ -15,6 +15,9 @@
*/ */
package androidx.media3.ui; package androidx.media3.ui;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.util.concurrent.TimeUnit.MINUTES;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -23,9 +26,14 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.provider.Settings; import android.provider.Settings;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.C;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.Events; import androidx.media3.common.Player.Events;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.SystemClock;
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.util.List; import java.util.List;
@ -33,7 +41,9 @@ import java.util.List;
/** /**
* A {@link Player.Listener} that launches a system dialog in response to {@link * A {@link Player.Listener} that launches a system dialog in response to {@link
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} to allow the user to connect a * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} to allow the user to connect a
* suitable audio output. * suitable audio output. Also, it auto-resumes the playback when the playback suppression reason is
* changed from {@link Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} to {@link
* Player#PLAYBACK_SUPPRESSION_REASON_NONE}.
* *
* <p>This listener only reacts to {@link * <p>This listener only reacts to {@link
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} on Wear OS devices, while being no-op * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} on Wear OS devices, while being no-op
@ -84,15 +94,56 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
*/ */
private static final int FILTER_TYPE_AUDIO = 1; private static final int FILTER_TYPE_AUDIO = 1;
private Context applicationContext; /**
* The default timeout for auto-resume of suppressed playback when the playback suppression reason
* as {@link Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} is removed, in
* milliseconds.
*/
public static final long DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS =
MINUTES.toMillis(5);
private final Context applicationContext;
private final long autoResumeTimeoutAfterUnsuitableOutputSuppressionMs;
private final Clock clock;
private long unsuitableOutputPlaybackSuppressionStartRealtimeMs;
/** /**
* Creates a new {@link WearUnsuitableOutputPlaybackSuppressionResolverListener} instance. * Creates a new instance.
*
* <p>See {@link #WearUnsuitableOutputPlaybackSuppressionResolverListener(Context, long)} for more
* details. The auto-resume timeout defaults to {@link
* #DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS}.
* *
* @param context Any context. * @param context Any context.
*/ */
public WearUnsuitableOutputPlaybackSuppressionResolverListener(Context context) { public WearUnsuitableOutputPlaybackSuppressionResolverListener(Context context) {
this(context, DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS);
}
/**
* Creates a new instance.
*
* @param context Any context.
* @param autoResumeTimeoutMs Duration in milliseconds after the playback suppression during which
* playback will be resumed automatically if the playback suppression reason is changed from
* {@link Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} to {@link
* Player#PLAYBACK_SUPPRESSION_REASON_NONE}. Calling with {@code autoResumeTimeoutMs = 0} will
* cause playback to never resume automatically.
*/
public WearUnsuitableOutputPlaybackSuppressionResolverListener(
Context context, @IntRange(from = 0) long autoResumeTimeoutMs) {
this(context, autoResumeTimeoutMs, SystemClock.DEFAULT);
}
@VisibleForTesting
/* package */ WearUnsuitableOutputPlaybackSuppressionResolverListener(
Context context, @IntRange(from = 0) long autoResumeTimeoutMs, Clock clock) {
checkArgument(autoResumeTimeoutMs >= 0);
applicationContext = context.getApplicationContext(); applicationContext = context.getApplicationContext();
autoResumeTimeoutAfterUnsuitableOutputSuppressionMs = autoResumeTimeoutMs;
this.clock = clock;
unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET;
} }
@Override @Override
@ -100,12 +151,23 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
if (!Util.isWear(applicationContext)) { if (!Util.isWear(applicationContext)) {
return; return;
} }
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED) if ((events.contains(Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)
|| events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED))
&& player.getPlayWhenReady() && player.getPlayWhenReady()
&& player.getPlaybackSuppressionReason() && player.getPlaybackSuppressionReason()
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) { == Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
player.pause(); player.pause();
launchSystemMediaOutputSwitcherUi(applicationContext); if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
launchSystemMediaOutputSwitcherUi(applicationContext);
unsuitableOutputPlaybackSuppressionStartRealtimeMs = clock.elapsedRealtime();
}
} else if (events.contains(Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)
&& player.getPlaybackSuppressionReason() == Player.PLAYBACK_SUPPRESSION_REASON_NONE
&& unsuitableOutputPlaybackSuppressionStartRealtimeMs != C.TIME_UNSET
&& (clock.elapsedRealtime() - unsuitableOutputPlaybackSuppressionStartRealtimeMs
< autoResumeTimeoutAfterUnsuitableOutputSuppressionMs)) {
unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET;
player.play();
} }
} }

View File

@ -15,9 +15,11 @@
*/ */
package androidx.media3.ui; package androidx.media3.ui;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlayWhenReady;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static androidx.test.ext.truth.content.IntentSubject.assertThat; import static androidx.test.ext.truth.content.IntentSubject.assertThat;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.stream;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import android.app.Application; import android.app.Application;
@ -34,11 +36,14 @@ import android.provider.Settings;
import androidx.media3.common.FlagSet; import androidx.media3.common.FlagSet;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.Listener; import androidx.media3.common.Player.PlayWhenReadyChangeReason;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.media3.test.utils.TestExoPlayerBuilder;
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;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@ -63,6 +68,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
private static final String FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME = "com.fake.btsettings"; private static final String FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME = "com.fake.btsettings";
private static final String FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME = private static final String FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME =
"com.fake.btsettings.BluetoothSettingsActivity"; "com.fake.btsettings.BluetoothSettingsActivity";
private static final long TEST_TIME_OUT_MS = Duration.ofMinutes(10).toMillis();
private ShadowPackageManager shadowPackageManager; private ShadowPackageManager shadowPackageManager;
private ShadowApplication shadowApplication; private ShadowApplication shadowApplication;
@ -74,9 +80,6 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setSuppressPlaybackOnUnsuitableOutput(true) .setSuppressPlaybackOnUnsuitableOutput(true)
.build(); .build();
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext()); shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext());
shadowPackageManager = shadowPackageManager =
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
@ -88,13 +91,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
} }
/** /**
* Test for the launch of system Output Switcher app when playback is suppressed due to unsuitable * Test end-to-end flow from launch of output switcher to playback getting resumed when the
* output and the system Output Switcher is present on the device. * playback is suppressed and then unsuppressed.
*/ */
@Test @Test
public void public void playbackSuppressionFollowedByResolution_shouldLaunchOSwAndChangePlayerStateToPlaying()
playEventWithPlaybackSuppressionWhenSystemOutputSwitcherPresent_shouldLaunchOutputSwitcher() throws TimeoutException {
throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
@ -102,12 +104,27 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
List<Boolean> playWhenReadyChangeSequence = new ArrayList<>();
testPlayer.addListener(
new Player.Listener() {
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @PlayWhenReadyChangeReason int reason) {
playWhenReadyChangeSequence.add(playWhenReady);
}
});
testPlayer.play(); testPlayer.play();
runUntilPlaybackState(testPlayer, Player.STATE_READY); runUntilPlaybackState(testPlayer, Player.STATE_READY);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ true);
Intent intentTriggered = shadowApplication.getNextStartedActivity(); Intent intentTriggered = shadowApplication.getNextStartedActivity();
assertThat(intentTriggered).isNotNull(); assertThat(intentTriggered).isNotNull();
@ -115,6 +132,8 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
assertThat(intentTriggered) assertThat(intentTriggered)
.hasComponent( .hasComponent(
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME); FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME);
assertThat(playWhenReadyChangeSequence).containsExactly(true, false, true);
assertThat(testPlayer.isPlaying()).isTrue();
} }
/** /**
@ -131,6 +150,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME,
ApplicationInfo.FLAG_UPDATED_SYSTEM_APP); ApplicationInfo.FLAG_UPDATED_SYSTEM_APP);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -166,6 +188,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
"com.fake.userinstalled.outputswitcher.OutputSwitcherActivity", "com.fake.userinstalled.outputswitcher.OutputSwitcherActivity",
/* applicationFlags= */ 0); /* applicationFlags= */ 0);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -196,6 +221,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -236,6 +264,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME, FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -265,6 +296,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME, FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -298,6 +332,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME, FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -333,6 +370,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
"com.fake.userinstalled.btsettings.BluetoothSettingsActivity", "com.fake.userinstalled.btsettings.BluetoothSettingsActivity",
/* applicationFlags= */ 0); /* applicationFlags= */ 0);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -364,6 +404,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
@ -374,6 +417,43 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
assertThat(shadowApplication.getNextStartedActivity()).isNull(); assertThat(shadowApplication.getNextStartedActivity()).isNull();
} }
/**
* Test for no launch of any system media output switching dialog app when playback is suppressed
* due to removal of all suitable audio outputs in mid of an ongoing playback.
*/
@Test
public void
playbackSuppressionDuringOngoingPlayback_shouldOnlyPauseButNotLaunchEitherOSwOrBTSettings()
throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM);
registerFakeActivity(
Settings.ACTION_BLUETOOTH_SETTINGS,
FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME,
FAKE_SYSTEM_BT_SETTINGS_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare();
testPlayer.play();
runUntilPlaybackState(testPlayer, Player.STATE_READY);
removeConnectedAudioOutput(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false);
assertThat(shadowApplication.getNextStartedActivity()).isNull();
assertThat(testPlayer.isPlaying()).isFalse();
}
/** Test for pause on the Player when the playback is suppressed due to unsuitable output. */ /** Test for pause on the Player when the playback is suppressed due to unsuitable output. */
@Test @Test
public void playEventWithSuppressedPlaybackCondition_shouldCallPauseOnPlayer() public void playEventWithSuppressedPlaybackCondition_shouldCallPauseOnPlayer()
@ -385,12 +465,15 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME,
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
testPlayer.addListener( testPlayer.addListener(
new Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
if (!playWhenReady) { if (!playWhenReady) {
@ -419,12 +502,15 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
ApplicationInfo.FLAG_SYSTEM); ApplicationInfo.FLAG_SYSTEM);
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext()));
testPlayer.setMediaItem( testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare(); testPlayer.prepare();
AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); AtomicBoolean isPlaybackPaused = new AtomicBoolean(false);
testPlayer.addListener( testPlayer.addListener(
new Listener() { new Player.Listener() {
@Override @Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
if (!playWhenReady) { if (!playWhenReady) {
@ -439,6 +525,95 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
assertThat(isPlaybackPaused.get()).isFalse(); assertThat(isPlaybackPaused.get()).isFalse();
} }
/**
* Test to ensure player is not playing when the playback suppression due to unsuitable output is
* removed after the default timeout.
*/
@Test
public void
playbackSuppressionChangeToNoneAfterDefaultTimeout_shouldNotChangePlaybackStateToPlaying()
throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext(),
WearUnsuitableOutputPlaybackSuppressionResolverListener
.DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS,
fakeClock));
testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare();
testPlayer.play();
runUntilPlaybackState(testPlayer, Player.STATE_READY);
fakeClock.advanceTime(
WearUnsuitableOutputPlaybackSuppressionResolverListener
.DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS
* 2);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false);
assertThat(testPlayer.isPlaying()).isFalse();
}
/**
* Test to ensure player is playing when the playback suppression due to unsuitable output is
* removed within the set timeout.
*/
@Test
public void playbackSuppressionChangeToNoneWithinSetTimeout_shouldChangePlaybackStateToPlaying()
throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext(), TEST_TIME_OUT_MS, fakeClock));
testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare();
testPlayer.play();
runUntilPlaybackState(testPlayer, Player.STATE_READY);
fakeClock.advanceTime(TEST_TIME_OUT_MS / 2);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ true);
assertThat(testPlayer.isPlaying()).isTrue();
}
/**
* Test to ensure player is not playing when the playback suppression due to unsuitable output is
* removed after the set timeout.
*/
@Test
public void
playbackSuppressionChangeToNoneAfterSetTimeout_shouldNotChangeFinalPlaybackStateToPlaying()
throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener(
new WearUnsuitableOutputPlaybackSuppressionResolverListener(
ApplicationProvider.getApplicationContext(), TEST_TIME_OUT_MS, fakeClock));
testPlayer.setMediaItem(
MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4"));
testPlayer.prepare();
testPlayer.play();
runUntilPlaybackState(testPlayer, Player.STATE_READY);
fakeClock.advanceTime(TEST_TIME_OUT_MS * 2);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false);
assertThat(testPlayer.isPlaying()).isFalse();
}
private void registerFakeActivity( private void registerFakeActivity(
String fakeActionName, String fakePackageName, String fakeClassName, int applicationFlags) { String fakeActionName, String fakePackageName, String fakeClassName, int applicationFlags) {
ComponentName fakeComponentName = new ComponentName(fakePackageName, fakeClassName); ComponentName fakeComponentName = new ComponentName(fakePackageName, fakeClassName);
@ -465,4 +640,24 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
} }
shadowAudioManager.setOutputDevices(deviceListBuilder.build()); shadowAudioManager.setOutputDevices(deviceListBuilder.build());
} }
private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) {
ShadowAudioManager shadowAudioManager =
shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
shadowAudioManager.addOutputDevice(
AudioDeviceInfoBuilder.newBuilder().setType(deviceTypes).build(),
notifyAudioDeviceCallbacks);
}
private void removeConnectedAudioOutput(int deviceType) {
ShadowAudioManager shadowAudioManager =
shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
stream(shadowAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
.filter(audioDeviceInfo -> deviceType == audioDeviceInfo.getType())
.findFirst()
.ifPresent(
filteredAudioDeviceInfo ->
shadowAudioManager.removeOutputDevice(
filteredAudioDeviceInfo, /* notifyAudioDeviceCallbacks= */ true));
}
} }