diff options
5 files changed, 302 insertions, 54 deletions
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 bc0512a1468d..f43fa5048298 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.MediaData 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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 6a91d1b15325..a2d7fb152e5c 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.MediaData 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 @@ import kotlinx.coroutines.flow.asStateFlow 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( commonModel !is MediaCommonModel.MediaControl || mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId } + sortedMedia = sortedMap } } @@ -271,21 +301,45 @@ constructor( 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 @@ constructor( 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/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 0a9b4fdf8ad8..c974e0dde5e6 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 @@ -755,7 +755,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { 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( @@ -825,7 +826,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { 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), @@ -917,7 +919,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { 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), @@ -1091,7 +1094,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { 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( 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 690bde7f5b47..7a04aa288dce 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 @@ package com.android.systemui.media.controls.data.repository 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 @@ val Kosmos.mediaFilterRepository by 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 000000000000..c63dec523228 --- /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) } |