Merge "On Smartspace removal update, only dismiss media recommendation/player when it's invisible to users." into sc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index a1c06fc..09da9d2 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 @@
         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 @@
 
             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 @@
                 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 @@
         // 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 @@
 
     private fun addSmartspaceMediaRecommendations(
         key: String,
-        data: SmartspaceTarget,
+        data: SmartspaceMediaData,
         shouldPrioritize: Boolean
     ) {
         Log.d(TAG, "Updating smartspace target in carousel")
@@ -342,6 +354,11 @@
             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 @@
         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 @@
 
             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 @@
         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 @@
     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 @@
         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 c806bcf..45ceceb 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 @@
     }
 
     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 27a4e93..55feea9 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 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.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 @@
 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 @@
         };
     }
 
-    /** 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 @@
         }
         // 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 @@
                     /* isRecommendationCard */ true);
             closeGuts();
             mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
-                    MediaViewController.GUTS_ANIMATION_DURATION + 100L);
+                    data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
         });
 
         mController = null;
@@ -753,38 +753,6 @@
     }
 
     /**
-     * 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 87af9e0..ee1d3ea 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 @@
     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 @@
 
     override fun onSmartspaceMediaDataLoaded(
         key: String,
-        data: SmartspaceTarget,
+        data: SmartspaceMediaData,
         shouldPrioritize: Boolean
     ) {
         listeners.toSet().forEach { it.onSmartspaceMediaDataLoaded(key, data) }
@@ -50,8 +54,8 @@
         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 3deb5d1..a611b60 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 @@
 
 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 @@
     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 @@
         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 @@
 
     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 @@
             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 @@
         }
     }
 
-    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 @@
             // 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 @@
             // 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 @@
      */
     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 c74f2fe..13c7f71 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.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 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 @@
         @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 @@
     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 @@
      * 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 @@
      *
      * 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 @@
      * 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 @@
 
     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 @@
          * 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
-         */
+        /** Called whenever a previously existing Media notification was removed. */
         fun onMediaDataRemoved(key: String) {}
 
-        /** Called whenever a previously existing Smartspace media data was removed.  */
-        fun onSmartspaceMediaDataRemoved(key: String) {}
+        /**
+         * 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 onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {}
+    }
+
+    /**
+     * 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 a993d00..52ecbea 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 @@
      */
     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 fe20dcb..43e2142 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 @@
         }
 
     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 @@
             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 7fe408f..9aeb63d 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 @@
         }
     }
 
-    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 3e5e8248..a4f33e3 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 @@
      * 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 @@
 
     override fun onSmartspaceMediaDataLoaded(
         key: String,
-        data: SmartspaceTarget,
+        data: SmartspaceMediaData,
         shouldPrioritize: Boolean
     ) {
         backgroundExecutor.execute {
@@ -153,15 +157,20 @@
         }
     }
 
-    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 @@
         }
     }
 
-    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 8bfe94b..bbea140 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 @@
      */
     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 0000000..9ac1289
--- /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 f99436f..a69b8d6 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.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.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 @@
         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 @@
             }
 
             @Override
-            public void onSmartspaceMediaDataRemoved(@NonNull String key) {}
+            public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {}
         });
     }
 
@@ -319,12 +318,12 @@
         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 @@
             }
 
             @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 4a487be..e20b426 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 @@
 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 @@
     @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 @@
         // 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 @@
         // 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 @@
 
     @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 @@
     @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 fc0506a..17f2a07 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 @@
     @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 @@
         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 @@
         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 @@
         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 @@
         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 @@
     }
 
     @Test
-    fun testOnSmartspaceMediaDataLoaded_noMedia_nonEmptyRec_prioritizesSmartspace() {
+    fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() {
         mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
         verify(listener)
@@ -238,18 +240,18 @@
     }
 
     @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 @@
     }
 
     @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_emptyRec_usesMedia() {
-        `when`(smartspaceData.iconGrid).thenReturn(listOf())
+    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))
+        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_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), 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 @@
         mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
         verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
-        assertThat(mediaDataFilter.hasSmartspace).isFalse()
+        assertThat(mediaDataFilter.hasActiveMedia()).isFalse()
     }
 
     @Test
@@ -331,9 +348,8 @@
 
         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 dfb149d..15cfee8 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 @@
 
 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.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 @@
     @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 @@
         // 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 @@
     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 @@
         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 @@
         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 @@
         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 @@
         // 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 @@
         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 @@
         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 @@
         // 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 @@
     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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 2d90cc4..c6d7e92 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.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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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 @@
         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))
     }
 }