diff options
author | 2021-05-31 20:24:40 +0000 | |
---|---|---|
committer | 2021-05-31 20:24:40 +0000 | |
commit | a0ecdaafd516b94f59d420a86b3632eaa9de55aa (patch) | |
tree | ca3257a8e0f9761a3d29f383bca30e855d9e46ed | |
parent | 7d83e6a272ada19a6ee2f509e92a6b83a4c1455b (diff) | |
parent | 38c6f2bf5704cc1c127c2ca12e5a3cf8929c5f57 (diff) |
Merge "On Smartspace removal update, only dismiss media recommendation/player when it's invisible to users." into sc-dev
17 files changed, 469 insertions, 265 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index a1c06fcec1cd..09da9d207f64 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -1,6 +1,5 @@ package com.android.systemui.media -import android.app.smartspace.SmartspaceTarget import android.content.Context import android.content.Intent import android.content.res.ColorStateList @@ -184,7 +183,12 @@ class MediaCarouselController @Inject constructor( visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, true /* persistent */) mediaManager.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { if (addOrUpdatePlayer(key, oldKey, data)) { MediaPlayerData.getMediaPlayer(key, null)?.let { logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED @@ -210,19 +214,23 @@ class MediaCarouselController @Inject constructor( override fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { Log.d(TAG, "My Smartspace media update is here") - addSmartspaceMediaRecommendations(key, data, shouldPrioritize) - MediaPlayerData.getMediaPlayer(key, null)?.let { - logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED - it.mInstanceId, - /* isRecommendationCard */ true, - it.surfaceForSmartspaceLogging) - } - if (mediaCarouselScrollHandler.visibleToUser) { - logSmartspaceImpression() + if (data.isActive) { + addSmartspaceMediaRecommendations(key, data, shouldPrioritize) + MediaPlayerData.getMediaPlayer(key, null)?.let { + logSmartspaceCardReported(759, // SMARTSPACE_CARD_RECEIVED + it.mInstanceId, + /* isRecommendationCard */ true, + it.surfaceForSmartspaceLogging) + } + if (mediaCarouselScrollHandler.visibleToUser) { + logSmartspaceImpression() + } + } else { + onSmartspaceMediaDataRemoved(data.targetId, immediately = true) } } @@ -230,9 +238,13 @@ class MediaCarouselController @Inject constructor( removePlayer(key) } - override fun onSmartspaceMediaDataRemoved(key: String) { + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { Log.d(TAG, "My Smartspace media removal request is received") - removePlayer(key) + if (immediately || visualStabilityManager.isReorderingAllowed) { + onMediaDataRemoved(key) + } else { + keysNeedRemoval.add(key) + } } }) mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> @@ -287,7 +299,7 @@ class MediaCarouselController @Inject constructor( // Automatically scroll to the active player if needed if (shouldScrollToActivePlayer) { shouldScrollToActivePlayer = false - val activeMediaIndex = MediaPlayerData.getActiveMediaIndex() + val activeMediaIndex = MediaPlayerData.activeMediaIndex() if (activeMediaIndex != -1) { mediaCarouselScrollHandler.scrollToActivePlayer(activeMediaIndex) } @@ -333,7 +345,7 @@ class MediaCarouselController @Inject constructor( private fun addSmartspaceMediaRecommendations( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { Log.d(TAG, "Updating smartspace target in carousel") @@ -342,6 +354,11 @@ class MediaCarouselController @Inject constructor( return } + val existingSmartspaceMediaKey = MediaPlayerData.smartspaceMediaKey() + existingSmartspaceMediaKey?.let { + MediaPlayerData.removeMediaPlayer(existingSmartspaceMediaKey) + } + var newRecs = mediaControlPanelFactory.get() newRecs.attachRecommendation( RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent)) @@ -349,7 +366,7 @@ class MediaCarouselController @Inject constructor( val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp) - newRecs.bindRecommendation(data, bgColor) + newRecs.bindRecommendation(data.copy(backgroundColor = bgColor)) MediaPlayerData.addMediaRecommendation(key, newRecs, shouldPrioritize) updatePlayerToState(newRecs, noAnimation = true) reorderAllPlayers() @@ -378,11 +395,11 @@ class MediaCarouselController @Inject constructor( if (dismissMediaData) { // Inform the media manager of a potentially late dismissal - mediaManager.dismissMediaData(key, 0L /* delaye */) + mediaManager.dismissMediaData(key, delay = 0L) } if (dismissRecommendation) { // Inform the media manager of a potentially late dismissal - mediaManager.dismissSmartspaceRecommendation(0L /* delay */) + mediaManager.dismissSmartspaceRecommendation(key, delay = 0L) } } } @@ -392,7 +409,7 @@ class MediaCarouselController @Inject constructor( pageIndicator.tintList = ColorStateList.valueOf(getForegroundColor()) MediaPlayerData.mediaData().forEach { (key, data) -> - removePlayer(key, dismissMediaData = false) + removePlayer(key, dismissMediaData = false, dismissRecommendation = false) addOrUpdatePlayer(key = key, oldKey = null, data = data) } } @@ -732,7 +749,7 @@ internal object MediaPlayerData { fun players() = mediaPlayers.values /** Returns the index of the first non-timeout media. */ - fun getActiveMediaIndex(): Int { + fun activeMediaIndex(): Int { mediaPlayers.entries.forEachIndexed { index, e -> if (!e.key.isSsMediaRec && e.key.data.active) { return index @@ -741,6 +758,16 @@ internal object MediaPlayerData { return -1 } + /** Returns the existing Smartspace target id. */ + fun smartspaceMediaKey(): String? { + mediaData.entries.forEach { e -> + if (e.value.isSsMediaRec) { + return e.key + } + } + return null + } + fun playerKeys() = mediaPlayers.keys @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index c806bcfed345..45ceceba5cba 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -560,12 +560,10 @@ class MediaCarouselScrollHandler( } fun scrollToActivePlayer(activePlayerIndex: Int) { - var destIndex = activePlayerIndex - destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) + val destIndex = Math.min(mediaContent.getChildCount() - 1, activePlayerIndex) val view = mediaContent.getChildAt(destIndex) // We need to post this to wait for the active player becomes visible. mainExecutor.executeDelayed({ - visibleMediaIndex = activePlayerIndex scrollView.smoothScrollTo(view.left, scrollView.scrollY) }, SCROLL_DELAY) } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 27a4e93db4ad..55feea970e84 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -20,7 +20,6 @@ import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; import android.app.PendingIntent; import android.app.smartspace.SmartspaceAction; -import android.app.smartspace.SmartspaceTarget; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -34,7 +33,6 @@ import android.graphics.drawable.Icon; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.os.Bundle; import android.text.Layout; import android.util.Log; import android.view.View; @@ -74,7 +72,6 @@ import kotlin.Unit; public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; private static final float DISABLED_ALPHA = 0.38f; - private static final String EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name"; private static final String EXTRAS_SMARTSPACE_INTENT = "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"; private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"; @@ -493,27 +490,30 @@ public class MediaControlPanel { }; } - /** Bind this recommendation view based on the data given. */ - public void bindRecommendation(@NonNull SmartspaceTarget target, @NonNull int backgroundColor) { + /** Bind this recommendation view based on the given data. */ + public void bindRecommendation(@NonNull SmartspaceMediaData data) { if (mRecommendationViewHolder == null) { return; } - mInstanceId = target.getSmartspaceTargetId().hashCode(); + mInstanceId = data.getTargetId().hashCode(); + mBackgroundColor = data.getBackgroundColor(); mRecommendationViewHolder.getRecommendations() - .setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); - mBackgroundColor = backgroundColor; + .setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); - List<SmartspaceAction> mediaRecommendationList = target.getIconGrid(); + List<SmartspaceAction> mediaRecommendationList = data.getRecommendations(); if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) { Log.w(TAG, "Empty media recommendations"); return; } // Set up recommendation card's header. - ApplicationInfo applicationInfo = getApplicationInfo(target); - if (applicationInfo == null) { - Log.w(TAG, "No valid application info is found for media recommendations"); + ApplicationInfo applicationInfo = null; + try { + applicationInfo = mContext.getPackageManager() + .getApplicationInfo(data.getPackageName(), 0 /* flags */); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Fail to get media recommendation's app info", e); return; } @@ -531,7 +531,7 @@ public class MediaControlPanel { } // Set up media card's tap action if applicable. setSmartspaceRecItemOnClickListener( - mRecommendationViewHolder.getRecommendations(), target.getBaseAction()); + mRecommendationViewHolder.getRecommendations(), data.getCardAction()); List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems(); List<Integer> mediaCoverItemsResIds = mRecommendationViewHolder.getMediaCoverItemsResIds(); @@ -574,7 +574,7 @@ public class MediaControlPanel { /* isRecommendationCard */ true); closeGuts(); mMediaDataManagerLazy.get().dismissSmartspaceRecommendation( - MediaViewController.GUTS_ANIMATION_DURATION + 100L); + data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L); }); mController = null; @@ -753,38 +753,6 @@ public class MediaControlPanel { } /** - * Returns the application info for the media recommendation's source app. - * - * @param target Smartspace target contains a list of media recommendations. Each item should - * contain the same source app's info. - * - * @return The source app's application info. This value can be null if no valid application - * info can be obtained. - */ - private ApplicationInfo getApplicationInfo(@NonNull SmartspaceTarget target) { - List<SmartspaceAction> mediaRecommendationList = target.getIconGrid(); - if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) { - return null; - } - - for (SmartspaceAction recommendation: mediaRecommendationList) { - Bundle extras = recommendation.getExtras(); - if (extras != null && extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME) != null) { - // Get the logo from app's package name when applicable. - String packageName = extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME); - try { - return mContext.getPackageManager() - .getApplicationInfo(packageName, 0 /* flags */); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Fail to get media recommendation's app info", e); - } - } - } - - return null; - } - - /** * Get the surface given the current end location for MediaViewController * @return surface used for Smartspace logging */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index 87af9e08bd71..ee1d3ea87da8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -16,7 +16,6 @@ package com.android.systemui.media -import android.app.smartspace.SmartspaceTarget import javax.inject.Inject /** @@ -28,7 +27,12 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf() - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) { entries[key] = data to entries.remove(oldKey)?.second update(key, oldKey) @@ -40,7 +44,7 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, override fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) } @@ -50,8 +54,8 @@ class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, remove(key) } - override fun onSmartspaceMediaDataRemoved(key: String) { - listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key) } + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } override fun onMediaDeviceChanged( diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt index 3deb5d13a27f..a611b600f47f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -16,8 +16,6 @@ package com.android.systemui.media -import android.app.smartspace.SmartspaceAction -import android.app.smartspace.SmartspaceTarget import android.os.SystemProperties import android.util.Log import com.android.internal.annotations.VisibleForTesting @@ -34,6 +32,7 @@ import kotlin.collections.LinkedHashMap private const val TAG = "MediaDataFilter" private const val DEBUG = true +private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" /** * Maximum age of a media control to re-activate on smartspace signal. If there is no media control @@ -67,8 +66,7 @@ class MediaDataFilter @Inject constructor( private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() - var hasSmartspace: Boolean = false - private set + private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA private var reactivatedKey: String? = null init { @@ -81,7 +79,12 @@ class MediaDataFilter @Inject constructor( userTracker.startTracking() } - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { if (oldKey != null && oldKey != key) { allEntries.remove(oldKey) } @@ -104,18 +107,32 @@ class MediaDataFilter @Inject constructor( override fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { - var shouldPrioritizeMutable = shouldPrioritize - hasSmartspace = true + if (!data.isActive) { + Log.d(TAG, "Inactive recommendation data. Skip triggering.") + return + } + + // Override the pass-in value here, as the order of Smartspace card is only determined here. + var shouldPrioritizeMutable = false + smartspaceMediaData = data // Before forwarding the smartspace target, first check if we have recently inactive media val sorted = userEntries.toSortedMap(compareBy { userEntries.get(it)?.lastActive ?: -1 }) val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) - if (timeSinceActive < SMARTSPACE_MAX_AGE) { + var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE + data.cardAction?.let { + val smartspaceMaxAgeSeconds = + it.extras.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) + if (smartspaceMaxAgeSeconds > 0) { + smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) + } + } + if (timeSinceActive < smartspaceMaxAgeMillis) { val lastActiveKey = sorted.lastKey() // most recently active // Notify listeners to consider this media active Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") @@ -129,9 +146,8 @@ class MediaDataFilter @Inject constructor( shouldPrioritizeMutable = true } - // Only proceed with the Smartspace update if the recommendation is not empty. - if (isMediaRecommendationEmpty(data)) { - Log.d(TAG, "Empty media recommendations. Skip showing the card") + if (!data.isValid) { + Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") return } listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } @@ -147,9 +163,7 @@ class MediaDataFilter @Inject constructor( } } - override fun onSmartspaceMediaDataRemoved(key: String) { - hasSmartspace = false - + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { // First check if we had reactivated media instead of forwarding smartspace reactivatedKey?.let { val lastActiveKey = it @@ -158,12 +172,17 @@ class MediaDataFilter @Inject constructor( // Notify listeners to update with actual active value userEntries.get(lastActiveKey)?.let { mediaData -> listeners.forEach { - it.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData) + it.onMediaDataLoaded( + lastActiveKey, lastActiveKey, mediaData, immediately) } } } - listeners.forEach { it.onSmartspaceMediaDataRemoved(key) } + if (smartspaceMediaData.isActive) { + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) + } + listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } @VisibleForTesting @@ -202,20 +221,22 @@ class MediaDataFilter @Inject constructor( // Force updates to listeners, needed for re-activated card mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) } - if (hasSmartspace) { - mediaDataManager.dismissSmartspaceRecommendation(0L /* delay */) + if (smartspaceMediaData.isActive) { + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, isValid = smartspaceMediaData.isValid) } + mediaDataManager.dismissSmartspaceRecommendation(smartspaceMediaData.targetId, delay = 0L) } /** * Are there any media notifications active? */ - fun hasActiveMedia() = userEntries.any { it.value.active } || hasSmartspace + fun hasActiveMedia() = userEntries.any { it.value.active } || smartspaceMediaData.isActive /** * Are there any media entries we should display? */ - fun hasAnyMedia() = userEntries.isNotEmpty() || hasSmartspace + fun hasAnyMedia() = userEntries.isNotEmpty() || smartspaceMediaData.isActive /** * Add a listener for filtered [MediaData] changes @@ -227,12 +248,6 @@ class MediaDataFilter @Inject constructor( */ fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) - /** Check if the Smartspace sends an empty update. */ - private fun isMediaRecommendationEmpty(data: SmartspaceTarget): Boolean { - val mediaRecommendationList: List<SmartspaceAction> = data.getIconGrid() - return mediaRecommendationList == null || mediaRecommendationList.isEmpty() - } - /** * Return the time since last active for the most-recent media. * diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index c74f2fe64d6f..13c7f71f53ee 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -42,6 +42,7 @@ import android.os.UserHandle import android.service.notification.StatusBarNotification import android.text.TextUtils import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher @@ -76,6 +77,9 @@ private const val DEBUG = true private val LOADING = MediaData(-1, false, 0, null, null, null, null, null, emptyList(), emptyList(), "INVALID", null, null, null, true, null) +@VisibleForTesting +internal val EMPTY_SMARTSPACE_MEDIA_DATA = SmartspaceMediaData("INVALID", false, false, + "INVALID", null, emptyList(), 0) fun isMediaNotification(sbn: StatusBarNotification): Boolean { if (!sbn.notification.hasMediaSession()) { @@ -118,6 +122,10 @@ class MediaDataManager( @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + // Smartspace package name's extra key. + @JvmField + val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + // Maximum number of actions allowed in compact view @JvmField val MAX_COMPACT_ACTIONS = 3 @@ -137,7 +145,7 @@ class MediaDataManager( private val internalListeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() // There should ONLY be at most one Smartspace media recommendation. - private var smartspaceMediaTarget: SmartspaceTarget? = null + private var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA private var smartspaceSession: SmartspaceSession? = null @Inject @@ -360,7 +368,7 @@ class MediaDataManager( * External listeners registered with [addListener] will be notified after the event propagates * through the internal listener pipeline. */ - private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceTarget) { + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } } @@ -379,9 +387,13 @@ class MediaDataManager( * * External listeners registered with [addListener] will be notified after the event propagates * through the internal listener pipeline. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait until + * the next refresh-round before UI becomes visible. Should only be true if the update is + * initiated by user's interaction. */ - private fun notifySmartspaceMediaDataRemoved(key: String) { - internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key) } + private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } /** @@ -424,14 +436,18 @@ class MediaDataManager( * This will make the recommendation view to not be shown anymore during this headphone * connection session. */ - fun dismissSmartspaceRecommendation(delay: Long) { + fun dismissSmartspaceRecommendation(key: String, delay: Long) { Log.d(TAG, "Dismissing Smartspace media target") - // Do not set smartspaceMediaTarget to null. So the instance is preserved during the entire - // headphone connection, and will ONLY be set to null when headphones are disconnected. - smartspaceMediaTarget?.let { - foregroundExecutor.executeDelayed( - { notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) }, delay) + if (smartspaceMediaData.targetId != key) { + return + } + if (smartspaceMediaData.isActive) { + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId) } + foregroundExecutor.executeDelayed( + { notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, immediately = true) }, delay) } private fun loadMediaDataInBgForResumption( @@ -680,46 +696,41 @@ class MediaDataManager( override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { if (!Utils.allowMediaRecommendations(context)) { + Log.d(TAG, "Smartspace recommendation is disabled in Settings.") return } + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() when (mediaTargets.size) { 0 -> { Log.d(TAG, "Empty Smartspace media target") - smartspaceMediaTarget?.let { - Log.d(TAG, "Setting Smartspace media target to null") - notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) + if (!smartspaceMediaData.isActive) { + return } - smartspaceMediaTarget = null + Log.d(TAG, "Set Smartspace media to be inactive for the data update") + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId) + notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) } 1 -> { - // TODO(b/182811956): Reactivate the resumable media sessions whose last active - // time is within 3 hours. - // TODO(b/182813365): Wire this up with MediaTimeoutListener so the session can be - // expired after 30 seconds. val newMediaTarget = mediaTargets.get(0) - if (smartspaceMediaTarget != null && - smartspaceMediaTarget!!.smartspaceTargetId == - newMediaTarget.smartspaceTargetId) { - // The same Smartspace updates can be received. Only send the first one. + if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Skip the duplicate updates. Log.d(TAG, "Same Smartspace media update exists. Skip loading data.") } else { - smartspaceMediaTarget?.let { - notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) - } + Log.d(TAG, "Forwarding Smartspace media update.") + smartspaceMediaData = toSmartspaceMediaData(newMediaTarget, isActive = true) notifySmartspaceMediaDataLoaded( - newMediaTarget.smartspaceTargetId, newMediaTarget) - smartspaceMediaTarget = newMediaTarget + smartspaceMediaData.targetId, smartspaceMediaData) } } else -> { // There should NOT be more than 1 Smartspace media update. When it happens, it // indicates a bad state or an error. Reset the status accordingly. Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") - smartspaceMediaTarget?.let { - notifySmartspaceMediaDataRemoved(it.smartspaceTargetId) - } - smartspaceMediaTarget = null + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, false /* immediately */) + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA } } } @@ -797,28 +808,77 @@ class MediaDataManager( * oldKey is provided to check whether the view has changed keys, which can happen when a * player has gone from resume state (key is package name) to active state (key is * notification key) or vice versa. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in place + * immediately. */ - fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {} + fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean = true + ) {} /** * Called whenever there's new Smartspace media data loaded. * - * shouldPrioritize indicates the sorting priority of the Smartspace card. If true, it will - * be prioritized as the first card. Otherwise, it will show up as the last card as default. + * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, + * it will be prioritized as the first card. Otherwise, it will show up as the last card as + * default. */ fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean = false ) {} + /** Called whenever a previously existing Media notification was removed. */ + fun onMediaDataRemoved(key: String) {} + /** - * Called whenever a previously existing Media notification was removed + * Called whenever a previously existing Smartspace media data was removed. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in place + * immediately. */ - fun onMediaDataRemoved(key: String) {} + fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + } - /** Called whenever a previously existing Smartspace media data was removed. */ - fun onSmartspaceMediaDataRemoved(key: String) {} + /** + * Converts the pass-in SmartspaceTarget to SmartspaceMediaData with the pass-in active status. + * + * @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 { + packageName(target)?.let { + return SmartspaceMediaData(target.smartspaceTargetId, isActive, true, it, + target.baseAction, target.iconGrid, 0) + } + return EMPTY_SMARTSPACE_MEDIA_DATA + .copy(targetId = target.smartspaceTargetId, isActive = isActive) + } + + private fun packageName(target: SmartspaceTarget): String? { + val recommendationList = target.iconGrid + if (recommendationList == null || recommendationList.isEmpty()) { + Log.d(TAG, "Empty or media recommendation list.") + return null + } + for (recommendation in recommendationList) { + val extras = recommendation.extras + extras?.let { + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { + packageName -> return packageName } + } + } + Log.d(TAG, "No valid package name is provided.") + return null } override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index a993d00df01e..52ecbea05924 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -63,7 +63,12 @@ class MediaDeviceManager @Inject constructor( */ fun removeListener(listener: Listener) = listeners.remove(listener) - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { if (oldKey != null && oldKey != key) { val oldEntry = entries.remove(oldKey) oldEntry?.stop() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index fe20dcbe8564..43e21424c45e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -1,6 +1,5 @@ package com.android.systemui.media -import android.app.smartspace.SmartspaceTarget import android.graphics.Rect import android.util.ArraySet import android.view.View @@ -57,13 +56,20 @@ class MediaHost constructor( } private val listener = object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { - updateViewVisibility() + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { + if (immediately) { + updateViewVisibility() + } } override fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { updateViewVisibility() @@ -73,8 +79,10 @@ class MediaHost constructor( updateViewVisibility() } - override fun onSmartspaceMediaDataRemoved(key: String) { - updateViewVisibility() + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + if (immediately) { + updateViewVisibility() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 7fe408fb0cb7..9aeb63d724e3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -155,7 +155,12 @@ class MediaResumeListener @Inject constructor( } } - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { if (useMediaResumption) { // If this had been started from a resume state, disconnect now that it's live mediaBrowser?.disconnect() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt index 3e5e82484df6..a4f33e354b68 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt @@ -16,7 +16,6 @@ package com.android.systemui.media -import android.app.smartspace.SmartspaceTarget import android.content.ComponentName import android.content.Context import android.media.session.MediaController @@ -92,39 +91,44 @@ class MediaSessionBasedFilter @Inject constructor( * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability * of the media controls. */ - override fun onMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { backgroundExecutor.execute { - info.token?.let { + data.token?.let { tokensWithNotifications.add(it) } val isMigration = oldKey != null && key != oldKey if (isMigration) { keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } } - if (info.token != null) { + if (data.token != null) { keyedTokens.get(key)?.let { tokens -> - tokens.add(info.token) + tokens.add(data.token) } ?: run { - val tokens = mutableSetOf(info.token) + val tokens = mutableSetOf(data.token) keyedTokens.put(key, tokens) } } // Determine if an app is casting by checking if it has a session with playback type // PLAYBACK_TYPE_REMOTE. - val remoteControllers = packageControllers.get(info.packageName)?.filter { + val remoteControllers = packageControllers.get(data.packageName)?.filter { it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE } // Limiting search to only apps with a single remote session. val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null - if (isMigration || remote == null || remote.sessionToken == info.token || + if (isMigration || remote == null || remote.sessionToken == data.token || !tokensWithNotifications.contains(remote.sessionToken)) { // Not filtering in this case. Passing the event along to listeners. - dispatchMediaDataLoaded(key, oldKey, info) + dispatchMediaDataLoaded(key, oldKey, data, immediately) } else { // Filtering this event because the app is casting and the loaded events is for a // local session. - Log.d(TAG, "filtering key=$key local=${info.token} remote=${remote?.sessionToken}") + Log.d(TAG, "filtering key=$key local=${data.token} remote=${remote?.sessionToken}") // If the local session uses a different notification key, then lets go a step // farther and dismiss the media data so that media controls for the local session // don't hang around while casting. @@ -137,7 +141,7 @@ class MediaSessionBasedFilter @Inject constructor( override fun onSmartspaceMediaDataLoaded( key: String, - data: SmartspaceTarget, + data: SmartspaceMediaData, shouldPrioritize: Boolean ) { backgroundExecutor.execute { @@ -153,15 +157,20 @@ class MediaSessionBasedFilter @Inject constructor( } } - override fun onSmartspaceMediaDataRemoved(key: String) { + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { backgroundExecutor.execute { - dispatchSmartspaceMediaDataRemoved(key) + dispatchSmartspaceMediaDataRemoved(key, immediately) } } - private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + private fun dispatchMediaDataLoaded( + key: String, + oldKey: String?, + info: MediaData, + immediately: Boolean + ) { foregroundExecutor.execute { - listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) } + listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info, immediately) } } } @@ -171,15 +180,15 @@ class MediaSessionBasedFilter @Inject constructor( } } - private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceTarget) { + private fun dispatchSmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { foregroundExecutor.execute { listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, info) } } } - private fun dispatchSmartspaceMediaDataRemoved(key: String) { + private fun dispatchSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { foregroundExecutor.execute { - listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key) } + listeners.toSet().forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 8bfe94bbd0de..bbea140ecfaf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -50,7 +50,12 @@ class MediaTimeoutListener @Inject constructor( */ lateinit var timeoutCallback: (String, Boolean) -> Unit - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean + ) { var reusedListener: PlaybackStateListener? = null // First check if we already have a listener diff --git a/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt new file mode 100644 index 000000000000..9ac128920edb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/SmartspaceMediaData.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.media + +import android.app.smartspace.SmartspaceAction + +/** State of a Smartspace media recommendations view. */ +data class SmartspaceMediaData( + /** + * Unique id of a Smartspace media target. + */ + val targetId: String, + /** + * Indicates if the status is active. + */ + val isActive: Boolean, + /** + * Indicates if all the required data field is valid. + */ + val isValid: Boolean, + /** + * Package name of the media recommendations' provider-app. + */ + val packageName: String, + /** + * Action to perform when the card is tapped. Also contains the target's extra info. + */ + val cardAction: SmartspaceAction?, + /** + * List of media recommendations. + */ + val recommendations: List<SmartspaceAction>, + /** + * View's background color. + */ + val backgroundColor: Int +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index f99436f67728..a69b8d60681c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -24,7 +24,6 @@ import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; -import android.app.smartspace.SmartspaceTarget; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; @@ -55,6 +54,7 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.media.MediaData; import com.android.systemui.media.MediaDataManager; +import com.android.systemui.media.SmartspaceMediaData; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; @@ -245,13 +245,12 @@ public class NotificationMediaManager implements Dumpable { mMediaDataManager.addListener(new MediaDataManager.Listener() { @Override public void onMediaDataLoaded(@NonNull String key, - @Nullable String oldKey, @NonNull MediaData data) { + @Nullable String oldKey, @NonNull MediaData data, boolean immediately) { } @Override public void onSmartspaceMediaDataLoaded(@NonNull String key, - @NonNull SmartspaceTarget data, boolean shouldPrioritize) { - + @NonNull SmartspaceMediaData data, boolean shouldPrioritize) { } @Override @@ -269,7 +268,7 @@ public class NotificationMediaManager implements Dumpable { } @Override - public void onSmartspaceMediaDataRemoved(@NonNull String key) {} + public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {} }); } @@ -319,12 +318,12 @@ public class NotificationMediaManager implements Dumpable { mMediaDataManager.addListener(new MediaDataManager.Listener() { @Override public void onMediaDataLoaded(@NonNull String key, - @Nullable String oldKey, @NonNull MediaData data) { + @Nullable String oldKey, @NonNull MediaData data, boolean immediately) { } @Override public void onSmartspaceMediaDataLoaded(@NonNull String key, - @NonNull SmartspaceTarget data, boolean shouldPrioritize) { + @NonNull SmartspaceMediaData data, boolean shouldPrioritize) { } @@ -341,7 +340,7 @@ public class NotificationMediaManager implements Dumpable { } @Override - public void onSmartspaceMediaDataRemoved(@NonNull String key) {} + public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {} }); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 4a487be914c2..e20b426907be 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -19,6 +19,7 @@ package com.android.systemui.media; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -81,9 +82,9 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { @Test public void eventNotEmittedWithoutDevice() { // WHEN data source emits an event without device data - mManager.onMediaDataLoaded(KEY, null, mMediaData); + mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); // THEN an event isn't emitted - verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any()); + verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean()); } @Test @@ -91,7 +92,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { // WHEN device source emits an event without media data mManager.onMediaDeviceChanged(KEY, null, mDeviceData); // THEN an event isn't emitted - verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any()); + verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean()); } @Test @@ -99,80 +100,80 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { // GIVEN that a device event has already been received mManager.onMediaDeviceChanged(KEY, null, mDeviceData); // WHEN media event is received - mManager.onMediaDataLoaded(KEY, null, mMediaData); + mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void emitEventAfterMediaFirst() { // GIVEN that media event has already been received - mManager.onMediaDataLoaded(KEY, null, mMediaData); + mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); // WHEN device event is received mManager.onMediaDeviceChanged(KEY, null, mDeviceData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void migrateKeyMediaFirst() { // GIVEN that media and device info has already been received - mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData); + mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); reset(mListener); // WHEN a key migration event is received - mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData); + mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void migrateKeyDeviceFirst() { // GIVEN that media and device info has already been received - mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData); + mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); reset(mListener); // WHEN a key migration event is received mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void migrateKeyMediaAfter() { // GIVEN that media and device info has already been received - mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData); + mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData); reset(mListener); // WHEN a second key migration event is received for media - mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData); + mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */); // THEN the key has already been migrated ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @Test public void migrateKeyDeviceAfter() { // GIVEN that media and device info has already been received - mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData); + mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); - mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData); + mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */); reset(mListener); // WHEN a second key migration event is received for the device mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData); // THEN the key has already be migrated ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture()); + verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); } @@ -186,7 +187,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { @Test public void mediaDataRemovedAfterMediaEvent() { - mManager.onMediaDataLoaded(KEY, null, mMediaData); + mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); mManager.onMediaDataRemoved(KEY); verify(mListener).onMediaDataRemoved(eq(KEY)); } @@ -201,12 +202,13 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { @Test public void mediaDataKeyUpdated() { // GIVEN that device and media events have already been received - mManager.onMediaDataLoaded(KEY, null, mMediaData); + mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(KEY, null, mDeviceData); // WHEN the key is changed - mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData); + mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData, true /* immediately */); // THEN the listener gets a load event with the correct keys ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); - verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture()); + verify(mListener).onMediaDataLoaded( + eq("NEW_KEY"), any(), captor.capture(), anyBoolean()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt index fc0506abbdac..17f2a07eb249 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.media import android.app.smartspace.SmartspaceAction -import android.app.smartspace.SmartspaceTarget import android.graphics.Color import androidx.test.filters.SmallTest import android.testing.AndroidTestingRunner @@ -74,7 +73,7 @@ class MediaDataFilterTest : SysuiTestCase() { @Mock private lateinit var executor: Executor @Mock - private lateinit var smartspaceData: SmartspaceTarget + private lateinit var smartspaceData: SmartspaceMediaData @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction @@ -102,8 +101,11 @@ class MediaDataFilterTest : SysuiTestCase() { dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, null, null, device, true, null) - `when`(smartspaceData.smartspaceTargetId).thenReturn(SMARTSPACE_KEY) - `when`(smartspaceData.iconGrid).thenReturn(listOf(smartspaceMediaRecommendationItem)) + `when`(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) + `when`(smartspaceData.isActive).thenReturn(true) + `when`(smartspaceData.isValid).thenReturn(true) + `when`(smartspaceData.packageName).thenReturn(PACKAGE) + `when`(smartspaceData.recommendations).thenReturn(listOf(smartspaceMediaRecommendationItem)) } private fun setUser(id: Int) { @@ -118,7 +120,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) // THEN we should tell the listener - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true)) } @Test @@ -127,7 +129,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) // THEN we should NOT tell the listener - verify(listener, never()).onMediaDataLoaded(any(), any(), any()) + verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean()) } @Test @@ -173,10 +175,10 @@ class MediaDataFilterTest : SysuiTestCase() { setUser(USER_GUEST) // THEN we should add back the guest user media - verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest)) + verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true)) // but not the main user's - verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain)) + verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean()) } @Test @@ -229,7 +231,7 @@ class MediaDataFilterTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noMedia_nonEmptyRec_prioritizesSmartspace() { + fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) verify(listener) @@ -238,18 +240,18 @@ class MediaDataFilterTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noMedia_emptyRec_showsNothing() { - `when`(smartspaceData.iconGrid).thenReturn(listOf()) + fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() { + `when`(smartspaceData.isActive).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean()) - assertThat(mediaDataFilter.hasActiveMedia()).isTrue() + verify(listener, never()).onMediaDataLoaded(any(), any(), any(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } @Test - fun testOnSmartspaceMediaDataLoaded_noRecentMedia_nonEmptyRec_prioritizesSmartspace() { + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() { val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) clock.advanceTime(SMARTSPACE_MAX_AGE + 100) @@ -261,53 +263,68 @@ class MediaDataFilterTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noRecentMedia_emptyRec_showsNothing() { - `when`(smartspaceData.iconGrid).thenReturn(listOf()) + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() { + `when`(smartspaceData.isActive).thenReturn(false) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) clock.advanceTime(SMARTSPACE_MAX_AGE + 100) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean()) - assertThat(mediaDataFilter.hasActiveMedia()).isTrue() + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() { + `when`(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()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true)) + + // AND we get a smartspace signal + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } @Test - fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_emptyRec_usesMedia() { - `when`(smartspaceData.iconGrid).thenReturn(listOf()) + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() { + `when`(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()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() // Smartspace update shouldn't be propagated for the empty rec list. - verify(listener, never()) - .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) } @Test - fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_nonEmptyRec_usesBoth() { + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() { // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true)) // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should tell listeners to treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrentAndActive), eq(true)) assertThat(mediaDataFilter.hasActiveMedia()).isTrue() // Smartspace update should also be propagated but not prioritized. verify(listener) @@ -320,7 +337,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - assertThat(mediaDataFilter.hasSmartspace).isFalse() + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() } @Test @@ -331,9 +348,8 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), eq(dataCurrent), eq(true)) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) assertThat(mediaDataFilter.hasActiveMedia()).isFalse() - assertThat(mediaDataFilter.hasSmartspace).isFalse() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index dfb149daa6d5..15cfee828293 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.media import android.app.Notification.MediaStyle import android.app.PendingIntent +import android.app.smartspace.SmartspaceAction import android.app.smartspace.SmartspaceTarget import android.graphics.Bitmap import android.media.MediaDescription @@ -9,6 +10,7 @@ import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSession import android.provider.Settings +import android.os.Bundle import android.service.notification.StatusBarNotification import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -79,6 +81,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var activityStarter: ActivityStarter lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget + @Mock private lateinit var mediaRecommendationItem: SmartspaceAction lateinit var mediaDataManager: MediaDataManager lateinit var mediaNotification: StatusBarNotification @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> @@ -137,8 +140,12 @@ class MediaDataManagerTest : SysuiTestCase() { // treat mediaSessionBasedFilter as a listener for testing. listener = mediaSessionBasedFilter + val recommendationExtras = Bundle() + recommendationExtras.putString("package_name", PACKAGE_NAME) + whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras) whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf(mediaRecommendationItem)) } @After @@ -172,7 +179,7 @@ class MediaDataManagerTest : SysuiTestCase() { fun testOnMetaDataLoaded_callsListener() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java)) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject()) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject(), eq(true)) } @Test @@ -183,7 +190,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value!!.active).isTrue() } @@ -202,14 +209,15 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) val data = mediaDataCaptor.value assertThat(data.resumption).isFalse() mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) // WHEN the notification is removed mediaDataManager.onNotificationRemoved(KEY) // THEN the media data indicates that it is for resumption - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.resumption).isTrue() } @@ -221,7 +229,8 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY_2, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) val data = mediaDataCaptor.value assertThat(data.resumption).isFalse() val resumableData = data.copy(resumeAction = Runnable {}) @@ -231,14 +240,16 @@ class MediaDataManagerTest : SysuiTestCase() { // WHEN the first is removed mediaDataManager.onNotificationRemoved(KEY) // THEN the data is for resumption and the key is migrated to the package name - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener, never()).onMediaDataRemoved(eq(KEY)) // WHEN the second is removed mediaDataManager.onNotificationRemoved(KEY_2) // THEN the data is for resumption and the second key is removed - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(PACKAGE_NAME), - capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), eq(PACKAGE_NAME), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.resumption).isTrue() verify(listener).onMediaDataRemoved(eq(KEY_2)) } @@ -252,7 +263,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) val data = mediaDataCaptor.value val dataRemoteWithResume = data.copy(resumeAction = Runnable {}, isLocalSession = false) mediaDataManager.onMediaDataLoaded(KEY, null, dataRemoteWithResume) @@ -277,7 +288,8 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN the media data indicates that it is for resumption - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor), eq(true)) val data = mediaDataCaptor.value assertThat(data.resumption).isTrue() assertThat(data.song).isEqualTo(SESSION_TITLE) @@ -316,14 +328,29 @@ class MediaDataManagerTest : SysuiTestCase() { // THEN it still loads assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) } @Test - fun testOnSmartspaceMediaDataLoaded_hasNewMediaTarget_callsListener() { + fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() { smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) verify(listener).onSmartspaceMediaDataLoaded( - eq(KEY_MEDIA_SMARTSPACE), eq(mediaSmartspaceTarget), eq(false)) + eq(KEY_MEDIA_SMARTSPACE), + eq(SmartspaceMediaData(KEY_MEDIA_SMARTSPACE, true /* isActive */, true /*isValid */, + PACKAGE_NAME, null, listOf(mediaRecommendationItem), 0)), + eq(false)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() { + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(listener).onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq(EMPTY_SMARTSPACE_MEDIA_DATA + .copy(targetId = KEY_MEDIA_SMARTSPACE, isActive = true, isValid = false)), + eq(false)) } @Test @@ -337,7 +364,7 @@ class MediaDataManagerTest : SysuiTestCase() { fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) smartspaceMediaDataProvider.onTargetsAvailable(listOf()) - verify(listener).onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE) + verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) } @Test @@ -358,7 +385,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime) } @@ -375,7 +402,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.setTimedOut(KEY, true, true) // THEN the last active time is not changed - verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(KEY), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) } @@ -386,7 +413,7 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationAdded(KEY, mediaNotification) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) val data = mediaDataCaptor.value assertThat(data.resumption).isFalse() mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) @@ -397,7 +424,8 @@ class MediaDataManagerTest : SysuiTestCase() { mediaDataManager.onNotificationRemoved(KEY) // THEN the last active time is not changed - verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor)) + verify(listener) + .onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.resumption).isTrue() assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) } @@ -423,7 +451,7 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN only the first MAX_COMPACT_ACTIONS are actually set - verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor)) + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor), eq(true)) assertThat(mediaDataCaptor.value.actionsToShowInCompact.size).isEqualTo( MediaDataManager.MAX_COMPACT_ACTIONS) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt index 2d90cc4f6712..c6d7e92175eb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt @@ -36,6 +36,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any @@ -184,7 +185,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { filter.onMediaDataLoaded(KEY, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) } @Test @@ -206,7 +207,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) } @Test @@ -235,7 +236,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) } @Test @@ -250,13 +251,14 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered - verify(mediaListener, never()).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2)) + verify(mediaListener, never()).onMediaDataLoaded( + eq(KEY), eq(null), eq(mediaData2), anyBoolean()) } @Test @@ -272,7 +274,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { fgExecutor.runAllReady() // THEN the event is not filtered because there isn't a notification for the remote // session. - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) } @Test @@ -289,13 +291,14 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is filtered - verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2)) + verify(mediaListener, never()) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean()) // AND there should be a removed event for key2 verify(mediaListener).onMediaDataRemoved(eq(key2)) } @@ -314,13 +317,13 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1), eq(true)) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2)) + verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), eq(true)) } @Test @@ -336,13 +339,13 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) // WHEN a loaded event is received that matches the local session filter.onMediaDataLoaded(KEY, null, mediaData2) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2), eq(true)) } @Test @@ -360,7 +363,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the event is not filtered - verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1), eq(true)) } @Test @@ -382,7 +385,7 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2)) + verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2), eq(true)) } @Test @@ -411,12 +414,13 @@ public class MediaSessionBasedFilterTest : SysuiTestCase() { bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is filtered - verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2)) + verify(mediaListener, never()) + .onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2), anyBoolean()) // WHEN a loaded event is received that matches the remote session filter.onMediaDataLoaded(key2, null, mediaData1) bgExecutor.runAllReady() fgExecutor.runAllReady() // THEN the key migration event is fired - verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1)) + verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1), eq(true)) } } |