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

This commit is contained in:
Beth Thibodeau
2023-01-31 23:10:08 +00:00
committed by Android (Google) Code Review
14 changed files with 610 additions and 88 deletions

View File

@@ -364,6 +364,9 @@ object Flags {
// TODO(b/267007629): Tracking Bug // TODO(b/267007629): Tracking Bug
val MEDIA_RESUME_PROGRESS = unreleasedFlag(915, "media_resume_progress") 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 // 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")

View File

@@ -41,10 +41,12 @@ data class SmartspaceMediaData(
val recommendations: List<SmartspaceAction>, val recommendations: List<SmartspaceAction>,
/** Intent for the user's initiated dismissal. */ /** Intent for the user's initiated dismissal. */
val dismissIntent: Intent?, val dismissIntent: Intent?,
/** The timestamp in milliseconds that headphone is connected. */ /** The timestamp in milliseconds that the card was generated */
val headphoneConnectionTimeMillis: Long, val headphoneConnectionTimeMillis: Long,
/** Instance ID for [MediaUiEventLogger] */ /** 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. * 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 const val NUM_REQUIRED_RECOMMENDATIONS = 3
private val TAG = SmartspaceMediaData::class.simpleName!! 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.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData 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.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -66,7 +67,8 @@ constructor(
private val lockscreenUserManager: NotificationLockscreenUserManager, private val lockscreenUserManager: NotificationLockscreenUserManager,
@Main private val executor: Executor, @Main private val executor: Executor,
private val systemClock: SystemClock, private val systemClock: SystemClock,
private val logger: MediaUiEventLogger private val logger: MediaUiEventLogger,
private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener { ) : MediaDataManager.Listener {
private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
internal val listeners: Set<MediaDataManager.Listener> internal val listeners: Set<MediaDataManager.Listener>
@@ -121,7 +123,9 @@ constructor(
data: SmartspaceMediaData, data: SmartspaceMediaData,
shouldPrioritize: Boolean 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.") Log.d(TAG, "Inactive recommendation data. Skip triggering.")
return return
} }
@@ -141,7 +145,7 @@ constructor(
} }
} }
val shouldReactivate = !hasActiveMedia() && hasAnyMedia() val shouldReactivate = !hasActiveMedia() && hasAnyMedia() && data.isActive
if (timeSinceActive < smartspaceMaxAgeMillis) { if (timeSinceActive < smartspaceMaxAgeMillis) {
// It could happen there are existing active media resume cards, then we don't need to // 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. // Mark to prioritize Smartspace card if no recent media.
shouldPrioritizeMutable = true shouldPrioritizeMutable = true
} }
@@ -252,7 +256,7 @@ constructor(
if (dismissIntent == null) { if (dismissIntent == null) {
Log.w( Log.w(
TAG, TAG,
"Cannot create dismiss action click action: " + "extras missing dismiss_intent." "Cannot create dismiss action click action: extras missing dismiss_intent."
) )
} else if ( } else if (
dismissIntent.getComponent() != null && dismissIntent.getComponent() != null &&
@@ -264,15 +268,21 @@ constructor(
} else { } else {
broadcastSender.sendBroadcast(dismissIntent) broadcastSender.sendBroadcast(dismissIntent)
} }
smartspaceMediaData =
EMPTY_SMARTSPACE_MEDIA_DATA.copy( if (mediaFlags.isPersistentSsCardEnabled()) {
targetId = smartspaceMediaData.targetId, smartspaceMediaData = smartspaceMediaData.copy(isActive = false)
instanceId = smartspaceMediaData.instanceId 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)) (smartspaceMediaData.isValid() || reactivatedKey != null))
/** Are there any media entries we should display? */ /** Are there any media entries we should display? */
fun hasAnyMediaOrRecommendation() = fun hasAnyMediaOrRecommendation(): Boolean {
userEntries.isNotEmpty() || (smartspaceMediaData.isActive && smartspaceMediaData.isValid()) 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)? */ /** Are there any media notifications active (excluding the recommendation)? */
fun hasActiveMedia() = userEntries.any { it.value.active } 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.text.TextUtils
import android.util.Log import android.util.Log
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.logging.InstanceId import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.R 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.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData 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.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.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener import com.android.systemui.media.controls.resume.MediaResumeListener
@@ -119,7 +120,6 @@ private val LOADING =
appUid = Process.INVALID_UID appUid = Process.INVALID_UID
) )
@VisibleForTesting
internal val EMPTY_SMARTSPACE_MEDIA_DATA = internal val EMPTY_SMARTSPACE_MEDIA_DATA =
SmartspaceMediaData( SmartspaceMediaData(
targetId = "INVALID", targetId = "INVALID",
@@ -129,7 +129,8 @@ internal val EMPTY_SMARTSPACE_MEDIA_DATA =
recommendations = emptyList(), recommendations = emptyList(),
dismissIntent = null, dismissIntent = null,
headphoneConnectionTimeMillis = 0, headphoneConnectionTimeMillis = 0,
instanceId = InstanceId.fakeInstanceId(-1) instanceId = InstanceId.fakeInstanceId(-1),
expiryTimeMs = 0,
) )
fun isMediaNotification(sbn: StatusBarNotification): Boolean { fun isMediaNotification(sbn: StatusBarNotification): Boolean {
@@ -548,6 +549,11 @@ class MediaDataManager(
if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut")
onMediaDataLoaded(key, key, it) 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 */ /** 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 * Called whenever the recommendation has been expired or removed by the user. This will remove
* recommendation view to not be shown anymore during this headphone connection session. * the recommendation card entirely from the carousel.
*/ */
fun dismissSmartspaceRecommendation(key: String, delay: Long) { fun dismissSmartspaceRecommendation(key: String, delay: Long) {
if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { 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( private fun loadMediaDataInBgForResumption(
userId: Int, userId: Int,
desc: MediaDescription, desc: MediaDescription,
@@ -1265,12 +1288,25 @@ class MediaDataManager(
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Set Smartspace media to be inactive for the data update") Log.d(TAG, "Set Smartspace media to be inactive for the data update")
} }
smartspaceMediaData = if (mediaFlags.isPersistentSsCardEnabled()) {
EMPTY_SMARTSPACE_MEDIA_DATA.copy( // Smartspace uses this signal to hide the card (e.g. when it expires or user
targetId = smartspaceMediaData.targetId, // disconnects headphones), so treat as setting inactive when flag is on
instanceId = smartspaceMediaData.instanceId 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 -> { 1 -> {
val newMediaTarget = mediaTargets.get(0) val newMediaTarget = mediaTargets.get(0)
@@ -1279,7 +1315,7 @@ class MediaDataManager(
return return
} }
if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.")
smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) smartspaceMediaData = toSmartspaceMediaData(newMediaTarget)
notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData)
} }
else -> { else -> {
@@ -1288,7 +1324,7 @@ class MediaDataManager(
Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...")
notifySmartspaceMediaDataRemoved( notifySmartspaceMediaDataRemoved(
smartspaceMediaData.targetId, smartspaceMediaData.targetId,
false /* immediately */ immediately = false,
) )
smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA 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 * @return An empty SmartspaceMediaData with the valid target Id is returned if the
* SmartspaceTarget's data is invalid. * SmartspaceTarget's data is invalid.
*/ */
private fun toSmartspaceMediaData( private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData {
target: SmartspaceTarget,
isActive: Boolean
): SmartspaceMediaData {
var dismissIntent: Intent? = null var dismissIntent: Intent? = null
if (target.baseAction != null && target.baseAction.extras != null) { if (target.baseAction != null && target.baseAction.extras != null) {
dismissIntent = dismissIntent =
target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) target.baseAction.extras.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY)
as Intent? 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 { packageName(target)?.let {
return SmartspaceMediaData( return SmartspaceMediaData(
targetId = target.smartspaceTargetId, targetId = target.smartspaceTargetId,
@@ -1518,7 +1561,8 @@ class MediaDataManager(
recommendations = target.iconGrid, recommendations = target.iconGrid,
dismissIntent = dismissIntent, dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis, headphoneConnectionTimeMillis = target.creationTimeMillis,
instanceId = logger.getNewInstanceId() instanceId = logger.getNewInstanceId(),
expiryTimeMs = target.expiryTimeMillis,
) )
} }
return EMPTY_SMARTSPACE_MEDIA_DATA.copy( return EMPTY_SMARTSPACE_MEDIA_DATA.copy(
@@ -1526,7 +1570,8 @@ class MediaDataManager(
isActive = isActive, isActive = isActive,
dismissIntent = dismissIntent, dismissIntent = dismissIntent,
headphoneConnectionTimeMillis = target.creationTimeMillis, 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.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.models.player.MediaData 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.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.SysuiStatusBarStateController
@@ -49,10 +51,12 @@ constructor(
@Main private val mainExecutor: DelayableExecutor, @Main private val mainExecutor: DelayableExecutor,
private val logger: MediaTimeoutLogger, private val logger: MediaTimeoutLogger,
statusBarStateController: SysuiStatusBarStateController, statusBarStateController: SysuiStatusBarStateController,
private val systemClock: SystemClock private val systemClock: SystemClock,
private val mediaFlags: MediaFlags,
) : MediaDataManager.Listener { ) : MediaDataManager.Listener {
private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf() private val mediaListeners: MutableMap<String, PlaybackStateListener> = mutableMapOf()
private val recommendationListeners: MutableMap<String, RecommendationListener> = mutableMapOf()
/** /**
* Callback representing that a media object is now expired: * Callback representing that a media object is now expired:
@@ -93,6 +97,16 @@ constructor(
listener.doTimeout() 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() 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 { fun isTimedOut(key: String): Boolean {
return mediaListeners[key]?.timedOut ?: false return mediaListeners[key]?.timedOut ?: false
} }
@@ -335,4 +373,53 @@ constructor(
} }
return true 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 str1 = key
str2 = reason 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, data: SmartspaceMediaData,
shouldPrioritize: Boolean shouldPrioritize: Boolean
) { ) {
debugLogger.logRecommendationLoaded(key) debugLogger.logRecommendationLoaded(key, data.isActive)
// Log the case where the hidden media carousel with the existed inactive resume // Log the case where the hidden media carousel with the existed inactive resume
// media is shown by the Smartspace signal. // media is shown by the Smartspace signal.
if (data.isActive) { if (data.isActive) {
@@ -442,7 +442,12 @@ constructor(
logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded) logSmartspaceImpression(mediaCarouselScrollHandler.qsExpanded)
} }
} else { } 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") { traceSection("MediaCarouselController#addSmartspaceMediaRecommendations") {
if (DEBUG) Log.d(TAG, "Updating smartspace target in carousel") 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") Log.w(TAG, "Skip adding smartspace target in carousel")
return return
} }
@@ -672,7 +689,7 @@ constructor(
newRecs, newRecs,
shouldPrioritize, shouldPrioritize,
systemClock, systemClock,
debugLogger debugLogger,
) )
updatePlayerToState(newRecs, noAnimation = true) updatePlayerToState(newRecs, noAnimation = true)
reorderAllPlayers(curVisibleMediaKey) reorderAllPlayers(curVisibleMediaKey)
@@ -1225,17 +1242,18 @@ internal object MediaPlayerData {
player: MediaControlPanel, player: MediaControlPanel,
shouldPrioritize: Boolean, shouldPrioritize: Boolean,
clock: SystemClock, clock: SystemClock,
debugLogger: MediaCarouselControllerLogger? = null debugLogger: MediaCarouselControllerLogger? = null,
update: Boolean = false
) { ) {
shouldPrioritizeSs = shouldPrioritize shouldPrioritizeSs = shouldPrioritize
val removedPlayer = removeMediaPlayer(key) val removedPlayer = removeMediaPlayer(key)
if (removedPlayer != null && removedPlayer != player) { if (!update && removedPlayer != null && removedPlayer != player) {
debugLogger?.logPotentialMemoryLeak(key) debugLogger?.logPotentialMemoryLeak(key)
} }
val sortKey = val sortKey =
MediaSortKey( MediaSortKey(
isSsMediaRec = true, isSsMediaRec = true,
EMPTY.copy(isPlaying = false), EMPTY.copy(active = data.isActive, isPlaying = false),
key, key,
clock.currentTimeMillis(), clock.currentTimeMillis(),
isSsReactivated = true isSsReactivated = true

View File

@@ -48,8 +48,16 @@ constructor(@MediaCarouselControllerLog private val buffer: LogBuffer) {
fun logMediaRemoved(key: String) = fun logMediaRemoved(key: String) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" }) buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "removing player $str1" })
fun logRecommendationLoaded(key: String) = fun logRecommendationLoaded(key: String, isActive: Boolean) =
buffer.log(TAG, LogLevel.DEBUG, { str1 = key }, { "add recommendation $str1" }) buffer.log(
TAG,
LogLevel.DEBUG,
{
str1 = key
bool1 = isActive
},
{ "add recommendation $str1, active $bool1" }
)
fun logRecommendationRemoved(key: String, immediately: Boolean) = fun logRecommendationRemoved(key: String, immediately: Boolean) =
buffer.log( 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 */ /** Check whether to get progress information for resume players */
fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS) 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(), recommendations = emptyList(),
dismissIntent = null, dismissIntent = null,
headphoneConnectionTimeMillis = 0, 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.player.MediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
import com.android.systemui.media.controls.ui.MediaPlayerData 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.media.controls.util.MediaUiEventLogger
import com.android.systemui.settings.UserTracker import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor import java.util.concurrent.Executor
@@ -40,11 +42,11 @@ import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.reset import org.mockito.Mockito.reset
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
private const val KEY = "TEST_KEY" private const val KEY = "TEST_KEY"
@@ -72,6 +74,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Mock private lateinit var smartspaceData: SmartspaceMediaData @Mock private lateinit var smartspaceData: SmartspaceMediaData
@Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction
@Mock private lateinit var logger: MediaUiEventLogger @Mock private lateinit var logger: MediaUiEventLogger
@Mock private lateinit var mediaFlags: MediaFlags
private lateinit var mediaDataFilter: MediaDataFilter private lateinit var mediaDataFilter: MediaDataFilter
private lateinit var dataMain: MediaData private lateinit var dataMain: MediaData
@@ -82,6 +85,7 @@ class MediaDataFilterTest : SysuiTestCase() {
fun setup() { fun setup() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
MediaPlayerData.clear() MediaPlayerData.clear()
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
mediaDataFilter = mediaDataFilter =
MediaDataFilter( MediaDataFilter(
context, context,
@@ -90,7 +94,8 @@ class MediaDataFilterTest : SysuiTestCase() {
lockscreenUserManager, lockscreenUserManager,
executor, executor,
clock, clock,
logger logger,
mediaFlags
) )
mediaDataFilter.mediaDataManager = mediaDataManager mediaDataFilter.mediaDataManager = mediaDataManager
mediaDataFilter.addListener(listener) mediaDataFilter.addListener(listener)
@@ -108,19 +113,20 @@ class MediaDataFilterTest : SysuiTestCase() {
) )
dataGuest = dataMain.copy(userId = USER_GUEST) dataGuest = dataMain.copy(userId = USER_GUEST)
`when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY)
`when`(smartspaceData.isActive).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(true)
`when`(smartspaceData.isValid()).thenReturn(true) whenever(smartspaceData.isValid()).thenReturn(true)
`when`(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE)
`when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) whenever(smartspaceData.recommendations)
`when`(smartspaceData.headphoneConnectionTimeMillis) .thenReturn(listOf(smartspaceMediaRecommendationItem))
whenever(smartspaceData.headphoneConnectionTimeMillis)
.thenReturn(clock.currentTimeMillis() - 100) .thenReturn(clock.currentTimeMillis() - 100)
`when`(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID)
} }
private fun setUser(id: Int) { private fun setUser(id: Int) {
`when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false)
`when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true) whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true)
mediaDataFilter.handleUserSwitched(id) mediaDataFilter.handleUserSwitched(id)
} }
@@ -277,7 +283,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() { fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() {
`when`(smartspaceData.isActive).thenReturn(false) whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -285,7 +291,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() { fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() {
`when`(smartspaceData.isValid()).thenReturn(false) whenever(smartspaceData.isValid()).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
@@ -293,8 +299,8 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() { fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() {
`when`(smartspaceData.isActive).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(true)
`when`(smartspaceData.isValid()).thenReturn(true) whenever(smartspaceData.isValid()).thenReturn(true)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isTrue()
@@ -349,7 +355,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() { fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() {
`when`(smartspaceData.isActive).thenReturn(false) whenever(smartspaceData.isActive).thenReturn(false)
mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -379,7 +385,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() { fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() {
`when`(smartspaceData.isActive).thenReturn(false) whenever(smartspaceData.isActive).thenReturn(false)
val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld)
@@ -395,7 +401,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() { 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 // WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -418,7 +424,7 @@ class MediaDataFilterTest : SysuiTestCase() {
@Test @Test
fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() { 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 // WHEN we have media that was recently played, but not currently active
val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
@@ -513,4 +519,59 @@ class MediaDataFilterTest : SysuiTestCase() {
assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse() assertThat(mediaDataFilter.hasActiveMediaOrRecommendation()).isFalse()
assertThat(mediaDataFilter.hasActiveMedia()).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.broadcast.BroadcastDispatcher
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.models.player.MediaData 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.SmartspaceMediaData
import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
import com.android.systemui.media.controls.resume.MediaResumeListener 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 = "KEY"
private const val KEY_2 = "KEY_2" private const val KEY_2 = "KEY_2"
private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" 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 PACKAGE_NAME = "com.example.app"
private const val SYSTEM_PACKAGE_NAME = "com.android.systemui" private const val SYSTEM_PACKAGE_NAME = "com.android.systemui"
private const val APP_NAME = "SystemUI" private const val APP_NAME = "SystemUI"
@@ -230,10 +234,12 @@ class MediaDataManagerTest : SysuiTestCase() {
whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE)
whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA)
whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) 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.areMediaSessionActionsEnabled(any(), any())).thenReturn(false)
whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true)
whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
} }
@@ -847,8 +853,9 @@ class MediaDataManagerTest : SysuiTestCase() {
cardAction = mediaSmartspaceBaseAction, cardAction = mediaSmartspaceBaseAction,
recommendations = validRecommendationList, recommendations = validRecommendationList,
dismissIntent = DISMISS_INTENT, dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = 1234L, headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId) instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
) )
), ),
eq(false) eq(false)
@@ -870,8 +877,9 @@ class MediaDataManagerTest : SysuiTestCase() {
targetId = KEY_MEDIA_SMARTSPACE, targetId = KEY_MEDIA_SMARTSPACE,
isActive = true, isActive = true,
dismissIntent = DISMISS_INTENT, dismissIntent = DISMISS_INTENT,
headphoneConnectionTimeMillis = 1234L, headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId) instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
) )
), ),
eq(false) eq(false)
@@ -901,8 +909,9 @@ class MediaDataManagerTest : SysuiTestCase() {
targetId = KEY_MEDIA_SMARTSPACE, targetId = KEY_MEDIA_SMARTSPACE,
isActive = true, isActive = true,
dismissIntent = null, dismissIntent = null,
headphoneConnectionTimeMillis = 1234L, headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME,
instanceId = InstanceId.fakeInstanceId(instanceId) instanceId = InstanceId.fakeInstanceId(instanceId),
expiryTimeMs = SMARTSPACE_EXPIRY_TIME,
) )
), ),
eq(false) eq(false)
@@ -930,6 +939,129 @@ class MediaDataManagerTest : SysuiTestCase() {
verifyNoMoreInteractions(logger) 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 @Test
fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() {
// WHEN media recommendation setting is off // 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.SysuiTestCase
import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.MediaTestUtils
import com.android.systemui.media.controls.models.player.MediaData 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.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaFlags
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.Before import org.junit.Before
@@ -48,7 +51,6 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.never import org.mockito.Mockito.never
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
private const val KEY = "KEY" 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_KEY = "SESSION_KEY"
private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_ARTIST = "SESSION_ARTIST"
private const val SESSION_TITLE = "SESSION_TITLE" private const val SESSION_TITLE = "SESSION_TITLE"
private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private fun <T> anyObject(): T { private fun <T> anyObject(): T {
return Mockito.anyObject<T>() return Mockito.anyObject<T>()
@@ -85,10 +88,13 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
private lateinit var resumeData: MediaData private lateinit var resumeData: MediaData
private lateinit var mediaTimeoutListener: MediaTimeoutListener private lateinit var mediaTimeoutListener: MediaTimeoutListener
private var clock = FakeSystemClock() private var clock = FakeSystemClock()
@Mock private lateinit var mediaFlags: MediaFlags
@Mock private lateinit var smartspaceData: SmartspaceMediaData
@Before @Before
fun setup() { fun setup() {
`when`(mediaControllerFactory.create(any())).thenReturn(mediaController) whenever(mediaControllerFactory.create(any())).thenReturn(mediaController)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
executor = FakeExecutor(clock) executor = FakeExecutor(clock)
mediaTimeoutListener = mediaTimeoutListener =
MediaTimeoutListener( MediaTimeoutListener(
@@ -96,7 +102,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
executor, executor,
logger, logger,
statusBarStateController, statusBarStateController,
clock clock,
mediaFlags,
) )
mediaTimeoutListener.timeoutCallback = timeoutCallback mediaTimeoutListener.timeoutCallback = timeoutCallback
mediaTimeoutListener.stateCallback = stateCallback mediaTimeoutListener.stateCallback = stateCallback
@@ -133,9 +140,9 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
@Test @Test
fun testOnMediaDataLoaded_registersPlaybackListener() { fun testOnMediaDataLoaded_registersPlaybackListener() {
val playingState = mock(android.media.session.PlaybackState::class.java) 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) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
verify(logger).logPlaybackState(eq(KEY), eq(playingState)) verify(logger).logPlaybackState(eq(KEY), eq(playingState))
@@ -188,8 +195,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// To playing // To playing
val playingState = mock(android.media.session.PlaybackState::class.java) 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(newKey, KEY, mediaData) mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
verify(mediaController).unregisterCallback(anyObject()) verify(mediaController).unregisterCallback(anyObject())
verify(mediaController).registerCallback(anyObject()) verify(mediaController).registerCallback(anyObject())
@@ -208,8 +215,8 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// Migrate, still not playing // Migrate, still not playing
val playingState = mock(android.media.session.PlaybackState::class.java) val playingState = mock(android.media.session.PlaybackState::class.java)
`when`(playingState.state).thenReturn(PlaybackState.STATE_PAUSED) whenever(playingState.state).thenReturn(PlaybackState.STATE_PAUSED)
`when`(mediaController.playbackState).thenReturn(playingState) whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData) mediaTimeoutListener.onMediaDataLoaded(newKey, KEY, mediaData)
// The number of queued timeout tasks remains the same. The timeout task isn't cancelled nor // 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 // WHEN we get an update with media playing
val playingState = mock(android.media.session.PlaybackState::class.java) 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)
val mediaPlaying = mediaData.copy(isPlaying = true) val mediaPlaying = mediaData.copy(isPlaying = true)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaPlaying)
@@ -347,7 +354,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// WHEN regular media is paused // WHEN regular media is paused
val pausedState = val pausedState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(pausedState) whenever(mediaController.playbackState).thenReturn(pausedState)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
assertThat(executor.numPending()).isEqualTo(1) assertThat(executor.numPending()).isEqualTo(1)
@@ -379,7 +386,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
// AND that media is resumed // AND that media is resumed
val playingState = val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build() PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
`when`(mediaController.playbackState).thenReturn(playingState) whenever(mediaController.playbackState).thenReturn(playingState)
mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData) mediaTimeoutListener.onMediaDataLoaded(KEY, PACKAGE, mediaData)
// THEN the timeout length is changed to a regular media control // THEN the timeout length is changed to a regular media control
@@ -593,8 +600,91 @@ class MediaTimeoutListenerTest : SysuiTestCase() {
assertThat(executor.numPending()).isEqualTo(1) 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) { private fun loadMediaDataWithPlaybackState(state: PlaybackState) {
`when`(mediaController.playbackState).thenReturn(state) whenever(mediaController.playbackState).thenReturn(state)
mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) 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 com.android.systemui.util.time.FakeSystemClock
import javax.inject.Provider import javax.inject.Provider
import junit.framework.Assert.assertEquals import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue import junit.framework.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
@@ -126,6 +127,7 @@ class MediaCarouselControllerTest : SysuiTestCase() {
whenever(mediaControlPanelFactory.get()).thenReturn(panel) whenever(mediaControlPanelFactory.get()).thenReturn(panel)
whenever(panel.mediaViewController).thenReturn(mediaViewController) whenever(panel.mediaViewController).thenReturn(mediaViewController)
whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
MediaPlayerData.clear() MediaPlayerData.clear()
} }
@@ -703,4 +705,39 @@ class MediaCarouselControllerTest : SysuiTestCase() {
mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex 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)
}
} }