Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c885b11 authored by amehfooz's avatar amehfooz
Browse files

[ROSP] Add play/pause support for MediaControlChip

This CL adds a playOrPause action to the MediaControlChipModel
which is used to populate the hover icon and click action on
it.
Also, added some tweaks to StatusBarPopupChip's icon to make
the hover icons look better.

Test: Added state change tests in MediaControlChipInteractorTest
Test: Manually confirm music can be play/paused.
See b/385202193 comment#2 for video

Bug: b/385202193
Flag: com.android.systemui.status_bar_popup_chips
Change-Id: I6ca8e0c3efc4def6fb54c81602255280572510a1
parent e891c195
Loading
Loading
Loading
Loading
+70 −0
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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()
        }
}
+7 −1
Original line number Diff line number Diff line
@@ -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
@@ -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),
    )
}
+2 −0
Original line number Diff line number Diff line
@@ -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?,
)
+37 −24
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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() } },
        )
    }
}
+13 −6
Original line number Diff line number Diff line
@@ -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
@@ -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