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:
@@ -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")
|
||||
|
||||
|
||||
@@ -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!!
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -113,5 +113,6 @@ private val DEFAULT_DATA =
|
||||
recommendations = emptyList(),
|
||||
dismissIntent = null,
|
||||
headphoneConnectionTimeMillis = 0,
|
||||
instanceId = InstanceId.fakeInstanceId(-1)
|
||||
instanceId = InstanceId.fakeInstanceId(-1),
|
||||
expiryTimeMs = 0,
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user