Loading packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +10 −6 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } } Loading packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +39 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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<ComponentName> = ConcurrentLinkedQueue() private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> = ConcurrentLinkedQueue() private lateinit var mediaDataManager: MediaDataManager Loading Loading @@ -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() } } /** Loading @@ -149,11 +170,14 @@ class MediaResumeListener @Inject constructor( return } val now = systemClock.currentTimeMillis() resumeComponents.forEach { val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) { val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first) browser.findRecentMedia() } } } override fun onMediaDataLoaded( key: String, Loading Loading @@ -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) Loading packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +35 −11 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. */ Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -159,13 +168,20 @@ 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") } 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) { if (DEBUG) { Loading @@ -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) { Loading @@ -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 Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +20 −1 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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) Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt +110 −3 Original line number Diff line number Diff line Loading @@ -91,10 +91,12 @@ class MediaResumeListenerTest : SysuiTestCase() { @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback> @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable> @Captor lateinit var componentCaptor: ArgumentCaptor<String> 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) Loading Loading @@ -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) Loading Loading @@ -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()) Loading Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +10 −6 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } } Loading
packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +39 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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<ComponentName> = ConcurrentLinkedQueue() private val resumeComponents: ConcurrentLinkedQueue<Pair<ComponentName, Long>> = ConcurrentLinkedQueue() private lateinit var mediaDataManager: MediaDataManager Loading Loading @@ -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() } } /** Loading @@ -149,11 +170,14 @@ class MediaResumeListener @Inject constructor( return } val now = systemClock.currentTimeMillis() resumeComponents.forEach { val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) if (now.minus(it.second) <= RESUME_MEDIA_TIMEOUT) { val browser = mediaBrowserFactory.create(mediaBrowserCallback, it.first) browser.findRecentMedia() } } } override fun onMediaDataLoaded( key: String, Loading Loading @@ -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) Loading
packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +35 −11 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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. */ Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -159,13 +168,20 @@ 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") } 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) { if (DEBUG) { Loading @@ -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) { Loading @@ -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 Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +20 −1 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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) Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaResumeListenerTest.kt +110 −3 Original line number Diff line number Diff line Loading @@ -91,10 +91,12 @@ class MediaResumeListenerTest : SysuiTestCase() { @Captor lateinit var callbackCaptor: ArgumentCaptor<ResumeMediaBrowser.Callback> @Captor lateinit var actionCaptor: ArgumentCaptor<Runnable> @Captor lateinit var componentCaptor: ArgumentCaptor<String> 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) Loading Loading @@ -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) Loading Loading @@ -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()) Loading Loading @@ -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