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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user