Loading packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -165,6 +165,23 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void emitEventAfterDeviceAndSuggestionChangedTogether() { // GIVEN that media event has already been received mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData); mManager.onSuggestionDataChanged(KEY, OLD_KEY, mSuggestionData); reset(mListener); // WHEN suggestion event is received mManager.onMediaDeviceAndSuggestionDataChanged(KEY, null, mDeviceData, mSuggestionData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyMediaFirst() { // GIVEN that media and device info has already been received Loading Loading @@ -210,6 +227,24 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyDeviceAndSuggestionFirst() { // GIVEN that media and device info has already been received mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); mManager.onSuggestionDataChanged(OLD_KEY, null, mSuggestionData); reset(mListener); // WHEN a key migration event is received mManager.onMediaDeviceAndSuggestionDataChanged(KEY, OLD_KEY, mDeviceData, mSuggestionData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded( eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyMediaAfter() { // GIVEN that media and device info has already been received Loading Loading @@ -257,6 +292,25 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyDeviceAndSuggestionAfter() { // GIVEN that media and device info has already been received mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); mManager.onSuggestionDataChanged(OLD_KEY, null, mSuggestionData); mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */); reset(mListener); // WHEN a second key migration event is received for the device and suggestion mManager.onMediaDeviceAndSuggestionDataChanged(KEY, OLD_KEY, mDeviceData, mSuggestionData); // THEN the key has already be migrated ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void mediaDataRemoved() { // WHEN media data is removed without first receiving device or data Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt +19 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,25 @@ class MediaDataCombineLatest @Inject constructor() : } } override fun onMediaDeviceAndSuggestionDataChanged( key: String, oldKey: String?, device: MediaDeviceData?, suggestion: SuggestionData?, ) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) { val previousEntry = entries.remove(oldKey) val mediaData = previousEntry?.first entries[key] = Triple(mediaData, device, suggestion) update(key, oldKey) } else { val previousEntry = entries[key] val mediaData = previousEntry?.first entries[key] = Triple(mediaData, device, suggestion) update(key, key) } } override fun onKeyRemoved(key: String, userInitiated: Boolean) { remove(key, userInitiated) } Loading packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +138 −84 Original line number Diff line number Diff line Loading @@ -155,8 +155,20 @@ constructor( } @MainThread private fun processSuggestionData(key: String, oldKey: String?, device: SuggestionData?) { listeners.forEach { it.onSuggestionDataChanged(key, oldKey, device) } private fun processSuggestionData(key: String, oldKey: String?, suggestion: SuggestionData?) { listeners.forEach { it.onSuggestionDataChanged(key, oldKey, suggestion) } } @MainThread private fun processMediaDeviceAndSuggestionData( key: String, oldKey: String?, device: MediaDeviceData?, suggestion: SuggestionData?, ) { listeners.forEach { it.onMediaDeviceAndSuggestionDataChanged(key, oldKey, device, suggestion) } } interface Listener { Loading @@ -168,6 +180,17 @@ constructor( /** Called when the suggested route has changed for a given notification. */ fun onSuggestionDataChanged(key: String, oldKey: String?, data: SuggestionData?) /** * Called when the both the route and the suggested route has changed for a given * notification. */ fun onMediaDeviceAndSuggestionDataChanged( key: String, oldKey: String?, deviceData: MediaDeviceData?, suggestionData: SuggestionData?, ) } private inner class Entry( Loading @@ -191,23 +214,8 @@ constructor( bgExecutor.execute { localMediaManager.requestDeviceSuggestion() } } private var current: MediaDeviceData? = null set(value) { val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) if (!started || !sameWithoutIcon) { field = value fgExecutor.execute { processDevice(key, oldKey, value) } } } private var suggestionData = SuggestionData(onSuggestionSpaceVisible = requestSuggestionRunnable) set(value) { val sameWithoutConnect = value.equalsWithoutConnect(field) if (!sameWithoutConnect) { field = value fgExecutor.execute { processSuggestionData(key, oldKey, value) } } } private var suggestionData: SuggestionData? = null // 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. Loading @@ -226,9 +234,6 @@ constructor( if (!started) { // Fetch in case a suggestion already exists before registering for suggestions localMediaManager.registerCallback(this) if (enableSuggestedDeviceUi()) { onSuggestedDeviceUpdated(localMediaManager.getSuggestedDevice()) } if (!Flags.removeUnnecessaryRouteScanning()) { localMediaManager.startScan() } Loading @@ -236,7 +241,23 @@ constructor( playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN playbackVolumeControlId = controller?.playbackInfo?.volumeControlId controller?.registerCallback(this) if (enableSuggestedDeviceUi()) { updateCurrent(notifyListeners = false) updateSuggestion( localMediaManager.getSuggestedDevice(), notifyListeners = false, ) fgExecutor.execute { processMediaDeviceAndSuggestionData( key, oldKey, current, suggestionData, ) } } else { updateCurrent() } started = true configurationController.addCallback(configListener) } Loading Loading @@ -299,21 +320,7 @@ constructor( if (!enableSuggestedDeviceUi()) { return } bgExecutor.execute { suggestionData = SuggestionData( suggestedMediaDeviceData = state?.let { SuggestedMediaDeviceData( name = it.suggestedDeviceInfo.getDeviceDisplayName(), icon = it.getIcon(context), connectionState = it.connectionState, connect = { localMediaManager.connectSuggestedDevice(it) }, ) }, onSuggestionSpaceVisible = requestSuggestionRunnable, ) } bgExecutor.execute { updateSuggestion(state) } } override fun onAboutToConnectDeviceAdded( Loading Loading @@ -380,18 +387,53 @@ constructor( override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} @WorkerThread private fun updateCurrent() { private fun updateSuggestion( state: SuggestedDeviceState?, notifyListeners: Boolean = true, ) { val oldSuggestion = suggestionData val newSuggestion = SuggestionData( suggestedMediaDeviceData = state?.let { SuggestedMediaDeviceData( name = it.suggestedDeviceInfo.getDeviceDisplayName(), icon = it.getIcon(context), connectionState = it.connectionState, connect = { localMediaManager.connectSuggestedDevice(it) }, ) }, onSuggestionSpaceVisible = requestSuggestionRunnable, ) val updated = !newSuggestion.equalsWithoutConnect(oldSuggestion) if (updated) { suggestionData = newSuggestion if (notifyListeners) { fgExecutor.execute { processSuggestionData(key, oldKey, newSuggestion) } } } } @WorkerThread private fun updateCurrent(notifyListeners: Boolean = true) { val oldCurrent = current val newCurrent = if (isLeAudioBroadcastEnabled()) { current = getLeAudioBroadcastDeviceData() getLeAudioBroadcastDeviceData() } else { val activeDevice: MediaDeviceData? // LocalMediaManager provides the connected device based on PlaybackInfo. // TODO (b/342197065): Simplify nullability once we make currentConnectedDevice // TODO (b/342197065): Simplify nullability once we make // currentConnectedDevice // non-null. val connectedDevice = localMediaManager.currentConnectedDevice?.toMediaDeviceData() val connectedDevice = localMediaManager.currentConnectedDevice?.toMediaDeviceData() if (controller?.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { if ( controller?.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE ) { val routingSession = mr2manager.get().getRoutingSessionForMediaController(controller) Loading @@ -401,13 +443,16 @@ constructor( if (it.selectedRoutes.size > 1) { MediaControlDrawables.getGroupDevice(context) } else { connectedDevice?.icon // Single route. We don't change the icon. connectedDevice ?.icon // Single route. We don't change the icon. } // For a remote session, always use the current device from // LocalMediaManager. Override with routing session information if // LocalMediaManager. Override with routing session information // if // available: // - Name: To show the dynamic group name. // - Icon: To show the group icon if there's more than one selected // - Icon: To show the group icon if there's more than one // selected // route. connectedDevice?.copy( name = it.name ?: connectedDevice.name, Loading @@ -417,7 +462,8 @@ constructor( ?: MediaDeviceData( enabled = false, icon = MediaControlDrawables.getHomeDevices(context), name = context.getString(R.string.media_seamless_other_device), name = context.getString(R.string.media_seamless_other_device), showBroadcastButton = false, ) logger.logRemoteDevice(routingSession?.name, connectedDevice) Loading @@ -428,8 +474,16 @@ constructor( logger.logLocalDevice(sassDevice, connectedDevice) } current = activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA logger.logNewDeviceName(current?.name?.toString()) activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA } .also { logger.logNewDeviceName(it?.name?.toString()) } val updated = newCurrent == null || !newCurrent.equalsWithoutIcon(oldCurrent) if (!started || updated) { current = newCurrent if (notifyListeners) { fgExecutor.execute { processDevice(key, oldKey, newCurrent) } } } } Loading packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +143 −36 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -165,6 +165,23 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void emitEventAfterDeviceAndSuggestionChangedTogether() { // GIVEN that media event has already been received mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData); mManager.onSuggestionDataChanged(KEY, OLD_KEY, mSuggestionData); reset(mListener); // WHEN suggestion event is received mManager.onMediaDeviceAndSuggestionDataChanged(KEY, null, mDeviceData, mSuggestionData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded(eq(KEY), any(), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyMediaFirst() { // GIVEN that media and device info has already been received Loading Loading @@ -210,6 +227,24 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyDeviceAndSuggestionFirst() { // GIVEN that media and device info has already been received mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); mManager.onSuggestionDataChanged(OLD_KEY, null, mSuggestionData); reset(mListener); // WHEN a key migration event is received mManager.onMediaDeviceAndSuggestionDataChanged(KEY, OLD_KEY, mDeviceData, mSuggestionData); // THEN the listener receives a combined event ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded( eq(KEY), eq(OLD_KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyMediaAfter() { // GIVEN that media and device info has already been received Loading Loading @@ -257,6 +292,25 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void migrateKeyDeviceAndSuggestionAfter() { // GIVEN that media and device info has already been received mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData, true /* immediately */); mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData); mManager.onSuggestionDataChanged(OLD_KEY, null, mSuggestionData); mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */); reset(mListener); // WHEN a second key migration event is received for the device and suggestion mManager.onMediaDeviceAndSuggestionDataChanged(KEY, OLD_KEY, mDeviceData, mSuggestionData); // THEN the key has already be migrated ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class); verify(mListener) .onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture(), anyBoolean()); assertThat(captor.getValue().getDevice()).isNotNull(); assertThat(captor.getValue().getSuggestionData()).isNotNull(); } @Test public void mediaDataRemoved() { // WHEN media data is removed without first receiving device or data Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatest.kt +19 −0 Original line number Diff line number Diff line Loading @@ -80,6 +80,25 @@ class MediaDataCombineLatest @Inject constructor() : } } override fun onMediaDeviceAndSuggestionDataChanged( key: String, oldKey: String?, device: MediaDeviceData?, suggestion: SuggestionData?, ) { if (oldKey != null && oldKey != key && entries.contains(oldKey)) { val previousEntry = entries.remove(oldKey) val mediaData = previousEntry?.first entries[key] = Triple(mediaData, device, suggestion) update(key, oldKey) } else { val previousEntry = entries[key] val mediaData = previousEntry?.first entries[key] = Triple(mediaData, device, suggestion) update(key, key) } } override fun onKeyRemoved(key: String, userInitiated: Boolean) { remove(key, userInitiated) } Loading
packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +138 −84 Original line number Diff line number Diff line Loading @@ -155,8 +155,20 @@ constructor( } @MainThread private fun processSuggestionData(key: String, oldKey: String?, device: SuggestionData?) { listeners.forEach { it.onSuggestionDataChanged(key, oldKey, device) } private fun processSuggestionData(key: String, oldKey: String?, suggestion: SuggestionData?) { listeners.forEach { it.onSuggestionDataChanged(key, oldKey, suggestion) } } @MainThread private fun processMediaDeviceAndSuggestionData( key: String, oldKey: String?, device: MediaDeviceData?, suggestion: SuggestionData?, ) { listeners.forEach { it.onMediaDeviceAndSuggestionDataChanged(key, oldKey, device, suggestion) } } interface Listener { Loading @@ -168,6 +180,17 @@ constructor( /** Called when the suggested route has changed for a given notification. */ fun onSuggestionDataChanged(key: String, oldKey: String?, data: SuggestionData?) /** * Called when the both the route and the suggested route has changed for a given * notification. */ fun onMediaDeviceAndSuggestionDataChanged( key: String, oldKey: String?, deviceData: MediaDeviceData?, suggestionData: SuggestionData?, ) } private inner class Entry( Loading @@ -191,23 +214,8 @@ constructor( bgExecutor.execute { localMediaManager.requestDeviceSuggestion() } } private var current: MediaDeviceData? = null set(value) { val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) if (!started || !sameWithoutIcon) { field = value fgExecutor.execute { processDevice(key, oldKey, value) } } } private var suggestionData = SuggestionData(onSuggestionSpaceVisible = requestSuggestionRunnable) set(value) { val sameWithoutConnect = value.equalsWithoutConnect(field) if (!sameWithoutConnect) { field = value fgExecutor.execute { processSuggestionData(key, oldKey, value) } } } private var suggestionData: SuggestionData? = null // 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. Loading @@ -226,9 +234,6 @@ constructor( if (!started) { // Fetch in case a suggestion already exists before registering for suggestions localMediaManager.registerCallback(this) if (enableSuggestedDeviceUi()) { onSuggestedDeviceUpdated(localMediaManager.getSuggestedDevice()) } if (!Flags.removeUnnecessaryRouteScanning()) { localMediaManager.startScan() } Loading @@ -236,7 +241,23 @@ constructor( playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN playbackVolumeControlId = controller?.playbackInfo?.volumeControlId controller?.registerCallback(this) if (enableSuggestedDeviceUi()) { updateCurrent(notifyListeners = false) updateSuggestion( localMediaManager.getSuggestedDevice(), notifyListeners = false, ) fgExecutor.execute { processMediaDeviceAndSuggestionData( key, oldKey, current, suggestionData, ) } } else { updateCurrent() } started = true configurationController.addCallback(configListener) } Loading Loading @@ -299,21 +320,7 @@ constructor( if (!enableSuggestedDeviceUi()) { return } bgExecutor.execute { suggestionData = SuggestionData( suggestedMediaDeviceData = state?.let { SuggestedMediaDeviceData( name = it.suggestedDeviceInfo.getDeviceDisplayName(), icon = it.getIcon(context), connectionState = it.connectionState, connect = { localMediaManager.connectSuggestedDevice(it) }, ) }, onSuggestionSpaceVisible = requestSuggestionRunnable, ) } bgExecutor.execute { updateSuggestion(state) } } override fun onAboutToConnectDeviceAdded( Loading Loading @@ -380,18 +387,53 @@ constructor( override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} @WorkerThread private fun updateCurrent() { private fun updateSuggestion( state: SuggestedDeviceState?, notifyListeners: Boolean = true, ) { val oldSuggestion = suggestionData val newSuggestion = SuggestionData( suggestedMediaDeviceData = state?.let { SuggestedMediaDeviceData( name = it.suggestedDeviceInfo.getDeviceDisplayName(), icon = it.getIcon(context), connectionState = it.connectionState, connect = { localMediaManager.connectSuggestedDevice(it) }, ) }, onSuggestionSpaceVisible = requestSuggestionRunnable, ) val updated = !newSuggestion.equalsWithoutConnect(oldSuggestion) if (updated) { suggestionData = newSuggestion if (notifyListeners) { fgExecutor.execute { processSuggestionData(key, oldKey, newSuggestion) } } } } @WorkerThread private fun updateCurrent(notifyListeners: Boolean = true) { val oldCurrent = current val newCurrent = if (isLeAudioBroadcastEnabled()) { current = getLeAudioBroadcastDeviceData() getLeAudioBroadcastDeviceData() } else { val activeDevice: MediaDeviceData? // LocalMediaManager provides the connected device based on PlaybackInfo. // TODO (b/342197065): Simplify nullability once we make currentConnectedDevice // TODO (b/342197065): Simplify nullability once we make // currentConnectedDevice // non-null. val connectedDevice = localMediaManager.currentConnectedDevice?.toMediaDeviceData() val connectedDevice = localMediaManager.currentConnectedDevice?.toMediaDeviceData() if (controller?.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { if ( controller?.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE ) { val routingSession = mr2manager.get().getRoutingSessionForMediaController(controller) Loading @@ -401,13 +443,16 @@ constructor( if (it.selectedRoutes.size > 1) { MediaControlDrawables.getGroupDevice(context) } else { connectedDevice?.icon // Single route. We don't change the icon. connectedDevice ?.icon // Single route. We don't change the icon. } // For a remote session, always use the current device from // LocalMediaManager. Override with routing session information if // LocalMediaManager. Override with routing session information // if // available: // - Name: To show the dynamic group name. // - Icon: To show the group icon if there's more than one selected // - Icon: To show the group icon if there's more than one // selected // route. connectedDevice?.copy( name = it.name ?: connectedDevice.name, Loading @@ -417,7 +462,8 @@ constructor( ?: MediaDeviceData( enabled = false, icon = MediaControlDrawables.getHomeDevices(context), name = context.getString(R.string.media_seamless_other_device), name = context.getString(R.string.media_seamless_other_device), showBroadcastButton = false, ) logger.logRemoteDevice(routingSession?.name, connectedDevice) Loading @@ -428,8 +474,16 @@ constructor( logger.logLocalDevice(sassDevice, connectedDevice) } current = activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA logger.logNewDeviceName(current?.name?.toString()) activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA } .also { logger.logNewDeviceName(it?.name?.toString()) } val updated = newCurrent == null || !newCurrent.equalsWithoutIcon(oldCurrent) if (!started || updated) { current = newCurrent if (notifyListeners) { fgExecutor.execute { processDevice(key, oldKey, newCurrent) } } } } Loading
packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +143 −36 File changed.Preview size limit exceeded, changes collapsed. Show changes