Merge "Show recently active media on smartspace signal" into sc-dev

This commit is contained in:
Beth Thibodeau
2021-04-23 22:21:54 +00:00
committed by Android (Google) Code Review
6 changed files with 180 additions and 11 deletions

View File

@@ -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. */

View File

@@ -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()

View File

@@ -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))
}
}

View File

@@ -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);
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}