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

Commit b54f9e5e authored by Derek Jedral's avatar Derek Jedral
Browse files

Propagate device suggestions with new pipeline

This propagates suggestions through the MediaControlViewBinder pipeline

Test: atest
Bug: 409097221
Flag: com.android.systemui.enable_suggested_device_ui
Change-Id: Ifc26855f2b4c63fd126982e28e91d303f110a568
parent db990e60
Loading
Loading
Loading
Loading
+139 −1
Original line number Diff line number Diff line
@@ -21,9 +21,12 @@ import android.content.pm.ApplicationInfo
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.platform.test.annotations.EnableFlags
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState
import com.android.systemui.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_UI
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
@@ -31,6 +34,8 @@ import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter
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.MediaDeviceData
import com.android.systemui.media.controls.shared.model.SuggestedMediaDeviceData
import com.android.systemui.media.controls.shared.model.SuggestionData
import com.android.systemui.media.controls.util.mediaInstanceId
import com.android.systemui.res.R
import com.android.systemui.statusbar.notificationLockscreenUserManager
@@ -159,7 +164,127 @@ class MediaControlViewModelTest : SysuiTestCase() {
            assertThat(nextButton.isVisibleWhenScrubbing).isEqualTo(false)
        }

    private fun initMediaData(artist: String, title: String): MediaData {
    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithNoSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData = initMediaData(artist = ARTIST, title = TITLE, suggestionData = null)

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isFalse()
            assertThat(suggestionModel.buttonText).isNull()
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithConnectingSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTING),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(
                    context.getString(R.string.media_suggestion_disconnected_text, DEVICE_NAME)
                )
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isTrue()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithDisconnectedSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_DISCONNECTED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(
                    context.getString(R.string.media_suggestion_disconnected_text, DEVICE_NAME)
                )
            assertThat(suggestionModel.onClicked).isNotNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithErrorSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTING_FAILED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isTrue()
            assertThat(suggestionModel.buttonText)
                .isEqualTo(context.getString(R.string.media_suggestion_failure_text))
            assertThat(suggestionModel.onClicked).isNotNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNotNull()
        }

    @EnableFlags(FLAG_ENABLE_SUGGESTED_DEVICE_UI)
    @Test
    fun onMediaDataLoadedWithConnectedSuggestionData() =
        testScope.runTest {
            val playerModel by collectLastValue(underTest.player)
            val mediaData =
                initMediaData(
                    artist = ARTIST,
                    title = TITLE,
                    suggestionData =
                        createSuggestionData(DEVICE_NAME, MediaDeviceState.STATE_CONNECTED),
                )

            mediaDataFilter.onMediaDataLoaded(KEY, KEY, mediaData)

            val suggestionModel = playerModel!!.deviceSuggestion
            assertThat(suggestionModel.isValidSuggestion).isFalse()
            assertThat(suggestionModel.buttonText).isNull()
            assertThat(suggestionModel.onClicked).isNull()
            assertThat(suggestionModel.isConnecting).isFalse()
            assertThat(suggestionModel.icon).isNull()
        }

    private fun initMediaData(
        artist: String,
        title: String,
        suggestionData: SuggestionData? = null,
    ): MediaData {
        val device = MediaDeviceData(true, null, DEVICE_NAME, null, showBroadcastButton = true)

        // Create media session
@@ -187,10 +312,23 @@ class MediaControlViewModelTest : SysuiTestCase() {
            packageName = PACKAGE,
            token = session.sessionToken,
            device = device,
            suggestionData = suggestionData,
            instanceId = instanceId,
        )
    }

    private fun createSuggestionData(deviceName: String, state: Int) =
        SuggestionData(
            suggestedMediaDeviceData =
                SuggestedMediaDeviceData(
                    name = deviceName,
                    icon = drawable!!,
                    connectionState = state,
                    connect = {},
                ),
            onSuggestionSpaceVisible = Runnable {},
        )

    companion object {
        private const val USER_ID = 0
        private const val KEY = "key"
+1 −0
Original line number Diff line number Diff line
@@ -98,6 +98,7 @@ constructor(
                showExplicit = isExplicit,
                artwork = artwork,
                deviceData = device,
                suggestionData = suggestionData,
                semanticActionButtons = semanticActions,
                notificationActionButtons = getNotificationActions(data.actions, activityStarter),
                actionsToShowInCollapsed = actionsToShowInCompact,
+1 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ data class MediaControlModel(
    val showExplicit: Boolean,
    val artwork: Icon?,
    val deviceData: MediaDeviceData?,
    val suggestionData: SuggestionData?,
    /** [MediaButton] contains [MediaAction] objects which represent specific buttons in the UI */
    val semanticActionButtons: MediaButton?,
    val notificationActionButtons: List<MediaAction>,
+33 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.settingslib.widget.AdaptiveIcon
import com.android.systemui.Flags.enableSuggestedDeviceUi
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Background
@@ -54,6 +55,7 @@ import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Co
import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_COMPACT
import com.android.systemui.media.controls.ui.viewmodel.MediaOutputSwitcherViewModel
import com.android.systemui.media.controls.ui.viewmodel.MediaPlayerViewModel
import com.android.systemui.media.controls.ui.viewmodel.MediaSuggestionViewModel
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
@@ -154,6 +156,7 @@ object MediaControlViewBinder {
        bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager)
        bindActionButtons(viewHolder, viewModel, viewController, falsingManager)
        bindScrubbingTime(viewHolder, viewModel, viewController)
        bindSuggestionModel(viewHolder, viewModel.deviceSuggestion)

        val isSongUpdated = bindSongMetadata(viewHolder, viewModel, viewController)

@@ -210,6 +213,36 @@ object MediaControlViewBinder {
        viewHolder.seamlessText.text = viewModel.deviceString
    }

    private fun bindSuggestionModel(
        viewHolder: MediaViewHolder,
        viewModel: MediaSuggestionViewModel,
    ) {
        if (!enableSuggestedDeviceUi()) {
            return
        }

        with(viewHolder) {
            if (!viewModel.isValidSuggestion) {
                deviceSuggestionButton.visibility = View.GONE
                seamlessText.visibility = View.VISIBLE
                return
            }
            seamlessText.visibility = View.GONE
            deviceSuggestionButton.visibility = View.VISIBLE
            deviceSuggestionButton.setClickable(viewModel.onClicked != null)
            deviceSuggestionButton.setOnClickListener { viewModel.onClicked?.invoke() }
            deviceSuggestionText.text = viewModel.buttonText
            if (viewModel.isConnecting) {
                deviceSuggestionConnectingIcon.visibility = View.VISIBLE
                deviceSuggestionIcon.visibility = View.GONE
            } else {
                deviceSuggestionIcon.setImageDrawable(viewModel.icon?.drawable)
                deviceSuggestionConnectingIcon.visibility = View.GONE
                deviceSuggestionIcon.visibility = View.VISIBLE
            }
        }
    }

    private fun bindGutsViewModel(
        viewHolder: MediaViewHolder,
        viewModel: MediaPlayerViewModel,
+60 −0
Original line number Diff line number Diff line
@@ -26,7 +26,10 @@ import android.util.Log
import androidx.constraintlayout.widget.ConstraintSet
import com.android.internal.logging.InstanceId
import com.android.settingslib.flags.Flags.legacyLeAudioSharing
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState
import com.android.systemui.Flags.enableSuggestedDeviceUi
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.asIcon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
@@ -134,6 +137,7 @@ data class MediaControlViewModel(
            useSemanticActions = model.semanticActionButtons != null,
            actionButtons = toActionViewModels(model),
            outputSwitcher = toOutputSwitcherViewModel(model),
            deviceSuggestion = toSuggestionViewModel(model),
            gutsMenu = gutsViewModel,
            onClicked = { expandable ->
                model.clickIntent?.let { clickIntent ->
@@ -241,6 +245,62 @@ data class MediaControlViewModel(
        )
    }

    private fun toSuggestionViewModel(model: MediaControlModel): MediaSuggestionViewModel {
        if (!enableSuggestedDeviceUi()) {
            return MediaSuggestionViewModel(isValidSuggestion = false)
        }
        val suggestionData =
            model.suggestionData ?: return MediaSuggestionViewModel(isValidSuggestion = false)
        suggestionData.onSuggestionSpaceVisible.run()
        val suggestedDeviceData =
            suggestionData.suggestedMediaDeviceData
                ?: return MediaSuggestionViewModel(isValidSuggestion = false)
        with(suggestedDeviceData) {
            // Don't show the device as suggested if we're already connected to it
            if (
                !(connectionState == MediaDeviceState.STATE_DISCONNECTED ||
                    connectionState == MediaDeviceState.STATE_CONNECTING ||
                    connectionState == MediaDeviceState.STATE_GROUPING ||
                    connectionState == MediaDeviceState.STATE_CONNECTING_FAILED)
            ) {
                return MediaSuggestionViewModel(isValidSuggestion = false)
            }
            val onClick =
                if (
                    connectionState == MediaDeviceState.STATE_DISCONNECTED ||
                        connectionState == MediaDeviceState.STATE_CONNECTING_FAILED
                )
                    ({ connect() })
                else null
            val buttonText =
                when (connectionState) {
                    MediaDeviceState.STATE_DISCONNECTED,
                    MediaDeviceState.STATE_CONNECTING,
                    MediaDeviceState.STATE_GROUPING ->
                        applicationContext.getString(
                            R.string.media_suggestion_disconnected_text,
                            name,
                        )
                    MediaDeviceState.STATE_CONNECTING_FAILED ->
                        applicationContext.getString(R.string.media_suggestion_failure_text)
                    else -> {
                        Log.wtf(TAG, "Invalid media device state for suggestion: $connectionState")
                        null
                    }
                }
            val isConnecting =
                connectionState == MediaDeviceState.STATE_CONNECTING ||
                    connectionState == MediaDeviceState.STATE_GROUPING
            return MediaSuggestionViewModel(
                isValidSuggestion = true,
                onClicked = onClick,
                buttonText = buttonText,
                isConnecting = isConnecting,
                icon = icon?.asIcon(),
            )
        }
    }

    private fun toGutsViewModel(model: MediaControlModel): GutsViewModel {
        return GutsViewModel(
            gutsText =
Loading