Merge "Handle timeouts while dozing" into tm-dev am: 862611a552 am: 88bb00ccd4

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18246467

Change-Id: Ia64606c0b4564e7a27417a17ff21654c441bfe6a
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Beth Thibodeau
2022-05-12 20:18:09 +00:00
committed by Automerger Merge Worker
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)