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:
parent
832d5b5f98
commit
6732c0e286
@ -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:
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user