Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher 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.shared.model.PopupChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MediaControlChipViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.mediaControlChipViewModel @Test fun chip_noActiveMedia_IsHidden() = kosmos.runTest { val chip by collectLastValue(underTest.chip) assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java) } @Test fun chip_activeMedia_IsShown() = kosmos.runTest { val chip by collectLastValue(underTest.chip) val userMedia = MediaData(active = true, song = "test") val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(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) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +3 −3 Original line number Diff line number Diff line Loading @@ -41,14 +41,14 @@ import kotlinx.coroutines.flow.stateIn class MediaControlChipInteractor @Inject constructor( @Background private val applicationScope: CoroutineScope, @Background private val backgroundScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = mediaFilterRepository.currentMedia .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } .stateIn( scope = applicationScope, scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = emptyList(), ) Loading @@ -64,7 +64,7 @@ constructor( ?.toMediaControlChipModel() } .stateIn( scope = applicationScope, scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = null, ) Loading packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt 0 → 100644 +87 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.Context 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.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.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 kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** * [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 constructor( @Background private val backgroundScope: CoroutineScope, @Application private val applicationContext: Context, mediaControlChipInteractor: MediaControlChipInteractor, ) : StatusBarPopupChipViewModel { /** * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel] * whenever the underlying [MediaControlChipModel] changes. */ override val chip: StateFlow<PopupChipModel> = mediaControlChipInteractor.mediaControlModel .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), PopupChipModel.Hidden(PopupChipId.MediaControls), ) } private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { return PopupChipModel.Hidden(PopupChipId.MediaControls) } val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } return PopupChipModel.Shown( chipId = PopupChipId.MediaControls, icon = model.appIcon?.loadDrawable(context)?.let { Icon.Loaded(drawable = it, contentDescription = contentDescription) } ?: Icon.Resource( res = com.android.internal.R.drawable.ic_audio_media, contentDescription = contentDescription, ), chipText = model.songName.toString(), // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. onToggle = {}, // TODO(b/385202193): Add support for clicking on the icon on a media chip. onIconPressed = {}, ) } packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt +2 −2 Original line number Diff line number Diff line Loading @@ -17,13 +17,13 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.backgroundScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by Kosmos.Fixture { MediaControlChipInteractor( applicationScope = applicationCoroutineScope, backgroundScope = backgroundScope, mediaFilterRepository = mediaFilterRepository, ) } packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.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 Kosmos.Fixture { MediaControlChipViewModel( backgroundScope = applicationCoroutineScope, applicationContext = testableContext, mediaControlChipInteractor = mediaControlChipInteractor, ) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelTest.kt 0 → 100644 +82 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher 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.shared.model.PopupChipModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MediaControlChipViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.mediaControlChipViewModel @Test fun chip_noActiveMedia_IsHidden() = kosmos.runTest { val chip by collectLastValue(underTest.chip) assertThat(chip).isInstanceOf(PopupChipModel.Hidden::class.java) } @Test fun chip_activeMedia_IsShown() = kosmos.runTest { val chip by collectLastValue(underTest.chip) val userMedia = MediaData(active = true, song = "test") val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(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) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat((chip as PopupChipModel.Shown).chipText).isEqualTo(newSongName) } }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +3 −3 Original line number Diff line number Diff line Loading @@ -41,14 +41,14 @@ import kotlinx.coroutines.flow.stateIn class MediaControlChipInteractor @Inject constructor( @Background private val applicationScope: CoroutineScope, @Background private val backgroundScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = mediaFilterRepository.currentMedia .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } .stateIn( scope = applicationScope, scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = emptyList(), ) Loading @@ -64,7 +64,7 @@ constructor( ?.toMediaControlChipModel() } .stateIn( scope = applicationScope, scope = backgroundScope, started = SharingStarted.WhileSubscribed(), initialValue = null, ) Loading
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt 0 → 100644 +87 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.featurepods.media.ui.viewmodel import android.content.Context 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.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.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 kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** * [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 constructor( @Background private val backgroundScope: CoroutineScope, @Application private val applicationContext: Context, mediaControlChipInteractor: MediaControlChipInteractor, ) : StatusBarPopupChipViewModel { /** * A [StateFlow] of the current [PopupChipModel]. This flow emits a new [PopupChipModel] * whenever the underlying [MediaControlChipModel] changes. */ override val chip: StateFlow<PopupChipModel> = mediaControlChipInteractor.mediaControlModel .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), PopupChipModel.Hidden(PopupChipId.MediaControls), ) } private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { return PopupChipModel.Hidden(PopupChipId.MediaControls) } val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } return PopupChipModel.Shown( chipId = PopupChipId.MediaControls, icon = model.appIcon?.loadDrawable(context)?.let { Icon.Loaded(drawable = it, contentDescription = contentDescription) } ?: Icon.Resource( res = com.android.internal.R.drawable.ic_audio_media, contentDescription = contentDescription, ), chipText = model.songName.toString(), // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. onToggle = {}, // TODO(b/385202193): Add support for clicking on the icon on a media chip. onIconPressed = {}, ) }
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt +2 −2 Original line number Diff line number Diff line Loading @@ -17,13 +17,13 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.backgroundScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by Kosmos.Fixture { MediaControlChipInteractor( applicationScope = applicationCoroutineScope, backgroundScope = backgroundScope, mediaFilterRepository = mediaFilterRepository, ) }
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModelKosmos.kt 0 → 100644 +31 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.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 Kosmos.Fixture { MediaControlChipViewModel( backgroundScope = applicationCoroutineScope, applicationContext = testableContext, mediaControlChipInteractor = mediaControlChipInteractor, ) }