Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt 0 → 100644 +105 −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.domain.interactor 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.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MediaControlChipInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.mediaControlChipInteractor @Test fun mediaControlModel_noActiveMedia_null() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) assertThat(model).isNull() } @Test fun mediaControlModel_activeMedia_notNull() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val userMedia = MediaData(active = true) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() } @Test fun mediaControlModel_mediaRemoved_null() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val userMedia = MediaData(active = true) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) assertThat(model).isNull() } @Test fun mediaControlModel_songNameChanged_emitsUpdatedModel() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) 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(model).isNotNull() assertThat(model?.songName).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.songName).isEqualTo(newSongName) } } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt 0 → 100644 +75 −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.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** * Interactor for managing the state of the media control chip in the status bar. * * Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media * control chip. Emits a new [MediaControlChipModel] when there is an active media session and the * corresponding user preference is found, otherwise emits null. */ @SysUISingleton class MediaControlChipInteractor @Inject constructor( @Background private val applicationScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = mediaFilterRepository.currentMedia .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = emptyList(), ) /** The currently active [MediaControlChipModel] */ val mediaControlModel: StateFlow<MediaControlChipModel?> = combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) { mediaControls, userEntries -> mediaControls .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } .firstOrNull { it.active } ?.toMediaControlChipModel() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = null, ) } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song) } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt 0 → 100644 +26 −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.shared.model import android.graphics.drawable.Icon /** Model used to display a media control chip in the status bar. */ data class MediaControlChipModel( val appIcon: Icon?, val appName: String?, val songName: CharSequence?, ) packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt 0 → 100644 +29 −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.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by Kosmos.Fixture { MediaControlChipInteractor( applicationScope = applicationCoroutineScope, mediaFilterRepository = mediaFilterRepository, ) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt 0 → 100644 +105 −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.domain.interactor 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.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class MediaControlChipInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val underTest = kosmos.mediaControlChipInteractor @Test fun mediaControlModel_noActiveMedia_null() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) assertThat(model).isNull() } @Test fun mediaControlModel_activeMedia_notNull() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val userMedia = MediaData(active = true) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() } @Test fun mediaControlModel_mediaRemoved_null() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val userMedia = MediaData(active = true) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(instanceId, userMedia)) .isTrue() mediaFilterRepository.addMediaDataLoadingState( MediaDataLoadingModel.Removed(instanceId) ) assertThat(model).isNull() } @Test fun mediaControlModel_songNameChanged_emitsUpdatedModel() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) 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(model).isNotNull() assertThat(model?.songName).isEqualTo(initialSongName) val updatedUserMedia = userMedia.copy(song = newSongName) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.songName).isEqualTo(newSongName) } }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt 0 → 100644 +75 −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.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.MediaCommonModel import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** * Interactor for managing the state of the media control chip in the status bar. * * Provides a [StateFlow] of [MediaControlChipModel] representing the current state of the media * control chip. Emits a new [MediaControlChipModel] when there is an active media session and the * corresponding user preference is found, otherwise emits null. */ @SysUISingleton class MediaControlChipInteractor @Inject constructor( @Background private val applicationScope: CoroutineScope, mediaFilterRepository: MediaFilterRepository, ) { private val currentMediaControls: StateFlow<List<MediaCommonModel.MediaControl>> = mediaFilterRepository.currentMedia .map { mediaList -> mediaList.filterIsInstance<MediaCommonModel.MediaControl>() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = emptyList(), ) /** The currently active [MediaControlChipModel] */ val mediaControlModel: StateFlow<MediaControlChipModel?> = combine(currentMediaControls, mediaFilterRepository.selectedUserEntries) { mediaControls, userEntries -> mediaControls .mapNotNull { userEntries[it.mediaLoadedModel.instanceId] } .firstOrNull { it.active } ?.toMediaControlChipModel() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = null, ) } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song) }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt 0 → 100644 +26 −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.shared.model import android.graphics.drawable.Icon /** Model used to display a media control chip in the status bar. */ data class MediaControlChipModel( val appIcon: Icon?, val appName: String?, val songName: CharSequence?, )
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorKosmos.kt 0 → 100644 +29 −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.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.media.controls.data.repository.mediaFilterRepository val Kosmos.mediaControlChipInteractor: MediaControlChipInteractor by Kosmos.Fixture { MediaControlChipInteractor( applicationScope = applicationCoroutineScope, mediaFilterRepository = mediaFilterRepository, ) }