Allow inactive recommendation card and set timeout

When the flag is enabled, the recommendation card can be inactive,
rather than being removed immediately once it is swiped away. If the
smartspace update is marked as "PERIODIC_TRIGGER", it will not make the
carousel active. The card can still be removed manually with the long
press menu. In addition recommendation cards will now be removed automatically
when its expiry time is reached rather than waiting for an empty update.

Smartspace updates not marked as periodic will have the same behavior as
before (make carousel active, showing recent media first if it exists)

Test: atest com.android.systemui.media.controls
Test: manual with test build + smartspace flags
Bug: 264690420
Change-Id: I78d365a2df8b7654041bd425745343232cd19bc4
This commit is contained in:
Beth Thibodeau
2023-01-29 17:51:15 -06:00
parent 5c906402b9
commit 243ed58819
14 changed files with 610 additions and 88 deletions

View File

@@ -364,6 +364,9 @@ object Flags {
// TODO(b/267007629): Tracking Bug
val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress")
// TODO(b/267166152) : Tracking Bug
val MEDIA_RETAIN_RECOMMENDATIONS = unreleasedFlag(916, "media_retain_recommendations")
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -41,10 +41,12 @@ data class SmartspaceMediaData(
val recommendations: List<SmartspaceAction>,
/** Intent for the user's initiated dismissal. */
val dismissIntent: Intent?,
/** The timestamp in milliseconds that headphone is connected. */
/** The timestamp in milliseconds that the card was generated */
val headphoneConnectionTimeMillis: Long,
/** Instance ID for [MediaUiEventLogger] */
val instanceId: InstanceId
val instanceId: InstanceId,
/** The timestamp in milliseconds indicating when the card should be removed */
val expiryTimeMs: Long,
) {
/**
* Indicates if all the data is valid.
@@ -86,5 +88,12 @@ data class SmartspaceMediaData(
}
}
/** Key for extras [SmartspaceMediaData.cardAction] indicating why the card was sent */
const val EXTRA_KEY_TRIGGER_SOURCE = "MEDIA_RECOMMENDATION_TRIGGER_SOURCE"
/** Value for [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent on headphone connection */
const val EXTRA_VALUE_TRIGGER_HEADPHONE = "HEADPHONE_CONNECTION"
/** Value for key [EXTRA_KEY_TRIGGER_SOURCE] when the card is sent as a regular update */
const val EXTRA_VALUE_TRIGGER_PERIODIC = "PERIODIC_TRIGGER"
const val NUM_REQUIRED_RECOMMENDATIONS = 3
private val TAG = SmartspaceMediaData::class.simpleName!!

View File

@@ -24,6 +24,7 @@ import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,7 +67,8 @@ constructor(
private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor,
private val systemClock: SystemClock,
private val logger: MediaUiEventLogger
private val logger: MediaUiEventLogger,
private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
internal val listeners: Set<MediaDataManager.Listener>
@@ -121,7 +123,9 @@ constructor(
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
if (!data.isActive) {
// With persistent recommendation card, we could get a background update while inactive
// Otherwise, consider it an invalid update
if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) {
Log.d(TAG, "Inactive recommendation data. Skip triggering.")
return
}
@@ -141,7 +145,7 @@ constructor(
}
}
val shouldReactivate = !hasActiveMedia() && hasAnyMedia()
val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive
if (timeSinceActive < smartspaceMaxAgeMillis) {
// It could happen there are existing active media resume cards, then we don't need to
@@ -169,7 +173,7 @@ constructor(
)
}
}
} else {
} else if (data.isActive) {
// Mark to prioritize Smartspace card if no recent media.
shouldPrioritizeMutable = true
}
@@ -252,7 +256,7 @@ constructor(
if (dismissIntent == null) {
Log.w(
TAG,
"Cannot create dismiss action click action: " + "extras missing dismiss_intent."
"Cannot create dismiss action click action: extras missing dismiss_intent."
)
} else if (
dismissIntent.getComponent() != null &&
@@ -264,15 +268,21 @@ constructor(
} else {
broadcastSender.sendBroadcast(dismissIntent)
}
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
if (mediaFlags.isPersistentSsCardEnabled()) {
smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId)
} else {
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId,
)
mediaDataManager.dismissSmartspaceRecommendation(
smartspaceMediaData.targetId,
delay = 0L,
)
mediaDataManager.dismissSmartspaceRecommendation(
smartspaceMediaData.targetId,
delay = 0L
)
}
}
}
@@ -283,8 +293,15 @@ constructor(
(smartspaceMediaData.isValid() || reactivatedKey != null))
/** Are there any media entries we should display? */
fun hasAnyMediaOrRecommendation() =
userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid())
fun hasAnyMediaOrRecommendation(): Boolean {
val hasSmartspace =
if (mediaFlags.isPersistentSsCardEnabled()) {
smartspaceMediaData.isValid()
} else {
smartspaceMediaData.isActive && smartspaceMediaData.isValid()
}
return userEntries.isNotEmpty() || hasSmartspace
}
/** Are there any media notifications active (excluding the recommendation)? */
fun hasActiveMedia() = userEntries.any { it.value.active }

View File

@@ -49,7 +49,6 @@ import android.support.v4.media.MediaMetadataCompat
import android.text.TextUtils
import android.util.Log
import androidx.media.utils.MediaConstants
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.R
@@ -63,6 +62,8 @@ import com.android.systemui.media.controls.models.player.MediaButton
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.models.player.MediaViewHolder
import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener
@@ -119,7 +120,6 @@ private val LOADING =
appUid = Process.INVALID_UID
)
@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA =
SmartspaceMediaData(
targetId = "INVALID",
@@ -129,7 +129,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA =
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1)
instanceId = InstanceId.fakeInstanceId(-1),
expiryTimeMs = 0,
)
fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -548,6 +549,11 @@ class MediaDataManager(
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it)
}
if (key == smartspaceMediaData.targetId) {
if (DEBUG) Log.d(TAG, "smartspace card expired")
dismissSmartspaceRecommendation(key, delay = 0L)
}
}
/** Called when the player's [PlaybackState] has been updated with new actions and/or state */
@@ -605,8 +611,8 @@ class MediaDataManager(
}
/**
* Called whenever the recommendation has been expired, or swiped from QQS. This will make the
* recommendation view to not be shown anymore during this headphone connection session.
* Called whenever the recommendation has been expired or removed by the user. This will remove
* the recommendation card entirely from the carousel.
*/
fun dismissSmartspaceRecommendation(key: String, delay: Long) {
if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
@@ -628,6 +634,23 @@ class MediaDataManager(
)
}
/** Called when the recommendation card should no longer be visible in QQS or lockscreen */
fun setRecommendationInactive(key: String) {
if (!mediaFlags.isPersistentSsCardEnabled()) {
Log.e(TAG, "Only persistent recommendation can be inactive!")
return
}
if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive")
if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) {
// If this doesn't match, or we've already invalidated the data, no action needed
return
}
smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
}
private fun loadMediaDataInBgForResumption(
userId: Int,
desc: MediaDescription,
@@ -1265,12 +1288,25 @@ class MediaDataManager(
if (DEBUG) {
Log.d(TAG, "Set Smartspace media to be inactive for the data update")
}
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId
if (mediaFlags.isPersistentSsCardEnabled()) {
// Smartspace uses this signal to hide the card (e.g. when it expires or user
// disconnects headphones), so treat as setting inactive when flag is on
smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
notifySmartspaceMediaDataLoaded(
smartspaceMediaData.targetId,
smartspaceMediaData,
)
notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false)
} else {
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = smartspaceMediaData.targetId,
instanceId = smartspaceMediaData.instanceId,
)
notifySmartspaceMediaDataRemoved(
smartspaceMediaData.targetId,
immediately = false,
)
}
}
1 -> {
val newMediaTarget = mediaTargets.get(0)
@@ -1279,7 +1315,7 @@ class MediaDataManager(
return
}
if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true)
smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
}
else -> {
@@ -1288,7 +1324,7 @@ class MediaDataManager(
Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
notifySmartspaceMediaDataRemoved(
smartspaceMediaData.targetId,
false /* immediately */
immediately = false,
)
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA
}
@@ -1494,21 +1530,28 @@ class MediaDataManager(
}
/**
* Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status.
* Converts the pass-in SmartspaceTarget to SmartspaceMediaData
*
* @return An empty SmartspaceMediaData with the valid target Id is returned if the
* SmartspaceTarget's data is invalid.
*/
private fun toSmartspaceMediaData(
target: SmartspaceTarget,
isActive: Boolean
): SmartspaceMediaData {
private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
var dismissIntent: Intent? = null
if (target.baseAction != null && target.baseAction.extras != null) {
dismissIntent =
target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
as Intent?
}
val isActive =
when {
!mediaFlags.isPersistentSsCardEnabled() -> true
target.baseAction == null -> true
else ->
target.baseAction.extras.getString(EXTRA_KEY_TRIGGER_SOURCE) !=
EXTRA_VALUE_TRIGGER_PERIODIC
}
packageName(target)?.let {
return SmartspaceMediaData(
targetId = target.smartspaceTargetId,
@@ -1518,7 +1561,8 @@ class MediaDataManager(
recommendations = target.iconGrid,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
instanceId = logger.getNewInstanceId()
instanceId = logger.getNewInstanceId(),
expiryTimeMs = target.expiryTimeMillis,
)
}
return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -1526,7 +1570,8 @@ class MediaDataManager(
isActive = isActive,
dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis,
instanceId = logger.getNewInstanceId()
instanceId = logger.getNewInstanceId(),
expiryTimeMs = target.expiryTimeMillis,
)
}

View File

@@ -23,7 +23,9 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -49,10 +51,12 @@ constructor(
@Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController,
private val systemClock: SystemClock
private val systemClock: SystemClock,
private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener {
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
/**
* Callback representing that a media object is now expired:
@@ -93,6 +97,16 @@ constructor(
listener.doTimeout()
}
}
recommendationListeners.forEach { (key, listener) ->
if (
listener.cancellation != null &&
listener.expiration <= systemClock.currentTimeMillis()
) {
logger.logTimeoutCancelled(key, "Timed out while dozing")
listener.doTimeout()
}
}
}
}
}
@@ -155,6 +169,30 @@ constructor(
mediaListeners.remove(key)?.destroy()
}
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
if (!mediaFlags.isPersistentSsCardEnabled()) return
// First check if we already have a listener
recommendationListeners.get(key)?.let {
if (!it.destroyed) {
it.recommendationData = data
return
}
}
// Otherwise, create a new one
recommendationListeners[key] = RecommendationListener(key, data)
}
override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {
if (!mediaFlags.isPersistentSsCardEnabled()) return
recommendationListeners.remove(key)?.destroy()
}
fun isTimedOut(key: String): Boolean {
return mediaListeners[key]?.timedOut ?: false
}
@@ -335,4 +373,53 @@ constructor(
}
return true
}
/** Listens to changes in recommendation card data and schedules a timeout for its expiration */
private inner class RecommendationListener(var key: String, data: SmartspaceMediaData) {
private var timedOut = false
var destroyed = false
var expiration = Long.MAX_VALUE
private set
var cancellation: Runnable? = null
private set
var recommendationData: SmartspaceMediaData = data
set(value) {
destroyed = false
field = value
processUpdate()
}
init {
recommendationData = data
}
fun destroy() {
cancellation?.run()
cancellation = null
destroyed = true
}
private fun processUpdate() {
if (recommendationData.expiryTimeMs != expiration) {
// The expiry time changed - cancel and reschedule
val timeout =
recommendationData.expiryTimeMs -
recommendationData.headphoneConnectionTimeMillis
logger.logRecommendationTimeoutScheduled(key, timeout)
cancellation?.run()
cancellation = mainExecutor.executeDelayed({ doTimeout() }, timeout)
expiration = recommendationData.expiryTimeMs
}
}
fun doTimeout() {
cancellation?.run()
cancellation = null
logger.logTimeout(key)
timedOut = true
expiration = Long.MAX_VALUE
timeoutCallback(key, timedOut)
}
}
}

View File

@@ -107,6 +107,17 @@ constructor(@MediaTimeoutListenerLog private val buffer: LogBuffer) {
str1 = key
str2 = reason
},
{ "media timeout cancelled for $str1, reason: $str2" }
{ "timeout cancelled for $str1, reason: $str2" }
)
fun logRecommendationTimeoutScheduled(key: String, timeout: Long) =
buffer.log(
TAG,
LogLevel.VERBOSE,
{
str1 = key
long1 = timeout
},
{ "recommendation timeout scheduled for $str1 in $long1 ms" }
)
}

View File

@@ -368,7 +368,7 @@ constructor(
data: SmartspaceMediaData,
shouldPrioritize: Boolean
) {
debugLogger.logRecommendationLoaded(key)
debugLogger.logRecommendationLoaded(key, data.isActive)
// Log the case where the hidden media carousel with the existed inactive resume
// media is shown by the Smartspace signal.
if (data.isActive) {
@@ -442,7 +442,12 @@ constructor(
logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
}
} else {
onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
if (!mediaFlags.isPersistentSsCardEnabled()) {
// Handle update to inactive as a removal
onSmartspaceMediaDataRemoved(data.targetId, immediately = true)
} else {
addSmartspaceMediaRecommendations(key, data, shouldPrioritize)
}
}
}
@@ -633,7 +638,19 @@ constructor(
) =
traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel")
if (MediaPlayerData.getMediaPlayer(key) != null) {
MediaPlayerData.getMediaPlayer(key)?.let {
if (mediaFlags.isPersistentSsCardEnabled()) {
// The card exists, but could have changed active state, so update for sorting
MediaPlayerData.addMediaRecommendation(
key,
data,
it,
shouldPrioritize,
systemClock,
debugLogger,
update = true,
)
}
Log.w(TAG, "Skip adding smartspace target in carousel")
return
}
@@ -672,7 +689,7 @@ constructor(
newRecs,
shouldPrioritize,
systemClock,
debugLogger
debugLogger,
)
updatePlayerToState(newRecs, noAnimation = true)
reorderAllPlayers(curVisibleMediaKey)
@@ -1225,17 +1242,18 @@ internal object MediaPlayerData {
player: MediaControlPanel,
shouldPrioritize: Boolean,
clock: SystemClock,
debugLogger: MediaCarouselControllerLogger? = null
debugLogger: MediaCarouselControllerLogger? = null,
update: Boolean = false
) {
shouldPrioritizeSs = shouldPrioritize
val removedPlayer = removeMediaPlayer(key)
if (removedPlayer != null && removedPlayer != player) {
if (!update && removedPlayer != null && removedPlayer != player) {
debugLogger?.logPotentialMemoryLeak(key)
}
val sortKey =
MediaSortKey(
isSsMediaRec = true,
EMPTY.copy(isPlaying = false),
EMPTY.copy(active = data.isActive, isPlaying = false),
key,
clock.currentTimeMillis(),
isSsReactivated = true

View File

@@ -48,8 +48,16 @@ constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) {
fun logMediaRemoved(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
fun logRecommendationLoaded(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" })
fun logRecommendationLoaded(key: String, isActive: Boolean) =
buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
bool1 = isActive
},
{ "add recommendation $str1, active $bool1" }
)
fun logRecommendationRemoved(key: String, immediately: Boolean) =
buffer.log(

View File

@@ -58,4 +58,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** Check whether to get progress information for resume players */
fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS)
/** If true, do not automatically dismiss the recommendation card */
fun isPersistentSsCardEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_RECOMMENDATIONS)
}

View File

@@ -113,5 +113,6 @@ private val DEFAULT_DATA =
recommendations = emptyList(),
dismissIntent = null,
headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1)
instanceId = InstanceId.fakeInstanceId(-1),
expiryTimeMs = 0,
)

View File

@@ -27,11 +27,13 @@ import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.ui.MediaPlayerData
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
@@ -40,11 +42,11 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
private const val KEY = "TEST_KEY"
@@ -72,6 +74,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger
@Mock private lateinit var mediaFlags: MediaFlags
private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData
@@ -82,6 +85,7 @@ class MediaDataFilterTest : SysuiTestCase() {
fun setup() {
MockitoAnnotations.initMocks(this)
MediaPlayerData.clear()
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
mediaDataFilter =
MediaDataFilter(
context,
@@ -90,7 +94,8 @@ class MediaDataFilterTest : SysuiTestCase() {
lockscreenUserManager,
executor,
clock,
logger
logger,
mediaFlags
)
mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener)
@@ -108,19 +113,20 @@ class MediaDataFilterTest : SysuiTestCase() {
)
dataGuest = dataMain.copy(userId = USER_GUEST)
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
`when`(smartspaceData.isActive).thenReturn(true)
`when`(smartspaceData.isValid()).thenReturn(true)
`when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
`when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem))
`when`(smartspaceData.headphoneConnectionTimeMillis)
whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
whenever(smartspaceData.isActive).thenReturn(true)
whenever(smartspaceData.isValid()).thenReturn(true)
whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
whenever(smartspaceData.recommendations)
.thenReturn(listOf(smartspaceMediaRecommendationItem))
whenever(smartspaceData.headphoneConnectionTimeMillis)
.thenReturn(clock.currentTimeMillis() - 100)
`when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
}
private fun setUser(id: Int) {
`when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
`when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
mediaDataFilter.handleUserSwitched(id)
}
@@ -277,7 +283,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
`when`(smartspaceData.isActive).thenReturn(false)
whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -285,7 +291,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
`when`(smartspaceData.isValid()).thenReturn(false)
whenever(smartspaceData.isValid()).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -293,8 +299,8 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
`when`(smartspaceData.isActive).thenReturn(true)
`when`(smartspaceData.isValid()).thenReturn(true)
whenever(smartspaceData.isActive).thenReturn(true)
whenever(smartspaceData.isValid()).thenReturn(true)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
@@ -349,7 +355,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
`when`(smartspaceData.isActive).thenReturn(false)
whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -379,7 +385,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
`when`(smartspaceData.isActive).thenReturn(false)
whenever(smartspaceData.isActive).thenReturn(false)
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
@@ -395,7 +401,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() {
`when`(smartspaceData.isActive).thenReturn(false)
whenever(smartspaceData.isActive).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -418,7 +424,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() {
`when`(smartspaceData.isValid()).thenReturn(false)
whenever(smartspaceData.isValid()).thenReturn(false)
// WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -513,4 +519,59 @@ class MediaDataFilterTest : SysuiTestCase() {
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
}
@Test
fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
}
@Test
fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
whenever(smartspaceData.isActive).thenReturn(false)
// If there is media that was recently played but inactive
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
verify(listener)
.onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
// And an inactive recommendation is loaded
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
// Smartspace is loaded but the media stays inactive
verify(listener)
.onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
verify(listener, never())
.onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasAnyMediaOrRecommendation()).isTrue()
}
@Test
fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
val data =
EMPTY_SMARTSPACE_MEDIA_DATA.copy(
targetId = SMARTSPACE_KEY,
isActive = true,
packageName = SMARTSPACE_PACKAGE,
recommendations = listOf(smartspaceMediaRecommendationItem),
)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data)
mediaDataFilter.onSwipeToDismiss()
verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY))
verify(mediaDataManager, never())
.dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong())
}
}

View File

@@ -46,6 +46,8 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.EXTRA_KEY_TRIGGER_SOURCE
import com.android.systemui.media.controls.models.recommendation.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener
@@ -82,6 +84,8 @@ import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY"
private const val KEY_2 = "KEY_2"
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID"
private const val SMARTSPACE_CREATION_TIME = 1234L
private const val SMARTSPACE_EXPIRY_TIME = 5678L
private const val PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI"
@@ -230,10 +234,12 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList)
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L)
whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME)
whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME)
whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
}
@@ -847,8 +853,9 @@ class MediaDataManagerTest : SysuiTestCase() {
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId)
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -870,8 +877,9 @@ class MediaDataManagerTest : SysuiTestCase() {
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId)
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -901,8 +909,9 @@ class MediaDataManagerTest : SysuiTestCase() {
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
dismissIntent = null,
headphoneConnectionTimeMillis = 1234L,
instanceId = InstanceId.fakeInstanceId(instanceId)
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
@@ -930,6 +939,129 @@ class MediaDataManagerTest : SysuiTestCase() {
verifyNoMoreInteractions(logger)
}
@Test
fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
val instanceId = instanceIdSequence.lastInstanceId
verify(listener)
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = true,
packageName = PACKAGE_NAME,
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
)
}
@Test
fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
val extras =
Bundle().apply {
putString("package_name", PACKAGE_NAME)
putParcelable("dismiss_intent", DISMISS_INTENT)
putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC)
}
whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras)
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
val instanceId = instanceIdSequence.lastInstanceId
verify(listener)
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = false,
packageName = PACKAGE_NAME,
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
)
}
@Test
fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
val instanceId = instanceIdSequence.lastInstanceId
smartspaceMediaDataProvider.onTargetsAvailable(listOf())
uiExecutor.advanceClockToLast()
uiExecutor.runAllReady()
verify(listener)
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = false,
packageName = PACKAGE_NAME,
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
)
verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false))
}
@Test
fun testSetRecommendationInactive_notifiesListeners() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget))
val instanceId = instanceIdSequence.lastInstanceId
mediaDataManager.setRecommendationInactive(KEY_MEDIA_SMARTSPACE)
uiExecutor.advanceClockToLast()
uiExecutor.runAllReady()
verify(listener)
.onSmartspaceMediaDataLoaded(
eq(KEY_MEDIA_SMARTSPACE),
eq(
SmartspaceMediaData(
targetId = KEY_MEDIA_SMARTSPACE,
isActive = false,
packageName = PACKAGE_NAME,
cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
)
),
eq(false)
)
}
@Test
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
// WHEN media recommendation setting is off

View File

@@ -25,13 +25,16 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -48,7 +51,6 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY"
@@ -56,6 +58,7 @@ private const val PACKAGE = "PKG"
private const val SESSION_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE"
private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private fun <T> anyObject(): T {
return Mockito.anyObject<T>()
@@ -85,10 +88,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener
private var clock = FakeSystemClock()
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@Before
fun setup() {
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController)
whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
executor = FakeExecutor(clock)
mediaTimeoutListener =
MediaTimeoutListener(
@@ -96,7 +102,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
executor,
logger,
statusBarStateController,
clock
clock,
mediaFlags,
)
mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback
@@ -133,9 +140,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test
fun testOnMediaDataLoaded_registersPlaybackListener() {
val playingState = mock(android.media.session.PlaybackState::class.java)
`when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
`when`(mediaController.playbackState).thenReturn(playingState)
whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
verify(logger).logPlaybackState(eq(KEY), eq(playingState))
@@ -188,8 +195,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// To playing
val playingState = mock(android.media.session.PlaybackState::class.java)
`when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
`when`(mediaController.playbackState).thenReturn(playingState)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
verify(mediaController).unregisterCallback(anyObject())
verify(mediaController).registerCallback(anyObject())
@@ -208,8 +215,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Migrate, still not playing
val playingState = mock(android.media.session.PlaybackState::class.java)
`when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
`when`(mediaController.playbackState).thenReturn(playingState)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
// The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor
@@ -296,8 +303,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// WHEN we get an update with media playing
val playingState = mock(android.media.session.PlaybackState::class.java)
`when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
`when`(mediaController.playbackState).thenReturn(playingState)
whenever(playingState.state).thenReturn(PlaybackState.STATE_PLAYING)
whenever(mediaController.playbackState).thenReturn(playingState)
val mediaPlaying = mediaData.copy(isPlaying = true)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
@@ -347,7 +354,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// WHEN regular media is paused
val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(pausedState)
whenever(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1)
@@ -379,7 +386,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// AND that media is resumed
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(playingState)
whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
// THEN the timeout length is changed to a regular media control
@@ -593,8 +600,91 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
assertThat(executor.numPending()).isEqualTo(1)
}
@Test
fun testSmartspaceDataLoaded_schedulesTimeout() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
val duration = 60_000
val createTime = 1234L
val expireTime = createTime + duration
whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(executor.numPending()).isEqualTo(1)
assertThat(executor.advanceClockToNext()).isEqualTo(duration)
}
@Test
fun testSmartspaceMediaData_timesOut_invokesCallback() {
// Given a pending timeout
testSmartspaceDataLoaded_schedulesTimeout()
executor.runAllReady()
verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
}
@Test
fun testSmartspaceDataLoaded_alreadyExists_updatesTimeout() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
val duration = 100
val createTime = 1234L
val expireTime = createTime + duration
whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(executor.numPending()).isEqualTo(1)
val expiryLonger = expireTime + duration
whenever(smartspaceData.expiryTimeMs).thenReturn(expiryLonger)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(executor.numPending()).isEqualTo(1)
assertThat(executor.advanceClockToNext()).isEqualTo(duration * 2)
}
@Test
fun testSmartspaceDataRemoved_cancelTimeout() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(executor.numPending()).isEqualTo(1)
mediaTimeoutListener.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
assertThat(executor.numPending()).isEqualTo(0)
}
@Test
fun testSmartspaceData_dozedPastTimeout_invokedOnWakeup() {
// Given a pending timeout
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
val duration = 60_000
val createTime = 1234L
val expireTime = createTime + duration
whenever(smartspaceData.headphoneConnectionTimeMillis).thenReturn(createTime)
whenever(smartspaceData.expiryTimeMs).thenReturn(expireTime)
mediaTimeoutListener.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(executor.numPending()).isEqualTo(1)
// And we doze past the scheduled timeout
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time + duration * 2)
assertThat(executor.numPending()).isEqualTo(1)
// Then when no longer dozing, the timeout runs immediately
dozingCallbackCaptor.value.onDozingChanged(false)
verify(timeoutCallback).invoke(eq(SMARTSPACE_KEY), eq(true))
verify(logger).logTimeout(eq(SMARTSPACE_KEY))
// and cancel any later scheduled timeout
assertThat(executor.numPending()).isEqualTo(0)
}
private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
`when`(mediaController.playbackState).thenReturn(state)
whenever(mediaController.playbackState).thenReturn(state)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
}

View File

@@ -47,6 +47,7 @@ import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import javax.inject.Provider
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
@@ -126,6 +127,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
whenever(mediaControlPanelFactory.get()).thenReturn(panel)
whenever(panel.mediaViewController).thenReturn(mediaViewController)
whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
MediaPlayerData.clear()
}
@@ -703,4 +705,39 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
)
}
@Test
fun testRecommendation_persistentEnabled_newSmartspaceLoaded_updatesSort() {
testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded()
// When an update to existing smartspace data is loaded
listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA.copy(isActive = true),
true
)
// Then the carousel is updated
assertTrue(MediaPlayerData.playerKeys().elementAt(0).data.active)
assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
}
@Test
fun testRecommendation_persistentEnabled_inactiveSmartspaceDataLoaded_isAdded() {
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true)
// When inactive smartspace data is loaded
listener.value.onSmartspaceMediaDataLoaded(
SMARTSPACE_KEY,
EMPTY_SMARTSPACE_MEDIA_DATA,
false
)
// Then it is added to the carousel with correct state
assertTrue(MediaPlayerData.playerKeys().elementAt(0).isSsMediaRec)
assertFalse(MediaPlayerData.playerKeys().elementAt(0).data.active)
assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec)
assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
}
}