Loading packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +27 −8 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.media import android.content.Context import android.media.MediaRouter2Manager import android.media.session.MediaController import androidx.annotation.AnyThread Loading @@ -24,20 +23,22 @@ import androidx.annotation.MainThread import androidx.annotation.WorkerThread import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager import java.io.FileDescriptor import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject private const val PLAYBACK_TYPE_UNKNOWN = 0 /** * Provides information about the route (ie. device) where playback is occurring. */ class MediaDeviceManager @Inject constructor( private val context: Context, private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, @Main private val fgExecutor: Executor, Loading Loading @@ -71,7 +72,7 @@ class MediaDeviceManager @Inject constructor( if (entry == null || entry?.token != data.token) { entry?.stop() val controller = data.token?.let { MediaController(context, it) controllerFactory.create(it) } entry = Entry(key, oldKey, controller, localMediaManagerFactory.create(data.packageName)) Loading Loading @@ -122,11 +123,12 @@ class MediaDeviceManager @Inject constructor( val oldKey: String?, val controller: MediaController?, val localMediaManager: LocalMediaManager ) : LocalMediaManager.DeviceCallback { ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { val token get() = controller?.sessionToken private var started = false private var playbackType = PLAYBACK_TYPE_UNKNOWN private var current: MediaDevice? = null set(value) { if (!started || value != field) { Loading @@ -141,6 +143,8 @@ class MediaDeviceManager @Inject constructor( fun start() = bgExecutor.execute { localMediaManager.registerCallback(this) localMediaManager.startScan() playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN controller?.registerCallback(this) updateCurrent() started = true } Loading @@ -148,20 +152,35 @@ class MediaDeviceManager @Inject constructor( @AnyThread fun stop() = bgExecutor.execute { started = false controller?.unregisterCallback(this) localMediaManager.stopScan() localMediaManager.unregisterCallback(this) } fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { val route = controller?.let { val routingSession = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType println(" PlaybackType=$type (1 for local, 2 for remote)") println(" route=$route") println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") println(" routingSession=$routingSession") println(" selectedRoutes=$selectedRoutes") } } @WorkerThread override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN if (newPlaybackType == playbackType) { return } playbackType = newPlaybackType updateCurrent() } override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { Loading packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +46 −28 Original line number Diff line number Diff line Loading @@ -16,13 +16,12 @@ package com.android.systemui.media import android.app.Notification import android.graphics.drawable.Drawable import android.media.MediaMetadata import android.media.MediaRouter2Manager import android.media.RoutingSessionInfo import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest Loading Loading @@ -55,7 +54,6 @@ private const val KEY = "TEST_KEY" private const val KEY_OLD = "TEST_KEY_OLD" private const val PACKAGE = "PKG" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" private const val DEVICE_NAME = "DEVICE_NAME" private const val USER_ID = 0 Loading @@ -68,6 +66,7 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var manager: MediaDeviceManager @Mock private lateinit var controllerFactory: MediaControllerFactory @Mock private lateinit var lmmFactory: LocalMediaManagerFactory @Mock private lateinit var lmm: LocalMediaManager @Mock private lateinit var mr2: MediaRouter2Manager Loading @@ -78,10 +77,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var device: MediaDevice @Mock private lateinit var icon: Drawable @Mock private lateinit var route: RoutingSessionInfo @Mock private lateinit var controller: MediaController @Mock private lateinit var playbackInfo: PlaybackInfo private lateinit var session: MediaSession private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var playbackBuilder: PlaybackState.Builder private lateinit var notifBuilder: Notification.Builder private lateinit var mediaData: MediaData @JvmField @Rule val mockito = MockitoJUnit.rule() Loading @@ -89,8 +87,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setUp() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock()) manager = MediaDeviceManager(context, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor, dumpster) manager = MediaDeviceManager(controllerFactory, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor, dumpster) manager.addListener(listener) // Configure mocks. Loading @@ -101,28 +99,13 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route) // Create a media sesssion and notification for testing. metadataBuilder = MediaMetadata.Builder().apply { putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } playbackBuilder = PlaybackState.Builder().apply { setState(PlaybackState.STATE_PAUSED, 6000L, 1f) setActions(PlaybackState.ACTION_PLAY) } session = MediaSession(context, SESSION_KEY).apply { setMetadata(metadataBuilder.build()) setPlaybackState(playbackBuilder.build()) } session.setActive(true) notifBuilder = Notification.Builder(context, "NONE").apply { setContentTitle(SESSION_TITLE) setContentText(SESSION_ARTIST) setSmallIcon(android.R.drawable.ic_media_pause) setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken())) } session = MediaSession(context, SESSION_KEY) mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) whenever(controllerFactory.create(session.sessionToken)) .thenReturn(controller) } @After Loading Loading @@ -336,6 +319,41 @@ public class MediaDeviceManagerTest : SysuiTestCase() { assertThat(data.icon).isNull() } @Test fun audioInfoChanged() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with local playback type manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java) verify(controller).registerCallback(captor.capture()) captor.value.onAudioInfoChanged(playbackInfo) // THEN the route is checked verify(mr2).getRoutingSessionForMediaController(eq(controller)) } @Test fun audioInfoHasntChanged() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with remote playback type manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java) verify(controller).registerCallback(captor.capture()) captor.value.onAudioInfoChanged(playbackInfo) // THEN the route is not checked verify(mr2, never()).getRoutingSessionForMediaController(eq(controller)) } fun captureCallback(): LocalMediaManager.DeviceCallback { val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java) verify(lmm).registerCallback(captor.capture()) Loading Loading
packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +27 −8 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.media import android.content.Context import android.media.MediaRouter2Manager import android.media.session.MediaController import androidx.annotation.AnyThread Loading @@ -24,20 +23,22 @@ import androidx.annotation.MainThread import androidx.annotation.WorkerThread import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.Dumpable import com.android.systemui.dump.DumpManager import java.io.FileDescriptor import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject private const val PLAYBACK_TYPE_UNKNOWN = 0 /** * Provides information about the route (ie. device) where playback is occurring. */ class MediaDeviceManager @Inject constructor( private val context: Context, private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, @Main private val fgExecutor: Executor, Loading Loading @@ -71,7 +72,7 @@ class MediaDeviceManager @Inject constructor( if (entry == null || entry?.token != data.token) { entry?.stop() val controller = data.token?.let { MediaController(context, it) controllerFactory.create(it) } entry = Entry(key, oldKey, controller, localMediaManagerFactory.create(data.packageName)) Loading Loading @@ -122,11 +123,12 @@ class MediaDeviceManager @Inject constructor( val oldKey: String?, val controller: MediaController?, val localMediaManager: LocalMediaManager ) : LocalMediaManager.DeviceCallback { ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { val token get() = controller?.sessionToken private var started = false private var playbackType = PLAYBACK_TYPE_UNKNOWN private var current: MediaDevice? = null set(value) { if (!started || value != field) { Loading @@ -141,6 +143,8 @@ class MediaDeviceManager @Inject constructor( fun start() = bgExecutor.execute { localMediaManager.registerCallback(this) localMediaManager.startScan() playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN controller?.registerCallback(this) updateCurrent() started = true } Loading @@ -148,20 +152,35 @@ class MediaDeviceManager @Inject constructor( @AnyThread fun stop() = bgExecutor.execute { started = false controller?.unregisterCallback(this) localMediaManager.stopScan() localMediaManager.unregisterCallback(this) } fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { val route = controller?.let { val routingSession = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType println(" PlaybackType=$type (1 for local, 2 for remote)") println(" route=$route") println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") println(" routingSession=$routingSession") println(" selectedRoutes=$selectedRoutes") } } @WorkerThread override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN if (newPlaybackType == playbackType) { return } playbackType = newPlaybackType updateCurrent() } override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { Loading
packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +46 −28 Original line number Diff line number Diff line Loading @@ -16,13 +16,12 @@ package com.android.systemui.media import android.app.Notification import android.graphics.drawable.Drawable import android.media.MediaMetadata import android.media.MediaRouter2Manager import android.media.RoutingSessionInfo import android.media.session.MediaController import android.media.session.MediaController.PlaybackInfo import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest Loading Loading @@ -55,7 +54,6 @@ private const val KEY = "TEST_KEY" private const val KEY_OLD = "TEST_KEY_OLD" private const val PACKAGE = "PKG" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" private const val DEVICE_NAME = "DEVICE_NAME" private const val USER_ID = 0 Loading @@ -68,6 +66,7 @@ private fun <T> eq(value: T): T = Mockito.eq(value) ?: value public class MediaDeviceManagerTest : SysuiTestCase() { private lateinit var manager: MediaDeviceManager @Mock private lateinit var controllerFactory: MediaControllerFactory @Mock private lateinit var lmmFactory: LocalMediaManagerFactory @Mock private lateinit var lmm: LocalMediaManager @Mock private lateinit var mr2: MediaRouter2Manager Loading @@ -78,10 +77,9 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var device: MediaDevice @Mock private lateinit var icon: Drawable @Mock private lateinit var route: RoutingSessionInfo @Mock private lateinit var controller: MediaController @Mock private lateinit var playbackInfo: PlaybackInfo private lateinit var session: MediaSession private lateinit var metadataBuilder: MediaMetadata.Builder private lateinit var playbackBuilder: PlaybackState.Builder private lateinit var notifBuilder: Notification.Builder private lateinit var mediaData: MediaData @JvmField @Rule val mockito = MockitoJUnit.rule() Loading @@ -89,8 +87,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() { fun setUp() { fakeFgExecutor = FakeExecutor(FakeSystemClock()) fakeBgExecutor = FakeExecutor(FakeSystemClock()) manager = MediaDeviceManager(context, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor, dumpster) manager = MediaDeviceManager(controllerFactory, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor, dumpster) manager.addListener(listener) // Configure mocks. Loading @@ -101,28 +99,13 @@ public class MediaDeviceManagerTest : SysuiTestCase() { whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(route) // Create a media sesssion and notification for testing. metadataBuilder = MediaMetadata.Builder().apply { putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) } playbackBuilder = PlaybackState.Builder().apply { setState(PlaybackState.STATE_PAUSED, 6000L, 1f) setActions(PlaybackState.ACTION_PLAY) } session = MediaSession(context, SESSION_KEY).apply { setMetadata(metadataBuilder.build()) setPlaybackState(playbackBuilder.build()) } session.setActive(true) notifBuilder = Notification.Builder(context, "NONE").apply { setContentTitle(SESSION_TITLE) setContentText(SESSION_ARTIST) setSmallIcon(android.R.drawable.ic_media_pause) setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken())) } session = MediaSession(context, SESSION_KEY) mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) whenever(controllerFactory.create(session.sessionToken)) .thenReturn(controller) } @After Loading Loading @@ -336,6 +319,41 @@ public class MediaDeviceManagerTest : SysuiTestCase() { assertThat(data.icon).isNull() } @Test fun audioInfoChanged() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with local playback type manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java) verify(controller).registerCallback(captor.capture()) captor.value.onAudioInfoChanged(playbackInfo) // THEN the route is checked verify(mr2).getRoutingSessionForMediaController(eq(controller)) } @Test fun audioInfoHasntChanged() { whenever(playbackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE) whenever(controller.getPlaybackInfo()).thenReturn(playbackInfo) // GIVEN a controller with remote playback type manager.onMediaDataLoaded(KEY, null, mediaData) fakeBgExecutor.runAllReady() fakeFgExecutor.runAllReady() reset(mr2) // WHEN onAudioInfoChanged fires with remote playback type val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java) verify(controller).registerCallback(captor.capture()) captor.value.onAudioInfoChanged(playbackInfo) // THEN the route is not checked verify(mr2, never()).getRoutingSessionForMediaController(eq(controller)) } fun captureCallback(): LocalMediaManager.DeviceCallback { val captor = ArgumentCaptor.forClass(LocalMediaManager.DeviceCallback::class.java) verify(lmm).registerCallback(captor.capture()) Loading