Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +137 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.shared.model.MediaCommonModel 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 Loading Loading @@ -172,12 +173,147 @@ class MediaFilterRepositoryTest : SysuiTestCase() { val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE) underTest.setRecommedationsLoadingState(recommendationsLoadingModel) underTest.setRecommendationsLoadingState(recommendationsLoadingModel) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) } @Test fun addMediaControlPlayingThenRemote() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val playingInstanceId = InstanceId.fakeInstanceId(123) val remoteInstanceId = InstanceId.fakeInstanceId(321) val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId) val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId) underTest.addSelectedUserMediaEntry(playingData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId)) underTest.addSelectedUserMediaEntry(remoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(remoteInstanceId)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId), MediaCommonModel.MediaControl(remoteInstanceId) ) } @Test fun switchMediaControlsPlaying() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val playingInstanceId1 = InstanceId.fakeInstanceId(123) val playingInstanceId2 = InstanceId.fakeInstanceId(321) var playingData1 = createMediaData("app1", true, LOCAL, false, playingInstanceId1) var playingData2 = createMediaData("app2", false, LOCAL, false, playingInstanceId2) underTest.addSelectedUserMediaEntry(playingData1) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1)) underTest.addSelectedUserMediaEntry(playingData2) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId1), MediaCommonModel.MediaControl(playingInstanceId2) ) .inOrder() playingData1 = createMediaData("app1", false, LOCAL, false, playingInstanceId1) playingData2 = createMediaData("app2", true, LOCAL, false, playingInstanceId2) underTest.addSelectedUserMediaEntry(playingData1) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1)) underTest.addSelectedUserMediaEntry(playingData2) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId2), MediaCommonModel.MediaControl(playingInstanceId1) ) .inOrder() } @Test fun fullOrderTest() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val instanceId1 = InstanceId.fakeInstanceId(123) val instanceId2 = InstanceId.fakeInstanceId(456) val instanceId3 = InstanceId.fakeInstanceId(321) val instanceId4 = InstanceId.fakeInstanceId(654) val instanceId5 = InstanceId.fakeInstanceId(124) val playingAndLocalData = createMediaData("app1", true, LOCAL, false, instanceId1) val playingAndRemoteData = createMediaData("app2", true, REMOTE, false, instanceId2) val stoppedAndLocalData = createMediaData("app3", false, LOCAL, false, instanceId3) 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)) underTest.addSelectedUserMediaEntry(stoppedAndRemoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId4)) underTest.addSelectedUserMediaEntry(canResumeData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId5)) underTest.addSelectedUserMediaEntry(playingAndLocalData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1)) underTest.addSelectedUserMediaEntry(playingAndRemoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2)) underTest.setRecommendation(mediaRecommendations) underTest.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) ) assertThat(sortedMedia?.size).isEqualTo(6) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(instanceId1), MediaCommonModel.MediaControl(instanceId2), MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE), MediaCommonModel.MediaControl(instanceId4), MediaCommonModel.MediaControl(instanceId3), MediaCommonModel.MediaControl(instanceId5), ) .inOrder() } private fun createMediaData( app: String, playing: Boolean, playbackLocation: Int, isResume: Boolean, instanceId: InstanceId, ): MediaData { return MediaData( playbackLocation = playbackLocation, resumption = isResume, notificationKey = "key: $app", isPlaying = playing, instanceId = instanceId ) } companion object { private const val LOCAL = MediaData.PLAYBACK_LOCAL private const val REMOTE = MediaData.PLAYBACK_CAST_LOCAL private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +23 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter import com.android.systemui.media.controls.shared.model.MediaCommonModel 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 Loading Loading @@ -94,22 +95,29 @@ class MediaCarouselInteractorTest : SysuiTestCase() { collectLastValue(underTest.hasActiveMediaOrRecommendation) val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) val sortedMedia by collectLastValue(underTest.sortedMedia) val userMedia = MediaData(active = false) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isTrue() assertThat(sortedMedia).containsExactly(MediaCommonModel.MediaControl(instanceId)) assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isFalse() assertThat(sortedMedia).isEmpty() } @Test Loading @@ -119,6 +127,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { collectLastValue(underTest.hasActiveMediaOrRecommendation) val hasAnyMediaOrRecommendation by collectLastValue(underTest.hasAnyMediaOrRecommendation) val sortedMedia by collectLastValue(underTest.sortedMedia) kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) val icon = Icon.createWithResource(context, R.drawable.ic_media_play) Loading @@ -131,14 +140,28 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val userMedia = MediaData(active = false) mediaFilterRepository.setRecommendation(userMediaRecommendation) mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) ) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) .containsExactly(MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE)) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(userMedia.instanceId) ) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) .containsExactly( MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE), MediaCommonModel.MediaControl(userMedia.instanceId) ) .inOrder() } @Test Loading packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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.data.model import com.android.internal.logging.InstanceId import com.android.systemui.media.controls.shared.model.MediaData.Companion.PLAYBACK_LOCAL data class MediaSortKeyModel( /** Whether the item represents a Smartspace media recommendation that should be prioritized. */ val isPrioritizedRec: Boolean = false, val isPlaying: Boolean? = null, val playbackLocation: Int = PLAYBACK_LOCAL, val active: Boolean = true, val isResume: Boolean = false, val lastActive: Long = 0L, val notificationKey: String? = null, val updateTime: Long = 0, val instanceId: InstanceId? = null, ) packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +108 −2 Original line number Diff line number Diff line Loading @@ -18,10 +18,14 @@ package com.android.systemui.media.controls.data.repository import com.android.internal.logging.InstanceId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.data.model.MediaSortKeyModel import com.android.systemui.media.controls.shared.model.MediaCommonModel 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.util.time.SystemClock import java.util.TreeMap import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow Loading @@ -29,7 +33,7 @@ import kotlinx.coroutines.flow.asStateFlow /** A repository that holds the state of filtered media data on the device. */ @SysUISingleton class MediaFilterRepository @Inject constructor() { class MediaFilterRepository @Inject constructor(private val systemClock: SystemClock) { /** Instance id of media control that recommendations card reactivated. */ private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null) Loading Loading @@ -58,6 +62,26 @@ class MediaFilterRepository @Inject constructor() { val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> = _recommendationsLoadingState.asStateFlow() private val comparator = compareByDescending<MediaSortKeyModel> { it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL } .thenByDescending { it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL } .thenByDescending { it.active } .thenByDescending { it.isPrioritizedRec } .thenByDescending { !it.isResume } .thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } .thenByDescending { it.lastActive } .thenByDescending { it.updateTime } .thenByDescending { it.notificationKey } private val _sortedMedia: MutableStateFlow<TreeMap<MediaSortKeyModel, MediaCommonModel>> = MutableStateFlow(TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)) val sortedMedia: StateFlow<Map<MediaSortKeyModel, MediaCommonModel>> = _sortedMedia.asStateFlow() fun addMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) entries[key] = data Loading Loading @@ -138,9 +162,91 @@ class MediaFilterRepository @Inject constructor() { } else { emptyList() } addMediaLoadingToSortedMap(mediaDataLoadingModel) } fun setRecommedationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { _recommendationsLoadingState.value = smartspaceMediaLoadingModel addRecsLoadingToSortedMap(smartspaceMediaLoadingModel) } private fun addMediaLoadingToSortedMap(mediaDataLoadingModel: MediaDataLoadingModel) { val instanceId = when (mediaDataLoadingModel) { is MediaDataLoadingModel.Loaded -> mediaDataLoadingModel.instanceId is MediaDataLoadingModel.Removed -> mediaDataLoadingModel.instanceId MediaDataLoadingModel.Unknown -> null } val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator) sortedMap.putAll( _sortedMedia.value.filter { (_, commonModel) -> commonModel !is MediaCommonModel.MediaControl || commonModel.instanceId != instanceId } ) _selectedUserEntries.value[instanceId]?.let { val sortKey = MediaSortKeyModel( isPrioritizedRec = false, it.isPlaying, it.playbackLocation, it.active, it.resumption, it.lastActive, it.notificationKey, systemClock.currentTimeMillis(), it.instanceId, ) if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { sortedMap[sortKey] = MediaCommonModel.MediaControl(it.instanceId) } } _sortedMedia.value = sortedMap } private fun addRecsLoadingToSortedMap( smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel ) { val isPrioritized: Boolean val key: String? when (smartspaceMediaLoadingModel) { is SmartspaceMediaLoadingModel.Loaded -> { isPrioritized = smartspaceMediaLoadingModel.isPrioritized key = smartspaceMediaLoadingModel.key } is SmartspaceMediaLoadingModel.Removed -> { isPrioritized = false key = smartspaceMediaLoadingModel.key } SmartspaceMediaLoadingModel.Unknown -> { isPrioritized = false key = null } } val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator) sortedMap.putAll( _sortedMedia.value.filter { (_, commonModel) -> commonModel !is MediaCommonModel.MediaRecommendations || commonModel.key != key } ) key?.let { val sortKey = MediaSortKeyModel( isPrioritizedRec = isPrioritized, isPlaying = false, active = _smartspaceMediaData.value.isActive, ) if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) { sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(key) } } _sortedMedia.value = sortedMap } } packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +2 −2 Original line number Diff line number Diff line Loading @@ -186,7 +186,7 @@ constructor( smartspaceMediaData.packageName, smartspaceMediaData.instanceId ) mediaFilterRepository.setRecommedationsLoadingState( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable) ) } Loading Loading @@ -224,7 +224,7 @@ constructor( ) ) } mediaFilterRepository.setRecommedationsLoadingState( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Removed(key, immediately) ) } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt +137 −1 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.shared.model.MediaCommonModel 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 Loading Loading @@ -172,12 +173,147 @@ class MediaFilterRepositoryTest : SysuiTestCase() { val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE) underTest.setRecommedationsLoadingState(recommendationsLoadingModel) underTest.setRecommendationsLoadingState(recommendationsLoadingModel) assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) } @Test fun addMediaControlPlayingThenRemote() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val playingInstanceId = InstanceId.fakeInstanceId(123) val remoteInstanceId = InstanceId.fakeInstanceId(321) val playingData = createMediaData("app1", true, LOCAL, false, playingInstanceId) val remoteData = createMediaData("app2", true, REMOTE, false, remoteInstanceId) underTest.addSelectedUserMediaEntry(playingData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId)) underTest.addSelectedUserMediaEntry(remoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(remoteInstanceId)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId), MediaCommonModel.MediaControl(remoteInstanceId) ) } @Test fun switchMediaControlsPlaying() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val playingInstanceId1 = InstanceId.fakeInstanceId(123) val playingInstanceId2 = InstanceId.fakeInstanceId(321) var playingData1 = createMediaData("app1", true, LOCAL, false, playingInstanceId1) var playingData2 = createMediaData("app2", false, LOCAL, false, playingInstanceId2) underTest.addSelectedUserMediaEntry(playingData1) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1)) underTest.addSelectedUserMediaEntry(playingData2) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId1), MediaCommonModel.MediaControl(playingInstanceId2) ) .inOrder() playingData1 = createMediaData("app1", false, LOCAL, false, playingInstanceId1) playingData2 = createMediaData("app2", true, LOCAL, false, playingInstanceId2) underTest.addSelectedUserMediaEntry(playingData1) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId1)) underTest.addSelectedUserMediaEntry(playingData2) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(playingInstanceId2)) assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(playingInstanceId2), MediaCommonModel.MediaControl(playingInstanceId1) ) .inOrder() } @Test fun fullOrderTest() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) val instanceId1 = InstanceId.fakeInstanceId(123) val instanceId2 = InstanceId.fakeInstanceId(456) val instanceId3 = InstanceId.fakeInstanceId(321) val instanceId4 = InstanceId.fakeInstanceId(654) val instanceId5 = InstanceId.fakeInstanceId(124) val playingAndLocalData = createMediaData("app1", true, LOCAL, false, instanceId1) val playingAndRemoteData = createMediaData("app2", true, REMOTE, false, instanceId2) val stoppedAndLocalData = createMediaData("app3", false, LOCAL, false, instanceId3) 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)) underTest.addSelectedUserMediaEntry(stoppedAndRemoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId4)) underTest.addSelectedUserMediaEntry(canResumeData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId5)) underTest.addSelectedUserMediaEntry(playingAndLocalData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1)) underTest.addSelectedUserMediaEntry(playingAndRemoteData) underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2)) underTest.setRecommendation(mediaRecommendations) underTest.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) ) assertThat(sortedMedia?.size).isEqualTo(6) assertThat(sortedMedia?.values) .containsExactly( MediaCommonModel.MediaControl(instanceId1), MediaCommonModel.MediaControl(instanceId2), MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE), MediaCommonModel.MediaControl(instanceId4), MediaCommonModel.MediaControl(instanceId3), MediaCommonModel.MediaControl(instanceId5), ) .inOrder() } private fun createMediaData( app: String, playing: Boolean, playbackLocation: Int, isResume: Boolean, instanceId: InstanceId, ): MediaData { return MediaData( playbackLocation = playbackLocation, resumption = isResume, notificationKey = "key: $app", isPlaying = playing, instanceId = instanceId ) } companion object { private const val LOCAL = MediaData.PLAYBACK_LOCAL private const val REMOTE = MediaData.PLAYBACK_CAST_LOCAL private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +23 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter import com.android.systemui.media.controls.shared.model.MediaCommonModel 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 Loading Loading @@ -94,22 +95,29 @@ class MediaCarouselInteractorTest : SysuiTestCase() { collectLastValue(underTest.hasActiveMediaOrRecommendation) val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) val sortedMedia by collectLastValue(underTest.sortedMedia) val userMedia = MediaData(active = false) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isTrue() assertThat(sortedMedia).containsExactly(MediaCommonModel.MediaControl(instanceId)) assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isFalse() assertThat(sortedMedia).isEmpty() } @Test Loading @@ -119,6 +127,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { collectLastValue(underTest.hasActiveMediaOrRecommendation) val hasAnyMediaOrRecommendation by collectLastValue(underTest.hasAnyMediaOrRecommendation) val sortedMedia by collectLastValue(underTest.sortedMedia) kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) val icon = Icon.createWithResource(context, R.drawable.ic_media_play) Loading @@ -131,14 +140,28 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val userMedia = MediaData(active = false) mediaFilterRepository.setRecommendation(userMediaRecommendation) mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) ) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) .containsExactly(MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE)) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Loaded(userMedia.instanceId) ) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) .containsExactly( MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE), MediaCommonModel.MediaControl(userMedia.instanceId) ) .inOrder() } @Test Loading
packages/SystemUI/src/com/android/systemui/media/controls/data/model/MediaSortKeyModel.kt 0 → 100644 +33 −0 Original line number Diff line number Diff line /* * 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.data.model import com.android.internal.logging.InstanceId import com.android.systemui.media.controls.shared.model.MediaData.Companion.PLAYBACK_LOCAL data class MediaSortKeyModel( /** Whether the item represents a Smartspace media recommendation that should be prioritized. */ val isPrioritizedRec: Boolean = false, val isPlaying: Boolean? = null, val playbackLocation: Int = PLAYBACK_LOCAL, val active: Boolean = true, val isResume: Boolean = false, val lastActive: Long = 0L, val notificationKey: String? = null, val updateTime: Long = 0, val instanceId: InstanceId? = null, )
packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt +108 −2 Original line number Diff line number Diff line Loading @@ -18,10 +18,14 @@ package com.android.systemui.media.controls.data.repository import com.android.internal.logging.InstanceId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.data.model.MediaSortKeyModel import com.android.systemui.media.controls.shared.model.MediaCommonModel 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.util.time.SystemClock import java.util.TreeMap import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow Loading @@ -29,7 +33,7 @@ import kotlinx.coroutines.flow.asStateFlow /** A repository that holds the state of filtered media data on the device. */ @SysUISingleton class MediaFilterRepository @Inject constructor() { class MediaFilterRepository @Inject constructor(private val systemClock: SystemClock) { /** Instance id of media control that recommendations card reactivated. */ private val _reactivatedId: MutableStateFlow<InstanceId?> = MutableStateFlow(null) Loading Loading @@ -58,6 +62,26 @@ class MediaFilterRepository @Inject constructor() { val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> = _recommendationsLoadingState.asStateFlow() private val comparator = compareByDescending<MediaSortKeyModel> { it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL } .thenByDescending { it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_CAST_LOCAL } .thenByDescending { it.active } .thenByDescending { it.isPrioritizedRec } .thenByDescending { !it.isResume } .thenByDescending { it.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE } .thenByDescending { it.lastActive } .thenByDescending { it.updateTime } .thenByDescending { it.notificationKey } private val _sortedMedia: MutableStateFlow<TreeMap<MediaSortKeyModel, MediaCommonModel>> = MutableStateFlow(TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator)) val sortedMedia: StateFlow<Map<MediaSortKeyModel, MediaCommonModel>> = _sortedMedia.asStateFlow() fun addMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) entries[key] = data Loading Loading @@ -138,9 +162,91 @@ class MediaFilterRepository @Inject constructor() { } else { emptyList() } addMediaLoadingToSortedMap(mediaDataLoadingModel) } fun setRecommedationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { _recommendationsLoadingState.value = smartspaceMediaLoadingModel addRecsLoadingToSortedMap(smartspaceMediaLoadingModel) } private fun addMediaLoadingToSortedMap(mediaDataLoadingModel: MediaDataLoadingModel) { val instanceId = when (mediaDataLoadingModel) { is MediaDataLoadingModel.Loaded -> mediaDataLoadingModel.instanceId is MediaDataLoadingModel.Removed -> mediaDataLoadingModel.instanceId MediaDataLoadingModel.Unknown -> null } val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator) sortedMap.putAll( _sortedMedia.value.filter { (_, commonModel) -> commonModel !is MediaCommonModel.MediaControl || commonModel.instanceId != instanceId } ) _selectedUserEntries.value[instanceId]?.let { val sortKey = MediaSortKeyModel( isPrioritizedRec = false, it.isPlaying, it.playbackLocation, it.active, it.resumption, it.lastActive, it.notificationKey, systemClock.currentTimeMillis(), it.instanceId, ) if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { sortedMap[sortKey] = MediaCommonModel.MediaControl(it.instanceId) } } _sortedMedia.value = sortedMap } private fun addRecsLoadingToSortedMap( smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel ) { val isPrioritized: Boolean val key: String? when (smartspaceMediaLoadingModel) { is SmartspaceMediaLoadingModel.Loaded -> { isPrioritized = smartspaceMediaLoadingModel.isPrioritized key = smartspaceMediaLoadingModel.key } is SmartspaceMediaLoadingModel.Removed -> { isPrioritized = false key = smartspaceMediaLoadingModel.key } SmartspaceMediaLoadingModel.Unknown -> { isPrioritized = false key = null } } val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator) sortedMap.putAll( _sortedMedia.value.filter { (_, commonModel) -> commonModel !is MediaCommonModel.MediaRecommendations || commonModel.key != key } ) key?.let { val sortKey = MediaSortKeyModel( isPrioritizedRec = isPrioritized, isPlaying = false, active = _smartspaceMediaData.value.isActive, ) if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) { sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(key) } } _sortedMedia.value = sortedMap } }
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +2 −2 Original line number Diff line number Diff line Loading @@ -186,7 +186,7 @@ constructor( smartspaceMediaData.packageName, smartspaceMediaData.instanceId ) mediaFilterRepository.setRecommedationsLoadingState( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable) ) } Loading Loading @@ -224,7 +224,7 @@ constructor( ) ) } mediaFilterRepository.setRecommedationsLoadingState( mediaFilterRepository.setRecommendationsLoadingState( SmartspaceMediaLoadingModel.Removed(key, immediately) ) } Loading