Merge changes Iffe0f84b,Ice8b7f18 into main

* changes:
  Add media smartspace received logs.
  Add smartspace logger
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
index bc0512a..f43fa50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -30,11 +30,21 @@
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
+import com.android.systemui.media.controls.util.SmallHash
+import com.android.systemui.media.controls.util.mediaSmartspaceLogger
+import com.android.systemui.media.controls.util.mockMediaSmartspaceLogger
 import com.android.systemui.testKosmos
+import com.android.systemui.util.time.systemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -42,8 +52,20 @@
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
+    private val smartspaceLogger = kosmos.mockMediaSmartspaceLogger
+    private val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
+    private val mediaRecommendation =
+        SmartspaceMediaData(
+            targetId = KEY_MEDIA_SMARTSPACE,
+            isActive = true,
+            recommendations = MediaTestHelper.getValidRecommendationList(icon),
+        )
 
-    private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository
+    private val underTest: MediaFilterRepository =
+        with(kosmos) {
+            mediaSmartspaceLogger = mockMediaSmartspaceLogger
+            mediaFilterRepository
+        }
 
     @Test
     fun addSelectedUserMediaEntry_activeThenInactivate() =
@@ -137,14 +159,6 @@
         testScope.runTest {
             val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData)
 
-            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
-            val mediaRecommendation =
-                SmartspaceMediaData(
-                    targetId = KEY_MEDIA_SMARTSPACE,
-                    isActive = true,
-                    recommendations = MediaTestHelper.getValidRecommendationList(icon),
-                )
-
             underTest.setRecommendation(mediaRecommendation)
 
             assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation)
@@ -164,16 +178,38 @@
             val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId)
             val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId)
 
+            underTest.setRecommendation(mediaRecommendation)
+            underTest.setRecommendationsLoadingState(
+                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+            )
             underTest.addSelectedUserMediaEntry(playingData)
             underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId))
+
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    playingData.smartspaceId,
+                    playingData.appUid,
+                    cardinality = 2
+                )
+
             underTest.addSelectedUserMediaEntry(remoteData)
             underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(remoteInstanceId))
 
-            assertThat(currentMedia?.size).isEqualTo(2)
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    remoteData.smartspaceId,
+                    playingData.appUid,
+                    cardinality = 3,
+                    rank = 1
+                )
+            assertThat(currentMedia?.size).isEqualTo(3)
             assertThat(currentMedia)
                 .containsExactly(
                     MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId)),
-                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(remoteInstanceId))
+                    MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(remoteInstanceId)),
+                    MediaCommonModel.MediaRecommendations(
+                        SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+                    )
                 )
                 .inOrder()
         }
@@ -222,6 +258,16 @@
 
             underTest.setOrderedMedia()
 
+            verify(smartspaceLogger, never())
+                .logSmartspaceCardReceived(
+                    anyInt(),
+                    anyInt(),
+                    anyInt(),
+                    anyBoolean(),
+                    anyBoolean(),
+                    anyInt(),
+                    anyInt()
+                )
             assertThat(currentMedia?.size).isEqualTo(2)
             assertThat(currentMedia)
                 .containsExactly(
@@ -248,14 +294,6 @@
             val stoppedAndRemoteData = createMediaData("app4", false, REMOTE, false, instanceId4)
             val canResumeData = createMediaData("app5", false, LOCAL, true, instanceId5)
 
-            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
-            val mediaRecommendations =
-                SmartspaceMediaData(
-                    targetId = KEY_MEDIA_SMARTSPACE,
-                    isActive = true,
-                    recommendations = MediaTestHelper.getValidRecommendationList(icon),
-                )
-
             underTest.addSelectedUserMediaEntry(stoppedAndLocalData)
             underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId3))
 
@@ -271,11 +309,33 @@
             underTest.addSelectedUserMediaEntry(playingAndRemoteData)
             underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2))
 
-            underTest.setRecommendation(mediaRecommendations)
+            underTest.setRecommendation(mediaRecommendation)
             underTest.setRecommendationsLoadingState(
                 SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
             )
+            underTest.setOrderedMedia()
 
+            val smartspaceId = SmallHash.hash(mediaRecommendation.targetId)
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    eq(smartspaceId),
+                    anyInt(),
+                    eq(6),
+                    anyBoolean(),
+                    anyBoolean(),
+                    eq(2),
+                    anyInt()
+                )
+            verify(smartspaceLogger, never())
+                .logSmartspaceCardReceived(
+                    eq(playingAndLocalData.smartspaceId),
+                    anyInt(),
+                    anyInt(),
+                    anyBoolean(),
+                    anyBoolean(),
+                    anyInt(),
+                    anyInt()
+                )
             assertThat(currentMedia?.size).isEqualTo(6)
             assertThat(currentMedia)
                 .containsExactly(
@@ -312,18 +372,10 @@
                     isPlaying = true,
                     notificationKey = KEY_2
                 )
-            val icon = Icon.createWithResource(context, R.drawable.ic_media_play)
-            val mediaRecommendations =
-                SmartspaceMediaData(
-                    targetId = KEY_MEDIA_SMARTSPACE,
-                    isActive = true,
-                    packageName = PACKAGE_NAME,
-                    recommendations = MediaTestHelper.getValidRecommendationList(icon),
-                )
 
             underTest.setMediaFromRecPackageName(PACKAGE_NAME)
             underTest.addSelectedUserMediaEntry(data)
-            underTest.setRecommendation(mediaRecommendations)
+            underTest.setRecommendation(mediaRecommendation)
             underTest.setRecommendationsLoadingState(
                 SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE)
             )
@@ -365,6 +417,88 @@
     fun hasActiveMedia_noMediaSet_returnsFalse() =
         testScope.runTest { assertThat(underTest.hasActiveMedia()).isFalse() }
 
+    @Test
+    fun updateMediaWithLatency_smartspaceIsLogged() =
+        testScope.runTest {
+            val instanceId = InstanceId.fakeInstanceId(123)
+            val data = createMediaData("app", true, LOCAL, false, instanceId)
+
+            underTest.setRecommendation(mediaRecommendation)
+            underTest.setRecommendationsLoadingState(
+                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+            )
+
+            val smartspaceId = SmallHash.hash(mediaRecommendation.targetId)
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    eq(smartspaceId),
+                    anyInt(),
+                    eq(1),
+                    eq(true),
+                    anyBoolean(),
+                    eq(0),
+                    anyInt()
+                )
+            reset(smartspaceLogger)
+
+            underTest.addSelectedUserMediaEntry(data)
+            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId))
+
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(data.smartspaceId, data.appUid, cardinality = 2)
+
+            reset(smartspaceLogger)
+
+            underTest.addSelectedUserMediaEntry(data)
+            underTest.addMediaDataLoadingState(
+                MediaDataLoadingModel.Loaded(instanceId, receivedSmartspaceCardLatency = 123)
+            )
+
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    SmallHash.hash(data.appUid + kosmos.systemClock.currentTimeMillis().toInt()),
+                    data.appUid,
+                    cardinality = 2,
+                    rank = 0,
+                    receivedLatencyMillis = 123
+                )
+        }
+
+    @Test
+    fun resumeMedia_loadSmartspace_allSmartspaceIsLogged() =
+        testScope.runTest {
+            val resumeInstanceId = InstanceId.fakeInstanceId(123)
+            val data = createMediaData("app", false, LOCAL, true, resumeInstanceId)
+
+            underTest.addSelectedUserMediaEntry(data.copy(active = false))
+            underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(resumeInstanceId))
+            underTest.setRecommendation(mediaRecommendation)
+            underTest.setRecommendationsLoadingState(
+                SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true)
+            )
+
+            assertThat(underTest.hasActiveMedia()).isFalse()
+            assertThat(underTest.hasAnyMedia()).isTrue()
+            val smartspaceId = SmallHash.hash(mediaRecommendation.targetId)
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    eq(smartspaceId),
+                    anyInt(),
+                    eq(2),
+                    eq(true),
+                    anyBoolean(),
+                    eq(0),
+                    anyInt()
+                )
+            verify(smartspaceLogger)
+                .logSmartspaceCardReceived(
+                    SmallHash.hash(data.appUid + kosmos.systemClock.currentTimeMillis().toInt()),
+                    data.appUid,
+                    cardinality = 2,
+                    rank = 1
+                )
+        }
+
     private fun createMediaData(
         app: String,
         playing: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index 6a91d1b..a2d7fb1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -26,6 +26,8 @@
 import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData
 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel
+import com.android.systemui.media.controls.util.MediaSmartspaceLogger
+import com.android.systemui.media.controls.util.SmallHash
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.time.SystemClock
 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
@@ -43,9 +45,10 @@
 class MediaFilterRepository
 @Inject
 constructor(
-    @Application applicationContext: Context,
+    @Application private val applicationContext: Context,
     private val systemClock: SystemClock,
     private val configurationController: ConfigurationController,
+    private val smartspaceLogger: MediaSmartspaceLogger,
 ) {
 
     val onAnyMediaConfigurationChange: Flow<Unit> = conflatedCallbackFlow {
@@ -211,6 +214,12 @@
                         isMediaFromRec(it)
                     )
                 sortedMap[sortKey] = newCommonModel
+                val isUpdate =
+                    sortedMedia.values.any { commonModel ->
+                        commonModel is MediaCommonModel.MediaControl &&
+                            commonModel.mediaLoadedModel.instanceId ==
+                                mediaDataLoadingModel.instanceId
+                    }
 
                 // On Addition or tapping on recommendations, we should show the new order of media.
                 if (mediaFromRecPackageName == it.packageName) {
@@ -218,30 +227,50 @@
                         mediaFromRecPackageName = null
                         _currentMedia.value = sortedMap.values.toList()
                     }
-                } else if (sortedMap.size > _currentMedia.value.size && it.active) {
-                    _currentMedia.value = sortedMap.values.toList()
                 } else {
-                    // When loading an update for an existing media control.
+                    var isNewToCurrentMedia = true
                     val currentList =
                         mutableListOf<MediaCommonModel>().apply { addAll(_currentMedia.value) }
                     currentList.forEachIndexed { index, mediaCommonModel ->
                         if (
                             mediaCommonModel is MediaCommonModel.MediaControl &&
                                 mediaCommonModel.mediaLoadedModel.instanceId ==
-                                    mediaDataLoadingModel.instanceId &&
-                                mediaCommonModel != newCommonModel
+                                    mediaDataLoadingModel.instanceId
                         ) {
-                            // Update media model if changed.
-                            currentList[index] = newCommonModel
+                            // When loading an update for an existing media control.
+                            isNewToCurrentMedia = false
+                            if (mediaCommonModel != newCommonModel) {
+                                // Update media model if changed.
+                                currentList[index] = newCommonModel
+                            }
                         }
                     }
-                    _currentMedia.value = currentList
+                    if (isNewToCurrentMedia && it.active) {
+                        _currentMedia.value = sortedMap.values.toList()
+                    } else {
+                        _currentMedia.value = currentList
+                    }
+                }
+
+                sortedMedia = sortedMap
+
+                if (!isUpdate) {
+                    val rank = sortedMedia.values.indexOf(newCommonModel)
+                    if (isSmartspaceLoggingEnabled(newCommonModel, rank)) {
+                        smartspaceLogger.logSmartspaceCardReceived(
+                            it.smartspaceId,
+                            it.appUid,
+                            cardinality = _currentMedia.value.size,
+                            isSsReactivated = mediaDataLoadingModel.isSsReactivated,
+                            rank = rank,
+                        )
+                    }
+                } else if (mediaDataLoadingModel.receivedSmartspaceCardLatency != 0) {
+                    logSmartspaceAllMediaCards(mediaDataLoadingModel.receivedSmartspaceCardLatency)
                 }
             }
         }
 
-        sortedMedia = sortedMap
-
         // On removal we want to keep the order being shown to user.
         if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) {
             _currentMedia.value =
@@ -249,6 +278,7 @@
                     commonModel !is MediaCommonModel.MediaControl ||
                         mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId
                 }
+            sortedMedia = sortedMap
         }
     }
 
@@ -271,21 +301,45 @@
                 isPlaying = false,
                 active = _smartspaceMediaData.value.isActive,
             )
+        val newCommonModel = MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel)
         when (smartspaceMediaLoadingModel) {
-            is SmartspaceMediaLoadingModel.Loaded ->
-                sortedMap[sortKey] =
-                    MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel)
-            is SmartspaceMediaLoadingModel.Removed ->
+            is SmartspaceMediaLoadingModel.Loaded -> {
+                sortedMap[sortKey] = newCommonModel
+                _currentMedia.value = sortedMap.values.toList()
+                sortedMedia = sortedMap
+
+                if (isRecommendationActive()) {
+                    val hasActivatedExistedResumeMedia =
+                        !hasActiveMedia() &&
+                            hasAnyMedia() &&
+                            smartspaceMediaLoadingModel.isPrioritized
+                    if (hasActivatedExistedResumeMedia) {
+                        // Log resume card received if resumable media card is reactivated and
+                        // recommendation card is valid and ranked first
+                        logSmartspaceAllMediaCards(
+                            (systemClock.currentTimeMillis() -
+                                    _smartspaceMediaData.value.headphoneConnectionTimeMillis)
+                                .toInt()
+                        )
+                    }
+
+                    smartspaceLogger.logSmartspaceCardReceived(
+                        SmallHash.hash(_smartspaceMediaData.value.targetId),
+                        _smartspaceMediaData.value.getUid(applicationContext),
+                        cardinality = _currentMedia.value.size,
+                        isRecommendationCard = true,
+                        rank = _currentMedia.value.indexOf(newCommonModel),
+                    )
+                }
+            }
+            is SmartspaceMediaLoadingModel.Removed -> {
                 _currentMedia.value =
                     _currentMedia.value.filter { commonModel ->
                         commonModel !is MediaCommonModel.MediaRecommendations
                     }
+                sortedMedia = sortedMap
+            }
         }
-
-        if (sortedMap.size > sortedMedia.size) {
-            _currentMedia.value = sortedMap.values.toList()
-        }
-        sortedMedia = sortedMap
     }
 
     fun setOrderedMedia() {
@@ -315,4 +369,35 @@
     private fun isMediaFromRec(data: MediaData): Boolean {
         return data.isPlaying == true && mediaFromRecPackageName == data.packageName
     }
+
+    /** Log all media cards if smartspace logging is enabled for each. */
+    private fun logSmartspaceAllMediaCards(receivedSmartspaceCardLatency: Int) {
+        sortedMedia.values.forEachIndexed { index, mediaCommonModel ->
+            if (mediaCommonModel is MediaCommonModel.MediaControl) {
+                _selectedUserEntries.value[mediaCommonModel.mediaLoadedModel.instanceId]?.let {
+                    it.smartspaceId =
+                        SmallHash.hash(it.appUid + systemClock.currentTimeMillis().toInt())
+                    it.isImpressed = false
+
+                    if (isSmartspaceLoggingEnabled(mediaCommonModel, index)) {
+                        smartspaceLogger.logSmartspaceCardReceived(
+                            it.smartspaceId,
+                            it.appUid,
+                            cardinality = _currentMedia.value.size,
+                            isSsReactivated = mediaCommonModel.mediaLoadedModel.isSsReactivated,
+                            rank = index,
+                            receivedLatencyMillis = receivedSmartspaceCardLatency,
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    private fun isSmartspaceLoggingEnabled(commonModel: MediaCommonModel, index: Int): Boolean {
+        return sortedMedia.size > index &&
+            (_smartspaceMediaData.value.expiryTimeMs != 0L ||
+                isRecommendationActive() ||
+                commonModel is MediaCommonModel.MediaRecommendations)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
index f78a0f9..31bd4fb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt
@@ -180,7 +180,13 @@
                     mediaData.instanceId
                 )
                 mediaFilterRepository.addMediaDataLoadingState(
-                    MediaDataLoadingModel.Loaded(lastActiveId)
+                    MediaDataLoadingModel.Loaded(
+                        lastActiveId,
+                        receivedSmartspaceCardLatency =
+                            (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis)
+                                .toInt(),
+                        isSsReactivated = true
+                    )
                 )
                 mediaLoadingLogger.logMediaLoaded(
                     mediaData.instanceId,
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
index 37dffd1..adcfba7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt
@@ -86,6 +86,7 @@
 import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
+import com.android.systemui.media.controls.util.SmallHash
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.res.R
@@ -721,6 +722,7 @@
                     appUid = appUid,
                     isExplicit = isExplicit,
                     resumeProgress = progress,
+                    smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
                 )
             )
         }
@@ -902,6 +904,7 @@
                     instanceId = instanceId,
                     appUid = appUid,
                     isExplicit = isExplicit,
+                    smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()),
                 )
             )
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
index 11a5629..40b3477 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt
@@ -99,6 +99,12 @@
 
     /** Track progress (0 - 1) to display for players where [resumption] is true */
     val resumeProgress: Double? = null,
+
+    /** Smartspace Id, used for logging. */
+    var smartspaceId: Int = -1,
+
+    /** If media card was visible to user, used for logging. */
+    var isImpressed: Boolean = false,
 ) {
     companion object {
         /** Media is playing on the local device */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
index 170f1f7..c8a02fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt
@@ -27,6 +27,8 @@
     data class Loaded(
         override val instanceId: InstanceId,
         val immediatelyUpdateUi: Boolean = true,
+        val receivedSmartspaceCardLatency: Int = 0,
+        val isSsReactivated: Boolean = false,
     ) : MediaDataLoadingModel()
 
     /** Media data has been removed. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
index 9e15dbb..96c3fa8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt
@@ -48,6 +48,8 @@
     val instanceId: InstanceId? = null,
     /** The timestamp in milliseconds indicating when the card should be removed */
     val expiryTimeMs: Long = 0L,
+    /** If recommendation card was visible to user, used for logging. */
+    var isImpressed: Boolean = false,
 ) {
     /**
      * Indicates if all the data is valid.
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
new file mode 100644
index 0000000..01fbf4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSmartspaceLogger.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 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.controls.util
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shared.system.SysUiStatsLog
+import javax.inject.Inject
+
+/** Logger class for Smartspace logging events. */
+@SysUISingleton
+class MediaSmartspaceLogger @Inject constructor() {
+    /**
+     * Log Smartspace card received event
+     *
+     * @param instanceId id to uniquely identify a card.
+     * @param uid uid for the application that media comes from.
+     * @param cardinality number of card in carousel.
+     * @param isRecommendationCard whether media card being logged is a recommendations card.
+     * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+     *   recommendation signal
+     * @param rank the rank for media card in the media carousel, starting from 0
+     * @param receivedLatencyMillis latency in milliseconds for card received events.
+     */
+    fun logSmartspaceCardReceived(
+        instanceId: Int,
+        uid: Int,
+        cardinality: Int,
+        isRecommendationCard: Boolean = false,
+        isSsReactivated: Boolean = false,
+        rank: Int = 0,
+        receivedLatencyMillis: Int = 0,
+    ) {
+        logSmartspaceCardReported(
+            SMARTSPACE_CARD_RECEIVED_EVENT,
+            instanceId,
+            uid,
+            surfaces =
+                intArrayOf(
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE,
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN,
+                    SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY,
+                ),
+            cardinality,
+            isRecommendationCard,
+            isSsReactivated,
+            rank = rank,
+            receivedLatencyMillis = receivedLatencyMillis,
+        )
+    }
+
+    /**
+     * Log Smartspace card UI event
+     *
+     * @param eventId id of the event. eg: dismiss, click, or seen.
+     * @param instanceId id to uniquely identify a card.
+     * @param uid uid for the application that media comes from.
+     * @param location location of media carousel holding media card.
+     * @param cardinality number of card in carousel.
+     * @param isRecommendationCard whether media card being logged is a recommendations card.
+     * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+     *   recommendation signal
+     * @param rank the rank for media card in the media carousel, starting from 0
+     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
+     */
+    fun logSmartspaceCardUIEvent(
+        eventId: Int,
+        instanceId: Int,
+        uid: Int,
+        location: Int,
+        cardinality: Int,
+        isRecommendationCard: Boolean = false,
+        isSsReactivated: Boolean = false,
+        rank: Int = 0,
+        isSwipeToDismiss: Boolean = false,
+    ) {
+        logSmartspaceCardReported(
+            eventId,
+            instanceId,
+            uid,
+            surfaces = intArrayOf(location),
+            cardinality,
+            isRecommendationCard,
+            isSsReactivated,
+            rank = rank,
+            isSwipeToDismiss = isSwipeToDismiss,
+        )
+    }
+
+    /**
+     * Log Smartspace events
+     *
+     * @param eventId UI event id (e.g. 800 for SMARTSPACE_CARD_SEEN)
+     * @param instanceId id to uniquely identify a card, e.g. each headphone generates a new
+     *   instanceId
+     * @param uid uid for the application that media comes from
+     * @param surfaces list of display surfaces the media card is on (e.g. lockscreen, shade) when
+     *   the event happened
+     * @param cardinality number of card in carousel.
+     * @param isRecommendationCard whether media card being logged is a recommendations card.
+     * @param isSsReactivated indicates resume media card is reactivated by Smartspace
+     *   recommendation signal
+     * @param interactedSubcardRank the rank for interacted media item for recommendation card, -1
+     *   for tapping on card but not on any media item, 0 for first media item, 1 for second, etc.
+     * @param interactedSubcardCardinality how many media items were shown to the user when there is
+     *   user interaction
+     * @param rank the rank for media card in the media carousel, starting from 0
+     * @param receivedLatencyMillis latency in milliseconds for card received events. E.g. latency
+     *   between headphone connection to sysUI displays media recommendation card
+     * @param isSwipeToDismiss whether is to log swipe-to-dismiss event
+     */
+    private fun logSmartspaceCardReported(
+        eventId: Int,
+        instanceId: Int,
+        uid: Int,
+        surfaces: IntArray,
+        cardinality: Int,
+        isRecommendationCard: Boolean,
+        isSsReactivated: Boolean,
+        interactedSubcardRank: Int = 0,
+        interactedSubcardCardinality: Int = 0,
+        rank: Int = 0,
+        receivedLatencyMillis: Int = 0,
+        isSwipeToDismiss: Boolean = false,
+    ) {
+        surfaces.forEach { surface ->
+            SysUiStatsLog.write(
+                SysUiStatsLog.SMARTSPACE_CARD_REPORTED,
+                eventId,
+                instanceId,
+                // Deprecated, replaced with AiAi feature type so we don't need to create logging
+                // card type for each new feature.
+                SysUiStatsLog.SMART_SPACE_CARD_REPORTED__CARD_TYPE__UNKNOWN_CARD,
+                surface,
+                // Use -1 as rank value to indicate user swipe to dismiss the card
+                if (isSwipeToDismiss) -1 else rank,
+                cardinality,
+                if (isRecommendationCard) {
+                    15 // MEDIA_RECOMMENDATION
+                } else if (isSsReactivated) {
+                    43 // MEDIA_RESUME_SS_ACTIVATED
+                } else {
+                    31 // MEDIA_RESUME
+                },
+                uid,
+                interactedSubcardRank,
+                interactedSubcardCardinality,
+                receivedLatencyMillis,
+                null, // Media cards cannot have subcards.
+                null // Media cards don't have dimensions today.
+            )
+
+            if (DEBUG) {
+                Log.d(
+                    TAG,
+                    "Log Smartspace card event id: $eventId instance id: $instanceId" +
+                        " surface: $surface rank: $rank cardinality: $cardinality " +
+                        "isRecommendationCard: $isRecommendationCard " +
+                        "isSsReactivated: $isSsReactivated" +
+                        "uid: $uid " +
+                        "interactedSubcardRank: $interactedSubcardRank " +
+                        "interactedSubcardCardinality: $interactedSubcardCardinality " +
+                        "received_latency_millis: $receivedLatencyMillis"
+                )
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "MediaSmartspaceLogger"
+        private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+        private const val SMARTSPACE_CARD_RECEIVED_EVENT = 759
+        const val SMARTSPACE_CARD_CLICK_EVENT = 760
+        const val SMARTSPACE_CARD_DISMISS_EVENT = 761
+        const val SMARTSPACE_CARD_SEEN_EVENT = 800
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
index 544350c..1d4b090 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java
@@ -79,7 +79,7 @@
                 USER_ID, true, APP, null, ARTIST, TITLE, null,
                 new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
                 MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 0L,
-                InstanceId.fakeInstanceId(-1), -1, false, null);
+                InstanceId.fakeInstanceId(-1), -1, false, null, -1, false);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 4da56b5..c974e0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -740,11 +740,8 @@
 
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-            val controlCommonModel =
-                MediaCommonModel.MediaControl(
-                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
-                    true
-                )
+            val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
+            var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent)
             repository.setOrderedMedia()
             assertThat(currentMedia).containsExactly(controlCommonModel)
@@ -758,7 +755,15 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
-            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            val dataCurrentAndActive =
+                dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
+            controlCommonModel =
+                controlCommonModel.copy(
+                    mediaLoadingModel.copy(
+                        receivedSmartspaceCardLatency = 100,
+                        isSsReactivated = true
+                    )
+                )
             assertThat(currentMedia).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -800,11 +805,8 @@
             val currentMedia by collectLastValue(repository.currentMedia)
             // WHEN we have media that was recently played, but not currently active
             val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime())
-            val controlCommonModel =
-                MediaCommonModel.MediaControl(
-                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
-                    true
-                )
+            val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
+            var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
             val recsCommonModel =
                 MediaCommonModel.MediaRecommendations(
                     SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
@@ -824,7 +826,8 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
-            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            val dataCurrentAndActive =
+                dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
             verify(listener)
                 .onMediaDataLoaded(
                     eq(KEY),
@@ -849,6 +852,13 @@
                 )
                 .isTrue()
             // Smartspace update should also be propagated but not prioritized.
+            controlCommonModel =
+                controlCommonModel.copy(
+                    mediaLoadingModel.copy(
+                        receivedSmartspaceCardLatency = 100,
+                        isSsReactivated = true
+                    )
+                )
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
@@ -909,7 +919,8 @@
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
-            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            val dataCurrentAndActive =
+                dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
             verify(listener)
                 .onMediaDataLoaded(
                     eq(KEY),
@@ -1063,11 +1074,8 @@
                 MediaCommonModel.MediaRecommendations(
                     SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY)
                 )
-            val controlCommonModel =
-                MediaCommonModel.MediaControl(
-                    MediaDataLoadingModel.Loaded(dataMain.instanceId),
-                    true
-                )
+            val mediaLoadingModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
+            var controlCommonModel = MediaCommonModel.MediaControl(mediaLoadingModel, true)
             // 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)
@@ -1086,7 +1094,15 @@
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // THEN we should treat the media as active instead
-            val dataCurrentAndActive = dataCurrent.copy(active = true)
+            val dataCurrentAndActive =
+                dataMain.copy(active = true, lastActive = clock.elapsedRealtime())
+            controlCommonModel =
+                controlCommonModel.copy(
+                    mediaLoadingModel.copy(
+                        receivedSmartspaceCardLatency = 100,
+                        isSsReactivated = true
+                    )
+                )
             verify(listener)
                 .onMediaDataLoaded(
                     eq(KEY),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt
index 690bde7..7a04aa2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.content.applicationContext
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.media.controls.util.mediaSmartspaceLogger
 import com.android.systemui.statusbar.policy.configurationController
 import com.android.systemui.util.time.systemClock
 
@@ -26,6 +27,7 @@
         MediaFilterRepository(
             applicationContext = applicationContext,
             systemClock = systemClock,
-            configurationController = configurationController
+            configurationController = configurationController,
+            smartspaceLogger = mediaSmartspaceLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaSmartspaceLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaSmartspaceLoggerKosmos.kt
new file mode 100644
index 0000000..c63dec5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaSmartspaceLoggerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 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.controls.util
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.Mockito.mock
+
+var Kosmos.mediaSmartspaceLogger by Kosmos.Fixture { MediaSmartspaceLogger() }
+val Kosmos.mockMediaSmartspaceLogger by Kosmos.Fixture { mock(MediaSmartspaceLogger::class.java) }