diff options
15 files changed, 758 insertions, 355 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 f685058c77fa..e39511fdfdab 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 @@ -148,37 +148,6 @@ class MediaFilterRepositoryTest : SysuiTestCase() { } @Test - fun addMediaDataLoadingState() = - testScope.runTest { - val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates) - val instanceId = InstanceId.fakeInstanceId(123) - val mediaLoadedStates = mutableListOf(MediaDataLoadingModel.Loaded(instanceId)) - - underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - - mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId)) - - underTest.addMediaDataLoadingState(MediaDataLoadingModel.Removed(instanceId)) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - } - - @Test - fun setRecommendationsLoadingState() = - testScope.runTest { - val recommendationsLoadingState by - collectLastValue(underTest.recommendationsLoadingState) - val recommendationsLoadingModel = - SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE) - - underTest.setRecommendationsLoadingState(recommendationsLoadingModel) - - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) - } - - @Test fun addMediaControlPlayingThenRemote() = testScope.runTest { val sortedMedia by collectLastValue(underTest.sortedMedia) @@ -195,9 +164,10 @@ class MediaFilterRepositoryTest : SysuiTestCase() { assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( - MediaCommonModel.MediaControl(playingInstanceId), - MediaCommonModel.MediaControl(remoteInstanceId) + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(remoteInstanceId)) ) + .inOrder() } @Test @@ -217,8 +187,8 @@ class MediaFilterRepositoryTest : SysuiTestCase() { assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( - MediaCommonModel.MediaControl(playingInstanceId1), - MediaCommonModel.MediaControl(playingInstanceId2) + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)) ) .inOrder() @@ -233,8 +203,8 @@ class MediaFilterRepositoryTest : SysuiTestCase() { assertThat(sortedMedia?.size).isEqualTo(2) assertThat(sortedMedia?.values) .containsExactly( - MediaCommonModel.MediaControl(playingInstanceId2), - MediaCommonModel.MediaControl(playingInstanceId1) + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId2)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(playingInstanceId1)) ) .inOrder() } @@ -285,12 +255,14 @@ class MediaFilterRepositoryTest : SysuiTestCase() { 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), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId1)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId2)), + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) + ), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId4)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId3)), + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(instanceId5)), ) .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt index c15776e5345e..a2991fddc0ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt @@ -20,7 +20,6 @@ import android.R import android.graphics.drawable.Icon import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags @@ -29,18 +28,14 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.data.repository.mediaFilterRepository -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 import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel -import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before @@ -54,8 +49,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter - private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor @@ -95,7 +88,6 @@ 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 @@ -106,7 +98,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isTrue() - assertThat(sortedMedia).containsExactly(MediaCommonModel.MediaControl(instanceId)) assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() @@ -117,7 +108,6 @@ class MediaCarouselInteractorTest : SysuiTestCase() { assertThat(hasActiveMediaOrRecommendation).isFalse() assertThat(hasActiveMedia).isFalse() assertThat(hasAnyMedia).isFalse() - assertThat(sortedMedia).isEmpty() } @Test @@ -138,28 +128,26 @@ class MediaCarouselInteractorTest : SysuiTestCase() { recommendations = MediaTestHelper.getValidRecommendationList(icon), ) val userMedia = MediaData(active = false) + val recsLoadingModel = SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) + val mediaLoadingModel = MediaDataLoadingModel.Loaded(userMedia.instanceId) mediaFilterRepository.setRecommendation(userMediaRecommendation) - mediaFilterRepository.setRecommendationsLoadingState( - SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, true) - ) + mediaFilterRepository.setRecommendationsLoadingState(recsLoadingModel) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) - .containsExactly(MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE)) + .containsExactly(MediaCommonModel.MediaRecommendations(recsLoadingModel)) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) - mediaFilterRepository.addMediaDataLoadingState( - MediaDataLoadingModel.Loaded(userMedia.instanceId) - ) + mediaFilterRepository.addMediaDataLoadingState(mediaLoadingModel) assertThat(hasActiveMediaOrRecommendation).isTrue() assertThat(hasAnyMediaOrRecommendation).isTrue() assertThat(sortedMedia) .containsExactly( - MediaCommonModel.MediaRecommendations(KEY_MEDIA_SMARTSPACE), - MediaCommonModel.MediaControl(userMedia.instanceId) + MediaCommonModel.MediaRecommendations(recsLoadingModel), + MediaCommonModel.MediaControl(mediaLoadingModel, true) ) .inOrder() } @@ -238,80 +226,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } - @Test - fun onMediaDataUpdated_updatesLoadingState() = - testScope.runTest { - whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) - whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) - val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates) - val instanceId = InstanceId.fakeInstanceId(123) - val mediaLoadedStates: MutableList<MediaDataLoadingModel> = mutableListOf() - - mediaLoadedStates.add(MediaDataLoadingModel.Loaded(instanceId)) - mediaDataFilter.onMediaDataLoaded( - KEY, - KEY, - MediaData(userId = USER_ID, instanceId = instanceId) - ) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - - val newInstanceId = InstanceId.fakeInstanceId(321) - - mediaLoadedStates.add(MediaDataLoadingModel.Loaded(newInstanceId)) - mediaDataFilter.onMediaDataLoaded( - KEY_2, - KEY_2, - MediaData(userId = USER_ID, instanceId = newInstanceId) - ) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - - mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId)) - - mediaDataFilter.onMediaDataRemoved(KEY) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - - mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(newInstanceId)) - - mediaDataFilter.onMediaDataRemoved(KEY_2) - - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) - } - - @Test - fun onMediaRecommendationsUpdated_updatesLoadingState() = - testScope.runTest { - whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) - whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) - val recommendationsLoadingState by - collectLastValue(underTest.recommendationsLoadingState) - val icon = Icon.createWithResource(context, R.drawable.ic_media_play) - val mediaRecommendations = - SmartspaceMediaData( - targetId = KEY_MEDIA_SMARTSPACE, - isActive = true, - recommendations = MediaTestHelper.getValidRecommendationList(icon), - ) - var recommendationsLoadingModel: SmartspaceMediaLoadingModel = - SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, isPrioritized = true) - - mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, mediaRecommendations) - - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) - - recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(KEY_MEDIA_SMARTSPACE) - - mediaDataFilter.onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE) - - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) - } - companion object { - private const val KEY = "key" - private const val KEY_2 = "key2" - private const val USER_ID = 0 private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt new file mode 100644 index 000000000000..4b5fecd9c68d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt @@ -0,0 +1,147 @@ +/* + * 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.ui.viewmodel + +import android.R +import android.content.packageManager +import android.content.pm.ApplicationInfo +import android.graphics.drawable.Icon +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +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.mediaDataFilter +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaCarouselViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter + private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager + private val packageManager = kosmos.packageManager + private val icon = Icon.createWithResource(context, R.drawable.ic_media_play) + private val drawable = context.getDrawable(R.drawable.ic_media_play) + private val smartspaceMediaData: SmartspaceMediaData = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + recommendations = MediaTestHelper.getValidRecommendationList(icon), + ) + + private val underTest: MediaCarouselViewModel = kosmos.mediaCarouselViewModel + + @Before + fun setUp() { + kosmos.mediaCarouselInteractor.start() + + whenever(packageManager.getApplicationIcon(Mockito.anyString())).thenReturn(drawable) + whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java))) + .thenReturn(drawable) + whenever(packageManager.getApplicationInfo(eq(PACKAGE_NAME), ArgumentMatchers.anyInt())) + .thenReturn(ApplicationInfo()) + whenever(packageManager.getApplicationLabel(any())).thenReturn(PACKAGE_NAME) + + context.setMockPackageManager(packageManager) + } + + @Test + fun loadMediaControls_mediaItemsAreUpdated() = + testScope.runTest { + val sortedMedia by collectLastValue(underTest.mediaItems) + val instanceId1 = InstanceId.fakeInstanceId(123) + val instanceId2 = InstanceId.fakeInstanceId(456) + + loadMediaControl(KEY, instanceId1) + loadMediaControl(KEY_2, instanceId2) + + val firstMediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl + val secondMediaControl = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl + assertThat(firstMediaControl.instanceId).isEqualTo(instanceId2) + assertThat(secondMediaControl.instanceId).isEqualTo(instanceId1) + } + + @Test + fun loadMediaControlsAndRecommendations_mediaItemsAreUpdated() = + testScope.runTest { + val sortedMedia by collectLastValue(underTest.mediaItems) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + val instanceId1 = InstanceId.fakeInstanceId(123) + val instanceId2 = InstanceId.fakeInstanceId(456) + + loadMediaControl(KEY, instanceId1) + loadMediaControl(KEY_2, instanceId2) + loadMediaRecommendations() + + val firstMediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl + val secondMediaControl = sortedMedia?.get(1) as MediaCommonViewModel.MediaControl + val recsCard = sortedMedia?.get(2) as MediaCommonViewModel.MediaRecommendations + assertThat(firstMediaControl.instanceId).isEqualTo(instanceId2) + assertThat(secondMediaControl.instanceId).isEqualTo(instanceId1) + assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) + } + + private fun loadMediaControl(key: String, instanceId: InstanceId) { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val mediaData = + MediaData( + userId = USER_ID, + packageName = PACKAGE_NAME, + notificationKey = key, + instanceId = instanceId + ) + + mediaDataFilter.onMediaDataLoaded(key, key, mediaData) + } + + private fun loadMediaRecommendations(key: String = KEY_MEDIA_SMARTSPACE) { + mediaDataFilter.onSmartspaceMediaDataLoaded(key, smartspaceMediaData) + } + + companion object { + private const val USER_ID = 0 + private const val KEY = "key" + private const val KEY_2 = "key2" + private const val PACKAGE_NAME = "com.example.app" + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} 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 7e57cf4af4a3..8ee3adcb46ef 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 @@ -52,16 +52,6 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC MutableStateFlow(LinkedHashMap()) val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow() - private val _mediaDataLoadedStates: MutableStateFlow<List<MediaDataLoadingModel>> = - MutableStateFlow(mutableListOf()) - val mediaDataLoadedStates: StateFlow<List<MediaDataLoadingModel>> = - _mediaDataLoadedStates.asStateFlow() - - private val _recommendationsLoadingState: MutableStateFlow<SmartspaceMediaLoadingModel> = - MutableStateFlow(SmartspaceMediaLoadingModel.Unknown) - val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> = - _recommendationsLoadingState.asStateFlow() - private val comparator = compareByDescending<MediaSortKeyModel> { it.isPlaying == true && it.playbackLocation == MediaData.PLAYBACK_LOCAL @@ -148,46 +138,15 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC } fun addMediaDataLoadingState(mediaDataLoadingModel: MediaDataLoadingModel) { - // Filter out previous loading state that has same [InstanceId]. - val loadedStates = - _mediaDataLoadedStates.value.filter { loadedModel -> - loadedModel !is MediaDataLoadingModel.Loaded || - !loadedModel.equalInstanceIds(mediaDataLoadingModel) - } - - _mediaDataLoadedStates.value = - loadedStates + - if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { - listOf(mediaDataLoadingModel) - } else { - emptyList() - } - - addMediaLoadingToSortedMap(mediaDataLoadingModel) - } - - 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 + commonModel.mediaLoadedModel.instanceId != mediaDataLoadingModel.instanceId } ) - _selectedUserEntries.value[instanceId]?.let { + _selectedUserEntries.value[mediaDataLoadingModel.instanceId]?.let { val sortKey = MediaSortKeyModel( isPrioritizedRec = false, @@ -202,51 +161,41 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC ) if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { - sortedMap[sortKey] = MediaCommonModel.MediaControl(it.instanceId) + sortedMap[sortKey] = + MediaCommonModel.MediaControl(mediaDataLoadingModel, canBeRemoved(it)) } } _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 + fun setRecommendationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { + val isPrioritized = + when (smartspaceMediaLoadingModel) { + is SmartspaceMediaLoadingModel.Loaded -> smartspaceMediaLoadingModel.isPrioritized + else -> false } - } val sortedMap = TreeMap<MediaSortKeyModel, MediaCommonModel>(comparator) sortedMap.putAll( _sortedMedia.value.filter { (_, commonModel) -> - commonModel !is MediaCommonModel.MediaRecommendations || commonModel.key != key + commonModel !is MediaCommonModel.MediaRecommendations } ) - key?.let { - val sortKey = - MediaSortKeyModel( - isPrioritizedRec = isPrioritized, - isPlaying = false, - active = _smartspaceMediaData.value.isActive, - ) - if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) { - sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(key) - } + val sortKey = + MediaSortKeyModel( + isPrioritizedRec = isPrioritized, + isPlaying = false, + active = _smartspaceMediaData.value.isActive, + ) + if (smartspaceMediaLoadingModel is SmartspaceMediaLoadingModel.Loaded) { + sortedMap[sortKey] = MediaCommonModel.MediaRecommendations(smartspaceMediaLoadingModel) } _sortedMedia.value = sortedMap } + + private fun canBeRemoved(data: MediaData): Boolean { + return data.isPlaying?.let { !it } ?: data.isClearable && !data.active + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index b04e93835418..dc2c6512deda 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -21,6 +21,7 @@ import android.media.MediaDescription import android.media.session.MediaSession import android.media.session.PlaybackState import android.service.notification.StatusBarNotification +import com.android.internal.logging.InstanceId import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -35,20 +36,15 @@ import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilt import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener import com.android.systemui.media.controls.domain.resume.MediaResumeListener import com.android.systemui.media.controls.shared.model.MediaCommonModel -import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel -import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.media.controls.util.MediaControlsRefactorFlag import com.android.systemui.media.controls.util.MediaFlags import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn @@ -82,8 +78,11 @@ constructor( (smartspaceMediaData.isActive && (smartspaceMediaData.isValid() || reactivatedKey != null)) } - .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** Are there any media entries we should display, including the recommendations? */ val hasAnyMediaOrRecommendation: StateFlow<Boolean> = @@ -98,34 +97,41 @@ constructor( smartspaceMediaData.isActive && smartspaceMediaData.isValid() }) } - .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** Are there any media notifications active, excluding the recommendations? */ val hasActiveMedia: StateFlow<Boolean> = mediaFilterRepository.selectedUserEntries .mapLatest { entries -> entries.any { it.value.active } } - .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** Are there any media notifications, excluding the recommendations? */ val hasAnyMedia: StateFlow<Boolean> = mediaFilterRepository.selectedUserEntries .mapLatest { entries -> entries.isNotEmpty() } - .distinctUntilChanged() - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) - - /** The most recent list of loaded media controls. */ - val mediaDataLoadedStates: Flow<List<MediaDataLoadingModel>> = - mediaFilterRepository.mediaDataLoadedStates - - /** The most recent change to loaded media recommendations. */ - val recommendationsLoadingState: Flow<SmartspaceMediaLoadingModel> = - mediaFilterRepository.recommendationsLoadingState + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** The most recent sorted set for user media instances */ - val sortedMedia: Flow<List<MediaCommonModel>> = - mediaFilterRepository.sortedMedia.map { it.values.toList() } + val sortedMedia: StateFlow<List<MediaCommonModel>> = + mediaFilterRepository.sortedMedia + .mapLatest { it.values.toList() } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList(), + ) override fun start() { if (!mediaFlags.isMediaControlsRefactorEnabled()) { @@ -210,6 +216,10 @@ constructor( return mediaDataProcessor.dismissMediaData(key, delay) } + fun removeMediaControl(instanceId: InstanceId, delay: Long) { + mediaDataProcessor.dismissMediaData(instanceId, delay) + } + override fun dismissSmartspaceRecommendation(key: String, delay: Long) { return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt index 74cd2fee94ec..c0bb628b23b8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt @@ -43,14 +43,18 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.kotlin.pairwiseBy +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Encapsulates business logic for single media control. */ -class MediaControlInteractor( +class MediaControlInteractor +@AssistedInject +constructor( @Application applicationContext: Context, - private val instanceId: InstanceId, + @Assisted private val instanceId: InstanceId, repository: MediaFilterRepository, private val mediaDataProcessor: MediaDataProcessor, private val keyguardStateController: KeyguardStateController, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt new file mode 100644 index 000000000000..d5686008911d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline.interactor.factory + +import com.android.internal.logging.InstanceId +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor +import dagger.assisted.AssistedFactory + +/** Factory to create [MediaControlInteractor] for each media control. */ +@SysUISingleton +@AssistedFactory +interface MediaControlInteractorFactory { + + fun create(instanceId: InstanceId): MediaControlInteractor +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt index 83e2765bc832..562fe7a9ca67 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt @@ -16,11 +16,13 @@ package com.android.systemui.media.controls.shared.model -import com.android.internal.logging.InstanceId - /** Models any type of media. */ sealed class MediaCommonModel { - data class MediaControl(val instanceId: InstanceId) : MediaCommonModel() + data class MediaControl( + val mediaLoadedModel: MediaDataLoadingModel.Loaded, + val canBeRemoved: Boolean = false + ) : MediaCommonModel() - data class MediaRecommendations(val key: String) : MediaCommonModel() + data class MediaRecommendations(val recsLoadingModel: SmartspaceMediaLoadingModel) : + MediaCommonModel() } 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 bd42a4df7262..170f1f78a5a6 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 @@ -20,27 +20,17 @@ import com.android.internal.logging.InstanceId /** Models media data loading state. */ sealed class MediaDataLoadingModel { - /** The initial loading state when no media data has yet loaded. */ - data object Unknown : MediaDataLoadingModel() + + abstract val instanceId: InstanceId /** Media data has been loaded. */ data class Loaded( - val instanceId: InstanceId, + override val instanceId: InstanceId, val immediatelyUpdateUi: Boolean = true, - ) : MediaDataLoadingModel() { - - /** Returns true if [other] has the same instance id, false otherwise. */ - fun equalInstanceIds(other: MediaDataLoadingModel): Boolean { - return when (other) { - is Loaded -> other.instanceId == instanceId - is Removed -> other.instanceId == instanceId - Unknown -> false - } - } - } + ) : MediaDataLoadingModel() /** Media data has been removed. */ data class Removed( - val instanceId: InstanceId, + override val instanceId: InstanceId, ) : MediaDataLoadingModel() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt index 6c1e536f8c02..90ddadf09d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt @@ -18,18 +18,18 @@ package com.android.systemui.media.controls.shared.model /** Models smartspace media loading state. */ sealed class SmartspaceMediaLoadingModel { - /** The initial loading state when no smartspace media has yet loaded. */ - data object Unknown : SmartspaceMediaLoadingModel() + + abstract val key: String /** Smartspace media has been loaded. */ data class Loaded( - val key: String, + override val key: String, val isPrioritized: Boolean = false, ) : SmartspaceMediaLoadingModel() /** Smartspace media has been removed. */ data class Removed( - val key: String, + override val key: String, val immediatelyUpdateUi: Boolean = true, ) : SmartspaceMediaLoadingModel() } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt new file mode 100644 index 000000000000..75fc1e0952c0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt @@ -0,0 +1,209 @@ +/* + * 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.ui.viewmodel + +import android.content.Context +import com.android.internal.logging.InstanceId +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.factory.MediaControlInteractorFactory +import com.android.systemui.media.controls.shared.model.MediaCommonModel +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider +import com.android.systemui.util.Utils +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user inputs for media carousel */ +@SysUISingleton +class MediaCarouselViewModel +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Application private val applicationContext: Context, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, + private val visualStabilityProvider: VisualStabilityProvider, + private val interactor: MediaCarouselInteractor, + private val controlInteractorFactory: MediaControlInteractorFactory, + private val recommendationsViewModel: MediaRecommendationsViewModel, + private val logger: MediaUiEventLogger, + private val mediaFlags: MediaFlags, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + val mediaItems: StateFlow<List<MediaCommonViewModel>> = + conflatedCallbackFlow { + val listener = OnReorderingAllowedListener { trySend(Unit) } + visualStabilityProvider.addPersistentReorderingAllowedListener(listener) + trySend(Unit) + awaitClose { visualStabilityProvider.removeReorderingAllowedListener(listener) } + } + .flatMapLatest { + interactor.sortedMedia.map { sortedItems -> + buildList { + val reorderAllowed = isReorderingAllowed() + sortedItems.forEach { commonModel -> + if (!reorderAllowed || !modelsPendingRemoval.contains(commonModel)) { + when (commonModel) { + is MediaCommonModel.MediaControl -> + add(toViewModel(commonModel)) + is MediaCommonModel.MediaRecommendations -> + add(toViewModel(commonModel)) + } + } + } + if (reorderAllowed) { + modelsPendingRemoval.clear() + } + } + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = emptyList(), + ) + + private val mediaControlByInstanceId = + mutableMapOf<InstanceId, MediaCommonViewModel.MediaControl>() + + private var mediaRecs: MediaCommonViewModel.MediaRecommendations? = null + + private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf() + + fun onSwipeToDismiss() { + logger.logSwipeDismiss() + interactor.onSwipeToDismiss() + } + + private fun toViewModel( + commonModel: MediaCommonModel.MediaControl + ): MediaCommonViewModel.MediaControl { + val instanceId = commonModel.mediaLoadedModel.instanceId + return mediaControlByInstanceId[instanceId]?.copy( + immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi + ) + ?: MediaCommonViewModel.MediaControl( + instanceId = instanceId, + immediatelyUpdateUi = commonModel.mediaLoadedModel.immediatelyUpdateUi, + controlViewModel = createMediaControlViewModel(instanceId), + onAdded = { onMediaControlAddedOrUpdated(it, commonModel) }, + onRemoved = { _, _ -> + interactor.removeMediaControl(instanceId, delay = 0L) + mediaControlByInstanceId.remove(instanceId) + }, + onUpdated = { onMediaControlAddedOrUpdated(it, commonModel) }, + ) + .also { mediaControlByInstanceId[instanceId] = it } + } + + private fun createMediaControlViewModel(instanceId: InstanceId): MediaControlViewModel { + return MediaControlViewModel( + applicationContext = applicationContext, + backgroundDispatcher = backgroundDispatcher, + backgroundExecutor = backgroundExecutor, + interactor = controlInteractorFactory.create(instanceId), + logger = logger, + ) + } + + private fun toViewModel( + commonModel: MediaCommonModel.MediaRecommendations + ): MediaCommonViewModel.MediaRecommendations { + return mediaRecs?.copy( + key = commonModel.recsLoadingModel.key, + loadingEnabled = + interactor.isRecommendationActive() || mediaFlags.isPersistentSsCardEnabled() + ) + ?: MediaCommonViewModel.MediaRecommendations( + key = commonModel.recsLoadingModel.key, + loadingEnabled = + interactor.isRecommendationActive() || + mediaFlags.isPersistentSsCardEnabled(), + recsViewModel = recommendationsViewModel, + onAdded = { commonViewModel -> + onMediaRecommendationAddedOrUpdated(commonViewModel) + }, + onRemoved = { _, immediatelyRemove -> + onMediaRecommendationRemoved(commonModel, immediatelyRemove) + }, + onUpdated = { commonViewModel -> + onMediaRecommendationAddedOrUpdated(commonViewModel) + }, + ) + .also { mediaRecs = it } + } + + private fun onMediaControlAddedOrUpdated( + commonViewModel: MediaCommonViewModel, + commonModel: MediaCommonModel.MediaControl + ) { + // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED) + if (commonModel.canBeRemoved && !Utils.useMediaResumption(applicationContext)) { + // This media control is due for removal as it is now paused + timed out, and resumption + // setting is off. + if (isReorderingAllowed()) { + commonViewModel.onRemoved(commonViewModel, true) + } else { + modelsPendingRemoval.add(commonModel) + } + } else { + modelsPendingRemoval.remove(commonModel) + } + } + + private fun onMediaRecommendationAddedOrUpdated(commonViewModel: MediaCommonViewModel) { + if (!interactor.isRecommendationActive()) { + if (!mediaFlags.isPersistentSsCardEnabled()) { + commonViewModel.onRemoved(commonViewModel, true) + } + } else { + // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_RECEIVED) + } + } + + private fun onMediaRecommendationRemoved( + commonModel: MediaCommonModel.MediaRecommendations, + immediatelyRemove: Boolean + ) { + if (immediatelyRemove || isReorderingAllowed()) { + interactor.dismissSmartspaceRecommendation(commonModel.recsLoadingModel.key, 0L) + // TODO if not immediate remove update host visibility + } else { + modelsPendingRemoval.add(commonModel) + } + } + + private fun isReorderingAllowed(): Boolean { + return visualStabilityProvider.isReorderingAllowed + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt new file mode 100644 index 000000000000..253f194a04eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt @@ -0,0 +1,45 @@ +/* + * 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.ui.viewmodel + +import com.android.internal.logging.InstanceId + +/** Models media view model UI state. */ +sealed class MediaCommonViewModel { + + abstract val onAdded: (MediaCommonViewModel) -> Unit + abstract val onRemoved: (MediaCommonViewModel, Boolean) -> Unit + abstract val onUpdated: (MediaCommonViewModel) -> Unit + + data class MediaControl( + val instanceId: InstanceId, + val immediatelyUpdateUi: Boolean, + val controlViewModel: MediaControlViewModel, + override val onAdded: (MediaCommonViewModel) -> Unit, + override val onRemoved: (MediaCommonViewModel, Boolean) -> Unit, + override val onUpdated: (MediaCommonViewModel) -> Unit, + ) : MediaCommonViewModel() + + data class MediaRecommendations( + val key: String, + val loadingEnabled: Boolean, + val recsViewModel: MediaRecommendationsViewModel, + override val onAdded: (MediaCommonViewModel) -> Unit, + override val onRemoved: (MediaCommonViewModel, Boolean) -> Unit, + override val onUpdated: (MediaCommonViewModel) -> Unit, + ) : MediaCommonViewModel() +} 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 8f73811199ba..b3cfcf251a47 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 @@ -28,6 +28,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME +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 @@ -165,86 +166,88 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun onDataLoadedForCurrentUser_updatesLoadedStates() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val mediaCommonModel = + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) + assertThat(sortedMedia?.values).containsExactly(mediaCommonModel) } @Test fun onDataLoadedForGuest_doesNotUpdateLoadedStates() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val mediaCommonModel = + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) verify(listener, never()) .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) - assertThat(mediaDataLoadedStates).isNotEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).doesNotContain(mediaCommonModel) } @Test fun onRemovedForCurrent_updatesLoadedStates() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val mediaLoadedStatesModel = - mutableListOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val mediaCommonModel = + MediaCommonModel.MediaControl(MediaDataLoadingModel.Loaded(dataMain.instanceId)) // GIVEN a media was removed for main user mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(mediaCommonModel) - mediaLoadedStatesModel.remove(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataRemoved(KEY) verify(listener).onMediaDataRemoved(eq(KEY)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).doesNotContain(mediaCommonModel) } @Test fun onRemovedForGuest_doesNotUpdateLoadedStates() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val sortedMedia by collectLastValue(repository.sortedMedia) // GIVEN a media was removed for guest user mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) mediaDataFilter.onMediaDataRemoved(KEY) verify(listener, never()).onMediaDataRemoved(eq(KEY)) - assertThat(mediaDataLoadedStates).isEmpty() + assertThat(sortedMedia).isEmpty() } @Test fun onUserSwitched_removesOldUserControls() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val mediaLoaded = MediaDataLoadingModel.Loaded(dataMain.instanceId) // GIVEN that we have a media loaded for main user mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values) + .containsExactly(MediaCommonModel.MediaControl(mediaLoaded)) // and we switch to guest user setUser(USER_GUEST) // THEN we should remove the main user's media verify(listener).onMediaDataRemoved(eq(KEY)) - assertThat(mediaDataLoadedStates).isEmpty() + assertThat(sortedMedia).isEmpty() } @Test fun onUserSwitched_addsNewUserControls() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val guestLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataGuest.instanceId)) - val mainLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val guestLoadedStatesModel = MediaDataLoadingModel.Loaded(dataGuest.instanceId) + val mainLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId) // GIVEN that we had some media for both users mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) @@ -267,14 +270,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { anyInt(), anyBoolean() ) - assertThat(mediaDataLoadedStates).isEqualTo(guestLoadedStatesModel) - assertThat(mediaDataLoadedStates).isNotEqualTo(mainLoadedStatesModel) + assertThat(sortedMedia?.values) + .containsExactly(MediaCommonModel.MediaControl(guestLoadedStatesModel)) + assertThat(sortedMedia?.values) + .doesNotContain(MediaCommonModel.MediaControl(mainLoadedStatesModel)) } @Test fun onProfileChanged_profileUnavailable_updateStates() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val sortedMedia by collectLastValue(repository.sortedMedia) // GIVEN that we had some media for both profiles mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) @@ -283,10 +288,11 @@ class MediaDataFilterImplTest : SysuiTestCase() { // and we change profile status setPrivateProfileUnavailable() - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val mediaLoadedStatesModel = MediaDataLoadingModel.Loaded(dataMain.instanceId) // THEN we should remove the private profile media verify(listener).onMediaDataRemoved(eq(KEY_ALT)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values) + .containsExactly(MediaCommonModel.MediaControl(mediaLoadedStatesModel)) } @Test @@ -515,14 +521,14 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) + val sortedMedia by collectLastValue(repository.sortedMedia) val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values) + .containsExactly(MediaCommonModel.MediaRecommendations(recommendationsLoadingModel)) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -544,14 +550,13 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) + val sortedMedia by collectLastValue(repository.sortedMedia) whenever(smartspaceData.isActive).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) + assertThat(sortedMedia).isEmpty() assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -574,16 +579,22 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = - SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true) + val sortedMedia by collectLastValue(repository.sortedMedia) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true) + ) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(recsCommonModel, controlCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -605,8 +616,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) + val sortedMedia by collectLastValue(repository.sortedMedia) whenever(smartspaceData.isActive).thenReturn(false) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) @@ -614,7 +624,12 @@ class MediaDataFilterImplTest : SysuiTestCase() { clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) + assertThat(sortedMedia?.values) + .doesNotContain( + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) + ) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -635,18 +650,20 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val sortedMedia by collectLastValue(repository.sortedMedia) whenever(smartspaceData.isActive).thenReturn(false) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) @@ -654,7 +671,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // THEN we should treat the media as not active instead - assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -677,16 +694,18 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val sortedMedia by collectLastValue(repository.sortedMedia) whenever(smartspaceData.isValid()).thenReturn(false) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) @@ -696,7 +715,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { // THEN we should treat the media as active instead val dataCurrentAndActive = dataCurrent.copy(active = true) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -715,7 +734,6 @@ class MediaDataFilterImplTest : SysuiTestCase() { eq(true) ) // Smartspace update shouldn't be propagated for the empty rec list. - assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) @@ -727,17 +745,22 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val sortedMedia by collectLastValue(repository.sortedMedia) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) - val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) @@ -756,7 +779,6 @@ class MediaDataFilterImplTest : SysuiTestCase() { eq(100), eq(true) ) - assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -766,7 +788,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() // Smartspace update should also be propagated but not prioritized. - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel) verify(listener) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) @@ -779,15 +801,13 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY) + val sortedMedia by collectLastValue(repository.sortedMedia) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).isEmpty() assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -805,15 +825,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) @@ -830,12 +851,11 @@ class MediaDataFilterImplTest : SysuiTestCase() { eq(100), eq(true) ) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -853,9 +873,11 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + val sortedMedia by collectLastValue(repository.sortedMedia) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(false) @@ -863,7 +885,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { verify(listener) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(recsCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -882,11 +904,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(false) @@ -897,7 +924,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) // And an inactive recommendation is loaded mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) @@ -907,7 +934,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) verify(listener, never()) .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -945,18 +972,23 @@ class MediaDataFilterImplTest : SysuiTestCase() { val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) val reactivatedKey by collectLastValue(repository.reactivatedId) - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + 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) verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) // AND we get a smartspace signal with extra to trigger resume runCurrent() @@ -975,7 +1007,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { eq(100), eq(true) ) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -985,7 +1017,6 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() // And update the smartspace data state, but not prioritized - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) verify(listener) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) } @@ -993,11 +1024,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun smartspaceLoaded_notShouldTriggerResume_doesNotTrigger() = testScope.runTest { - val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - val recommendationsLoadingState by - collectLastValue(repository.recommendationsLoadingState) - val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) - val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val sortedMedia by collectLastValue(repository.sortedMedia) + val recsCommonModel = + MediaCommonModel.MediaRecommendations( + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + ) + val controlCommonModel = + MediaCommonModel.MediaControl( + MediaDataLoadingModel.Loaded(dataMain.instanceId), + true + ) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) @@ -1005,7 +1041,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { verify(listener) .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) - assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel) // AND we get a smartspace signal with extra to not trigger resume val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) } @@ -1018,7 +1054,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { // But the smartspace update is still propagated verify(listener) .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) - assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + assertThat(sortedMedia?.values).containsExactly(controlCommonModel, recsCommonModel) } private fun hasActiveMediaOrRecommendation( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt new file mode 100644 index 000000000000..461eaa24426f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline.interactor.factory + +import android.content.applicationContext +import com.android.internal.logging.InstanceId +import com.android.systemui.activityIntentHelper +import com.android.systemui.bluetooth.mockBroadcastDialogController +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor +import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor +import com.android.systemui.media.mediaOutputDialogManager +import com.android.systemui.plugins.activityStarter +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.statusbar.policy.keyguardStateController + +val Kosmos.mediaControlInteractorFactory by + Kosmos.Fixture { + object : MediaControlInteractorFactory { + override fun create(instanceId: InstanceId): MediaControlInteractor { + return MediaControlInteractor( + applicationContext = applicationContext, + instanceId = instanceId, + repository = mediaFilterRepository, + mediaDataProcessor = mediaDataProcessor, + keyguardStateController = keyguardStateController, + activityStarter = activityStarter, + activityIntentHelper = activityIntentHelper, + lockscreenUserManager = notificationLockscreenUserManager, + mediaOutputDialogManager = mediaOutputDialogManager, + broadcastDialogController = mockBroadcastDialogController, + ) + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt new file mode 100644 index 000000000000..8ebe552b4c3c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt @@ -0,0 +1,44 @@ +/* + * 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.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.media.controls.domain.pipeline.interactor.factory.mediaControlInteractorFactory +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.media.controls.util.mediaUiEventLogger +import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider + +val Kosmos.mediaCarouselViewModel by + Kosmos.Fixture { + MediaCarouselViewModel( + applicationScope = applicationCoroutineScope, + applicationContext = applicationContext, + backgroundDispatcher = testDispatcher, + backgroundExecutor = fakeExecutor, + visualStabilityProvider = VisualStabilityProvider(), + interactor = mediaCarouselInteractor, + controlInteractorFactory = mediaControlInteractorFactory, + recommendationsViewModel = mediaRecommendationsViewModel, + logger = mediaUiEventLogger, + mediaFlags = mediaFlags, + ) + } |