Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +163 −29 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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() = Loading Loading @@ -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) Loading @@ -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() } Loading Loading @@ -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( Loading @@ -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)) Loading @@ -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( Loading Loading @@ -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) ) Loading Loading @@ -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, Loading packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +105 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading Loading @@ -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) { Loading @@ -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 ) { // 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) } } } // On removal we want to keep the order being shown to user. if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) { _currentMedia.value = Loading @@ -249,6 +278,7 @@ constructor( commonModel !is MediaCommonModel.MediaControl || mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId } sortedMedia = sortedMap } } Loading @@ -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() { Loading Loading @@ -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) } } packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +7 −1 Original line number Diff line number Diff line Loading @@ -180,7 +180,13 @@ constructor( mediaData.instanceId ) mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(lastActiveId) MediaDataLoadingModel.Loaded( lastActiveId, receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) .toInt(), isSsReactivated = true ) ) mediaLoadingLogger.logMediaLoaded( mediaData.instanceId, Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +3 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,7 @@ import com.android.systemui.media.controls.util.MediaControllerFactory 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 Loading Loading @@ -721,6 +722,7 @@ class MediaDataProcessor( appUid = appUid, isExplicit = isExplicit, resumeProgress = progress, smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), ) ) } Loading Loading @@ -902,6 +904,7 @@ class MediaDataProcessor( instanceId = instanceId, appUid = appUid, isExplicit = isExplicit, smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), ) ) } Loading packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt +6 −0 Original line number Diff line number Diff line Loading @@ -99,6 +99,12 @@ data class MediaData( /** 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 */ Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +163 −29 Original line number Diff line number Diff line Loading @@ -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) Loading @@ -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() = Loading Loading @@ -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) Loading @@ -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() } Loading Loading @@ -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( Loading @@ -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)) Loading @@ -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( Loading Loading @@ -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) ) Loading Loading @@ -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, Loading
packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +105 −20 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading Loading @@ -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) { Loading @@ -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 ) { // 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) } } } // On removal we want to keep the order being shown to user. if (mediaDataLoadingModel is MediaDataLoadingModel.Removed) { _currentMedia.value = Loading @@ -249,6 +278,7 @@ constructor( commonModel !is MediaCommonModel.MediaControl || mediaDataLoadingModel.instanceId != commonModel.mediaLoadedModel.instanceId } sortedMedia = sortedMap } } Loading @@ -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() { Loading Loading @@ -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) } }
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +7 −1 Original line number Diff line number Diff line Loading @@ -180,7 +180,13 @@ constructor( mediaData.instanceId ) mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(lastActiveId) MediaDataLoadingModel.Loaded( lastActiveId, receivedSmartspaceCardLatency = (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) .toInt(), isSsReactivated = true ) ) mediaLoadingLogger.logMediaLoaded( mediaData.instanceId, Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +3 −0 Original line number Diff line number Diff line Loading @@ -86,6 +86,7 @@ import com.android.systemui.media.controls.util.MediaControllerFactory 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 Loading Loading @@ -721,6 +722,7 @@ class MediaDataProcessor( appUid = appUid, isExplicit = isExplicit, resumeProgress = progress, smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), ) ) } Loading Loading @@ -902,6 +904,7 @@ class MediaDataProcessor( instanceId = instanceId, appUid = appUid, isExplicit = isExplicit, smartspaceId = SmallHash.hash(appUid + systemClock.currentTimeMillis().toInt()), ) ) } Loading
packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt +6 −0 Original line number Diff line number Diff line Loading @@ -99,6 +99,12 @@ data class MediaData( /** 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 */ Loading