Merge "Show recently active media on smartspace signal" into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
5529e39aa5
@@ -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. */
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user