Merge "Allow inactive recommendation card and set timeout" into tm-qpr-dev am: e221f89672

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21127520

Change-Id: Idb41291b458a85c52b8292dcbc45fb2ffcd535d9
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Beth Thibodeau
2023-01-31 23:33:18 +00:00
committed by Automerger Merge Worker
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)
}
}