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

Commit fa9d6da4 authored by Derek Jedral's avatar Derek Jedral Committed by Android (Google) Code Review
Browse files

Merge "Propagate device suggestions with new pipeline" into main

parents db070da9 b54f9e5e
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