summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt58
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt99
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelTest.kt147
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt95
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaControlInteractor.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactory.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaCommonModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModel.kt209
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaCommonViewModel.kt45
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt234
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/factory/MediaControlInteractorFactoryKosmos.kt50
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/viewmodel/MediaCarouselViewModelKosmos.kt44
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,
+ )
+ }