From c27ab67023494891527aa7feaba49238a79d7c32 Mon Sep 17 00:00:00 2001 From: Beth Thibodeau Date: Mon, 9 May 2022 23:28:34 -0500 Subject: [PATCH] Handle timeouts while dozing The executor might not run on time if the system started dozing since it was scheduled. So, check for expired timeouts when we wake up and cancel them at that time instead. Test: manual - wait 10 mins, turn on screen, media is gone Test: atest MediaTimeoutListenerTest Fixes: 229908880 Change-Id: I0f100c97ce0eb1dd111bd8d393dafa9da67b024c --- .../systemui/media/MediaTimeoutListener.kt | 48 ++++++++++++--- .../media/MediaTimeoutListenerTest.kt | 59 ++++++++++++++++++- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index fc8d38d59d59c..d4c4f2165339d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -22,8 +22,11 @@ import android.os.SystemProperties import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -42,7 +45,9 @@ val RESUME_MEDIA_TIMEOUT = SystemProperties class MediaTimeoutListener @Inject constructor( private val mediaControllerFactory: MediaControllerFactory, @Main private val mainExecutor: DelayableExecutor, - private val logger: MediaTimeoutLogger + private val logger: MediaTimeoutLogger, + statusBarStateController: SysuiStatusBarStateController, + private val systemClock: SystemClock ) : MediaDataManager.Listener { private val mediaListeners: MutableMap = mutableMapOf() @@ -62,6 +67,24 @@ class MediaTimeoutListener @Inject constructor( */ lateinit var stateCallback: (String, PlaybackState) -> Unit + init { + statusBarStateController.addCallback(object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + if (!isDozing) { + // Check whether any timeouts should have expired + mediaListeners.forEach { (key, listener) -> + if (listener.cancellation != null && + listener.expiration <= systemClock.elapsedRealtime()) { + // We dozed too long - timeout now, and cancel the pending one + listener.expireMediaTimeout(key, "timeout happened while dozing") + listener.doTimeout() + } + } + } + } + }) + } + override fun onMediaDataLoaded( key: String, oldKey: String?, @@ -131,6 +154,7 @@ class MediaTimeoutListener @Inject constructor( var lastState: PlaybackState? = null var resumption: Boolean? = null var destroyed = false + var expiration = Long.MAX_VALUE var mediaData: MediaData = data set(value) { @@ -150,7 +174,8 @@ class MediaTimeoutListener @Inject constructor( // Resume controls may have null token private var mediaController: MediaController? = null - private var cancellation: Runnable? = null + var cancellation: Runnable? = null + private set fun Int.isPlaying() = isPlayingState(this) fun isPlaying() = lastState?.state?.isPlaying() ?: false @@ -216,12 +241,9 @@ class MediaTimeoutListener @Inject constructor( } else { PAUSED_MEDIA_TIMEOUT } + expiration = systemClock.elapsedRealtime() + timeout cancellation = mainExecutor.executeDelayed({ - cancellation = null - logger.logTimeout(key) - timedOut = true - // this event is async, so it's safe even when `dispatchEvents` is false - timeoutCallback(key, timedOut) + doTimeout() }, timeout) } else { expireMediaTimeout(key, "playback started - $state, $key") @@ -232,11 +254,21 @@ class MediaTimeoutListener @Inject constructor( } } - private fun expireMediaTimeout(mediaKey: String, reason: String) { + fun doTimeout() { + cancellation = null + logger.logTimeout(key) + timedOut = true + expiration = Long.MAX_VALUE + // this event is async, so it's safe even when `dispatchEvents` is false + timeoutCallback(key, timedOut) + } + + fun expireMediaTimeout(mediaKey: String, reason: String) { cancellation?.apply { logger.logTimeoutCancelled(mediaKey, reason) run() } + expiration = Long.MAX_VALUE cancellation = null } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 91c0cc2ff8912..823d4ae8c447f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -23,6 +23,8 @@ import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture @@ -63,10 +65,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Mock private lateinit var mediaControllerFactory: MediaControllerFactory @Mock private lateinit var mediaController: MediaController @Mock private lateinit var logger: MediaTimeoutLogger + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController private lateinit var executor: FakeExecutor @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit @Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor + @Captor private lateinit var dozingCallbackCaptor: + ArgumentCaptor @JvmField @Rule val mockito = MockitoJUnit.rule() private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var playbackBuilder: PlaybackState.Builder @@ -74,12 +79,19 @@ class MediaTimeoutListenerTest : SysuiTestCase() { private lateinit var mediaData: MediaData private lateinit var resumeData: MediaData private lateinit var mediaTimeoutListener: MediaTimeoutListener + private var clock = FakeSystemClock() @Before fun setup() { `when`(mediaControllerFactory.create(any())).thenReturn(mediaController) - executor = FakeExecutor(FakeSystemClock()) - mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger) + executor = FakeExecutor(clock) + mediaTimeoutListener = MediaTimeoutListener( + mediaControllerFactory, + executor, + logger, + statusBarStateController, + clock + ) mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.stateCallback = stateCallback @@ -530,6 +542,49 @@ class MediaTimeoutListenerTest : SysuiTestCase() { verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!)) } + @Test + fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() { + // When paused media is loaded + testOnMediaDataLoaded_registersPlaybackListener() + mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) + + // And we doze past the scheduled timeout + val time = clock.currentTimeMillis() + clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT) + assertThat(executor.numPending()).isEqualTo(1) + + // Then when no longer dozing, the timeout runs immediately + dozingCallbackCaptor.value.onDozingChanged(false) + verify(timeoutCallback).invoke(eq(KEY), eq(true)) + verify(logger).logTimeout(eq(KEY)) + + // and cancel any later scheduled timeout + verify(logger).logTimeoutCancelled(eq(KEY), any()) + assertThat(executor.numPending()).isEqualTo(0) + } + + @Test + fun testTimeoutCallback_dozeShortTime_notInvokedOnWakeup() { + // When paused media is loaded + val time = clock.currentTimeMillis() + clock.setElapsedRealtime(time) + testOnMediaDataLoaded_registersPlaybackListener() + mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) + verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor)) + + // And we doze, but not past the scheduled timeout + clock.setElapsedRealtime(time + PAUSED_MEDIA_TIMEOUT / 2L) + assertThat(executor.numPending()).isEqualTo(1) + + // Then when no longer dozing, the timeout remains scheduled + dozingCallbackCaptor.value.onDozingChanged(false) + verify(timeoutCallback, never()).invoke(eq(KEY), eq(true)) + assertThat(executor.numPending()).isEqualTo(1) + } + private fun loadMediaDataWithPlaybackState(state: PlaybackState) { `when`(mediaController.playbackState).thenReturn(state) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)