diff options
| author | 2021-04-23 22:21:54 +0000 | |
|---|---|---|
| committer | 2021-04-23 22:21:54 +0000 | |
| commit | 5529e39aa52bf472f17237eb770547adb95b77de (patch) | |
| tree | 364ed5a3a394a82c619ff23f1a61b062d213423b | |
| parent | 0e7cd4bd0db79fc4948e1c95a0d520fa49d8268b (diff) | |
| parent | 3397068594dbdb899757b071a8701789c3ac3ab2 (diff) | |
Merge "Show recently active media on smartspace signal" into sc-dev
6 files changed, 180 insertions, 11 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 0ed96eeac402..6ef29d694873 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -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. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt index a274eabed198..5f73ae0814c7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -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 @@ -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). @@ -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) { @@ -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) } } @@ -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) } } @@ -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() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 5ba04a03a8d5..09ebeed38ba4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -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 @@ -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)) } } @@ -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 @@ -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)) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 609b8474d134..4a487be914c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt index 36b6527167f2..a9d256bf37cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt @@ -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 @@ -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() @@ -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 @@ -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) { @@ -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() } } 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 96eb4b096931..7486612926a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -280,11 +280,12 @@ class MediaDataManagerTest : SysuiTestCase() { @Test fun testAddResumptionControls() { - // WHEN resumption controls are added` + // WHEN resumption controls are added val desc = MediaDescription.Builder().run { setTitle(SESSION_TITLE) build() } + val currentTimeMillis = System.currentTimeMillis() mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, APP_NAME, pendingIntent, PACKAGE_NAME) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) @@ -296,6 +297,7 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(data.song).isEqualTo(SESSION_TITLE) assertThat(data.app).isEqualTo(APP_NAME) assertThat(data.actions).hasSize(1) + assertThat(data.lastActive).isAtLeast(currentTimeMillis) } @Test @@ -350,4 +352,52 @@ class MediaDataManagerTest : SysuiTestCase() { smartspaceMediaDataProvider.onTargetsAvailable(listOf()) verify(listener).onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE) } + + @Test + fun testOnMediaDataChanged_updatesLastActiveTime() { + val currentTimeMillis = System.currentTimeMillis() + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTimeMillis) + } + + @Test + fun testOnMediaDataTimedOut_doesNotUpdateLastActiveTime() { + // GIVEN that the manager has a notification + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // WHEN the notification times out + val currentTimeMillis = System.currentTimeMillis() + mediaDataManager.setTimedOut(KEY, true, true) + + // THEN the last active time is not changed + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor)) + assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTimeMillis) + } + + @Test + fun testOnActiveMediaConverted_doesNotUpdateLastActiveTime() { + // GIVEN that the manager has a notification with a resume action + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + + // WHEN the notification is removed + val currentTimeMillis = System.currentTimeMillis() + mediaDataManager.onNotificationRemoved(KEY) + + // THEN the last active time is not changed + verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor)) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTimeMillis) + } } |