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
This commit is contained in:
Beth Thibodeau
2022-05-09 23:28:34 -05:00
parent 253e417e76
commit c27ab67023
2 changed files with 97 additions and 10 deletions

View File

@@ -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
}
}

View File

@@ -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)