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