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) }