Manage wakelock when playback suppression is being handled.

After this change, a WakeLock of PowerManager#PARTIAL_WAKE_LOCK level would be acquired when the media is paused due to playback attempt without suitable output.
This WakeLock will be release either when the suitable media output has been connected or the set timeout to do so has expired.

PiperOrigin-RevId: 661570346
This commit is contained in:
Googler 2024-08-10 02:25:19 -07:00 committed by Copybara-Service
parent 65a471e3db
commit a76ff16179
2 changed files with 102 additions and 3 deletions

View File

@ -25,6 +25,8 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo; 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.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.provider.Settings; import android.provider.Settings;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -53,9 +55,10 @@ import java.util.List;
* href="https://developer.android.com/media/routing#output-switcher">Media Output Switcher</a> if * href="https://developer.android.com/media/routing#output-switcher">Media Output Switcher</a> if
* it is available on the device, or otherwise the Bluetooth settings screen. * it is available on the device, or otherwise the Bluetooth settings screen.
* *
* <p>This implementation also pauses playback when launching the system dialog. The underlying * <p>This implementation also pauses playback before opening the system dialog. If the user
* {@link Player} implementation (e.g. ExoPlayer) is expected to resume playback automatically when * connects a suitable media output within the specified timeout, playback resumes automatically.
* a suitable audio device is connected by the user. * During this timeout, a power wakelock of the {@link PowerManager#PARTIAL_WAKE_LOCK} level is
* obtained to prevent the system from freezing the app.
*/ */
@UnstableApi @UnstableApi
public final class WearUnsuitableOutputPlaybackSuppressionResolverListener public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
@ -94,6 +97,9 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
*/ */
private static final int FILTER_TYPE_AUDIO = 1; private static final int FILTER_TYPE_AUDIO = 1;
private static final String WAKE_LOCK_TAG =
"WearUnsuitableOutputPlaybackSuppressionResolverListener:WakeLock";
/** /**
* The default timeout for auto-resume of suppressed playback when the playback suppression reason * 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 * as {@link Player#PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT} is removed, in
@ -106,6 +112,8 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
private final long autoResumeTimeoutAfterUnsuitableOutputSuppressionMs; private final long autoResumeTimeoutAfterUnsuitableOutputSuppressionMs;
private final Clock clock; private final Clock clock;
@Nullable private final WakeLock wakeLock;
private long unsuitableOutputPlaybackSuppressionStartRealtimeMs; private long unsuitableOutputPlaybackSuppressionStartRealtimeMs;
/** /**
@ -144,6 +152,14 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
autoResumeTimeoutAfterUnsuitableOutputSuppressionMs = autoResumeTimeoutMs; autoResumeTimeoutAfterUnsuitableOutputSuppressionMs = autoResumeTimeoutMs;
this.clock = clock; this.clock = clock;
unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET; unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET;
WakeLock wakeLock = null;
PowerManager powerManager =
(PowerManager) applicationContext.getSystemService(Context.POWER_SERVICE);
if (powerManager != null) {
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
wakeLock.setReferenceCounted(false);
}
this.wakeLock = wakeLock;
} }
@Override @Override
@ -158,6 +174,9 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) { == Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
player.pause(); player.pause();
unsuitableOutputPlaybackSuppressionStartRealtimeMs = clock.elapsedRealtime(); unsuitableOutputPlaybackSuppressionStartRealtimeMs = clock.elapsedRealtime();
if (wakeLock != null && !wakeLock.isHeld()) {
wakeLock.acquire(autoResumeTimeoutAfterUnsuitableOutputSuppressionMs);
}
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) { if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
launchSystemMediaOutputSwitcherUi(applicationContext); launchSystemMediaOutputSwitcherUi(applicationContext);
} }
@ -168,6 +187,9 @@ public final class WearUnsuitableOutputPlaybackSuppressionResolverListener
< autoResumeTimeoutAfterUnsuitableOutputSuppressionMs)) { < autoResumeTimeoutAfterUnsuitableOutputSuppressionMs)) {
unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET; unsuitableOutputPlaybackSuppressionStartRealtimeMs = C.TIME_UNSET;
player.play(); player.play();
if (wakeLock != null) {
wakeLock.release();
}
} }
} }

View File

@ -55,6 +55,7 @@ import org.robolectric.shadows.AudioDeviceInfoBuilder;
import org.robolectric.shadows.ShadowApplication; import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowAudioManager; import org.robolectric.shadows.ShadowAudioManager;
import org.robolectric.shadows.ShadowPackageManager; import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowPowerManager;
/** Tests for the {@link WearUnsuitableOutputPlaybackSuppressionResolverListener}. */ /** Tests for the {@link WearUnsuitableOutputPlaybackSuppressionResolverListener}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -683,6 +684,82 @@ public class WearUnsuitableOutputPlaybackSuppressionResolverListenerTest {
assertThat(testPlayer.isPlaying()).isFalse(); assertThat(testPlayer.isPlaying()).isFalse();
} }
/** Test to ensure wake lock is acquired when playback is suppressed due to unsuitable output. */
@Test
public void playEventWithSuppressedPlaybackCondition_shouldAcquireWakeLock()
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);
assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull();
assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isTrue();
}
/**
* Test to ensure that the wake lock acquired with playback suppression due to unsuitable output
* is released after the set timeout.
*/
@Test
public void playEventWithSuppressedPlaybackCondition_shouldReleaseAcquiredWakeLockAfterTimeout()
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);
shadowOf(Looper.getMainLooper()).idle();
assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull();
assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).isFalse();
}
/**
* Test to ensure that the wake lock acquired with playback suppression due to unsuitable output
* is released after suitable output gets added.
*/
@Test
public void playEventWithSuppressedPlaybackConditionRemoved_shouldReleaseAcquiredWakeLock()
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);
addConnectedAudioOutput(
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, /* notifyAudioDeviceCallbacks= */ true);
runUntilPlayWhenReady(testPlayer, /* expectedPlayWhenReady= */ false);
shadowOf(Looper.getMainLooper()).idle();
assertThat(ShadowPowerManager.getLatestWakeLock()).isNotNull();
assertThat(ShadowPowerManager.getLatestWakeLock().isHeld()).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);