diff options
7 files changed, 157 insertions, 53 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt index d36dbbe8d36f..d4518e7299da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt @@ -21,9 +21,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn import com.android.systemui.media.controls.data.repository.mediaFilterRepository import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData @@ -47,7 +48,8 @@ import platform.test.runner.parameterized.Parameters class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val mediaControlChipInteractor by lazy { kosmos.mediaControlChipInteractor } - private val Kosmos.underTest by Kosmos.Fixture { kosmos.mediaControlChipViewModel } + private val Kosmos.underTest by + Kosmos.Fixture { kosmos.mediaControlChipViewModelFactory.create() } @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> companion object { @@ -62,6 +64,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas fun setUp() { MockitoAnnotations.initMocks(this) mediaControlChipInteractor.initialize() + kosmos.underTest.activateIn(kosmos.testScope) } init { @@ -71,7 +74,7 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun chip_noActiveMedia_IsHidden() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) + val chip = underTest.chip assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java) } @@ -79,30 +82,26 @@ class MediaControlChipViewModelTest(flags: FlagsParameterization) : SysuiTestCas @Test fun chip_activeMedia_IsShown() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) - val userMedia = MediaData(active = true, song = "test") updateMedia(userMedia) - assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) + assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java) } @Test fun chip_songNameChanges_chipTextUpdated() = kosmos.runTest { - val chip by collectLastValue(underTest.chip) - val initialSongName = "Initial Song" val newSongName = "New Song" val userMedia = MediaData(active = true, song = initialSongName) updateMedia(userMedia) - assertThat(chip).isInstanceOf(PopupChipModel.Shown::class.java) - assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) + assertThat(underTest.chip).isInstanceOf(PopupChipModel.Shown::class.java) + assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) updateMedia(updatedUserMedia) - assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) + assertThat((underTest.chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } private fun updateMedia(mediaData: MediaData) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt new file mode 100644 index 000000000000..134ab9322df0 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt @@ -0,0 +1,95 @@ +/* + * 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.featurepods.media.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.compose.runtime.snapshots.Snapshot +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId +import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.statusBarPopupChipsViewModelFactory +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnableFlags(StatusBarPopupChips.FLAG_NAME) +@RunWith(AndroidJUnit4::class) +class StatusBarPopupChipsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val underTest = kosmos.statusBarPopupChipsViewModelFactory.create() + + @Before + fun setUp() { + underTest.activateIn(kosmos.testScope) + } + + @Test + fun shownPopupChips_allHidden_empty() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + assertThat(shownPopupChips).isEmpty() + } + + @Test + fun shownPopupChips_activeMedia_restHidden_mediaControlChipShown() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + Snapshot.takeSnapshot { + assertThat(shownPopupChips).hasSize(1) + assertThat(shownPopupChips.first().chipId).isEqualTo(PopupChipId.MediaControl) + } + } + + @Test + fun shownPopupChips_mediaChipToggled_popupShown() = + kosmos.runTest { + val shownPopupChips = underTest.shownPopupChips + + val userMedia = MediaData(active = true, song = "test") + val instanceId = userMedia.instanceId + + mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + Snapshot.takeSnapshot { + assertThat(shownPopupChips).hasSize(1) + val mediaChip = shownPopupChips.first() + assertThat(mediaChip.isPopupShown).isFalse() + + mediaChip.showPopup.invoke() + assertThat(shownPopupChips.first().isPopupShown).isTrue() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt index 90f97df295f5..ec6508717e8f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt @@ -17,51 +17,51 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.Context +import androidx.compose.runtime.getValue import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.statusbar.featurepods.media.domain.interactor.MediaControlChipInteractor import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import com.android.systemui.statusbar.featurepods.popups.shared.model.HoverBehavior import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel import com.android.systemui.statusbar.featurepods.popups.ui.viewmodel.StatusBarPopupChipViewModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch /** * [StatusBarPopupChipViewModel] for a media control chip in the status bar. This view model is * responsible for converting the [MediaControlChipModel] to a [PopupChipModel] that can be used to * display a media control chip. */ -@SysUISingleton class MediaControlChipViewModel -@Inject +@AssistedInject constructor( - @Background private val backgroundScope: CoroutineScope, @Application private val applicationContext: Context, mediaControlChipInteractor: MediaControlChipInteractor, -) : StatusBarPopupChipViewModel { - +) : StatusBarPopupChipViewModel, ExclusiveActivatable() { + private val hydrator: Hydrator = Hydrator("MediaControlChipViewModel.hydrator") /** - * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel] + * A snapshot [State] of the current [PopupChipModel]. This emits a new [PopupChipModel] * whenever the underlying [MediaControlChipModel] changes. */ - override val chip: StateFlow<PopupChipModel> = - mediaControlChipInteractor.mediaControlChipModel - .map { mediaControlChipModel -> toPopupChipModel(mediaControlChipModel) } - .stateIn( - backgroundScope, - SharingStarted.WhileSubscribed(), - PopupChipModel.Hidden(PopupChipId.MediaControl), - ) + override val chip: PopupChipModel by + hydrator.hydratedStateOf( + traceName = "chip", + initialValue = PopupChipModel.Hidden(PopupChipId.MediaControl), + source = + mediaControlChipInteractor.mediaControlChipModel.map { model -> + toPopupChipModel(model) + }, + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { @@ -96,7 +96,12 @@ constructor( return HoverBehavior.Button( icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription), - onIconPressed = { backgroundScope.launch { action.run() } }, + onIconPressed = { action.run() }, ) } + + @AssistedFactory + interface Factory { + fun create(): MediaControlChipViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt index 5712be30ccd6..38f24137d355 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt @@ -16,14 +16,14 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel +import com.android.systemui.lifecycle.Activatable import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel -import kotlinx.coroutines.flow.StateFlow /** * Interface for a view model that knows the display requirements for a single type of status bar * popup chip. */ -interface StatusBarPopupChipViewModel { - /** A flow modeling the popup chip that should be shown (or not shown). */ - val chip: StateFlow<PopupChipModel> +interface StatusBarPopupChipViewModel : Activatable { + /** A snapshot [State] modeling the popup chip that should be shown (or not shown). */ + val chip: PopupChipModel } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt index 33bf90defb48..35f1a9981691 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.Hydrator import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.MediaControlChipViewModel import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId @@ -36,18 +35,17 @@ import kotlinx.coroutines.flow.map */ class StatusBarPopupChipsViewModel @AssistedInject -constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable() { - private val hydrator: Hydrator = Hydrator("StatusBarPopupChipsViewModel.hydrator") +constructor(mediaControlChipFactory: MediaControlChipViewModel.Factory) : ExclusiveActivatable() { + + private val mediaControlChip by lazy { mediaControlChipFactory.create() } /** The ID of the current chip that is showing its popup, or `null` if no chip is shown. */ private var currentShownPopupChipId by mutableStateOf<PopupChipId?>(null) - private val incomingPopupChipBundle: PopupChipBundle by - hydrator.hydratedStateOf( - traceName = "incomingPopupChipBundle", - initialValue = PopupChipBundle(), - source = mediaControlChip.chip.map { chip -> PopupChipBundle(media = chip) }, - ) + private val incomingPopupChipBundle: PopupChipBundle by derivedStateOf { + val mediaChip = mediaControlChip.chip + PopupChipBundle(media = mediaChip) + } val shownPopupChips: List<PopupChipModel.Shown> by derivedStateOf { if (StatusBarPopupChips.isEnabled) { @@ -66,7 +64,7 @@ constructor(mediaControlChip: MediaControlChipViewModel) : ExclusiveActivatable( } override suspend fun onActivated(): Nothing { - hydrator.activate() + mediaControlChip.activate() } private data class PopupChipBundle( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt index 7145907a14a8..39391d03a44b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt @@ -18,14 +18,19 @@ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.testableContext import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.featurepods.media.domain.interactor.mediaControlChipInteractor -val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by +private val Kosmos.mediaControlChipViewModel: MediaControlChipViewModel by Kosmos.Fixture { MediaControlChipViewModel( - backgroundScope = applicationCoroutineScope, applicationContext = testableContext, mediaControlChipInteractor = mediaControlChipInteractor, ) } + +val Kosmos.mediaControlChipViewModelFactory by + Kosmos.Fixture { + object : MediaControlChipViewModel.Factory { + override fun create(): MediaControlChipViewModel = mediaControlChipViewModel + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt index b876095fefe5..2a3167cb66f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.featurepods.popups.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModel +import com.android.systemui.statusbar.featurepods.media.ui.viewmodel.mediaControlChipViewModelFactory private val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by - Kosmos.Fixture { StatusBarPopupChipsViewModel(mediaControlChip = mediaControlChipViewModel) } + Kosmos.Fixture { + StatusBarPopupChipsViewModel(mediaControlChipFactory = mediaControlChipViewModelFactory) + } val Kosmos.statusBarPopupChipsViewModelFactory by Kosmos.Fixture { |