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

Commit 862611a5 authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Android (Google) Code Review
Browse files

Merge "Handle timeouts while dozing" into tm-dev

parents db743bb1 c27ab670
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)