Loading packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +40 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading @@ -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?, Loading Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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") Loading @@ -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 } } Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +57 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) Loading Loading
packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +40 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() Loading @@ -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?, Loading Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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") Loading @@ -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 } } Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +57 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) Loading