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

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

Merge "Notify listeners of device and suggestion data together" into main

parents a2f63c57 897ecda4
Loading
Loading
Loading
Loading
+54 −0
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
+19 −0
Original line number Diff line number Diff line
@@ -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)
    }
+138 −84
Original line number Diff line number Diff line
@@ -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 {
@@ -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(
@@ -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.
@@ -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()
                    }
@@ -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)
                }
@@ -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(
@@ -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)

@@ -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,
@@ -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)
@@ -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) }
                }
            }
        }

+143 −36

File changed.

Preview size limit exceeded, changes collapsed.