Merge "Allow inactive recommendation card and set timeout" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
e221f89672
@@ -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