diff options
| author | 2024-04-19 21:09:17 +0000 | |
|---|---|---|
| committer | 2024-04-24 14:32:07 +0000 | |
| commit | d74e08af1ba2dc1d26c9c205db9c94c5569df97c (patch) | |
| tree | 0711ffb8920090e1781d85e1c44a420bd63b08a0 | |
| parent | 3f548c0033a3918c7d80377c2598b7fb34369aed (diff) | |
Handle reordering when recommendation clicked.
Flag: ACONFIG com.android.systemui.media_controls_refactor DISABLED
Bug: 328207006
Test: atest SystemUiRoboTests:MediaCarouselViewModelTest
Change-Id: Ic85447cdc112de33c237fe98ba06c4dd096f0856
11 files changed, 163 insertions, 8 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 e39511fdfdab..1e5f31401e20 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 @@ -267,6 +267,35 @@ class MediaFilterRepositoryTest : SysuiTestCase() { .inOrder() } + @Test + fun loadMediaFromRec() = + testScope.runTest { + val isMediaFromRec by collectLastValue(underTest.isMediaFromRec) + val instanceId1 = InstanceId.fakeInstanceId(123) + val instanceId2 = InstanceId.fakeInstanceId(456) + val data = + MediaData( + active = true, + instanceId = instanceId1, + packageName = PACKAGE_NAME, + isPlaying = true + ) + val newData = MediaData(active = true, instanceId = instanceId2) + + assertThat(isMediaFromRec).isFalse() + + underTest.setMediaFromRecPackageName(PACKAGE_NAME) + underTest.addSelectedUserMediaEntry(data) + underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId1)) + + assertThat(isMediaFromRec).isTrue() + + underTest.addSelectedUserMediaEntry(newData) + underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId2)) + + assertThat(isMediaFromRec).isFalse() + } + private fun createMediaData( app: String, playing: Boolean, @@ -288,5 +317,6 @@ class MediaFilterRepositoryTest : SysuiTestCase() { private const val REMOTE = MediaData.PLAYBACK_CAST_LOCAL private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + private const val PACKAGE_NAME = "com.android.example" } } 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 a2991fddc0ca..e44affc7262b 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,6 +20,7 @@ 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,7 +30,9 @@ 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.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaRecommendationsInteractor import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaRecommendationsInteractor 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 @@ -50,6 +53,8 @@ class MediaCarouselInteractorTest : SysuiTestCase() { private val testScope = kosmos.testScope private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository + private val mediaRecommendationsInteractor: MediaRecommendationsInteractor = + kosmos.mediaRecommendationsInteractor private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor @@ -226,7 +231,29 @@ class MediaCarouselInteractorTest : SysuiTestCase() { fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } + @Test + fun loadMediaFromRec() = + testScope.runTest { + val isMediaFromRec by collectLastValue(underTest.isMediaFromRec) + val instanceId = InstanceId.fakeInstanceId(123) + val data = MediaData(active = true, instanceId = instanceId, packageName = PACKAGE_NAME) + + assertThat(isMediaFromRec).isFalse() + + mediaRecommendationsInteractor.switchToMediaControl(PACKAGE_NAME) + mediaFilterRepository.addSelectedUserMediaEntry(data) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(isMediaFromRec).isFalse() + + mediaFilterRepository.addSelectedUserMediaEntry(data.copy(isPlaying = true)) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(isMediaFromRec).isTrue() + } + companion object { private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + private const val PACKAGE_NAME = "com.android.example" } } 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 index 4b5fecd9c68d..d1e475f590dd 100644 --- 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 @@ -31,9 +31,11 @@ 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.interactor.mediaRecommendationsInteractor 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.notification.collection.provider.visualStabilityProvider import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -119,7 +121,35 @@ class MediaCarouselViewModelTest : SysuiTestCase() { assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) } - private fun loadMediaControl(key: String, instanceId: InstanceId) { + @Test + fun recommendationClicked_switchToPlayer() = + testScope.runTest { + val sortedMedia by collectLastValue(underTest.mediaItems) + kosmos.visualStabilityProvider.isReorderingAllowed = false + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + val instanceId = InstanceId.fakeInstanceId(123) + + loadMediaRecommendations() + kosmos.mediaRecommendationsInteractor.switchToMediaControl(PACKAGE_NAME) + + var recsCard = sortedMedia?.get(0) as MediaCommonViewModel.MediaRecommendations + assertThat(sortedMedia).hasSize(1) + assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) + + loadMediaControl(KEY, instanceId, false) + + recsCard = sortedMedia?.get(0) as MediaCommonViewModel.MediaRecommendations + assertThat(sortedMedia).hasSize(1) + assertThat(recsCard.key).isEqualTo(KEY_MEDIA_SMARTSPACE) + + loadMediaControl(KEY, instanceId, true) + + val mediaControl = sortedMedia?.get(0) as MediaCommonViewModel.MediaControl + assertThat(sortedMedia).hasSize(2) + assertThat(mediaControl.instanceId).isEqualTo(instanceId) + } + + private fun loadMediaControl(key: String, instanceId: InstanceId, isPlaying: Boolean = true) { whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) val mediaData = @@ -127,7 +157,8 @@ class MediaCarouselViewModelTest : SysuiTestCase() { userId = USER_ID, packageName = PACKAGE_NAME, notificationKey = key, - instanceId = instanceId + instanceId = instanceId, + isPlaying = isPlaying, ) mediaDataFilter.onMediaDataLoaded(key, key, mediaData) 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 8ee3adcb46ef..df3a974634d7 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 @@ -72,6 +72,11 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC val sortedMedia: StateFlow<Map<MediaSortKeyModel, MediaCommonModel>> = _sortedMedia.asStateFlow() + private val _isMediaFromRec: MutableStateFlow<Boolean> = MutableStateFlow(false) + val isMediaFromRec: StateFlow<Boolean> = _isMediaFromRec.asStateFlow() + + private var mediaFromRecPackageName: String? = null + fun addMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) entries[key] = data @@ -161,6 +166,12 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC ) if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { + val isMediaFromRec = isMediaFromRec(it) + + _isMediaFromRec.value = isMediaFromRec + if (isMediaFromRec) { + mediaFromRecPackageName = null + } sortedMap[sortKey] = MediaCommonModel.MediaControl(mediaDataLoadingModel, canBeRemoved(it)) } @@ -195,7 +206,15 @@ class MediaFilterRepository @Inject constructor(private val systemClock: SystemC _sortedMedia.value = sortedMap } + fun setMediaFromRecPackageName(packageName: String) { + mediaFromRecPackageName = packageName + } + private fun canBeRemoved(data: MediaData): Boolean { return data.isPlaying?.let { !it } ?: data.isClearable && !data.active } + + private fun isMediaFromRec(data: MediaData): Boolean { + return data.isPlaying == true && mediaFromRecPackageName == data.packageName + } } 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 dc2c6512deda..13f934ecda43 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 @@ -133,6 +133,9 @@ constructor( initialValue = emptyList(), ) + /** Whether the current change in media was done by clicking on a recommendation */ + val isMediaFromRec: StateFlow<Boolean> = mediaFilterRepository.isMediaFromRec + override fun start() { if (!mediaFlags.isMediaControlsRefactorEnabled()) { return diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt index d57b04938d2c..dea581083cb7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaRecommendationsInteractor.kt @@ -49,7 +49,7 @@ class MediaRecommendationsInteractor constructor( @Application applicationScope: CoroutineScope, @Application private val applicationContext: Context, - repository: MediaFilterRepository, + private val repository: MediaFilterRepository, private val mediaDataProcessor: MediaDataProcessor, private val broadcastSender: BroadcastSender, private val activityStarter: ActivityStarter, @@ -133,6 +133,10 @@ constructor( } } + fun switchToMediaControl(packageName: String) { + repository.setMediaFromRecPackageName(packageName) + } + companion object { private const val TAG = "MediaRecommendationsInteractor" 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 562fe7a9ca67..23860bb9868c 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 @@ -20,7 +20,7 @@ package com.android.systemui.media.controls.shared.model sealed class MediaCommonModel { data class MediaControl( val mediaLoadedModel: MediaDataLoadingModel.Loaded, - val canBeRemoved: Boolean = false + val canBeRemoved: Boolean = false, ) : MediaCommonModel() data class MediaRecommendations(val recsLoadingModel: 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 index c92965e5d6ba..5188132b571e 100644 --- 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 @@ -29,6 +29,7 @@ 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.util.kotlin.pairwiseBy import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -37,8 +38,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine 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 */ @@ -66,8 +67,11 @@ constructor( awaitClose { visualStabilityProvider.removeReorderingAllowedListener(listener) } } .flatMapLatest { - interactor.sortedMedia.map { sortedItems -> + combine(interactor.isMediaFromRec, interactor.sortedMedia) { + isRecsToMedia, + sortedItems -> buildList { + shouldReorder = isRecsToMedia val reorderAllowed = isReorderingAllowed() sortedItems.forEach { commonModel -> if (!reorderAllowed || !modelsPendingRemoval.contains(commonModel)) { @@ -85,6 +89,16 @@ constructor( } } } + .pairwiseBy { old, new -> + // This condition can only happen when view is attached. So the old emit is of the + // most recent list updated. + // If the old list is empty, it is okay to emit the new ordered list. + if (isReorderingAllowed() || shouldReorder || old.isEmpty()) { + new + } else { + old + } + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), @@ -98,6 +112,8 @@ constructor( private var modelsPendingRemoval: MutableSet<MediaCommonModel> = mutableSetOf() + private var shouldReorder = true + fun onSwipeToDismiss() { logger.logSwipeDismiss() interactor.onSwipeToDismiss() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt index a2307d407396..5cd980441b4c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaRecommendationsViewModel.kt @@ -107,6 +107,10 @@ constructor( logger.logRecommendationItemTap(packageName, instanceId, index) } // TODO (b/330897926) log smartspace card reported (SMARTSPACE_CARD_CLICK_EVENT). + + // set the package name of the player added by recommendation once the media is loaded. + interactor.switchToMediaControl(packageName) + interactor.startClickIntent(expandable, intent) } 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 index 9a181cd4ab72..6eae28f7c772 100644 --- 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 @@ -24,7 +24,7 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.factory.me 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 +import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider val Kosmos.mediaCarouselViewModel by Kosmos.Fixture { @@ -32,7 +32,7 @@ val Kosmos.mediaCarouselViewModel by applicationScope = applicationCoroutineScope, applicationContext = applicationContext, backgroundDispatcher = testDispatcher, - visualStabilityProvider = VisualStabilityProvider(), + visualStabilityProvider = visualStabilityProvider, interactor = mediaCarouselInteractor, controlInteractorFactory = mediaControlInteractorFactory, recommendationsViewModel = mediaRecommendationsViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProviderKosmos.kt new file mode 100644 index 000000000000..9a5f126584e8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/VisualStabilityProviderKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.statusbar.notification.collection.provider + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.visualStabilityProvider by Kosmos.Fixture { VisualStabilityProvider() } |