diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 0a28b47923dac..ba99f5df653ca 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -212,8 +212,8 @@ class MediaDataManager( mediaDataCombineLatest.addListener(mediaDataFilter) // Set up links back into the pipeline for listeners that need to send events upstream. - mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> - setTimedOut(token, timedOut) } + mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> + setTimedOut(key, timedOut) } mediaResumeListener.setManager(this) mediaDataFilter.mediaDataManager = this @@ -414,14 +414,18 @@ class MediaDataManager( * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ - internal fun setTimedOut(token: String, timedOut: Boolean, forceUpdate: Boolean = false) { - mediaEntries[token]?.let { + internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { + mediaEntries[key]?.let { if (it.active == !timedOut && !forceUpdate) { + if (it.resumption) { + if (DEBUG) Log.d(TAG, "timing out resume player $key") + dismissMediaData(key, 0L /* delay */) + } return } it.active = !timedOut - if (DEBUG) Log.d(TAG, "Updating $token timedOut: $timedOut") - onMediaDataLoaded(token, token, it) + if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") + onMediaDataLoaded(key, key, it) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index ab568c8c5a855..608c784f5d395 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -35,6 +35,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager import com.android.systemui.tuner.TunerService import com.android.systemui.util.Utils +import com.android.systemui.util.time.SystemClock import java.io.FileDescriptor import java.io.PrintWriter import java.util.concurrent.ConcurrentLinkedQueue @@ -53,11 +54,13 @@ class MediaResumeListener @Inject constructor( @Background private val backgroundExecutor: Executor, private val tunerService: TunerService, private val mediaBrowserFactory: ResumeMediaBrowserFactory, - dumpManager: DumpManager + dumpManager: DumpManager, + private val systemClock: SystemClock ) : MediaDataManager.Listener, Dumpable { private var useMediaResumption: Boolean = Utils.useMediaResumption(context) - private val resumeComponents: ConcurrentLinkedQueue = ConcurrentLinkedQueue() + private val resumeComponents: ConcurrentLinkedQueue> = + ConcurrentLinkedQueue() private lateinit var mediaDataManager: MediaDataManager @@ -131,14 +134,32 @@ class MediaResumeListener @Inject constructor( val listString = prefs.getString(MEDIA_PREFERENCE_KEY + currentUserId, null) val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex()) ?.dropLastWhile { it.isEmpty() } + var needsUpdate = false components?.forEach { val info = it.split("/") val packageName = info[0] val className = info[1] val component = ComponentName(packageName, className) - resumeComponents.add(component) + + val lastPlayed = if (info.size == 3) { + try { + info[2].toLong() + } catch (e: NumberFormatException) { + needsUpdate = true + systemClock.currentTimeMillis() + } + } else { + needsUpdate = true + systemClock.currentTimeMillis() + } + resumeComponents.add(component to lastPlayed) } Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}") + + if (needsUpdate) { + // Save any missing times that we had to fill in + writeSharedPrefs() + } } /** @@ -149,9 +170,12 @@ class MediaResumeListener @Inject constructor( return } + val now = systemClock.currentTimeMillis() resumeComponents.forEach { - val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) - browser.findRecentMedia() + if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) { + val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first) + browser.findRecentMedia() + } } } @@ -234,18 +258,24 @@ class MediaResumeListener @Inject constructor( */ private fun updateResumptionList(componentName: ComponentName) { // Remove if exists - resumeComponents.remove(componentName) + resumeComponents.remove(resumeComponents.find { it.first.equals(componentName) }) // Insert at front of queue - resumeComponents.add(componentName) + val currentTime = systemClock.currentTimeMillis() + resumeComponents.add(componentName to currentTime) // Remove old components if over the limit if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { resumeComponents.remove() } - // Save changes + writeSharedPrefs() + } + + private fun writeSharedPrefs() { val sb = StringBuilder() resumeComponents.forEach { - sb.append(it.flattenToString()) + sb.append(it.first.flattenToString()) + sb.append("/") + sb.append(it.second) sb.append(ResumeMediaBrowser.DELIMITER) } val prefs = context.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 9a3919326cbd7..6f047712c954b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -20,6 +20,7 @@ import android.media.session.MediaController import android.media.session.PlaybackState import android.os.SystemProperties import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState @@ -29,9 +30,15 @@ import javax.inject.Inject private const val DEBUG = true private const val TAG = "MediaTimeout" -private val PAUSED_MEDIA_TIMEOUT = SystemProperties + +@VisibleForTesting +val PAUSED_MEDIA_TIMEOUT = SystemProperties .getLong("debug.sysui.media_timeout", TimeUnit.MINUTES.toMillis(10)) +@VisibleForTesting +val RESUME_MEDIA_TIMEOUT = SystemProperties + .getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3)) + /** * Controller responsible for keeping track of playback states and expiring inactive streams. */ @@ -45,8 +52,9 @@ class MediaTimeoutListener @Inject constructor( /** * Callback representing that a media object is now expired: - * @param token Media session unique identifier - * @param pauseTimeout True when expired for {@code PAUSED_MEDIA_TIMEOUT} + * @param key Media control unique identifier + * @param timedOut True when expired for {@code PAUSED_MEDIA_TIMEOUT} for active media, + * or {@code RESUME_MEDIA_TIMEOUT} for resume media */ lateinit var timeoutCallback: (String, Boolean) -> Unit @@ -122,6 +130,7 @@ class MediaTimeoutListener @Inject constructor( var timedOut = false var playing: Boolean? = null + var resumption: Boolean? = null var destroyed = false var mediaData: MediaData = data @@ -159,12 +168,19 @@ class MediaTimeoutListener @Inject constructor( } override fun onSessionDestroyed() { - // If the session is destroyed, the controller is no longer valid, and we will need to - // recreate it if this key is updated later if (DEBUG) { Log.d(TAG, "Session destroyed for $key") } - destroy() + + if (resumption == true) { + // Some apps create a session when MBS is queried. We should unregister the + // controller since it will no longer be valid, but don't cancel the timeout + mediaController?.unregisterCallback(this) + } else { + // For active controls, if the session is destroyed, clean up everything since we + // will need to recreate it if this key is updated later + destroy() + } } private fun processState(state: PlaybackState?, dispatchEvents: Boolean) { @@ -173,20 +189,28 @@ class MediaTimeoutListener @Inject constructor( } val isPlaying = state != null && isPlayingState(state.state) - if (playing == isPlaying && playing != null) { + val resumptionChanged = resumption != mediaData.resumption + if (playing == isPlaying && playing != null && !resumptionChanged) { return } playing = isPlaying + resumption = mediaData.resumption if (!isPlaying) { if (DEBUG) { - Log.v(TAG, "schedule timeout for $key") + Log.v(TAG, "schedule timeout for $key playing $isPlaying, $resumption") } - if (cancellation != null) { + if (cancellation != null && !resumptionChanged) { + // if the media changed resume state, we'll need to adjust the timeout length if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.") return } - expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state") + expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state, $resumption") + val timeout = if (mediaData.resumption) { + RESUME_MEDIA_TIMEOUT + } else { + PAUSED_MEDIA_TIMEOUT + } cancellation = mainExecutor.executeDelayed({ cancellation = null if (DEBUG) { @@ -195,7 +219,7 @@ class MediaTimeoutListener @Inject constructor( timedOut = true // this event is async, so it's safe even when `dispatchEvents` is false timeoutCallback(key, timedOut) - }, PAUSED_MEDIA_TIMEOUT) + }, timeout) } else { expireMediaTimeout(key, "playback started - $state, $key") timedOut = false diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index ba6dfd3c453a7..5c3108c331476 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -163,7 +163,7 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testSetTimedOut_deactivatesMedia() { + fun testSetTimedOut_active_deactivatesMedia() { val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null, appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(), actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null, @@ -175,6 +175,25 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(data.active).isFalse() } + @Test + fun testSetTimedOut_resume_dismissesMedia() { + // WHEN resume controls are present, and time out + val desc = MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, + APP_NAME, pendingIntent, PACKAGE_NAME) + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true) + + // THEN it is removed and listeners are informed + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + verify(listener).onMediaDataRemoved(PACKAGE_NAME) + } + @Test fun testLoadsMetadataOnBackground() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt index 150f4545bd43d..359746bca0ff1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt @@ -91,10 +91,12 @@ class MediaResumeListenerTest : SysuiTestCase() { @Captor lateinit var callbackCaptor: ArgumentCaptor @Captor lateinit var actionCaptor: ArgumentCaptor + @Captor lateinit var componentCaptor: ArgumentCaptor private lateinit var executor: FakeExecutor private lateinit var data: MediaData private lateinit var resumeListener: MediaResumeListener + private val clock = FakeSystemClock() private var originalQsSetting = Settings.Global.getInt(context.contentResolver, Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1) @@ -122,9 +124,9 @@ class MediaResumeListenerTest : SysuiTestCase() { whenever(mockContext.packageManager).thenReturn(context.packageManager) whenever(mockContext.contentResolver).thenReturn(context.contentResolver) - executor = FakeExecutor(FakeSystemClock()) + executor = FakeExecutor(clock) resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, - tunerService, resumeBrowserFactory, dumpManager) + tunerService, resumeBrowserFactory, dumpManager, clock) resumeListener.setManager(mediaDataManager) mediaDataManager.addListener(resumeListener) @@ -163,7 +165,7 @@ class MediaResumeListenerTest : SysuiTestCase() { // When listener is created, we do NOT register a user change listener val listener = MediaResumeListener(context, broadcastDispatcher, executor, tunerService, - resumeBrowserFactory, dumpManager) + resumeBrowserFactory, dumpManager, clock) listener.setManager(mediaDataManager) verify(broadcastDispatcher, never()).registerReceiver(eq(listener.userChangeReceiver), any(), any(), any()) @@ -328,4 +330,109 @@ class MediaResumeListenerTest : SysuiTestCase() { // Then we call restart verify(resumeBrowser).restart() } + + @Test + fun testOnUserUnlock_missingTime_saves() { + val currentTime = clock.currentTimeMillis() + + // When resume components without a last played time are loaded + testOnUserUnlock_loadsTracks() + + // Then we save an update with the current time + verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) + componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) + ?.dropLastWhile { it.isEmpty() }.forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } + verify(sharedPrefsEditor, times(1)).apply() + } + + @Test + fun testLoadComponents_recentlyPlayed_adds() { + // Set up browser to return successfully + val description = MediaDescription.Builder().setTitle(TITLE).build() + val component = ComponentName(PACKAGE_NAME, CLASS_NAME) + whenever(resumeBrowser.token).thenReturn(token) + whenever(resumeBrowser.appIntent).thenReturn(pendingIntent) + whenever(resumeBrowser.findRecentMedia()).thenAnswer { + callbackCaptor.value.addTrack(description, component, resumeBrowser) + } + + // Set up shared preferences to have a component with a recent lastplayed time + val lastPlayed = clock.currentTimeMillis() + val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" + whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) + val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, + tunerService, resumeBrowserFactory, dumpManager, clock) + resumeListener.setManager(mediaDataManager) + mediaDataManager.addListener(resumeListener) + + // When we load a component that was played recently + val intent = Intent(Intent.ACTION_USER_UNLOCKED) + resumeListener.userChangeReceiver.onReceive(mockContext, intent) + + // We add its resume controls + verify(resumeBrowser, times(1)).findRecentMedia() + verify(mediaDataManager, times(1)).addResumptionControls(anyInt(), + any(), any(), any(), any(), any(), eq(PACKAGE_NAME)) + } + + @Test + fun testLoadComponents_old_ignores() { + // Set up shared preferences to have a component with an old lastplayed time + val lastPlayed = clock.currentTimeMillis() - RESUME_MEDIA_TIMEOUT - 100 + val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" + whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) + val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, + tunerService, resumeBrowserFactory, dumpManager, clock) + resumeListener.setManager(mediaDataManager) + mediaDataManager.addListener(resumeListener) + + // When we load a component that is not recent + val intent = Intent(Intent.ACTION_USER_UNLOCKED) + resumeListener.userChangeReceiver.onReceive(mockContext, intent) + + // We do not try to add resume controls + verify(resumeBrowser, times(0)).findRecentMedia() + verify(mediaDataManager, times(0)).addResumptionControls(anyInt(), + any(), any(), any(), any(), any(), any()) + } + + @Test + fun testOnLoad_hasService_updatesLastPlayed() { + // Set up browser to return successfully + val description = MediaDescription.Builder().setTitle(TITLE).build() + val component = ComponentName(PACKAGE_NAME, CLASS_NAME) + whenever(resumeBrowser.token).thenReturn(token) + whenever(resumeBrowser.appIntent).thenReturn(pendingIntent) + whenever(resumeBrowser.findRecentMedia()).thenAnswer { + callbackCaptor.value.addTrack(description, component, resumeBrowser) + } + + // Set up shared preferences to have a component with a lastplayed time + val currentTime = clock.currentTimeMillis() + val lastPlayed = currentTime - 1000 + val componentsString = "$PACKAGE_NAME/$CLASS_NAME/$lastPlayed:" + whenever(sharedPrefs.getString(any(), any())).thenReturn(componentsString) + val resumeListener = MediaResumeListener(mockContext, broadcastDispatcher, executor, + tunerService, resumeBrowserFactory, dumpManager, clock) + resumeListener.setManager(mediaDataManager) + mediaDataManager.addListener(resumeListener) + + // When media data is loaded that has not been checked yet, and does have a MBS + val dataCopy = data.copy(resumeAction = null, hasCheckedForResume = false) + resumeListener.onMediaDataLoaded(KEY, null, dataCopy) + + // Then we store the new lastPlayed time + verify(sharedPrefsEditor).putString(any(), (capture(componentCaptor))) + componentCaptor.value.split(ResumeMediaBrowser.DELIMITER.toRegex()) + ?.dropLastWhile { it.isEmpty() }.forEach { + val result = it.split("/") + assertThat(result.size).isEqualTo(3) + assertThat(result[2].toLong()).isEqualTo(currentTime) + } + verify(sharedPrefsEditor, times(1)).apply() + } } \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 0a573cd6020c5..de2235dd1c420 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -71,6 +71,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { private lateinit var playbackBuilder: PlaybackState.Builder private lateinit var session: MediaSession private lateinit var mediaData: MediaData + private lateinit var resumeData: MediaData private lateinit var mediaTimeoutListener: MediaTimeoutListener @Before @@ -97,6 +98,10 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) + + resumeData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, + emptyList(), emptyList(), PACKAGE, null, clickIntent = null, + device = null, active = false, resumeAction = null, resumption = true) } @Test @@ -120,6 +125,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) assertThat(executor.numPending()).isEqualTo(1) verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) + assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @Test @@ -188,6 +194,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()) assertThat(executor.numPending()).isEqualTo(1) + assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) } @Test @@ -229,7 +236,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { } @Test - fun testOnSessionDestroyed_clearsTimeout() { + fun testOnSessionDestroyed_active_clearsTimeout() { // GIVEN media that is paused val mediaPaused = mediaData.copy(isPlaying = false) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPaused) @@ -247,7 +254,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { @Test fun testSessionDestroyed_thenRestarts_resetsTimeout() { // Assuming we have previously destroyed the session - testOnSessionDestroyed_clearsTimeout() + testOnSessionDestroyed_active_clearsTimeout() // WHEN we get an update with media playing val playingState = mock(android.media.session.PlaybackState::class.java) @@ -264,4 +271,91 @@ class MediaTimeoutListenerTest : SysuiTestCase() { } verify(timeoutCallback).invoke(eq(KEY), eq(false)) } + + @Test + fun testOnSessionDestroyed_resume_continuesTimeout() { + // GIVEN resume media with session info + val resumeWithSession = resumeData.copy(token = session.sessionToken) + mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeWithSession) + verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) + assertThat(executor.numPending()).isEqualTo(1) + + // WHEN the session is destroyed + mediaCallbackCaptor.value.onSessionDestroyed() + + // THEN the controller is unregistered, but the timeout is still scheduled + verify(mediaController).unregisterCallback(anyObject()) + assertThat(executor.numPending()).isEqualTo(1) + } + + @Test + fun testOnMediaDataLoaded_activeToResume_registersTimeout() { + // WHEN a regular media is loaded + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + + // AND it turns into a resume control + mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData) + + // THEN we register a timeout + assertThat(executor.numPending()).isEqualTo(1) + verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) + assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + } + + @Test + fun testOnMediaDataLoaded_pausedToResume_updatesTimeout() { + // WHEN regular media is paused + val pausedState = PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f) + .build() + `when`(mediaController.playbackState).thenReturn(pausedState) + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + assertThat(executor.numPending()).isEqualTo(1) + + // AND it turns into a resume control + mediaTimeoutListener.onMediaDataLoaded(PACKAGE, KEY, resumeData) + + // THEN we update the timeout length + assertThat(executor.numPending()).isEqualTo(1) + verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) + assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + } + + @Test + fun testOnMediaDataLoaded_resumption_registersTimeout() { + // WHEN a resume media is loaded + mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) + + // THEN we register a timeout + assertThat(executor.numPending()).isEqualTo(1) + verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) + assertThat(executor.advanceClockToNext()).isEqualTo(RESUME_MEDIA_TIMEOUT) + } + + @Test + fun testOnMediaDataLoaded_resumeToActive_updatesTimeout() { + // WHEN we have a resume control + mediaTimeoutListener.onMediaDataLoaded(PACKAGE, null, resumeData) + + // AND that media is resumed + val playingState = PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 0f) + .build() + `when`(mediaController.playbackState).thenReturn(playingState) + mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) + + // THEN the timeout length is changed to a regular media control + assertThat(executor.advanceClockToNext()).isEqualTo(PAUSED_MEDIA_TIMEOUT) + } + + @Test + fun testOnMediaDataRemoved_resume_timeoutCancelled() { + // WHEN we have a resume control + testOnMediaDataLoaded_resumption_registersTimeout() + // AND the media is removed + mediaTimeoutListener.onMediaDataRemoved(PACKAGE) + + // THEN the timeout runnable is cancelled + assertThat(executor.numPending()).isEqualTo(0) + } }