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

Commit 25cd7b6b authored by Michael Mikhail's avatar Michael Mikhail
Browse files

Reuse created media drawables

For play/pause button drawables, we use mutate() in order not to share
the same state of the drawable with other UMOs.

Flag: com.android.systemui.media_controls_drawables_reuse
Bug: 358402034
Test: atest MediaDataProcessorTest
Test: atest MediaDeviceManagerTest
Change-Id: Ibdbc96330b9d06604e43d7ad9311c0293060bebb
parent 89e1efe0
Loading
Loading
Loading
Loading
+10 −10
Original line number Diff line number Diff line
@@ -73,6 +73,7 @@ import com.android.systemui.media.controls.data.repository.MediaDataRepository
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser
import com.android.systemui.media.controls.shared.MediaControlDrawables
import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE
import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC
import com.android.systemui.media.controls.shared.model.MediaAction
@@ -1043,14 +1044,13 @@ class MediaDataProcessor(
        val playOrPause =
            if (isConnectingState(state.state)) {
                // Spinner needs to be animating to render anything. Start it here.
                val drawable =
                    context.getDrawable(com.android.internal.R.drawable.progress_small_material)
                val drawable = MediaControlDrawables.getProgress(context)
                (drawable as Animatable).start()
                MediaAction(
                    drawable,
                    null, // no action to perform when clicked
                    context.getString(R.string.controls_media_button_connecting),
                    context.getDrawable(R.drawable.ic_media_connecting_container),
                    MediaControlDrawables.getConnecting(context),
                    // Specify a rebind id to prevent the spinner from restarting on later binds.
                    com.android.internal.R.drawable.progress_small_material
                )
@@ -1143,23 +1143,23 @@ class MediaDataProcessor(
        return when (action) {
            PlaybackState.ACTION_PLAY -> {
                MediaAction(
                    context.getDrawable(R.drawable.ic_media_play),
                    MediaControlDrawables.getPlayIcon(context),
                    { controller.transportControls.play() },
                    context.getString(R.string.controls_media_button_play),
                    context.getDrawable(R.drawable.ic_media_play_container)
                    MediaControlDrawables.getPlayBackground(context)
                )
            }
            PlaybackState.ACTION_PAUSE -> {
                MediaAction(
                    context.getDrawable(R.drawable.ic_media_pause),
                    MediaControlDrawables.getPauseIcon(context),
                    { controller.transportControls.pause() },
                    context.getString(R.string.controls_media_button_pause),
                    context.getDrawable(R.drawable.ic_media_pause_container)
                    MediaControlDrawables.getPauseBackground(context)
                )
            }
            PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
                MediaAction(
                    context.getDrawable(R.drawable.ic_media_prev),
                    MediaControlDrawables.getPrevIcon(context),
                    { controller.transportControls.skipToPrevious() },
                    context.getString(R.string.controls_media_button_prev),
                    null
@@ -1167,7 +1167,7 @@ class MediaDataProcessor(
            }
            PlaybackState.ACTION_SKIP_TO_NEXT -> {
                MediaAction(
                    context.getDrawable(R.drawable.ic_media_next),
                    MediaControlDrawables.getNextIcon(context),
                    { controller.transportControls.skipToNext() },
                    context.getString(R.string.controls_media_button_next),
                    null
@@ -1308,7 +1308,7 @@ class MediaDataProcessor(
                .loadDrawable(context),
            action,
            context.getString(R.string.controls_media_resume),
            context.getDrawable(R.drawable.ic_media_play_container)
            MediaControlDrawables.getPlayBackground(context)
        )
    }

+23 −19
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import com.android.settingslib.media.PhoneMediaDevice
import com.android.settingslib.media.flags.Flags
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.MediaControlDrawables
import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.util.LocalMediaManagerFactory
@@ -142,6 +143,7 @@ constructor(
    interface Listener {
        /** Called when the route has changed for a given notification. */
        fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)

        /** Called when the notification was removed. */
        fun onKeyRemoved(key: String, userInitiated: Boolean)
    }
@@ -159,6 +161,7 @@ constructor(

        val token
            get() = controller?.sessionToken

        private var started = false
        private var playbackType = PLAYBACK_TYPE_UNKNOWN
        private var playbackVolumeControlId: String? = null
@@ -170,6 +173,7 @@ constructor(
                    fgExecutor.execute { processDevice(key, oldKey, value) }
                }
            }

        // A device that is not yet connected but is expected to connect imminently. Because it's
        // expected to connect imminently, it should be displayed as the current device.
        private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
@@ -354,9 +358,9 @@ constructor(

                    activeDevice =
                        routingSession?.let {
                            val icon = if (it.selectedRoutes.size > 1) {
                                context.getDrawable(
                                        com.android.settingslib.R.drawable.ic_media_group_device)
                            val icon =
                                if (it.selectedRoutes.size > 1) {
                                    MediaControlDrawables.getGroupDevice(context)
                                } else {
                                    connectedDevice?.icon // Single route. We don't change the icon.
                                }
@@ -368,10 +372,12 @@ constructor(
                            //           route.
                            connectedDevice?.copy(
                                name = it.name ?: connectedDevice.name,
                                    icon = icon)
                        } ?: MediaDeviceData(
                                icon = icon
                            )
                        }
                            ?: MediaDeviceData(
                                enabled = false,
                            icon = context.getDrawable(R.drawable.ic_media_home_devices),
                                icon = MediaControlDrawables.getHomeDevices(context),
                                name = context.getString(R.string.media_seamless_other_device),
                                showBroadcastButton = false
                            )
@@ -434,10 +440,7 @@ constructor(
            return if (enableLeAudioSharing()) {
                MediaDeviceData(
                    enabled = false,
                    icon =
                        context.getDrawable(
                            com.android.settingslib.R.drawable.ic_bt_le_audio_sharing
                        ),
                    icon = MediaControlDrawables.getLeAudioSharing(context),
                    name = context.getString(R.string.audio_sharing_description),
                    intent = null,
                    showBroadcastButton = false
@@ -445,13 +448,14 @@ constructor(
            } else {
                MediaDeviceData(
                    enabled = true,
                    icon = context.getDrawable(R.drawable.settings_input_antenna),
                    icon = MediaControlDrawables.getAntenna(context),
                    name = broadcastDescription,
                    intent = null,
                    showBroadcastButton = true
                )
            }
        }

        /** Return a display name for the current device / route, or null if not possible */
        private fun getDeviceName(
            device: MediaDevice?,
+58 −47
Original line number Diff line number Diff line
@@ -45,128 +45,139 @@ object MediaControlDrawables {
    private var solid: Drawable? = null

    fun getProgress(context: Context): Drawable? {
        return progress
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(com.android.internal.R.drawable.progress_small_material)
        }
        return progress?.mutate()
            ?: context.getDrawable(com.android.internal.R.drawable.progress_small_material).also {
                if (!mediaControlsDrawablesReuse()) return@also
                progress = it
            }
    }

    fun getConnecting(context: Context): Drawable? {
        return connecting
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_connecting_container)
        }
        return connecting?.mutate()
            ?: context.getDrawable(R.drawable.ic_media_connecting_container).also {
                if (!mediaControlsDrawablesReuse()) return@also
                connecting = it
            }
    }

    fun getPlayIcon(context: Context): AnimatedVectorDrawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable?
        }
        return playIcon?.let {
            it.reset()
            it
            it.mutate() as AnimatedVectorDrawable
        }
            ?: (context.getDrawable(R.drawable.ic_media_play) as AnimatedVectorDrawable?).also {
                if (!mediaControlsDrawablesReuse()) return@also
                playIcon = it
            }
    }

    fun getPlayBackground(context: Context): AnimatedVectorDrawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_play_container)
                as AnimatedVectorDrawable?
        }
        return playBackground?.let {
            it.reset()
            it
            it.mutate() as AnimatedVectorDrawable
        }
            ?: (context.getDrawable(R.drawable.ic_media_play_container) as AnimatedVectorDrawable?)
                .also {
                    if (!mediaControlsDrawablesReuse()) return@also
                    playBackground = it
                }
                .also { playBackground = it }
    }

    fun getPauseIcon(context: Context): AnimatedVectorDrawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable?
        }
        return pauseIcon?.let {
            it.reset()
            it
            it.mutate() as AnimatedVectorDrawable
        }
            ?: (context.getDrawable(R.drawable.ic_media_pause) as AnimatedVectorDrawable?).also {
                if (!mediaControlsDrawablesReuse()) return@also
                pauseIcon = it
            }
    }

    fun getPauseBackground(context: Context): AnimatedVectorDrawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_pause_container)
                as AnimatedVectorDrawable?
        }
        return pauseBackground?.let {
            it.reset()
            it
            it.mutate() as AnimatedVectorDrawable
        }
            ?: (context.getDrawable(R.drawable.ic_media_pause_container) as AnimatedVectorDrawable?)
                .also {
                    if (!mediaControlsDrawablesReuse()) return@also
                    pauseBackground = it
                }
                .also { pauseBackground = it }
    }

    fun getNextIcon(context: Context): Drawable? {
        return nextIcon
            ?: context.getDrawable(R.drawable.ic_media_next).also {
                if (!mediaControlsDrawablesReuse()) return@also
                nextIcon = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_next)
        }
        return nextIcon ?: context.getDrawable(R.drawable.ic_media_next).also { nextIcon = it }
    }

    fun getPrevIcon(context: Context): Drawable? {
        return prevIcon
            ?: context.getDrawable(R.drawable.ic_media_prev).also {
                if (!mediaControlsDrawablesReuse()) return@also
                prevIcon = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_prev)
        }
        return prevIcon ?: context.getDrawable(R.drawable.ic_media_prev).also { prevIcon = it }
    }

    fun getLeAudioSharing(context: Context): Drawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
        }
        return leAudioSharing
            ?: context.getDrawable(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing).also {
                if (!mediaControlsDrawablesReuse()) return@also
                leAudioSharing = it
            }
    }

    fun getAntenna(context: Context): Drawable? {
        return antenna
            ?: context.getDrawable(R.drawable.settings_input_antenna).also {
                if (!mediaControlsDrawablesReuse()) return@also
                antenna = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.settings_input_antenna)
        }
        return antenna
            ?: context.getDrawable(R.drawable.settings_input_antenna).also { antenna = it }
    }

    fun getGroupDevice(context: Context): Drawable? {
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(com.android.settingslib.R.drawable.ic_media_group_device)
        }
        return groupDevice
            ?: context.getDrawable(com.android.settingslib.R.drawable.ic_media_group_device).also {
                if (!mediaControlsDrawablesReuse()) return@also
                groupDevice = it
            }
    }

    fun getHomeDevices(context: Context): Drawable? {
        return homeDevices
            ?: context.getDrawable(R.drawable.ic_media_home_devices).also {
                if (!mediaControlsDrawablesReuse()) return@also
                homeDevices = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.ic_media_home_devices)
        }
        return homeDevices
            ?: context.getDrawable(R.drawable.ic_media_home_devices).also { homeDevices = it }
    }

    fun getOutline(context: Context): Drawable? {
        return outline
            ?: context.getDrawable(R.drawable.qs_media_outline_button).also {
                if (!mediaControlsDrawablesReuse()) return@also
                outline = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.qs_media_outline_button)
        }
        return outline
            ?: context.getDrawable(R.drawable.qs_media_outline_button).also { outline = it }
    }

    fun getSolid(context: Context): Drawable? {
        return solid
            ?: context.getDrawable(R.drawable.qs_media_solid_button).also {
                if (!mediaControlsDrawablesReuse()) return@also
                solid = it
        if (!mediaControlsDrawablesReuse()) {
            return context.getDrawable(R.drawable.qs_media_solid_button)
        }
        return solid ?: context.getDrawable(R.drawable.qs_media_solid_button).also { solid = it }
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.domain.pipeline.interactor.MediaControlInteractor
import com.android.systemui.media.controls.shared.MediaControlDrawables
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.MediaControlModel
@@ -284,9 +285,9 @@ class MediaControlViewModel(
            },
            cancelTextBackground =
                if (model.isDismissible) {
                    applicationContext.getDrawable(R.drawable.qs_media_outline_button)
                    MediaControlDrawables.getOutline(applicationContext)
                } else {
                    applicationContext.getDrawable(R.drawable.qs_media_solid_button)
                    MediaControlDrawables.getSolid(applicationContext)
                },
            onSettingsClicked = {
                logger.logLongPressSettings(model.uid, model.packageName, model.instanceId)
+90 −1
Original line number Diff line number Diff line
@@ -38,6 +38,9 @@ import android.media.session.MediaSession
import android.media.session.PlaybackState
import android.net.Uri
import android.os.Bundle
import android.os.UserHandle
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.provider.Settings
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper
@@ -48,10 +51,13 @@ import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.logging.InstanceId
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.controls.data.repository.MediaDataRepository
import com.android.systemui.media.controls.data.repository.MediaFilterRepository
import com.android.systemui.media.controls.data.repository.mediaFilterRepository
@@ -69,6 +75,7 @@ import com.android.systemui.media.controls.util.MediaUiEventLogger
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.SbnBuilder
import com.android.systemui.statusbar.notificationLockscreenUserManager
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.settings.FakeSettings
@@ -79,6 +86,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -146,7 +154,6 @@ class MediaDataProcessorTest : SysuiTestCase() {
    @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
    @Mock lateinit var mediaDeviceManager: MediaDeviceManager
    @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
    @Mock lateinit var mediaDataFilter: MediaDataFilterImpl
    @Mock lateinit var listener: MediaDataManager.Listener
    @Mock lateinit var pendingIntent: PendingIntent
    @Mock lateinit var activityStarter: ActivityStarter
@@ -185,7 +192,9 @@ class MediaDataProcessorTest : SysuiTestCase() {
            Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION,
            1
        )
    private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
    private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository
    private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter

    private lateinit var staticMockSession: MockitoSession

@@ -258,6 +267,7 @@ class MediaDataProcessorTest : SysuiTestCase() {
        session = MediaSession(context, "MediaDataProcessorTestSession")
        mediaNotification =
            SbnBuilder().run {
                setUser(UserHandle(USER_ID))
                setPkg(PACKAGE_NAME)
                modifyNotification(context).also {
                    it.setSmallIcon(android.R.drawable.ic_media_pause)
@@ -1797,6 +1807,85 @@ class MediaDataProcessorTest : SysuiTestCase() {
            .isEqualTo(context.getString(R.string.controls_media_button_connecting))
    }

    @Test
    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE)
    fun postWithPlaybackActions_drawablesReused() =
        kosmos.testScope.runTest {
            whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            val stateActions =
                PlaybackState.ACTION_PAUSE or
                    PlaybackState.ACTION_SKIP_TO_PREVIOUS or
                    PlaybackState.ACTION_SKIP_TO_NEXT
            val stateBuilder =
                PlaybackState.Builder()
                    .setState(PlaybackState.STATE_PLAYING, 0, 10f)
                    .setActions(stateActions)
            whenever(controller.playbackState).thenReturn(stateBuilder.build())
            val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)

            mediaDataProcessor.addInternalListener(mediaDataFilter)
            mediaDataFilter.mediaDataProcessor = mediaDataProcessor
            addNotificationAndLoad()

            assertThat(userEntries).hasSize(1)
            val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!!

            addNotificationAndLoad()

            assertThat(userEntries).hasSize(1)
            val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!!
            assertThat(secondSemanticActions.playOrPause?.icon)
                .isEqualTo(firstSemanticActions.playOrPause?.icon)
            assertThat(secondSemanticActions.playOrPause?.background)
                .isEqualTo(firstSemanticActions.playOrPause?.background)
            assertThat(secondSemanticActions.nextOrCustom?.icon)
                .isEqualTo(firstSemanticActions.nextOrCustom?.icon)
            assertThat(secondSemanticActions.prevOrCustom?.icon)
                .isEqualTo(firstSemanticActions.prevOrCustom?.icon)
        }

    @Test
    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_DRAWABLES_REUSE)
    fun postWithPlaybackActions_drawablesNotReused() =
        kosmos.testScope.runTest {
            whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
            whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
            whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
            val stateActions =
                PlaybackState.ACTION_PAUSE or
                    PlaybackState.ACTION_SKIP_TO_PREVIOUS or
                    PlaybackState.ACTION_SKIP_TO_NEXT
            val stateBuilder =
                PlaybackState.Builder()
                    .setState(PlaybackState.STATE_PLAYING, 0, 10f)
                    .setActions(stateActions)
            whenever(controller.playbackState).thenReturn(stateBuilder.build())
            val userEntries by collectLastValue(mediaFilterRepository.selectedUserEntries)

            mediaDataProcessor.addInternalListener(mediaDataFilter)
            mediaDataFilter.mediaDataProcessor = mediaDataProcessor
            addNotificationAndLoad()

            assertThat(userEntries).hasSize(1)
            val firstSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!!

            addNotificationAndLoad()

            assertThat(userEntries).hasSize(1)
            val secondSemanticActions = userEntries?.values?.toList()?.get(0)?.semanticActions!!

            assertThat(secondSemanticActions.playOrPause?.icon)
                .isNotEqualTo(firstSemanticActions.playOrPause?.icon)
            assertThat(secondSemanticActions.playOrPause?.background)
                .isNotEqualTo(firstSemanticActions.playOrPause?.background)
            assertThat(secondSemanticActions.nextOrCustom?.icon)
                .isNotEqualTo(firstSemanticActions.nextOrCustom?.icon)
            assertThat(secondSemanticActions.prevOrCustom?.icon)
                .isNotEqualTo(firstSemanticActions.prevOrCustom?.icon)
        }

    @Test
    fun testPlaybackActions_reservedSpace() {
        val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
Loading