Loading packages/SystemUI/src/com/android/systemui/media/MediaData.kt +6 −1 Original line number Diff line number Diff line Loading @@ -104,7 +104,12 @@ data class MediaData( /** * Set from the notification and used as fallback when PlaybackState cannot be determined */ val isClearable: Boolean = true val isClearable: Boolean = true, /** * Timestamp when this player was last active. */ var lastActive: Long = 0L ) /** State of a media action. */ Loading packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +53 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.media import android.app.smartspace.SmartspaceTarget import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher Loading @@ -24,14 +25,23 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import javax.inject.Inject private const val TAG = "MediaDataFilter" private const val DEBUG = true /** * Maximum age of a media control to re-activate on smartspace signal. If there is no media control * available within this time window, smartspace recommendations will be shown instead. */ private val SMARTSPACE_MAX_AGE = SystemProperties .getLong("debug.sysui.smartspace_max_age", TimeUnit.HOURS.toMillis(3)) /** * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user * switches (removing entries for the previous user, adding back entries for the current user) * switches (removing entries for the previous user, adding back entries for the current user). Also * filters out smartspace updates in favor of local recent media, when avaialble. * * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). Loading @@ -52,6 +62,7 @@ class MediaDataFilter @Inject constructor( // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private var hasSmartspace: Boolean = false private var reactivatedKey: String? = null init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { Loading Loading @@ -86,6 +97,30 @@ class MediaDataFilter @Inject constructor( override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { hasSmartspace = true // Before forwarding the smartspace target, first check if we have recently inactive media val now = System.currentTimeMillis() val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) if (sorted.size > 0) { val lastActiveKey = sorted.lastKey() // most recently active val timeSinceActive = sorted.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE if (timeSinceActive < SMARTSPACE_MAX_AGE) { // Notify listeners to consider this media active Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") reactivatedKey = lastActiveKey val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) listeners.forEach { it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData) } return } } // If no recent media, continue with smartspace update listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) } } Loading @@ -101,6 +136,21 @@ class MediaDataFilter @Inject constructor( override fun onSmartspaceMediaDataRemoved(key: String) { hasSmartspace = false // First check if we had reactivated media instead of forwarding smartspace reactivatedKey?.let { val lastActiveKey = it reactivatedKey = null Log.d(TAG, "expiring reactivated key $lastActiveKey") // Notify listeners to update with actual active value userEntries.get(lastActiveKey)?.let { mediaData -> listeners.forEach { it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData) } } return } listeners.forEach { it.onSmartspaceMediaDataRemoved(key) } } Loading Loading @@ -137,7 +187,8 @@ class MediaDataFilter @Inject constructor( if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = userEntries.keys.toSet() mediaKeys.forEach { mediaDataManager.setTimedOut(it, timedOut = true) // Force updates to listeners, needed for re-activated card mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) } if (hasSmartspace) { mediaDataManager.dismissSmartspaceRecommendation() Loading packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +7 −5 Original line number Diff line number Diff line Loading @@ -394,9 +394,9 @@ 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) { internal fun setTimedOut(token: String, timedOut: Boolean, forceUpdate: Boolean = false) { mediaEntries[token]?.let { if (it.active == !timedOut) { if (it.active == !timedOut && !forceUpdate) { return } it.active = !timedOut Loading Loading @@ -470,12 +470,13 @@ class MediaDataManager( } val mediaAction = getResumeMediaAction(resumeAction) val lastActive = System.currentTimeMillis() foregroundExecutor.execute { onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName, null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), packageName, token, appIntent, device = null, active = false, resumeAction = resumeAction, resumption = true, notificationKey = packageName, hasCheckedForResume = true)) hasCheckedForResume = true, lastActive = lastActive)) } } Loading Loading @@ -588,7 +589,7 @@ class MediaDataManager( val isLocalSession = mediaController.playbackInfo?.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null val lastActive = System.currentTimeMillis() foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true Loading @@ -598,7 +599,8 @@ class MediaDataManager( actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, active, resumeAction = resumeAction, isLocalSession = isLocalSession, notificationKey = key, hasCheckedForResume = hasCheckedForResume, isPlaying = isPlaying, isClearable = sbn.isClearable())) isPlaying = isPlaying, isClearable = sbn.isClearable(), lastActive = lastActive)) } } Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true, false, KEY, false, false, false); false, KEY, false, false, false, 0L); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +62 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.media import android.app.smartspace.SmartspaceTarget import android.graphics.Color import androidx.test.filters.SmallTest import android.testing.AndroidTestingRunner Loading Loading @@ -47,6 +48,7 @@ private const val PACKAGE = "PKG" private const val ARTIST = "ARTIST" private const val TITLE = "TITLE" private const val DEVICE_NAME = "DEVICE_NAME" private const val SMARTSPACE_KEY = "SMARTSPACE_KEY" private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> any(): T = Mockito.any() Loading @@ -68,6 +70,8 @@ class MediaDataFilterTest : SysuiTestCase() { private lateinit var lockscreenUserManager: NotificationLockscreenUserManager @Mock private lateinit var executor: Executor @Mock private lateinit var smartspaceData: SmartspaceTarget private lateinit var mediaDataFilter: MediaDataFilter private lateinit var dataMain: MediaData Loading @@ -91,6 +95,8 @@ class MediaDataFilterTest : SysuiTestCase() { dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, null, null, device, true, null) `when`(smartspaceData.smartspaceTargetId).thenReturn(SMARTSPACE_KEY) } private fun setUser(id: Int) { Loading Loading @@ -212,6 +218,61 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onSwipeToDismiss() verify(mediaDataManager).setTimedOut(eq(KEY), eq(true)) verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true)) } @Test fun testOnSmartspaceMediaDataLoaded_noMedia_usesSmartspace() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataLoaded_noRecentMedia_usesSmartspace() { val dataOld = dataMain.copy(active = false, lastActive = 0L) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_usesMedia() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsMedia() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } @Test fun testOnSmartspaceMediaDataRemoved_usedMedia_clearsMedia() { val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent)) assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } } Loading
packages/SystemUI/src/com/android/systemui/media/MediaData.kt +6 −1 Original line number Diff line number Diff line Loading @@ -104,7 +104,12 @@ data class MediaData( /** * Set from the notification and used as fallback when PlaybackState cannot be determined */ val isClearable: Boolean = true val isClearable: Boolean = true, /** * Timestamp when this player was last active. */ var lastActive: Long = 0L ) /** State of a media action. */ Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +53 −2 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.media import android.app.smartspace.SmartspaceTarget import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher Loading @@ -24,14 +25,23 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import javax.inject.Inject private const val TAG = "MediaDataFilter" private const val DEBUG = true /** * Maximum age of a media control to re-activate on smartspace signal. If there is no media control * available within this time window, smartspace recommendations will be shown instead. */ private val SMARTSPACE_MAX_AGE = SystemProperties .getLong("debug.sysui.smartspace_max_age", TimeUnit.HOURS.toMillis(3)) /** * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user * switches (removing entries for the previous user, adding back entries for the current user) * switches (removing entries for the previous user, adding back entries for the current user). Also * filters out smartspace updates in favor of local recent media, when avaialble. * * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). Loading @@ -52,6 +62,7 @@ class MediaDataFilter @Inject constructor( // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() private var hasSmartspace: Boolean = false private var reactivatedKey: String? = null init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { Loading Loading @@ -86,6 +97,30 @@ class MediaDataFilter @Inject constructor( override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) { hasSmartspace = true // Before forwarding the smartspace target, first check if we have recently inactive media val now = System.currentTimeMillis() val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) if (sorted.size > 0) { val lastActiveKey = sorted.lastKey() // most recently active val timeSinceActive = sorted.get(lastActiveKey)?.let { now - it.lastActive } ?: Long.MAX_VALUE if (timeSinceActive < SMARTSPACE_MAX_AGE) { // Notify listeners to consider this media active Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") reactivatedKey = lastActiveKey val mediaData = sorted.get(lastActiveKey)!!.copy(active = true) listeners.forEach { it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData) } return } } // If no recent media, continue with smartspace update listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) } } Loading @@ -101,6 +136,21 @@ class MediaDataFilter @Inject constructor( override fun onSmartspaceMediaDataRemoved(key: String) { hasSmartspace = false // First check if we had reactivated media instead of forwarding smartspace reactivatedKey?.let { val lastActiveKey = it reactivatedKey = null Log.d(TAG, "expiring reactivated key $lastActiveKey") // Notify listeners to update with actual active value userEntries.get(lastActiveKey)?.let { mediaData -> listeners.forEach { it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData) } } return } listeners.forEach { it.onSmartspaceMediaDataRemoved(key) } } Loading Loading @@ -137,7 +187,8 @@ class MediaDataFilter @Inject constructor( if (DEBUG) Log.d(TAG, "Media carousel swiped away") val mediaKeys = userEntries.keys.toSet() mediaKeys.forEach { mediaDataManager.setTimedOut(it, timedOut = true) // Force updates to listeners, needed for re-activated card mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) } if (hasSmartspace) { mediaDataManager.dismissSmartspaceRecommendation() Loading
packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +7 −5 Original line number Diff line number Diff line Loading @@ -394,9 +394,9 @@ 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) { internal fun setTimedOut(token: String, timedOut: Boolean, forceUpdate: Boolean = false) { mediaEntries[token]?.let { if (it.active == !timedOut) { if (it.active == !timedOut && !forceUpdate) { return } it.active = !timedOut Loading Loading @@ -470,12 +470,13 @@ class MediaDataManager( } val mediaAction = getResumeMediaAction(resumeAction) val lastActive = System.currentTimeMillis() foregroundExecutor.execute { onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName, null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), packageName, token, appIntent, device = null, active = false, resumeAction = resumeAction, resumption = true, notificationKey = packageName, hasCheckedForResume = true)) hasCheckedForResume = true, lastActive = lastActive)) } } Loading Loading @@ -588,7 +589,7 @@ class MediaDataManager( val isLocalSession = mediaController.playbackInfo?.playbackType == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null val lastActive = System.currentTimeMillis() foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true Loading @@ -598,7 +599,8 @@ class MediaDataManager( actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, active, resumeAction = resumeAction, isLocalSession = isLocalSession, notificationKey = key, hasCheckedForResume = hasCheckedForResume, isPlaying = isPlaying, isClearable = sbn.isClearable())) isPlaying = isPlaying, isClearable = sbn.isClearable(), lastActive = lastActive)) } } Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -74,7 +74,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, true, false, KEY, false, false, false); false, KEY, false, false, false, 0L); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); } Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +62 −1 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.media import android.app.smartspace.SmartspaceTarget import android.graphics.Color import androidx.test.filters.SmallTest import android.testing.AndroidTestingRunner Loading Loading @@ -47,6 +48,7 @@ private const val PACKAGE = "PKG" private const val ARTIST = "ARTIST" private const val TITLE = "TITLE" private const val DEVICE_NAME = "DEVICE_NAME" private const val SMARTSPACE_KEY = "SMARTSPACE_KEY" private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> any(): T = Mockito.any() Loading @@ -68,6 +70,8 @@ class MediaDataFilterTest : SysuiTestCase() { private lateinit var lockscreenUserManager: NotificationLockscreenUserManager @Mock private lateinit var executor: Executor @Mock private lateinit var smartspaceData: SmartspaceTarget private lateinit var mediaDataFilter: MediaDataFilter private lateinit var dataMain: MediaData Loading @@ -91,6 +95,8 @@ class MediaDataFilterTest : SysuiTestCase() { dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, null, null, device, true, null) `when`(smartspaceData.smartspaceTargetId).thenReturn(SMARTSPACE_KEY) } private fun setUser(id: Int) { Loading Loading @@ -212,6 +218,61 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onSwipeToDismiss() verify(mediaDataManager).setTimedOut(eq(KEY), eq(true)) verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true)) } @Test fun testOnSmartspaceMediaDataLoaded_noMedia_usesSmartspace() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataLoaded_noRecentMedia_usesSmartspace() { val dataOld = dataMain.copy(active = false, lastActive = 0L) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_usesMedia() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() } @Test fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsMedia() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } @Test fun testOnSmartspaceMediaDataRemoved_usedMedia_clearsMedia() { val dataCurrent = dataMain.copy(active = false, lastActive = System.currentTimeMillis()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent)) assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } }