Move unsuitable output path logic <API31 into SuitableOutputChecker

This avoids distributing the logic between multiple classes and
keeps ExoPlayerImpl simpler.

PiperOrigin-RevId: 726874038
(cherry picked from commit 1015ef8b565ed04e88a9c596798d294327d05536)
This commit is contained in:
tonihei 2025-02-14 04:34:51 -08:00
parent 841e27ae5c
commit 41af00f100
8 changed files with 301 additions and 220 deletions

View File

@ -15,9 +15,14 @@
*/ */
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaRoute2Info; import android.media.MediaRoute2Info;
import android.media.MediaRouter2; import android.media.MediaRouter2;
import android.media.MediaRouter2.ControllerCallback; import android.media.MediaRouter2.ControllerCallback;
@ -31,72 +36,120 @@ import androidx.annotation.RequiresApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Default implementation for {@link SuitableOutputChecker}. */ /** Default implementation for {@link SuitableOutputChecker}. */
@RequiresApi(35)
/* package */ final class DefaultSuitableOutputChecker implements SuitableOutputChecker { /* package */ final class DefaultSuitableOutputChecker implements SuitableOutputChecker {
@Nullable private final SuitableOutputChecker impl;
/**
* Creates the default {@link SuitableOutputChecker}.
*
* @param context A {@link Context}.
* @param eventHandler A {@link Handler} to trigger {@link Callback} methods on.
*/
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
if (Util.SDK_INT >= 35) {
impl = new ImplApi35(context, eventHandler);
} else if (Util.SDK_INT >= 23) {
impl = new ImplApi23(context, eventHandler);
} else {
impl = null;
}
}
@Override
public void enable(Callback callback) {
if (impl != null) {
impl.enable(callback);
}
}
@Override
public void disable() {
if (impl != null) {
impl.disable();
}
}
@Override
public boolean isSelectedOutputSuitableForPlayback() {
return impl == null || impl.isSelectedOutputSuitableForPlayback();
}
@RequiresApi(35)
private static final class ImplApi35 implements SuitableOutputChecker {
private static final RouteDiscoveryPreference EMPTY_DISCOVERY_PREFERENCE = private static final RouteDiscoveryPreference EMPTY_DISCOVERY_PREFERENCE =
new RouteDiscoveryPreference.Builder( new RouteDiscoveryPreference.Builder(
/* preferredFeatures= */ ImmutableList.of(), /* activeScan= */ false) /* preferredFeatures= */ ImmutableList.of(), /* activeScan= */ false)
.build(); .build();
private final MediaRouter2 router; private final Context applicationContext;
private final RouteCallback routeCallback; private final Handler eventHandler;
private final Executor executor;
private @MonotonicNonNull MediaRouter2 router;
private @MonotonicNonNull RouteCallback routeCallback;
@Nullable private ControllerCallback controllerCallback; @Nullable private ControllerCallback controllerCallback;
private boolean isPreviousSelectedOutputSuitableForPlayback; private boolean isSelectedOutputSuitableForPlayback;
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) { public ImplApi35(Context context, Handler eventHandler) {
router = MediaRouter2.getInstance(context); this.applicationContext = context.getApplicationContext();
this.eventHandler = eventHandler;
}
@SuppressLint("ThreadSafe") // Handler is thread-safe, but not annotated.
@Override
public void enable(Callback callback) {
router = MediaRouter2.getInstance(applicationContext);
routeCallback = new RouteCallback() {}; routeCallback = new RouteCallback() {};
executor = Executor executor =
new Executor() { new Executor() {
@Override @Override
public void execute(Runnable command) { public void execute(Runnable command) {
Util.postOrRun(eventHandler, command); Util.postOrRun(eventHandler, command);
} }
}; };
}
@Override
public void enable(Callback callback) {
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE); router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
controllerCallback = controllerCallback =
new ControllerCallback() { new ControllerCallback() {
@Override @Override
public void onControllerUpdated(RoutingController controller) { public void onControllerUpdated(RoutingController controller) {
boolean isCurrentSelectedOutputSuitableForPlayback = boolean isCurrentSelectedOutputSuitableForPlayback =
isSelectedOutputSuitableForPlayback(); isSelectedOutputSuitableForPlayback(router);
if (isPreviousSelectedOutputSuitableForPlayback if (isSelectedOutputSuitableForPlayback
!= isCurrentSelectedOutputSuitableForPlayback) { != isCurrentSelectedOutputSuitableForPlayback) {
isPreviousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback = isCurrentSelectedOutputSuitableForPlayback;
isCurrentSelectedOutputSuitableForPlayback;
callback.onSelectedOutputSuitabilityChanged( callback.onSelectedOutputSuitabilityChanged(
isCurrentSelectedOutputSuitableForPlayback); isCurrentSelectedOutputSuitableForPlayback);
} }
} }
}; };
router.registerControllerCallback(executor, controllerCallback); router.registerControllerCallback(executor, controllerCallback);
isPreviousSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback(); isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback(router);
} }
@Override @Override
public void disable() { public void disable() {
checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled"); checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled");
router.unregisterControllerCallback(controllerCallback); checkNotNull(router).unregisterControllerCallback(controllerCallback);
controllerCallback = null; controllerCallback = null;
router.unregisterRouteCallback(routeCallback); router.unregisterRouteCallback(checkNotNull(routeCallback));
} }
@Override @Override
public boolean isSelectedOutputSuitableForPlayback() { public boolean isSelectedOutputSuitableForPlayback() {
checkStateNotNull(controllerCallback, "SuitableOutputChecker is not enabled"); return isSelectedOutputSuitableForPlayback;
int transferReason = router.getSystemController().getRoutingSessionInfo().getTransferReason(); }
boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf();
private static boolean isSelectedOutputSuitableForPlayback(MediaRouter2 router) {
int transferReason =
checkNotNull(router).getSystemController().getRoutingSessionInfo().getTransferReason();
boolean wasTransferInitiatedBySelf =
router.getSystemController().wasTransferInitiatedBySelf();
for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) { for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) {
if (isRouteSuitableForMediaPlayback(routeInfo, transferReason, wasTransferInitiatedBySelf)) { if (isRouteSuitableForMediaPlayback(
routeInfo, transferReason, wasTransferInitiatedBySelf)) {
return true; return true;
} }
} }
@ -115,4 +168,99 @@ import java.util.concurrent.Executor;
return suitabilityStatus == MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER; return suitabilityStatus == MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER;
} }
}
@RequiresApi(23)
private static final class ImplApi23 implements SuitableOutputChecker {
private final Context applicationContext;
private final Handler eventHandler;
@Nullable private AudioManager audioManager;
private @MonotonicNonNull AudioDeviceCallback audioDeviceCallback;
private boolean isSelectedOutputSuitableForPlayback;
public ImplApi23(Context context, Handler eventHandler) {
this.applicationContext = context.getApplicationContext();
this.eventHandler = eventHandler;
}
@Override
public void enable(Callback callback) {
AudioManager audioManager =
(AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
isSelectedOutputSuitableForPlayback = true;
return;
}
this.audioManager = audioManager;
audioDeviceCallback =
new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
updateIsSelectedOutputSuitableForPlayback(callback);
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
updateIsSelectedOutputSuitableForPlayback(callback);
}
};
audioManager.registerAudioDeviceCallback(audioDeviceCallback, eventHandler);
isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput();
}
@Override
public void disable() {
if (audioManager != null) {
audioManager.unregisterAudioDeviceCallback(checkNotNull(audioDeviceCallback));
}
}
@Override
public boolean isSelectedOutputSuitableForPlayback() {
return isSelectedOutputSuitableForPlayback;
}
private void updateIsSelectedOutputSuitableForPlayback(Callback callback) {
boolean isSelectedOutputSuitableForPlayback = hasSupportedAudioOutput();
if (this.isSelectedOutputSuitableForPlayback != isSelectedOutputSuitableForPlayback) {
this.isSelectedOutputSuitableForPlayback = isSelectedOutputSuitableForPlayback;
callback.onSelectedOutputSuitabilityChanged(isSelectedOutputSuitableForPlayback);
}
}
private boolean hasSupportedAudioOutput() {
if (!Util.isWear(applicationContext)) {
return true;
}
AudioDeviceInfo[] audioDeviceInfos =
checkStateNotNull(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo device : audioDeviceInfos) {
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
|| device.getType() == AudioDeviceInfo.TYPE_LINE_ANALOG
|| device.getType() == AudioDeviceInfo.TYPE_LINE_DIGITAL
|| device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
return true;
}
if (Util.SDK_INT >= 26 && device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) {
return true;
}
if (Util.SDK_INT >= 28 && device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
return true;
}
if (Util.SDK_INT >= 31
&& (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET
|| device.getType() == AudioDeviceInfo.TYPE_BLE_SPEAKER)) {
return true;
}
if (Util.SDK_INT >= 33 && device.getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
return true;
}
}
return false;
}
}
} }

View File

@ -252,7 +252,7 @@ public interface ExoPlayer extends Player {
/* package */ boolean suppressPlaybackOnUnsuitableOutput; /* package */ boolean suppressPlaybackOnUnsuitableOutput;
/* package */ String playerName; /* package */ String playerName;
/* package */ boolean dynamicSchedulingEnabled; /* package */ boolean dynamicSchedulingEnabled;
@Nullable /* package */ SuitableOutputChecker suitableOutputChecker; /* package */ SuitableOutputChecker suitableOutputChecker;
/** /**
* Creates a builder. * Creates a builder.
@ -282,6 +282,7 @@ public interface ExoPlayer extends Player {
* <li>{@link AudioAttributes}: {@link AudioAttributes#DEFAULT}, not handling audio focus * <li>{@link AudioAttributes}: {@link AudioAttributes#DEFAULT}, not handling audio focus
* <li>{@link C.WakeMode}: {@link C#WAKE_MODE_NONE} * <li>{@link C.WakeMode}: {@link C#WAKE_MODE_NONE}
* <li>{@code handleAudioBecomingNoisy}: {@code false} * <li>{@code handleAudioBecomingNoisy}: {@code false}
* <li>{@code suppressPlaybackOnUnsuitableOutput}: {@code false}
* <li>{@code skipSilenceEnabled}: {@code false} * <li>{@code skipSilenceEnabled}: {@code false}
* <li>{@link C.VideoScalingMode}: {@link C#VIDEO_SCALING_MODE_DEFAULT} * <li>{@link C.VideoScalingMode}: {@link C#VIDEO_SCALING_MODE_DEFAULT}
* <li>{@link C.VideoChangeFrameRateStrategy}: {@link * <li>{@link C.VideoChangeFrameRateStrategy}: {@link
@ -459,6 +460,7 @@ public interface ExoPlayer extends Player {
usePlatformDiagnostics = true; usePlatformDiagnostics = true;
playerName = ""; playerName = "";
priority = C.PRIORITY_PLAYBACK; priority = C.PRIORITY_PLAYBACK;
suitableOutputChecker = new DefaultSuitableOutputChecker(context, new Handler(looper));
} }
/** /**
@ -1021,7 +1023,6 @@ public interface ExoPlayer extends Player {
@UnstableApi @UnstableApi
@RestrictTo(LIBRARY_GROUP) @RestrictTo(LIBRARY_GROUP)
@VisibleForTesting @VisibleForTesting
@RequiresApi(35)
public Builder setSuitableOutputChecker(SuitableOutputChecker suitableOutputChecker) { public Builder setSuitableOutputChecker(SuitableOutputChecker suitableOutputChecker) {
checkState(!buildCalled); checkState(!buildCalled);
this.suitableOutputChecker = suitableOutputChecker; this.suitableOutputChecker = suitableOutputChecker;
@ -1088,11 +1089,6 @@ public interface ExoPlayer extends Player {
public ExoPlayer build() { public ExoPlayer build() {
checkState(!buildCalled); checkState(!buildCalled);
buildCalled = true; buildCalled = true;
if (suitableOutputChecker == null
&& Util.SDK_INT >= 35
&& suppressPlaybackOnUnsuitableOutput) {
suitableOutputChecker = new DefaultSuitableOutputChecker(context, new Handler(looper));
}
return new ExoPlayerImpl(/* builder= */ this, /* wrappingPlayer= */ null); return new ExoPlayerImpl(/* builder= */ this, /* wrappingPlayer= */ null);
} }

View File

@ -45,9 +45,7 @@ import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo; import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -176,8 +174,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final WakeLockManager wakeLockManager; private final WakeLockManager wakeLockManager;
private final WifiLockManager wifiLockManager; private final WifiLockManager wifiLockManager;
private final long detachSurfaceTimeoutMs; private final long detachSurfaceTimeoutMs;
@Nullable private AudioManager audioManager;
private final boolean suppressPlaybackOnUnsuitableOutput;
@Nullable private final SuitableOutputChecker suitableOutputChecker; @Nullable private final SuitableOutputChecker suitableOutputChecker;
private final BackgroundThreadStateHandler<Integer> audioSessionIdState; private final BackgroundThreadStateHandler<Integer> audioSessionIdState;
@ -293,7 +289,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.applicationLooper = builder.looper; this.applicationLooper = builder.looper;
this.clock = builder.clock; this.clock = builder.clock;
this.wrappingPlayer = wrappingPlayer == null ? this : wrappingPlayer; this.wrappingPlayer = wrappingPlayer == null ? this : wrappingPlayer;
this.suppressPlaybackOnUnsuitableOutput = builder.suppressPlaybackOnUnsuitableOutput;
listeners = listeners =
new ListenerSet<>( new ListenerSet<>(
applicationLooper, applicationLooper,
@ -416,15 +411,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener); audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null); audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
if (builder.suppressPlaybackOnUnsuitableOutput) {
suitableOutputChecker = builder.suitableOutputChecker; suitableOutputChecker = builder.suitableOutputChecker;
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged); suitableOutputChecker.enable(this::onSelectedOutputSuitabilityChanged);
} else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) { } else {
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE); suitableOutputChecker = null;
Api23.registerAudioDeviceCallback(
audioManager,
new NoSuitableOutputPlaybackSuppressionAudioDeviceCallback(),
new Handler(applicationLooper));
} }
if (builder.deviceVolumeControlEnabled) { if (builder.deviceVolumeControlEnabled) {
@ -1037,6 +1028,9 @@ import java.util.concurrent.CopyOnWriteArraySet;
wakeLockManager.setStayAwake(false); wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false); wifiLockManager.setStayAwake(false);
audioFocusManager.release(); audioFocusManager.release();
if (suitableOutputChecker != null) {
suitableOutputChecker.disable();
}
if (!internalPlayer.release()) { if (!internalPlayer.release()) {
// One of the renderers timed out releasing its resources. // One of the renderers timed out releasing its resources.
listeners.sendEvent( listeners.sendEvent(
@ -1053,9 +1047,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (playbackInfo.sleepingForOffload) { if (playbackInfo.sleepingForOffload) {
playbackInfo = playbackInfo.copyWithEstimatedPosition(); playbackInfo = playbackInfo.copyWithEstimatedPosition();
} }
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.disable();
}
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE); playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId); playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs; playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
@ -2793,8 +2784,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (playerCommand == AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK) { if (playerCommand == AudioFocusManager.PLAYER_COMMAND_WAIT_FOR_CALLBACK) {
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS; return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
} }
if (suppressPlaybackOnUnsuitableOutput) { if (suitableOutputChecker != null) {
if (playWhenReady && !hasSupportedAudioOutput()) { if (playWhenReady && !suitableOutputChecker.isSelectedOutputSuitableForPlayback()) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT; return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
} }
if (!playWhenReady if (!playWhenReady
@ -2806,19 +2797,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
return Player.PLAYBACK_SUPPRESSION_REASON_NONE; return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
} }
private boolean hasSupportedAudioOutput() {
if (Util.SDK_INT >= 35 && suitableOutputChecker != null) {
return suitableOutputChecker.isSelectedOutputSuitableForPlayback();
} else if (Util.SDK_INT >= 23 && audioManager != null) {
return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
} else {
// The Audio Manager API to determine the list of connected audio devices is available only in
// API >= 23.
return true;
}
}
private void updateWakeAndWifiLock() { private void updateWakeAndWifiLock() {
@State int playbackState = getPlaybackState(); @State int playbackState = getPlaybackState();
switch (playbackState) { switch (playbackState) {
@ -2927,6 +2905,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
private void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback) { private void onSelectedOutputSuitabilityChanged(boolean isSelectedOutputSuitableForPlayback) {
if (playerReleased) {
// Stale event.
return;
}
if (isSelectedOutputSuitableForPlayback) { if (isSelectedOutputSuitableForPlayback) {
if (playbackInfo.playbackSuppressionReason if (playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) { == Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
@ -3397,78 +3379,4 @@ import java.util.concurrent.CopyOnWriteArraySet;
}); });
} }
} }
@RequiresApi(23)
private static final class Api23 {
private Api23() {}
public static boolean isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
Context context, AudioDeviceInfo[] audioDeviceInfos) {
if (!Util.isWear(context)) {
return true;
}
for (AudioDeviceInfo device : audioDeviceInfos) {
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
|| device.getType() == AudioDeviceInfo.TYPE_LINE_ANALOG
|| device.getType() == AudioDeviceInfo.TYPE_LINE_DIGITAL
|| device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
return true;
}
if (Util.SDK_INT >= 26 && device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) {
return true;
}
if (Util.SDK_INT >= 28 && device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
return true;
}
if (Util.SDK_INT >= 31
&& (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET
|| device.getType() == AudioDeviceInfo.TYPE_BLE_SPEAKER)) {
return true;
}
if (Util.SDK_INT >= 33 && device.getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
return true;
}
}
return false;
}
public static void registerAudioDeviceCallback(
AudioManager audioManager, AudioDeviceCallback audioDeviceCallback, Handler handler) {
audioManager.registerAudioDeviceCallback(audioDeviceCallback, handler);
}
}
/**
* A {@link AudioDeviceCallback} to change playback suppression reason when suitable audio outputs
* are either added in unsuitable output based playback suppression state or removed during an
* ongoing playback.
*/
@RequiresApi(23)
private final class NoSuitableOutputPlaybackSuppressionAudioDeviceCallback
extends AudioDeviceCallback {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
if (hasSupportedAudioOutput()
&& playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
if (!hasSupportedAudioOutput()) {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
}
}
}
} }

View File

@ -17,12 +17,10 @@ package androidx.media3.exoplayer;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
/** Provides methods to check the suitability of selected media outputs. */ /** Provides methods to check the suitability of selected media outputs. */
@RequiresApi(35)
@RestrictTo(LIBRARY_GROUP) @RestrictTo(LIBRARY_GROUP)
@UnstableApi @UnstableApi
public interface SuitableOutputChecker { public interface SuitableOutputChecker {

View File

@ -94,6 +94,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.annotation.Config.ALL_SDKS;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -14022,7 +14023,7 @@ public class ExoPlayerTest {
} }
@Test @Test
@Config(sdk = Config.ALL_SDKS) @Config(sdk = ALL_SDKS)
public void builder_inBackgroundThreadWithAllowedAnyThreadMethods_doesNotThrow() public void builder_inBackgroundThreadWithAllowedAnyThreadMethods_doesNotThrow()
throws Exception { throws Exception {
AtomicReference<Player> playerReference = new AtomicReference<>(); AtomicReference<Player> playerReference = new AtomicReference<>();
@ -15023,6 +15024,8 @@ public class ExoPlayerTest {
* Tests playback suppression for playback with only unsuitable outputs (e.g. builtin speaker) on * Tests playback suppression for playback with only unsuitable outputs (e.g. builtin speaker) on
* the Wear OS. * the Wear OS.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void play_withOnlyUnsuitableOutputsOnWear_shouldSuppressPlayback() throws Exception { public void play_withOnlyUnsuitableOutputsOnWear_shouldSuppressPlayback() throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
@ -15059,6 +15062,8 @@ public class ExoPlayerTest {
* Tests no playback suppression for playback with suitable output (e.g. BluetoothA2DP) on the * Tests no playback suppression for playback with suitable output (e.g. BluetoothA2DP) on the
* Wear OS. * Wear OS.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void play_withAtleastOneSuitableOutputOnWear_shouldNotSuppressPlayback() throws Exception { public void play_withAtleastOneSuitableOutputOnWear_shouldNotSuppressPlayback() throws Exception {
addWatchAsSystemFeature(); addWatchAsSystemFeature();
@ -15095,6 +15100,8 @@ public class ExoPlayerTest {
* Tests same playback suppression reason for multiple play calls with only unsuitable output * Tests same playback suppression reason for multiple play calls with only unsuitable output
* (e.g. builtin speaker) on the Wear OS. * (e.g. builtin speaker) on the Wear OS.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
play_callMultipleTimesOnUnsuitableOutputFollowedByPause_shouldRetainSameSuppressionReason() play_callMultipleTimesOnUnsuitableOutputFollowedByPause_shouldRetainSameSuppressionReason()
@ -15134,6 +15141,8 @@ public class ExoPlayerTest {
} }
/** Tests playback suppression for playback on the built-speaker on non-Wear OS surfaces. */ /** Tests playback suppression for playback on the built-speaker on non-Wear OS surfaces. */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void play_onBuiltinSpeakerWithoutWearPresentAsSystemFeature_shouldNotSuppressPlayback() public void play_onBuiltinSpeakerWithoutWearPresentAsSystemFeature_shouldNotSuppressPlayback()
throws Exception { throws Exception {
@ -15171,6 +15180,8 @@ public class ExoPlayerTest {
* speaker) on Wear OS but {@link * speaker) on Wear OS but {@link
* ExoPlayer.Builder#setSuppressPlaybackOnUnsuitableOutput(boolean)} is not called with true. * ExoPlayer.Builder#setSuppressPlaybackOnUnsuitableOutput(boolean)} is not called with true.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
play_withOnlyUnsuitableOutputsWithoutEnablingPlaybackSuppression_shouldNotSuppressPlayback() play_withOnlyUnsuitableOutputsWithoutEnablingPlaybackSuppression_shouldNotSuppressPlayback()
@ -15206,6 +15217,8 @@ public class ExoPlayerTest {
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when a suitable audio output is * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when a suitable audio output is
* added. * added.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void addSuitableOutputWhenPlaybackSuppressed_shouldRemovePlaybackSuppression() public void addSuitableOutputWhenPlaybackSuppressed_shouldRemovePlaybackSuppression()
throws Exception { throws Exception {
@ -15246,6 +15259,8 @@ public class ExoPlayerTest {
* Tests no change in the playback suppression reason when an unsuitable audio output is connected * Tests no change in the playback suppression reason when an unsuitable audio output is connected
* while playback was suppressed earlier. * while playback was suppressed earlier.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void addUnsuitableOutputWhenPlaybackIsSuppressed_shouldNotRemovePlaybackSuppression() public void addUnsuitableOutputWhenPlaybackIsSuppressed_shouldNotRemovePlaybackSuppression()
throws Exception { throws Exception {
@ -15277,6 +15292,8 @@ public class ExoPlayerTest {
* Tests no change in the playback suppression reason when a suitable audio output is added but * Tests no change in the playback suppression reason when a suitable audio output is added but
* playback was not suppressed earlier. * playback was not suppressed earlier.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void addSuitableOutputWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression() public void addSuitableOutputWhenPlaybackNotSuppressed_shouldNotRemovePlaybackSuppression()
throws Exception { throws Exception {
@ -15308,6 +15325,8 @@ public class ExoPlayerTest {
* Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when all the suitable audio outputs * Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} when all the suitable audio outputs
* have been removed during an ongoing playback. * have been removed during an ongoing playback.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void removeAllSuitableOutputsWhenPlaybackOngoing_shouldSetPlaybackSuppression() public void removeAllSuitableOutputsWhenPlaybackOngoing_shouldSetPlaybackSuppression()
throws Exception { throws Exception {
@ -15340,6 +15359,8 @@ public class ExoPlayerTest {
* Tests no change in the playback suppression reason when any unsuitable audio outputs has been * Tests no change in the playback suppression reason when any unsuitable audio outputs has been
* removed during an ongoing playback but some suitable audio outputs are still available. * removed during an ongoing playback but some suitable audio outputs are still available.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void removeAnyUnsuitableOutputWhenPlaybackOngoing_shouldNotSetPlaybackSuppression() public void removeAnyUnsuitableOutputWhenPlaybackOngoing_shouldNotSetPlaybackSuppression()
throws Exception { throws Exception {
@ -15376,6 +15397,8 @@ public class ExoPlayerTest {
* removed during an ongoing playback but at least one another suitable audio output is still * removed during an ongoing playback but at least one another suitable audio output is still
* connected to the device. * connected to the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
removeAnySuitableOutputButOneSuitableDeviceStillConnected_shouldNotSetPlaybackSuppression() removeAnySuitableOutputButOneSuitableDeviceStillConnected_shouldNotSetPlaybackSuppression()
@ -15406,9 +15429,8 @@ public class ExoPlayerTest {
player.release(); player.release();
} }
/** Tests suppression of playback when no situable output is found. */ /** Tests suppression of playback when no suitable output is found. */
@Test @Test
@Config(minSdk = 35)
public void verifySuitableOutput_shouldSuppressPlaybackWhenNoSuitableOutputAvailable() public void verifySuitableOutput_shouldSuppressPlaybackWhenNoSuitableOutputAvailable()
throws Exception { throws Exception {
FakeSuitableOutputChecker suitableMediaOutputChecker = FakeSuitableOutputChecker suitableMediaOutputChecker =
@ -15439,7 +15461,6 @@ public class ExoPlayerTest {
/** Tests no occurrences of suppression of playback when situable output is found. */ /** Tests no occurrences of suppression of playback when situable output is found. */
@Test @Test
@Config(minSdk = 35)
public void verifySuitableOutput_shouldNotSuppressPlaybackWhenSuitableOutputIsAvailable() public void verifySuitableOutput_shouldNotSuppressPlaybackWhenSuitableOutputIsAvailable()
throws Exception { throws Exception {
FakeSuitableOutputChecker suitableMediaOutputChecker = FakeSuitableOutputChecker suitableMediaOutputChecker =
@ -15472,7 +15493,6 @@ public class ExoPlayerTest {
* disabled. * disabled.
*/ */
@Test @Test
@Config(minSdk = 35)
public void public void
verifySuitableOutput_playbackSuppressionOnUnsuitableOutputDisabled_shouldNotSuppressPlayback() verifySuitableOutput_playbackSuppressionOnUnsuitableOutputDisabled_shouldNotSuppressPlayback()
throws Exception { throws Exception {
@ -15502,7 +15522,6 @@ public class ExoPlayerTest {
/** Tests removal of suppression of playback when a suitable output is added. */ /** Tests removal of suppression of playback when a suitable output is added. */
@Test @Test
@Config(minSdk = 35)
public void verifySuitableOutput_shouldRemovePlaybackSuppressionOnAdditionOfSuitableOutput() public void verifySuitableOutput_shouldRemovePlaybackSuppressionOnAdditionOfSuitableOutput()
throws Exception { throws Exception {
FakeSuitableOutputChecker suitableMediaOutputChecker = FakeSuitableOutputChecker suitableMediaOutputChecker =
@ -15538,7 +15557,6 @@ public class ExoPlayerTest {
/** Tests suppression of playback when a suitable output is removed. */ /** Tests suppression of playback when a suitable output is removed. */
@Test @Test
@Config(minSdk = 35)
public void verifySuitableOutput_shouldSuppressPlaybackOnRemovalOfSuitableOutput() public void verifySuitableOutput_shouldSuppressPlaybackOnRemovalOfSuitableOutput()
throws Exception { throws Exception {
FakeSuitableOutputChecker suitableMediaOutputChecker = FakeSuitableOutputChecker suitableMediaOutputChecker =
@ -15571,7 +15589,6 @@ public class ExoPlayerTest {
/** Tests suppression of playback back again when a suitable output added before is removed. */ /** Tests suppression of playback back again when a suitable output added before is removed. */
@Test @Test
@Config(minSdk = 35)
public void verifySuitableOutput_shouldSuppressPlaybackAgainAfterRemovalOfAddedSuitableOutput() public void verifySuitableOutput_shouldSuppressPlaybackAgainAfterRemovalOfAddedSuitableOutput()
throws Exception { throws Exception {
FakeSuitableOutputChecker suitableMediaOutputChecker = FakeSuitableOutputChecker suitableMediaOutputChecker =

View File

@ -19,7 +19,6 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.SuitableOutputChecker; import androidx.media3.exoplayer.SuitableOutputChecker;
@ -28,7 +27,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** Fake implementation for {@link SuitableOutputChecker}. */ /** Fake implementation for {@link SuitableOutputChecker}. */
@RestrictTo(LIBRARY_GROUP) @RestrictTo(LIBRARY_GROUP)
@UnstableApi @UnstableApi
@RequiresApi(35)
public final class FakeSuitableOutputChecker implements SuitableOutputChecker { public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
/** Builder for {@link FakeSuitableOutputChecker} instance. */ /** Builder for {@link FakeSuitableOutputChecker} instance. */

View File

@ -18,7 +18,6 @@ package androidx.media3.test.utils;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.content.Context; import android.content.Context;
import android.os.Build.VERSION;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
@ -423,7 +422,7 @@ public class TestExoPlayerBuilder {
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled) .setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput) .setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput)
.experimentalSetDynamicSchedulingEnabled(dynamicSchedulingEnabled); .experimentalSetDynamicSchedulingEnabled(dynamicSchedulingEnabled);
if (VERSION.SDK_INT >= 35 && suitableOutputChecker != null) { if (suitableOutputChecker != null) {
builder.setSuitableOutputChecker(suitableOutputChecker); builder.setSuitableOutputChecker(suitableOutputChecker);
} }
if (mediaSourceFactory != null) { if (mediaSourceFactory != null) {

View File

@ -43,7 +43,6 @@ import androidx.media3.test.utils.FakeSuitableOutputChecker;
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 java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -81,10 +80,13 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
@Before @Before
public void setUp() { public void setUp() {
shadowPackageManager =
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
TestExoPlayerBuilder builder = TestExoPlayerBuilder builder =
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setSuppressPlaybackOnUnsuitableOutput(true); .setSuppressPlaybackOnUnsuitableOutput(true);
if (Util.SDK_INT >= 35) { if (Util.SDK_INT >= 35) {
suitableMediaOutputChecker = suitableMediaOutputChecker =
new FakeSuitableOutputChecker.Builder() new FakeSuitableOutputChecker.Builder()
@ -92,11 +94,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
.build(); .build();
builder.setSuitableOutputChecker(suitableMediaOutputChecker); builder.setSuitableOutputChecker(suitableMediaOutputChecker);
} }
testPlayer = builder.build(); testPlayer = builder.build();
shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext()); shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext());
shadowPackageManager =
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
} }
@After @After
@ -108,10 +108,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test end-to-end flow from launch of output switcher to playback getting resumed when the * Test end-to-end flow from launch of output switcher to playback getting resumed when the
* playback is suppressed and then unsuppressed. * playback is suppressed and then unsuppressed.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionFollowedByResolution_shouldLaunchOutputSwitcherAndStartPlayback() public void playbackSuppressionFollowedByResolution_shouldLaunchOutputSwitcherAndStartPlayback()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -153,10 +154,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for the launch of system updated Output Switcher app when playback is suppressed due to * Test for the launch of system updated Output Switcher app when playback is suppressed due to
* unsuitable output and the system updated Output Switcher is present on the device. * unsuitable output and the system updated Output Switcher is present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithPlaybackSuppression_shouldLaunchOutputSwitcher() public void playEventWithPlaybackSuppression_shouldLaunchOutputSwitcher()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -185,11 +187,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for the launch of system Output Switcher app when playback is suppressed due to unsuitable * Test for the launch of system Output Switcher app when playback is suppressed due to unsuitable
* output and both the system as well as user installed Output Switcher are present on the device. * output and both the system as well as user installed Output Switcher are present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
playbackSuppressionWithSystemAndUserInstalledComponentsPresent_shouldLaunchSystemComponent() playbackSuppressionWithSystemAndUserInstalledComponentsPresent_shouldLaunchSystemComponent()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -223,11 +226,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for no launch of system Output Switcher app when running on non-Wear OS device with * Test for no launch of system Output Switcher app when running on non-Wear OS device with
* playback suppression conditions and the system Output Switcher present on the device. * playback suppression conditions and the system Output Switcher present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
playEventWithPlaybackSuppressionConditionsOnNonWearOSDevice_shouldNotLaunchOutputSwitcher() playEventWithPlaybackSuppressionConditionsOnNonWearOSDevice_shouldNotLaunchOutputSwitcher()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -266,11 +270,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* output with the system Bluetooth Settings app present while the system Output Switcher app is * output with the system Bluetooth Settings app present while the system Output Switcher app is
* not present on the device. * not present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
playEventWithPlaybackSuppressionWhenOnlySystemBTSettingsPresent_shouldLaunchBTSettings() playEventWithPlaybackSuppressionWhenOnlySystemBTSettingsPresent_shouldLaunchBTSettings()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
Settings.ACTION_BLUETOOTH_SETTINGS, Settings.ACTION_BLUETOOTH_SETTINGS,
FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME, FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME,
@ -299,10 +304,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* output with the updated system Bluetooth Settings app present while the Output Switcher app is * output with the updated system Bluetooth Settings app present while the Output Switcher app is
* not present on the device. * not present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionWhenOnlyUpdatedSystemBTSettingsPresent_shouldLaunchBTSettings() public void playbackSuppressionWhenOnlyUpdatedSystemBTSettingsPresent_shouldLaunchBTSettings()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
Settings.ACTION_BLUETOOTH_SETTINGS, Settings.ACTION_BLUETOOTH_SETTINGS,
FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME, FAKE_SYSTEM_BT_SETTINGS_PACKAGE_NAME,
@ -330,10 +336,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for the launch of Output Switcher app when playback is suppressed due to unsuitable output * Test for the launch of Output Switcher app when playback is suppressed due to unsuitable output
* and both Output Switcher as well as the Bluetooth settings are present on the device. * and both Output Switcher as well as the Bluetooth settings are present on the device.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionWhenMultipleSystemComponentsPresent_shouldLaunchOutputSwitcher() public void playbackSuppressionWhenMultipleSystemComponentsPresent_shouldLaunchOutputSwitcher()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -367,10 +374,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for no launch of the non-system and non-system updated Output Switcher app when playback * Test for no launch of the non-system and non-system updated Output Switcher app when playback
* is suppressed due to unsuitable output. * is suppressed due to unsuitable output.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionWhenOnlyUserInstalledComponentsPresent_shouldNotLaunchAnyApp() public void playbackSuppressionWhenOnlyUserInstalledComponentsPresent_shouldNotLaunchAnyApp()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
"com.fake.userinstalled.outputswitcher", "com.fake.userinstalled.outputswitcher",
@ -400,10 +408,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for no launch of any system media output switching dialog app when playback is not * Test for no launch of any system media output switching dialog app when playback is not
* suppressed due to unsuitable output. * suppressed due to unsuitable output.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithoutPlaybackSuppression_shouldNotLaunchOutputSwitcherOrBTSettings() public void playEventWithoutPlaybackSuppression_shouldNotLaunchOutputSwitcherOrBTSettings()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -433,10 +442,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for no launch of any system media output switching dialog app when playback is suppressed * 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. * due to removal of all suitable audio outputs in mid of an ongoing playback.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionDuringOngoingPlayback_shouldOnlyPauseButNotLaunchSystemComponent() public void playbackSuppressionDuringOngoingPlayback_shouldOnlyPauseButNotLaunchSystemComponent()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -475,10 +485,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
} }
/** 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. */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithSuppressedPlaybackCondition_shouldCallPauseOnPlayer() public void playEventWithSuppressedPlaybackCondition_shouldCallPauseOnPlayer()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -511,11 +522,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for automatic resumption of the ongoing playback when it is transferred from one suitable * Test for automatic resumption of the ongoing playback when it is transferred from one suitable
* device to another within set time out. * device to another within set time out.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
transferOnGoingPlaybackFromOneSuitableDeviceToAnotherWithinSetTimeOut_shouldContinuePlayback() transferOnGoingPlaybackFromOneSuitableDeviceToAnotherWithinSetTimeOut_shouldContinuePlayback()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
testPlayer.addListener( testPlayer.addListener(
@ -541,11 +553,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test for automatic pause of the ongoing playback when it is transferred from one suitable * Test for automatic pause of the ongoing playback when it is transferred from one suitable
* device to another and the time difference between switching is more than default time out * device to another and the time difference between switching is more than default time out
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
transferOnGoingPlaybackFromOneSuitableDeviceToAnotherAfterTimeOut_shouldNotContinuePlayback() transferOnGoingPlaybackFromOneSuitableDeviceToAnotherAfterTimeOut_shouldNotContinuePlayback()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput( setupConnectedAudioOutput(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP); AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
@ -587,10 +600,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
/** /**
* Test for no pause on the Player when the playback is not suppressed due to unsuitable output. * Test for no pause on the Player when the playback is not suppressed due to unsuitable output.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithoutSuppressedPlaybackCondition_shouldNotCallPauseOnPlayer() public void playEventWithoutSuppressedPlaybackCondition_shouldNotCallPauseOnPlayer()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
registerFakeActivity( registerFakeActivity(
OUTPUT_SWITCHER_INTENT_ACTION_NAME, OUTPUT_SWITCHER_INTENT_ACTION_NAME,
FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME,
@ -624,11 +638,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test to ensure player is not playing when the playback suppression due to unsuitable output is * Test to ensure player is not playing when the playback suppression due to unsuitable output is
* removed after the default timeout. * removed after the default timeout.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
playbackSuppressionChangeToNoneAfterDefaultTimeout_shouldNotChangePlaybackStateToPlaying() playbackSuppressionChangeToNoneAfterDefaultTimeout_shouldNotChangePlaybackStateToPlaying()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -667,10 +682,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test to ensure player is playing when the playback suppression due to unsuitable output is * Test to ensure player is playing when the playback suppression due to unsuitable output is
* removed within the set timeout. * removed within the set timeout.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playbackSuppressionChangeToNoneWithinSetTimeout_shouldChangePlaybackStateToPlaying() public void playbackSuppressionChangeToNoneWithinSetTimeout_shouldChangePlaybackStateToPlaying()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -694,11 +710,12 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test to ensure player is not playing when the playback suppression due to unsuitable output is * Test to ensure player is not playing when the playback suppression due to unsuitable output is
* removed after the set timeout. * removed after the set timeout.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void public void
playbackSuppressionChangeToNoneAfterSetTimeout_shouldNotChangeFinalPlaybackStateToPlaying() playbackSuppressionChangeToNoneAfterSetTimeout_shouldNotChangeFinalPlaybackStateToPlaying()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -728,10 +745,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
} }
/** Test to ensure wake lock is acquired when playback is suppressed due to unsuitable output. */ /** Test to ensure wake lock is acquired when playback is suppressed due to unsuitable output. */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithSuppressedPlaybackCondition_shouldAcquireWakeLock() public void playEventWithSuppressedPlaybackCondition_shouldAcquireWakeLock()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -752,10 +770,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test to ensure that the wake lock acquired with playback suppression due to unsuitable output * Test to ensure that the wake lock acquired with playback suppression due to unsuitable output
* is released after the set timeout. * is released after the set timeout.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithSuppressedPlaybackCondition_shouldReleaseAcquiredWakeLockAfterTimeout() public void playEventWithSuppressedPlaybackCondition_shouldReleaseAcquiredWakeLockAfterTimeout()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -778,10 +797,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* Test to ensure that the wake lock acquired with playback suppression due to unsuitable output * Test to ensure that the wake lock acquired with playback suppression due to unsuitable output
* is released after suitable output gets added. * is released after suitable output gets added.
*/ */
// TODO: remove maxSdk once Robolectric supports MediaRouter2 (b/382017156)
@Config(minSdk = 23, maxSdk = 34)
@Test @Test
public void playEventWithSuppressedPlaybackConditionRemoved_shouldReleaseAcquiredWakeLock() public void playEventWithSuppressedPlaybackConditionRemoved_shouldReleaseAcquiredWakeLock()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER); setupConnectedAudioOutput(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
testPlayer.addListener( testPlayer.addListener(
@ -805,10 +825,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
/** Test to verify that attempted playback is paused when the suitable output is not present. */ /** Test to verify that attempted playback is paused when the suitable output is not present. */
@Test @Test
@Config(minSdk = 35) @Config(minSdk = 35) // Remove minSdk once Robolectric supports MediaRouter2 (b/382017156)
public void playEvent_withSuitableOutputNotPresent_shouldPausePlaybackAndLaunchOutputSwitcher() public void playEvent_withSuitableOutputNotPresent_shouldPausePlaybackAndLaunchOutputSwitcher()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify(
/* isSelectedOutputSuitableForPlayback= */ false); /* isSelectedOutputSuitableForPlayback= */ false);
registerFakeActivity( registerFakeActivity(
@ -850,10 +869,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* present. * present.
*/ */
@Test @Test
@Config(minSdk = 35) @Config(minSdk = 35) // Remove minSdk once Robolectric supports MediaRouter2 (b/382017156)
public void playEvent_withSuitableOutputPresent_shouldNotPausePlaybackOrLaunchOutputSwitcher() public void playEvent_withSuitableOutputPresent_shouldNotPausePlaybackOrLaunchOutputSwitcher()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify(
/* isSelectedOutputSuitableForPlayback= */ false); /* isSelectedOutputSuitableForPlayback= */ false);
registerFakeActivity( registerFakeActivity(
@ -892,10 +910,9 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
* time out. * time out.
*/ */
@Test @Test
@Config(minSdk = 35) @Config(minSdk = 35) // Remove minSdk once Robolectric supports MediaRouter2 (b/382017156)
public void playEvent_suitableOutputAddedAfterTimeOut_shouldNotResumePlayback() public void playEvent_suitableOutputAddedAfterTimeOut_shouldNotResumePlayback()
throws TimeoutException { throws TimeoutException {
shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true);
suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify(
/* isSelectedOutputSuitableForPlayback= */ false); /* isSelectedOutputSuitableForPlayback= */ false);
testPlayer.setMediaItem( testPlayer.setMediaItem(
@ -945,11 +962,11 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
private void setupConnectedAudioOutput(int... deviceTypes) { private void setupConnectedAudioOutput(int... deviceTypes) {
ShadowAudioManager shadowAudioManager = ShadowAudioManager shadowAudioManager =
shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class)); shadowOf(ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
ImmutableList.Builder<AudioDeviceInfo> deviceListBuilder = ImmutableList.builder();
for (int deviceType : deviceTypes) { for (int deviceType : deviceTypes) {
deviceListBuilder.add(AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build()); shadowAudioManager.addOutputDevice(
AudioDeviceInfoBuilder.newBuilder().setType(deviceType).build(),
/* notifyAudioDeviceCallbacks= */ true);
} }
shadowAudioManager.setOutputDevices(deviceListBuilder.build());
} }
private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) { private void addConnectedAudioOutput(int deviceTypes, boolean notifyAudioDeviceCallbacks) {