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)