diff --git a/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java b/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java index 59f4022e75..c612d461f3 100644 --- a/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java +++ b/libraries/ui/src/test/java/androidx/media3/ui/WearUnsuitableOutputPlaybackSuppressionResolverListenerTest.java @@ -37,7 +37,9 @@ import androidx.media3.common.FlagSet; import androidx.media3.common.MediaItem; import androidx.media3.common.Player; import androidx.media3.common.Player.PlayWhenReadyChangeReason; +import androidx.media3.common.util.Util; import androidx.media3.test.utils.FakeClock; +import androidx.media3.test.utils.FakeSuitableOutputChecker; import androidx.media3.test.utils.TestExoPlayerBuilder; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -51,6 +53,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; import org.robolectric.shadows.AudioDeviceInfoBuilder; import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowAudioManager; @@ -74,13 +77,23 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { private ShadowPackageManager shadowPackageManager; private ShadowApplication shadowApplication; private Player testPlayer; + private FakeSuitableOutputChecker suitableMediaOutputChecker; @Before public void setUp() { - testPlayer = + TestExoPlayerBuilder builder = new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) - .setSuppressPlaybackOnUnsuitableOutput(true) - .build(); + .setSuppressPlaybackOnUnsuitableOutput(true); + + if (Util.SDK_INT >= 35) { + suitableMediaOutputChecker = + new FakeSuitableOutputChecker.Builder() + .setIsSuitableExternalOutputAvailable(/* isSuitableOutputAvailable= */ false) + .build(); + builder.setSuitableOutputChecker(suitableMediaOutputChecker); + } + + testPlayer = builder.build(); shadowApplication = shadowOf((Application) ApplicationProvider.getApplicationContext()); shadowPackageManager = shadowOf(ApplicationProvider.getApplicationContext().getPackageManager()); @@ -96,7 +109,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * playback is suppressed and then unsuppressed. */ @Test - public void playbackSuppressionFollowedByResolution_shouldLaunchOSwAndChangePlayerStateToPlaying() + public void playbackSuppressionFollowedByResolution_shouldLaunchOutputSwitcherAndStartPlayback() throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( @@ -133,8 +146,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { assertThat(intentTriggered) .hasComponent( FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME); - assertThat(playWhenReadyChangeSequence).containsExactly(true, false, true); - assertThat(testPlayer.isPlaying()).isTrue(); + assertThat(playWhenReadyChangeSequence).containsExactly(true, false, true).inOrder(); } /** @@ -142,7 +154,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * unsuitable output and the system updated Output Switcher is present on the device. */ @Test - public void playEventWithPlaybackSuppressionWhenUpdatedSystemOSwPresent_shouldLaunchOSw() + public void playEventWithPlaybackSuppression_shouldLaunchOutputSwitcher() throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( @@ -175,7 +187,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { */ @Test public void - playbackSuppressionWhenBothSystemAndUserInstalledOutputSwitcherPresent_shouldLaunchSystemOSw() + playbackSuppressionWithSystemAndUserInstalledComponentsPresent_shouldLaunchSystemComponent() throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( @@ -319,7 +331,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * and both Output Switcher as well as the Bluetooth settings are present on the device. */ @Test - public void playEventWithPlaybackSuppressionWhenOSwAndBTSettingsBothPresent_shouldLaunchOSw() + public void playbackSuppressionWhenMultipleSystemComponentsPresent_shouldLaunchOutputSwitcher() throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( @@ -356,9 +368,8 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * is suppressed due to unsuitable output. */ @Test - public void - playbackSuppressionWhenOnlyUserInstalledOSwAndBTSettingsPresent_shouldNotLaunchAnyApp() - throws TimeoutException { + public void playbackSuppressionWhenOnlyUserInstalledComponentsPresent_shouldNotLaunchAnyApp() + throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( OUTPUT_SWITCHER_INTENT_ACTION_NAME, @@ -390,7 +401,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * suppressed due to unsuitable output. */ @Test - public void playEventWithoutPlaybackSuppression_shouldNotLaunchEitherOSwOrBTSettings() + public void playEventWithoutPlaybackSuppression_shouldNotLaunchOutputSwitcherOrBTSettings() throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( @@ -423,9 +434,8 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { * due to removal of all suitable audio outputs in mid of an ongoing playback. */ @Test - public void - playbackSuppressionDuringOngoingPlayback_shouldOnlyPauseButNotLaunchEitherOSwOrBTSettings() - throws TimeoutException { + public void playbackSuppressionDuringOngoingPlayback_shouldOnlyPauseButNotLaunchSystemComponent() + throws TimeoutException { shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); registerFakeActivity( OUTPUT_SWITCHER_INTENT_ACTION_NAME, @@ -442,6 +452,15 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { testPlayer.addListener( new WearUnsuitableOutputPlaybackSuppressionResolverListener( ApplicationProvider.getApplicationContext())); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); testPlayer.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); testPlayer.prepare(); @@ -452,7 +471,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false); assertThat(shadowApplication.getNextStartedActivity()).isNull(); - assertThat(testPlayer.isPlaying()).isFalse(); + assertThat(lastPlayWhenReady.get()).isFalse(); } /** Test for pause on the Player when the playback is suppressed due to unsuitable output. */ @@ -472,21 +491,20 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { testPlayer.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); testPlayer.prepare(); - AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); testPlayer.addListener( new Player.Listener() { @Override - public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { - if (!playWhenReady) { - isPlaybackPaused.set(true); - } + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); } }); testPlayer.play(); runUntilPlaybackState(testPlayer, Player.STATE_READY); - assertThat(isPlaybackPaused.get()).isTrue(); + assertThat(lastPlayWhenReady.get()).isFalse(); } /** @@ -537,12 +555,20 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { WearUnsuitableOutputPlaybackSuppressionResolverListener .DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS, fakeClock)); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); 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); @@ -555,7 +581,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false); - assertThat(testPlayer.isPlaying()).isFalse(); + assertThat(lastPlayWhenReady.get()).isFalse(); } /** @@ -578,21 +604,20 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { testPlayer.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); testPlayer.prepare(); - AtomicBoolean isPlaybackPaused = new AtomicBoolean(false); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); testPlayer.addListener( new Player.Listener() { @Override - public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) { - if (!playWhenReady) { - isPlaybackPaused.set(true); - } + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); } }); testPlayer.play(); runUntilPlaybackState(testPlayer, Player.STATE_READY); - assertThat(isPlaybackPaused.get()).isFalse(); + assertThat(lastPlayWhenReady.get()).isTrue(); } /** @@ -612,6 +637,15 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { WearUnsuitableOutputPlaybackSuppressionResolverListener .DEFAULT_PLAYBACK_SUPPRESSION_AUTO_RESUME_TIMEOUT_MS, fakeClock)); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); testPlayer.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); testPlayer.prepare(); @@ -626,7 +660,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false); - assertThat(testPlayer.isPlaying()).isFalse(); + assertThat(lastPlayWhenReady.get()).isFalse(); } /** @@ -670,6 +704,15 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { testPlayer.addListener( new WearUnsuitableOutputPlaybackSuppressionResolverListener( ApplicationProvider.getApplicationContext(), TEST_TIME_OUT_MS, fakeClock)); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); testPlayer.setMediaItem( MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); testPlayer.prepare(); @@ -681,7 +724,7 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true); runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false); - assertThat(testPlayer.isPlaying()).isFalse(); + assertThat(lastPlayWhenReady.get()).isFalse(); } /** Test to ensure wake lock is acquired when playback is suppressed due to unsuitable output. */ @@ -760,6 +803,128 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest { assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isFalse(); } + /** Test to verify that attempted playback is paused when the suitable output is not present. */ + @Test + @Config(minSdk = 35) + public void playEvent_withSuitableOutputNotPresent_shouldPausePlaybackAndLaunchOutputSwitcher() + throws TimeoutException { + shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); + suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( + /* isSelectedOutputSuitableForPlayback= */ false); + registerFakeActivity( + OUTPUT_SWITCHER_INTENT_ACTION_NAME, + FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, + FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, + ApplicationInfo.FLAG_SYSTEM); + testPlayer.addListener( + new WearUnsuitableOutputPlaybackSuppressionResolverListener( + ApplicationProvider.getApplicationContext())); + testPlayer.setMediaItem( + MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); + testPlayer.prepare(); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); + + testPlayer.play(); + runUntilPlaybackState(testPlayer, Player.STATE_READY); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(lastPlayWhenReady.get()).isFalse(); + Intent intentTriggered = shadowApplication.getNextStartedActivity(); + assertThat(intentTriggered).isNotNull(); + assertThat(intentTriggered).hasAction(OUTPUT_SWITCHER_INTENT_ACTION_NAME); + assertThat(intentTriggered) + .hasComponent( + FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME); + } + + /** + * Test to verify that attempted playback is not paused when the suitable output is already + * present. + */ + @Test + @Config(minSdk = 35) + public void playEvent_withSuitableOutputPresent_shouldNotPausePlaybackOrLaunchOutputSwitcher() + throws TimeoutException { + shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); + suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( + /* isSelectedOutputSuitableForPlayback= */ false); + registerFakeActivity( + OUTPUT_SWITCHER_INTENT_ACTION_NAME, + FAKE_SYSTEM_OUTPUT_SWITCHER_PACKAGE_NAME, + FAKE_SYSTEM_OUTPUT_SWITCHER_CLASS_NAME, + ApplicationInfo.FLAG_SYSTEM); + suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( + /* isSelectedOutputSuitableForPlayback= */ true); + testPlayer.addListener( + new WearUnsuitableOutputPlaybackSuppressionResolverListener( + ApplicationProvider.getApplicationContext())); + testPlayer.setMediaItem( + MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); + testPlayer.prepare(); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); + + testPlayer.play(); + runUntilPlaybackState(testPlayer, Player.STATE_READY); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(lastPlayWhenReady.get()).isTrue(); + assertThat(shadowApplication.getNextStartedActivity()).isNull(); + } + + /** + * Test to verify that playback is not resumed when the suitable output is added after the tested + * time out. + */ + @Test + @Config(minSdk = 35) + public void playEvent_suitableOutputAddedAfterTimeOut_shouldNotResumePlayback() + throws TimeoutException { + shadowPackageManager.setSystemFeature(PackageManager.FEATURE_WATCH, /* supported= */ true); + suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( + /* isSelectedOutputSuitableForPlayback= */ false); + testPlayer.setMediaItem( + MediaItem.fromUri("asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4")); + FakeClock fakeClock = new FakeClock(/* isAutoAdvancing= */ true); + testPlayer.addListener( + new WearUnsuitableOutputPlaybackSuppressionResolverListener( + ApplicationProvider.getApplicationContext(), TEST_TIME_OUT_MS, fakeClock)); + AtomicBoolean lastPlayWhenReady = new AtomicBoolean(); + testPlayer.addListener( + new Player.Listener() { + @Override + public void onPlayWhenReadyChanged( + boolean playWhenReady, @PlayWhenReadyChangeReason int reason) { + lastPlayWhenReady.set(playWhenReady); + } + }); + testPlayer.prepare(); + testPlayer.play(); + runUntilPlaybackState(testPlayer, Player.STATE_READY); + + fakeClock.advanceTime(TEST_TIME_OUT_MS * 2); + suitableMediaOutputChecker.updateIsSelectedSuitableOutputAvailableAndNotify( + /* isSelectedOutputSuitableForPlayback= */ true); + shadowOf(Looper.getMainLooper()).idle(); + + assertThat(lastPlayWhenReady.get()).isFalse(); + } + private void registerFakeActivity( String fakeActionName, String fakePackageName, String fakeClassName, int applicationFlags) { ComponentName fakeComponentName = new ComponentName(fakePackageName, fakeClassName);