Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt +70 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import android.graphics.drawable.Drawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading @@ -23,12 +24,15 @@ 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.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton 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 import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) Loading Loading @@ -102,4 +106,70 @@ class MediaControlChipInteractorTest : SysuiTestCase() { assertThat(model?.songName).isEqualTo(newSongName) } @Test fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val mockDrawable = mock<Drawable>() val initialAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "Initial Action", background = mockDrawable, ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val newAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "New Action", background = mockDrawable, ) val updatedMediaButton = MediaButton(playOrPause = newAction) val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.playOrPause).isEqualTo(newAction) } @Test fun mediaControlModel_playPauseActionRemoved_playPauseNull() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val mockDrawable = mock<Drawable>() val initialAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "Initial Action", background = mockDrawable, ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val updatedUserMedia = userMedia.copy(semanticActions = MediaButton()) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.playOrPause).isNull() } } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +7 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ 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.res.R import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope Loading Loading @@ -71,5 +72,10 @@ constructor( } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song) return MediaControlChipModel( appIcon = this.appIcon, appName = this.app, songName = this.song, playOrPause = this.semanticActions?.getActionById(R.id.actionPlayPause), ) } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.featurepods.media.shared.model import android.graphics.drawable.Icon import com.android.systemui.media.controls.shared.model.MediaAction /** Model used to display a media control chip in the status bar. */ data class MediaControlChipModel( val appIcon: Icon?, val appName: String?, val songName: CharSequence?, val playOrPause: MediaAction?, ) packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +37 −24 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ 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.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 Loading @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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 Loading @@ -54,40 +56,51 @@ constructor( */ override val chip: StateFlow<PopupChipModel> = mediaControlChipInteractor.mediaControlModel .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } .map { mediaControlModel -> toPopupChipModel(mediaControlModel) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), PopupChipModel.Hidden(PopupChipId.MediaControl), ) } private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { return PopupChipModel.Hidden(PopupChipId.MediaControl) } val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } return PopupChipModel.Shown( chipId = PopupChipId.MediaControl, icon = model.appIcon?.loadDrawable(context)?.let { val defaultIcon = model.appIcon?.loadDrawable(applicationContext)?.let { Icon.Loaded(drawable = it, contentDescription = contentDescription) } ?: Icon.Resource( res = com.android.internal.R.drawable.ic_audio_media, contentDescription = contentDescription, ), hoverIcon = Icon.Resource( res = com.android.internal.R.drawable.ic_media_pause, contentDescription = null, ), ) return PopupChipModel.Shown( chipId = PopupChipId.MediaControl, icon = defaultIcon, chipText = model.songName.toString(), isToggled = false, // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. // 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 = {}, hoverBehavior = createHoverBehavior(model), ) } private fun createHoverBehavior(model: MediaControlChipModel): HoverBehavior { val playOrPause = model.playOrPause ?: return HoverBehavior.None val icon = playOrPause.icon ?: return HoverBehavior.None val action = playOrPause.action ?: return HoverBehavior.None val contentDescription = ContentDescription.Loaded(description = playOrPause.contentDescription.toString()) return HoverBehavior.Button( icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription), onIconPressed = { backgroundScope.launch { action.run() } }, ) } } packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt +13 −6 Original line number Diff line number Diff line Loading @@ -26,6 +26,18 @@ sealed class PopupChipId(val value: String) { data object MediaControl : PopupChipId("MediaControl") } /** Defines the behavior of the chip when hovered over. */ sealed interface HoverBehavior { /** No specific hover behavior. The default icon will be shown. */ data object None : HoverBehavior /** * Shows a button on hover with the given [icon] and executes [onIconPressed] when the icon is * pressed. */ data class Button(val icon: Icon, val onIconPressed: () -> Unit) : HoverBehavior } /** Model for individual status bar popup chips. */ sealed class PopupChipModel { abstract val logName: String Loading @@ -40,15 +52,10 @@ sealed class PopupChipModel { override val chipId: PopupChipId, /** Default icon displayed on the chip */ val icon: Icon, /** * Icon to be displayed if the chip is hovered. i.e. the mouse pointer is inside the bounds * of the chip. */ val hoverIcon: Icon, val chipText: String, val isToggled: Boolean = false, val onToggle: () -> Unit, val onIconPressed: () -> Unit, val hoverBehavior: HoverBehavior = HoverBehavior.None, ) : PopupChipModel() { override val logName = "Shown(id=$chipId, toggled=$isToggled)" } Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractorTest.kt +70 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.featurepods.media.domain.interactor import android.graphics.drawable.Drawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase Loading @@ -23,12 +24,15 @@ 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.MediaAction import com.android.systemui.media.controls.shared.model.MediaButton 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 import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) Loading Loading @@ -102,4 +106,70 @@ class MediaControlChipInteractorTest : SysuiTestCase() { assertThat(model?.songName).isEqualTo(newSongName) } @Test fun mediaControlModel_playPauseActionChanges_emitsUpdatedModel() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val mockDrawable = mock<Drawable>() val initialAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "Initial Action", background = mockDrawable, ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val newAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "New Action", background = mockDrawable, ) val updatedMediaButton = MediaButton(playOrPause = newAction) val updatedUserMedia = userMedia.copy(semanticActions = updatedMediaButton) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.playOrPause).isEqualTo(newAction) } @Test fun mediaControlModel_playPauseActionRemoved_playPauseNull() = kosmos.runTest { val model by collectLastValue(underTest.mediaControlModel) val mockDrawable = mock<Drawable>() val initialAction = MediaAction( icon = mockDrawable, action = {}, contentDescription = "Initial Action", background = mockDrawable, ) val mediaButton = MediaButton(playOrPause = initialAction) val userMedia = MediaData(active = true, semanticActions = mediaButton) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) mediaFilterRepository.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) assertThat(model).isNotNull() assertThat(model?.playOrPause).isEqualTo(initialAction) val updatedUserMedia = userMedia.copy(semanticActions = MediaButton()) mediaFilterRepository.addSelectedUserMediaEntry(updatedUserMedia) assertThat(model?.playOrPause).isNull() } }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/domain/interactor/MediaControlChipInteractor.kt +7 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ 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.res.R import com.android.systemui.statusbar.featurepods.media.shared.model.MediaControlChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope Loading Loading @@ -71,5 +72,10 @@ constructor( } private fun MediaData.toMediaControlChipModel(): MediaControlChipModel { return MediaControlChipModel(appIcon = this.appIcon, appName = this.app, songName = this.song) return MediaControlChipModel( appIcon = this.appIcon, appName = this.app, songName = this.song, playOrPause = this.semanticActions?.getActionById(R.id.actionPlayPause), ) }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/shared/model/MediaControlChipModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -17,10 +17,12 @@ package com.android.systemui.statusbar.featurepods.media.shared.model import android.graphics.drawable.Icon import com.android.systemui.media.controls.shared.model.MediaAction /** Model used to display a media control chip in the status bar. */ data class MediaControlChipModel( val appIcon: Icon?, val appName: String?, val songName: CharSequence?, val playOrPause: MediaAction?, )
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/media/ui/viewmodel/MediaControlChipViewModel.kt +37 −24 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ 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.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 Loading @@ -33,6 +34,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow 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 Loading @@ -54,40 +56,51 @@ constructor( */ override val chip: StateFlow<PopupChipModel> = mediaControlChipInteractor.mediaControlModel .map { mediaControlModel -> toPopupChipModel(mediaControlModel, applicationContext) } .map { mediaControlModel -> toPopupChipModel(mediaControlModel) } .stateIn( backgroundScope, SharingStarted.WhileSubscribed(), PopupChipModel.Hidden(PopupChipId.MediaControl), ) } private fun toPopupChipModel(model: MediaControlChipModel?, context: Context): PopupChipModel { private fun toPopupChipModel(model: MediaControlChipModel?): PopupChipModel { if (model == null || model.songName.isNullOrEmpty()) { return PopupChipModel.Hidden(PopupChipId.MediaControl) } val contentDescription = model.appName?.let { ContentDescription.Loaded(description = it) } return PopupChipModel.Shown( chipId = PopupChipId.MediaControl, icon = model.appIcon?.loadDrawable(context)?.let { val defaultIcon = model.appIcon?.loadDrawable(applicationContext)?.let { Icon.Loaded(drawable = it, contentDescription = contentDescription) } ?: Icon.Resource( res = com.android.internal.R.drawable.ic_audio_media, contentDescription = contentDescription, ), hoverIcon = Icon.Resource( res = com.android.internal.R.drawable.ic_media_pause, contentDescription = null, ), ) return PopupChipModel.Shown( chipId = PopupChipId.MediaControl, icon = defaultIcon, chipText = model.songName.toString(), isToggled = false, // TODO(b/385202114): Show a popup containing the media carousal when the chip is toggled. // 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 = {}, hoverBehavior = createHoverBehavior(model), ) } private fun createHoverBehavior(model: MediaControlChipModel): HoverBehavior { val playOrPause = model.playOrPause ?: return HoverBehavior.None val icon = playOrPause.icon ?: return HoverBehavior.None val action = playOrPause.action ?: return HoverBehavior.None val contentDescription = ContentDescription.Loaded(description = playOrPause.contentDescription.toString()) return HoverBehavior.Button( icon = Icon.Loaded(drawable = icon, contentDescription = contentDescription), onIconPressed = { backgroundScope.launch { action.run() } }, ) } }
packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt +13 −6 Original line number Diff line number Diff line Loading @@ -26,6 +26,18 @@ sealed class PopupChipId(val value: String) { data object MediaControl : PopupChipId("MediaControl") } /** Defines the behavior of the chip when hovered over. */ sealed interface HoverBehavior { /** No specific hover behavior. The default icon will be shown. */ data object None : HoverBehavior /** * Shows a button on hover with the given [icon] and executes [onIconPressed] when the icon is * pressed. */ data class Button(val icon: Icon, val onIconPressed: () -> Unit) : HoverBehavior } /** Model for individual status bar popup chips. */ sealed class PopupChipModel { abstract val logName: String Loading @@ -40,15 +52,10 @@ sealed class PopupChipModel { override val chipId: PopupChipId, /** Default icon displayed on the chip */ val icon: Icon, /** * Icon to be displayed if the chip is hovered. i.e. the mouse pointer is inside the bounds * of the chip. */ val hoverIcon: Icon, val chipText: String, val isToggled: Boolean = false, val onToggle: () -> Unit, val onIconPressed: () -> Unit, val hoverBehavior: HoverBehavior = HoverBehavior.None, ) : PopupChipModel() { override val logName = "Shown(id=$chipId, toggled=$isToggled)" } Loading