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
|
// 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")
|
||||||
|
|
||||||
|
|||||||
@@ -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!!
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user