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

Commit 576f3d2f authored by Ahmed Mehfooz's avatar Ahmed Mehfooz Committed by Android (Google) Code Review
Browse files

Merge "[ROSP] Add play/pause support for MediaControlChip" into main

parents 009177ce 8c885b11
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