Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c27ab670 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

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
parent 253e417e
Loading
Loading
Loading
Loading
+40 −8
Original line number Diff line number Diff line
@@ -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<String, PlaybackStateListener> = 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
        }
    }
+57 −2
Original line number Diff line number Diff line
@@ -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<MediaController.Callback>
    @Captor private lateinit var dozingCallbackCaptor:
        ArgumentCaptor<StatusBarStateController.StateListener>
    @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)