Add logs when media is loaded or removed

This CL adds logs to new media pipeline when media is loaded or removed
at the ending point of the pipeline.

Flag: com.android.systemui.scene_container
Bug: 326958702
Test: atest MediaDataFilterImplTest
Change-Id: I668022c2d567a188917754d1482f227dec84dc46
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 14890d7..1e79f42 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -364,6 +364,16 @@
         return factory.create("MediaCarouselCtlrLog", 20);
     }
 
+    /**
+     * Provides a buffer for media loading changes
+     */
+    @Provides
+    @SysUISingleton
+    @MediaLoadingLog
+    public static LogBuffer providesMediaLoadingLogBuffer(LogBufferFactory factory) {
+        return factory.create("MediaLoadingLog", 20);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.kt
new file mode 100644
index 0000000..05e1b2e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/MediaLoadingLog.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.log.dagger
+
+import com.android.systemui.log.LogBuffer
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for [com.android.systemui.media.controls.domain.pipeline.MediaLoadingLogger] */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MediaLoadingLog
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 8d19ce8..f78a0f9 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
@@ -24,6 +24,7 @@
 import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.logging.InstanceId
 import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
 import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME
@@ -56,6 +57,7 @@
  * This is added at the end of the pipeline since we may still need to handle callbacks from
  * background users (e.g. timeouts).
  */
+@SysUISingleton
 class MediaDataFilterImpl
 @Inject
 constructor(
@@ -68,11 +70,13 @@
     private val logger: MediaUiEventLogger,
     private val mediaFlags: MediaFlags,
     private val mediaFilterRepository: MediaFilterRepository,
+    private val mediaLoadingLogger: MediaLoadingLogger,
 ) : MediaDataManager.Listener {
     /** Non-UI listeners to media changes. */
     private val _listeners: MutableSet<MediaDataProcessor.Listener> = mutableSetOf()
     val listeners: Set<MediaDataProcessor.Listener>
         get() = _listeners.toSet()
+
     lateinit var mediaDataProcessor: MediaDataProcessor
 
     // Ensure the field (and associated reference) isn't removed during optimization.
@@ -114,6 +118,7 @@
 
         mediaFilterRepository.addSelectedUserMediaEntry(data)
 
+        mediaLoadingLogger.logMediaLoaded(data.instanceId, data.active, "loading media")
         mediaFilterRepository.addMediaDataLoadingState(
             MediaDataLoadingModel.Loaded(data.instanceId)
         )
@@ -167,7 +172,6 @@
             if (shouldReactivate) {
                 val lastActiveId = sorted.lastKey() // most recently active id
                 // Update loading state to consider this media active
-                Log.d(TAG, "reactivating $lastActiveId instead of smartspace")
                 mediaFilterRepository.setReactivatedId(lastActiveId)
                 val mediaData = sorted[lastActiveId]!!.copy(active = true)
                 logger.logRecommendationActivated(
@@ -178,6 +182,11 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    mediaData.instanceId,
+                    mediaData.active,
+                    "reactivating media instead of smartspace"
+                )
                 listeners.forEach { listener ->
                     getKey(lastActiveId)?.let { lastActiveKey ->
                         listener.onMediaDataLoaded(
@@ -210,6 +219,7 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable)
         )
+        mediaLoadingLogger.logRecommendationLoaded(key, data.isActive, "loading recommendations")
         listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) }
     }
 
@@ -220,6 +230,7 @@
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(instanceId)
                 )
+                mediaLoadingLogger.logMediaRemoved(instanceId, "removing media card")
                 // Only notify listeners if something actually changed
                 listeners.forEach { it.onMediaDataRemoved(key, userInitiated) }
             }
@@ -230,12 +241,16 @@
         // First check if we had reactivated media instead of forwarding smartspace
         mediaFilterRepository.reactivatedId.value?.let { lastActiveId ->
             mediaFilterRepository.setReactivatedId(null)
-            Log.d(TAG, "expiring reactivated key $lastActiveId")
             // Update loading state with actual active value
             mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let {
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(lastActiveId, immediately)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    lastActiveId,
+                    it.active,
+                    "expiring reactivated id"
+                )
                 listeners.forEach { listener ->
                     getKey(lastActiveId)?.let { lastActiveKey ->
                         listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, it, immediately)
@@ -256,6 +271,11 @@
         mediaFilterRepository.setRecommendationsLoadingState(
             SmartspaceMediaLoadingModel.Removed(key, immediately)
         )
+        mediaLoadingLogger.logRecommendationRemoved(
+            key,
+            immediately,
+            "removing recommendations card"
+        )
         listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) }
     }
 
@@ -265,11 +285,14 @@
         mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (!lockscreenUserManager.isProfileAvailable(data.userId)) {
                 // Only remove media when the profile is unavailable.
-                if (DEBUG) Log.d(TAG, "Removing $key after profile change")
                 mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data)
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Removed(data.instanceId)
                 )
+                mediaLoadingLogger.logMediaRemoved(
+                    data.instanceId,
+                    "Removing $key after profile change"
+                )
                 listeners.forEach { listener -> listener.onMediaDataRemoved(key, false) }
             }
         }
@@ -283,10 +306,10 @@
         // Clear the list first and update loading state to remove media from UI.
         mediaFilterRepository.clearSelectedUserMedia()
         keyCopy.forEach { instanceId ->
-            if (DEBUG) Log.d(TAG, "Removing $instanceId after user change")
             mediaFilterRepository.addMediaDataLoadingState(
                 MediaDataLoadingModel.Removed(instanceId)
             )
+            mediaLoadingLogger.logMediaRemoved(instanceId, "Removing media after user change")
             getKey(instanceId)?.let {
                 listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it, false) }
             }
@@ -294,15 +317,15 @@
 
         mediaFilterRepository.allUserEntries.value.forEach { (key, data) ->
             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
-                if (DEBUG)
-                    Log.d(
-                        TAG,
-                        "Re-adding $key with instanceId=${data.instanceId} after user change"
-                    )
                 mediaFilterRepository.addSelectedUserMediaEntry(data)
                 mediaFilterRepository.addMediaDataLoadingState(
                     MediaDataLoadingModel.Loaded(data.instanceId)
                 )
+                mediaLoadingLogger.logMediaLoaded(
+                    data.instanceId,
+                    data.active,
+                    "Re-adding $key after user change"
+                )
                 listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.kt
new file mode 100644
index 0000000..c6cfd65
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLogger.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.domain.pipeline
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.MediaLoadingLog
+import javax.inject.Inject
+
+/** A buffered log for media loading events. */
+@SysUISingleton
+class MediaLoadingLogger @Inject constructor(@MediaLoadingLog private val buffer: LogBuffer) {
+
+    fun logMediaLoaded(instanceId: InstanceId, active: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = instanceId.toString()
+                bool1 = active
+                str2 = reason
+            },
+            { "add media $str1, active: $bool1, reason: $str2" }
+        )
+    }
+
+    fun logMediaRemoved(instanceId: InstanceId, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = instanceId.toString()
+                str2 = reason
+            },
+            { "removing media $str1, reason: $str2" }
+        )
+    }
+
+    fun logRecommendationLoaded(key: String, isActive: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = isActive
+                str2 = reason
+            },
+            { "add recommendation $str1, active $bool1, reason: $str2" }
+        )
+    }
+
+    fun logRecommendationRemoved(key: String, immediately: Boolean, reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = key
+                bool1 = immediately
+                str2 = reason
+            },
+            { "removing recommendation $str1, immediate=$bool1, reason: $str2" }
+        )
+    }
+
+    companion object {
+        private const val TAG = "MediaLoadingLog"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
index 315a9fb..f0d8df5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt
@@ -155,13 +155,17 @@
                             mediaFlags.isPersistentSsCardEnabled(),
                     recsViewModel = recommendationsViewModel,
                     onAdded = { commonViewModel ->
-                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                        onMediaRecommendationAddedOrUpdated(
+                            commonViewModel as MediaCommonViewModel.MediaRecommendations
+                        )
                     },
                     onRemoved = { immediatelyRemove ->
                         onMediaRecommendationRemoved(commonModel, immediatelyRemove)
                     },
                     onUpdated = { commonViewModel ->
-                        onMediaRecommendationAddedOrUpdated(commonViewModel)
+                        onMediaRecommendationAddedOrUpdated(
+                            commonViewModel as MediaCommonViewModel.MediaRecommendations
+                        )
                     },
                 )
                 .also { mediaRecs = it }
@@ -185,7 +189,9 @@
         }
     }
 
-    private fun onMediaRecommendationAddedOrUpdated(commonViewModel: MediaCommonViewModel) {
+    private fun onMediaRecommendationAddedOrUpdated(
+        commonViewModel: MediaCommonViewModel.MediaRecommendations
+    ) {
         if (!interactor.isRecommendationActive()) {
             if (!mediaFlags.isPersistentSsCardEnabled()) {
                 commonViewModel.onRemoved(true)
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 064cf09..4da56b5 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
@@ -53,8 +53,10 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.kotlin.any
@@ -100,6 +102,7 @@
     private lateinit var dataPrivateProfile: MediaData
     private val clock = FakeSystemClock()
     private val repository: MediaFilterRepository = kosmos.mediaFilterRepository
+    private val mediaLoadingLogger = kosmos.mockMediaLoadingLogger
 
     @Before
     fun setup() {
@@ -118,6 +121,7 @@
                 logger,
                 mediaFlags,
                 repository,
+                mediaLoadingLogger,
             )
         mediaDataFilter.mediaDataProcessor = mediaDataProcessor
         mediaDataFilter.addListener(listener)
@@ -176,6 +180,8 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(mediaCommonModel)
         }
 
@@ -190,6 +196,7 @@
 
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
             assertThat(currentMedia).doesNotContain(mediaCommonModel)
         }
 
@@ -203,11 +210,14 @@
             // GIVEN a media was removed for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(mediaCommonModel)
 
             mediaDataFilter.onMediaDataRemoved(KEY, false)
 
             verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataMain.instanceId), anyString())
             assertThat(currentMedia).doesNotContain(mediaCommonModel)
         }
 
@@ -221,6 +231,8 @@
             mediaDataFilter.onMediaDataRemoved(KEY, false)
 
             verify(listener, never()).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger, never())
+                .logMediaRemoved(eq(dataGuest.instanceId), anyString())
             assertThat(currentMedia).isEmpty()
         }
 
@@ -233,6 +245,8 @@
             // GIVEN that we have a media loaded for main user
             mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
 
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataMain.instanceId), eq(dataMain.active), anyString())
             assertThat(currentMedia).containsExactly(MediaCommonModel.MediaControl(mediaLoaded))
 
             // and we switch to guest user
@@ -240,6 +254,7 @@
 
             // THEN we should remove the main user's media
             verify(listener).onMediaDataRemoved(eq(KEY), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataMain.instanceId), anyString())
             assertThat(currentMedia).isEmpty()
         }
 
@@ -260,6 +275,10 @@
             // THEN we should add back the guest user media
             verify(listener)
                 .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataGuest.instanceId), eq(dataGuest.active), anyString())
+
+            reset(mediaLoadingLogger)
 
             // but not the main user's
             verify(listener, never())
@@ -271,6 +290,8 @@
                     anyInt(),
                     anyBoolean()
                 )
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataMain.instanceId), anyBoolean(), anyString())
             assertThat(currentMedia)
                 .containsExactly(MediaCommonModel.MediaControl(guestLoadedStatesModel))
             assertThat(currentMedia)
@@ -292,6 +313,7 @@
             val mediaLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId)
             // THEN we should remove the private profile media
             verify(listener).onMediaDataRemoved(eq(KEY_ALT), eq(false))
+            verify(mediaLoadingLogger).logMediaRemoved(eq(dataGuest.instanceId), anyString())
             assertThat(currentMedia)
                 .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel))
         }
@@ -541,6 +563,8 @@
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -570,6 +594,9 @@
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -607,6 +634,8 @@
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -641,6 +670,8 @@
                 .isFalse()
             assertThat(hasActiveMedia(selectedUserEntries)).isFalse()
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -668,6 +699,10 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
+
+            reset(mediaLoadingLogger)
 
             // AND we get a smartspace signal
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -686,6 +721,10 @@
             verify(listener, never())
                 .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean())
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger, never()).logRecommendationActivated(any(), any(), any())
         }
@@ -711,6 +750,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             // AND we get a smartspace signal
             runCurrent()
@@ -736,8 +777,16 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             // Smartspace update shouldn't be propagated for the empty rec list.
             verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean())
+            verify(mediaLoadingLogger, never())
+                .logRecommendationLoaded(any(), anyBoolean(), anyString())
             verify(logger, never()).logRecommendationAdded(any(), any())
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -767,6 +816,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             // AND we get a smartspace signal
             runCurrent()
@@ -783,6 +834,12 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             assertThat(
                     hasActiveMediaOrRecommendation(
                         selectedUserEntries,
@@ -795,6 +852,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID)
             verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID))
         }
@@ -811,6 +870,8 @@
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            verify(mediaLoadingLogger)
+                .logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).isEmpty()
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -842,6 +903,8 @@
             assertThat(currentMedia).containsExactly(controlCommonModel)
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
 
             runCurrent()
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
@@ -856,10 +919,18 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
 
             mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
 
             verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY)
+            verify(mediaLoadingLogger)
+                .logRecommendationRemoved(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -890,6 +961,8 @@
 
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(false), anyString())
             assertThat(currentMedia).containsExactly(recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -930,16 +1003,23 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
+            reset(mediaLoadingLogger)
+
             // And an inactive recommendation is loaded
             mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData)
 
             // Smartspace is loaded but the media stays inactive
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(false), anyString())
             verify(listener, never())
                 .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean())
+            verify(mediaLoadingLogger, never()).logMediaLoaded(any(), anyBoolean(), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -995,6 +1075,8 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
             // AND we get a smartspace signal with extra to trigger resume
@@ -1014,6 +1096,12 @@
                     eq(100),
                     eq(true)
                 )
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(
+                    eq(dataCurrentAndActive.instanceId),
+                    eq(dataCurrentAndActive.active),
+                    anyString()
+                )
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
             assertThat(
                     hasActiveMediaOrRecommendation(
@@ -1026,6 +1114,8 @@
             // And update the smartspace data state, but not prioritized
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
         }
 
     @Test
@@ -1049,8 +1139,12 @@
 
             verify(listener)
                 .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false))
+            verify(mediaLoadingLogger)
+                .logMediaLoaded(eq(dataCurrent.instanceId), eq(dataCurrent.active), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel)
 
+            reset(mediaLoadingLogger)
+
             // AND we get a smartspace signal with extra to not trigger resume
             val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) }
             whenever(cardAction.extras).thenReturn(extras)
@@ -1059,9 +1153,13 @@
             // THEN listeners are not updated to show media
             verify(listener, never())
                 .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true))
+            verify(mediaLoadingLogger, never())
+                .logMediaLoaded(eq(dataCurrent.instanceId), anyBoolean(), anyString())
             // But the smartspace update is still propagated
             verify(listener)
                 .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false))
+            verify(mediaLoadingLogger)
+                .logRecommendationLoaded(eq(SMARTSPACE_KEY), eq(true), anyString())
             assertThat(currentMedia).containsExactly(controlCommonModel, recsCommonModel)
         }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
index d56222e..b8b0060 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt
@@ -45,5 +45,6 @@
             logger = mediaUiEventLogger,
             mediaFlags = mediaFlags,
             mediaFilterRepository = mediaFilterRepository,
+            mediaLoadingLogger = mediaLoadingLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt
new file mode 100644
index 0000000..96886f7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaLoadingLoggerKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.domain.pipeline
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+import org.mockito.Mockito.mock
+
+val Kosmos.mediaLoadingLogger by
+    Kosmos.Fixture { MediaLoadingLogger(logcatLogBuffer("MediaLoadingLogBuffer")) }
+val Kosmos.mockMediaLoadingLogger by Kosmos.Fixture { mock(MediaLoadingLogger::class.java) }