Merge "Handle timeouts while dozing" into tm-dev am: 862611a552
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18246467 Change-Id: Icb0635c4e6fee2d1a38298db5100499c84ce0b4e 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.internal.annotations.VisibleForTesting
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
import com.android.systemui.dagger.qualifiers.Main
|
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.NotificationMediaManager.isPlayingState
|
||||||
|
import com.android.systemui.statusbar.SysuiStatusBarStateController
|
||||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
|
import com.android.systemui.util.time.SystemClock
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -42,7 +45,9 @@ val RESUME_MEDIA_TIMEOUT = SystemProperties
|
|||||||
class MediaTimeoutListener @Inject constructor(
|
class MediaTimeoutListener @Inject constructor(
|
||||||
private val mediaControllerFactory: MediaControllerFactory,
|
private val mediaControllerFactory: MediaControllerFactory,
|
||||||
@Main private val mainExecutor: DelayableExecutor,
|
@Main private val mainExecutor: DelayableExecutor,
|
||||||
private val logger: MediaTimeoutLogger
|
private val logger: MediaTimeoutLogger,
|
||||||
|
statusBarStateController: SysuiStatusBarStateController,
|
||||||
|
private val systemClock: SystemClock
|
||||||
) : MediaDataManager.Listener {
|
) : MediaDataManager.Listener {
|
||||||
|
|
||||||
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
|
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
|
||||||
@@ -62,6 +67,24 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
lateinit var stateCallback: (String, PlaybackState) -> Unit
|
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(
|
override fun onMediaDataLoaded(
|
||||||
key: String,
|
key: String,
|
||||||
oldKey: String?,
|
oldKey: String?,
|
||||||
@@ -131,6 +154,7 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
var lastState: PlaybackState? = null
|
var lastState: PlaybackState? = null
|
||||||
var resumption: Boolean? = null
|
var resumption: Boolean? = null
|
||||||
var destroyed = false
|
var destroyed = false
|
||||||
|
var expiration = Long.MAX_VALUE
|
||||||
|
|
||||||
var mediaData: MediaData = data
|
var mediaData: MediaData = data
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -150,7 +174,8 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
|
|
||||||
// Resume controls may have null token
|
// Resume controls may have null token
|
||||||
private var mediaController: MediaController? = null
|
private var mediaController: MediaController? = null
|
||||||
private var cancellation: Runnable? = null
|
var cancellation: Runnable? = null
|
||||||
|
private set
|
||||||
|
|
||||||
fun Int.isPlaying() = isPlayingState(this)
|
fun Int.isPlaying() = isPlayingState(this)
|
||||||
fun isPlaying() = lastState?.state?.isPlaying() ?: false
|
fun isPlaying() = lastState?.state?.isPlaying() ?: false
|
||||||
@@ -216,12 +241,9 @@ class MediaTimeoutListener @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
PAUSED_MEDIA_TIMEOUT
|
PAUSED_MEDIA_TIMEOUT
|
||||||
}
|
}
|
||||||
|
expiration = systemClock.elapsedRealtime() + timeout
|
||||||
cancellation = mainExecutor.executeDelayed({
|
cancellation = mainExecutor.executeDelayed({
|
||||||
cancellation = null
|
doTimeout()
|
||||||
logger.logTimeout(key)
|
|
||||||
timedOut = true
|
|
||||||
// this event is async, so it's safe even when `dispatchEvents` is false
|
|
||||||
timeoutCallback(key, timedOut)
|
|
||||||
}, timeout)
|
}, timeout)
|
||||||
} else {
|
} else {
|
||||||
expireMediaTimeout(key, "playback started - $state, $key")
|
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 {
|
cancellation?.apply {
|
||||||
logger.logTimeoutCancelled(mediaKey, reason)
|
logger.logTimeoutCancelled(mediaKey, reason)
|
||||||
run()
|
run()
|
||||||
}
|
}
|
||||||
|
expiration = Long.MAX_VALUE
|
||||||
cancellation = null
|
cancellation = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import android.media.session.PlaybackState
|
|||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
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.concurrency.FakeExecutor
|
||||||
import com.android.systemui.util.mockito.any
|
import com.android.systemui.util.mockito.any
|
||||||
import com.android.systemui.util.mockito.capture
|
import com.android.systemui.util.mockito.capture
|
||||||
@@ -63,10 +65,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
|
@Mock private lateinit var mediaControllerFactory: MediaControllerFactory
|
||||||
@Mock private lateinit var mediaController: MediaController
|
@Mock private lateinit var mediaController: MediaController
|
||||||
@Mock private lateinit var logger: MediaTimeoutLogger
|
@Mock private lateinit var logger: MediaTimeoutLogger
|
||||||
|
@Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
|
||||||
private lateinit var executor: FakeExecutor
|
private lateinit var executor: FakeExecutor
|
||||||
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
@Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
|
||||||
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
|
@Mock private lateinit var stateCallback: (String, PlaybackState) -> Unit
|
||||||
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
@Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
|
||||||
|
@Captor private lateinit var dozingCallbackCaptor:
|
||||||
|
ArgumentCaptor<StatusBarStateController.StateListener>
|
||||||
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
@JvmField @Rule val mockito = MockitoJUnit.rule()
|
||||||
private lateinit var metadataBuilder: MediaMetadata.Builder
|
private lateinit var metadataBuilder: MediaMetadata.Builder
|
||||||
private lateinit var playbackBuilder: PlaybackState.Builder
|
private lateinit var playbackBuilder: PlaybackState.Builder
|
||||||
@@ -74,12 +79,19 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
private lateinit var mediaData: MediaData
|
private lateinit var mediaData: MediaData
|
||||||
private lateinit var resumeData: MediaData
|
private lateinit var resumeData: MediaData
|
||||||
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
private lateinit var mediaTimeoutListener: MediaTimeoutListener
|
||||||
|
private var clock = FakeSystemClock()
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
|
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
|
||||||
executor = FakeExecutor(FakeSystemClock())
|
executor = FakeExecutor(clock)
|
||||||
mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor, logger)
|
mediaTimeoutListener = MediaTimeoutListener(
|
||||||
|
mediaControllerFactory,
|
||||||
|
executor,
|
||||||
|
logger,
|
||||||
|
statusBarStateController,
|
||||||
|
clock
|
||||||
|
)
|
||||||
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
mediaTimeoutListener.timeoutCallback = timeoutCallback
|
||||||
mediaTimeoutListener.stateCallback = stateCallback
|
mediaTimeoutListener.stateCallback = stateCallback
|
||||||
|
|
||||||
@@ -530,6 +542,49 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
|
|||||||
verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
|
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) {
|
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
|
||||||
`when`(mediaController.playbackState).thenReturn(state)
|
`when`(mediaController.playbackState).thenReturn(state)
|
||||||
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
|
||||||
|
|||||||
Reference in New Issue
Block a user