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

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

Merge "Propagate suggested device info to MediaDeviceManager" into main

parents ab57ff9b 1e0fb529
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -317,6 +317,12 @@ public class LocalMediaManager implements BluetoothCallback {
        }
    }

    void dispatchOnSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {
        for (DeviceCallback callback : getCallbacks()) {
            callback.onSuggestedDeviceUpdated(device);
        }
    }

    /**
     * Dispatch a change in the about-to-connect device. See
     * {@link DeviceCallback#onAboutToConnectDeviceAdded} for more information.
@@ -746,6 +752,11 @@ public class LocalMediaManager implements BluetoothCallback {
            }
            dispatchOnRequestFailed(reason);
        }

        @Override
        public void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {
            dispatchOnSuggestedDeviceUpdated(device);
        }
    }

    private void unRegisterDeviceAttributeChangeCallback() {
@@ -823,6 +834,9 @@ public class LocalMediaManager implements BluetoothCallback {
         * Callback for notifying that we no longer have an about-to-connect device.
         */
        default void onAboutToConnectDeviceRemoved() {}

        /** Callback for notifying that the suggested device has been updated. */
        default void onSuggestedDeviceUpdated(@Nullable SuggestedDeviceState device) {}
    }

    /**
+83 −6
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import com.android.internal.logging.InstanceId;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.controls.shared.model.MediaData;
import com.android.systemui.media.controls.shared.model.MediaDeviceData;
import com.android.systemui.media.controls.shared.model.SuggestedMediaDeviceData;

import org.junit.Before;
import org.junit.Rule;
@@ -68,17 +69,44 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {

    private MediaData mMediaData;
    private MediaDeviceData mDeviceData;
    @Mock private SuggestedMediaDeviceData mSuggestedDeviceData;

    @Before
    public void setUp() {
        mManager = new MediaDataCombineLatest();
        mManager.addListener(mListener);

        mMediaData = new MediaData(
                USER_ID, true, APP, null, ARTIST, TITLE, null,
                new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null,
                MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, 0L,
                InstanceId.fakeInstanceId(-1), -1, false, null);
        mMediaData =
                new MediaData(
                        USER_ID,
                        true,
                        APP,
                        null,
                        ARTIST,
                        TITLE,
                        null,
                        new ArrayList<>(),
                        new ArrayList<>(),
                        null,
                        PACKAGE,
                        null,
                        null,
                        null,
                        null,
                        true,
                        null,
                        MediaData.PLAYBACK_LOCAL,
                        false,
                        KEY,
                        false,
                        false,
                        false,
                        0L,
                        0L,
                        InstanceId.fakeInstanceId(-1),
                        -1,
                        false,
                        null);
        mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false);
    }

@@ -111,7 +139,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
    }

    @Test
    public void emitEventAfterMediaFirst() {
    public void emitEventAfterMediaFirstAndMediaDeviceChangedSecond() {
        // GIVEN that media event has already been received
        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */);
        // WHEN device event is received
@@ -122,6 +150,21 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
        assertThat(captor.getValue().getDevice()).isNotNull();
    }

    @Test
    public void emitEventAfterMediaFirstAndMediaDeviceChangedSecondAndSuggestionChangedThird() {
        // GIVEN that media event has already been received
        mManager.onMediaDataLoaded(KEY, null, mMediaData, true /* immediately */);
        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
        reset(mListener);
        // WHEN suggestion event is received
        mManager.onSuggestedMediaDeviceChanged(KEY, null, mSuggestedDeviceData);
        // 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().getSuggestedDevice()).isNotNull();
    }

    @Test
    public void migrateKeyMediaFirst() {
        // GIVEN that media and device info has already been received
@@ -150,6 +193,23 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
        assertThat(captor.getValue().getDevice()).isNotNull();
    }

    @Test
    public void migrateKeySuggestionFirst() {
        // 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.onSuggestedMediaDeviceChanged(OLD_KEY, null, mSuggestedDeviceData);
        reset(mListener);
        // WHEN a key migration event is received
        mManager.onSuggestedMediaDeviceChanged(KEY, OLD_KEY, mSuggestedDeviceData);
        // 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().getSuggestedDevice()).isNotNull();
    }

    @Test
    public void migrateKeyMediaAfter() {
        // GIVEN that media and device info has already been received
@@ -180,6 +240,23 @@ public class MediaDataCombineLatestTest extends SysuiTestCase {
        assertThat(captor.getValue().getDevice()).isNotNull();
    }

    @Test
    public void migrateKeySuggestionAfter() {
        // 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.onSuggestedMediaDeviceChanged(OLD_KEY, null, mSuggestedDeviceData);
        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData, true /* immediately */);
        reset(mListener);
        // WHEN a second key migration event is received for the device
        mManager.onSuggestedMediaDeviceChanged(KEY, OLD_KEY, mSuggestedDeviceData);
        // 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().getSuggestedDevice()).isNotNull();
    }

    @Test
    public void mediaDataRemoved() {
        // WHEN media data is removed without first receiving device or data
+42 −8
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.media.controls.domain.pipeline

import com.android.systemui.media.controls.shared.model.MediaData
import com.android.systemui.media.controls.shared.model.MediaDeviceData
import com.android.systemui.media.controls.shared.model.SuggestedMediaDeviceData
import javax.inject.Inject

/** Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */
@@ -25,7 +26,9 @@ class MediaDataCombineLatest @Inject constructor() :
    MediaDataManager.Listener, MediaDeviceManager.Listener {

    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
    private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
    private val entries:
        MutableMap<String, Triple<MediaData?, MediaDeviceData?, SuggestedMediaDeviceData?>> =
        mutableMapOf()

    override fun onMediaDataLoaded(
        key: String,
@@ -34,10 +37,16 @@ class MediaDataCombineLatest @Inject constructor() :
        immediately: Boolean,
    ) {
        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
            entries[key] = data to entries.remove(oldKey)?.second
            val previousEntry = entries.remove(oldKey)
            val (mediaDeviceData, suggestedMediaDeviceData) =
                previousEntry?.second to previousEntry?.third
            entries[key] = Triple(data, mediaDeviceData, suggestedMediaDeviceData)
            update(key, oldKey)
        } else {
            entries[key] = data to entries[key]?.second
            val previousEntry = entries[key]
            val (mediaDeviceData, suggestedMediaDeviceData) =
                previousEntry?.second to previousEntry?.third
            entries[key] = Triple(data, mediaDeviceData, suggestedMediaDeviceData)
            update(key, key)
        }
    }
@@ -48,10 +57,32 @@ class MediaDataCombineLatest @Inject constructor() :

    override fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) {
        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
            entries[key] = entries.remove(oldKey)?.first to data
            val previousEntry = entries.remove(oldKey)
            val (mediaData, suggestedMediaDeviceData) = previousEntry?.first to previousEntry?.third
            entries[key] = Triple(mediaData, data, suggestedMediaDeviceData)
            update(key, oldKey)
        } else {
            entries[key] = entries[key]?.first to data
            val previousEntry = entries[key]
            val (mediaData, suggestedMediaDeviceData) = previousEntry?.first to previousEntry?.third
            entries[key] = Triple(mediaData, data, suggestedMediaDeviceData)
            update(key, key)
        }
    }

    override fun onSuggestedMediaDeviceChanged(
        key: String,
        oldKey: String?,
        data: SuggestedMediaDeviceData?,
    ) {
        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
            val previousEntry = entries.remove(oldKey)
            val (mediaData, mediaDeviceData) = previousEntry?.first to previousEntry?.second
            entries[key] = Triple(mediaData, mediaDeviceData, data)
            update(key, oldKey)
        } else {
            val previousEntry = entries[key]
            val (mediaData, mediaDeviceData) = previousEntry?.first to previousEntry?.second
            entries[key] = Triple(mediaData, mediaDeviceData, data)
            update(key, key)
        }
    }
@@ -69,9 +100,12 @@ class MediaDataCombineLatest @Inject constructor() :
    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)

    private fun update(key: String, oldKey: String?) {
        val (entry, device) = entries[key] ?: null to null
        if (entry != null && device != null) {
            val data = entry.copy(device = device)
        val mediaData = entries[key]?.first
        val mediaDeviceData = entries[key]?.second
        val suggestedMediaDeviceData = entries[key]?.third
        if (mediaData != null && mediaDeviceData != null) {
            val data =
                mediaData.copy(device = mediaDeviceData, suggestedDevice = suggestedMediaDeviceData)
            val listenersCopy = listeners.toSet()
            listenersCopy.forEach { it.onMediaDataLoaded(key, oldKey, data) }
        }
+40 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags.enableLeAudioSharing
import com.android.settingslib.flags.Flags.legacyLeAudioSharing
import com.android.settingslib.media.InfoMediaManager.SuggestedDeviceState
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
@@ -44,6 +45,7 @@ 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.shared.model.SuggestedMediaDeviceData
import com.android.systemui.media.controls.util.LocalMediaManagerFactory
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
@@ -167,12 +169,28 @@ constructor(
        }
    }

    @MainThread
    private fun processSuggestedDevice(
        key: String,
        oldKey: String?,
        device: SuggestedMediaDeviceData?,
    ) {
        listeners.forEach { it.onSuggestedMediaDeviceChanged(key, oldKey, device) }
    }

    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)

        /** Called when the suggested route has changed for a given notification. */
        fun onSuggestedMediaDeviceChanged(
            key: String,
            oldKey: String?,
            data: SuggestedMediaDeviceData?,
        )
    }

    private inner class Entry(
@@ -201,6 +219,14 @@ constructor(
                }
            }

        private var suggestedDevice: SuggestedMediaDeviceData? = null
            set(value) {
                if (field != value) {
                    field = value
                    fgExecutor.execute { processSuggestedDevice(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
@@ -283,6 +309,20 @@ constructor(
            bgExecutor.execute { updateCurrent() }
        }

        override fun onSuggestedDeviceUpdated(state: SuggestedDeviceState?) {
            bgExecutor.execute {
                suggestedDevice =
                    state?.let {
                        SuggestedMediaDeviceData(
                            name = it.suggestedDeviceInfo.getDeviceDisplayName(),
                            icon = it.getIcon(context),
                            connectionState = it.connectionState,
                            connect = { localMediaManager.connectSuggestedDevice(it) },
                        )
                    }
            }
        }

        override fun onAboutToConnectDeviceAdded(
            deviceAddress: String,
            deviceName: String,
+19 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.graphics.drawable.Icon
import android.media.session.MediaSession
import android.os.Process
import com.android.internal.logging.InstanceId
import com.android.settingslib.media.LocalMediaManager.MediaDeviceState
import com.android.systemui.res.R

/** State of a media view. */
@@ -55,6 +56,8 @@ data class MediaData(
    val clickIntent: PendingIntent? = null,
    /** Where the media is playing: phone, headphones, ear buds, remote session. */
    val device: MediaDeviceData? = null,
    /** Where the media is suggested to be played. */
    val suggestedDevice: SuggestedMediaDeviceData? = null,
    /**
     * When active, a player will be displayed on keyguard and quick-quick settings. This is
     * unrelated to the stream being playing or not, a player will not be active if timed out, or in
@@ -203,3 +206,19 @@ constructor(
            showBroadcastButton == other.showBroadcastButton
    }
}

/** State of the suggested media device. */
data class SuggestedMediaDeviceData
constructor(
    /** Device display name */
    val name: String,

    /** The current state of attempting to transfer to the suggested device. */
    @MediaDeviceState val connectionState: Int,

    /** The device icon. */
    val icon: Drawable,

    /** Action to invoke to transfer media playback to this device. */
    val connect: () -> Unit,
)
Loading