Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +10 −10 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 ) Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) ) } Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +23 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. } Loading @@ -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 ) Loading Loading @@ -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 Loading @@ -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?, Loading packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt +58 −47 Original line number Diff line number Diff line Loading @@ -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 } } } packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +3 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +90 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt +10 −10 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 ) Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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) ) } Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +23 −19 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. } Loading @@ -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 ) Loading Loading @@ -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 Loading @@ -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?, Loading
packages/SystemUI/src/com/android/systemui/media/controls/shared/MediaControlDrawables.kt +58 −47 Original line number Diff line number Diff line Loading @@ -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 } } }
packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/MediaControlViewModel.kt +3 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading
packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt +90 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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) Loading Loading @@ -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