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

Commit ee1966c2 authored by Yiyi Shen's avatar Yiyi Shen
Browse files

[Audiosharing] Fix the primary buds auto pick logic in hysteresis mode

In hysteresis mode, we will receive plenty of onReceiveStateChanged, e.g. play and pause music, system sounds... The onReceiveStateChanged with BIS >= 1, as a replacement of onSourceAdded, will trigger auto pick logic for primary headset. In some cases, when user change primary headset in Call audio section on audio sharing page under the hysteresis mode, the system sound will later trigger a onReceiveStateChanged with BIS >= 1, then the auto pick logic (always pick the earliest connected headset) is possible to override the user change. Thus here we have to save the user preferred primary headset in SettingsProvider and skip the auto pick if user has made changes.

Also add to code to do nothing for the work profile.

Test: atest
Bug: 355222285
Flag: com.android.settingslib.flags.audio_sharing_hysteresis_mode_fix
Change-Id: I49e1ef69743fff55e7a558d5076e34ae8c9d11ec
parent b02f8680
Loading
Loading
Loading
Loading
+88 −36
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.settingslib.bluetooth;

import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;

import static java.util.stream.Collectors.toList;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.bluetooth.BluetoothAdapter;
@@ -64,6 +66,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
@@ -84,6 +87,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
    public static final String EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE = "BT_DEVICE_TO_AUTO_ADD_SOURCE";
    public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
    public static final String EXTRA_PAIR_AND_JOIN_SHARING = "PAIR_AND_JOIN_SHARING";
    public static final String BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID =
            "bluetooth_le_broadcast_primary_device_group_id";
    public static final int BROADCAST_STATE_UNKNOWN = 0;
    public static final int BROADCAST_STATE_ON = 1;
    public static final int BROADCAST_STATE_OFF = 2;
@@ -1121,6 +1126,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {

    /** Update fallback active device if needed. */
    public void updateFallbackActiveDeviceIfNeeded() {
        if (isWorkProfile(mContext)) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded for work profile.");
            return;
        }
        if (mServiceBroadcast == null) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to broadcast profile is null");
            return;
@@ -1135,71 +1144,114 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null");
            return;
        }
        List<BluetoothDevice> devicesInBroadcast = getDevicesInBroadcast();
        if (devicesInBroadcast.isEmpty()) {
        Map<Integer, List<BluetoothDevice>> deviceGroupsInBroadcast = getDeviceGroupsInBroadcast();
        if (deviceGroupsInBroadcast.isEmpty()) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no sinks in broadcast");
            return;
        }
        List<BluetoothDevice> devices =
                BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
        BluetoothDevice targetDevice = null;
        // Find the earliest connected device in sharing session.
        int targetDeviceIdx = -1;
        for (BluetoothDevice device : devicesInBroadcast) {
            if (devices.contains(device)) {
                int idx = devices.indexOf(device);
                if (idx > targetDeviceIdx) {
                    targetDeviceIdx = idx;
                    targetDevice = device;
        int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
        int fallbackActiveGroupId = BluetoothUtils.getPrimaryGroupIdForBroadcast(
                mContext.getContentResolver());
        if (Flags.audioSharingHysteresisModeFix()) {
            int userPreferredPrimaryGroupId = getUserPreferredPrimaryGroupId();
            if (userPreferredPrimaryGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
                    && deviceGroupsInBroadcast.containsKey(userPreferredPrimaryGroupId)) {
                if (userPreferredPrimaryGroupId == fallbackActiveGroupId) {
                    Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already user preferred");
                    return;
                } else {
                    targetGroupId = userPreferredPrimaryGroupId;
                }
            }
            if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
                // If there is no user preferred primary device, set the earliest connected
                // device in sharing session as the fallback.
                targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast);
            }
        if (targetDevice == null) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, target is null");
            return;
        } else {
            // Set the earliest connected device in sharing session as the fallback.
            targetGroupId = getEarliestConnectedDeviceGroup(deviceGroupsInBroadcast);
        }
        CachedBluetoothDevice targetCachedDevice = mDeviceManager.findDevice(targetDevice);
        if (targetCachedDevice == null) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find cached bt device");
        Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, target group id = " + targetGroupId);
        if (targetGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return;
        if (targetGroupId == fallbackActiveGroupId) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, already is fallback");
            return;
        }
        int fallbackActiveGroupId = getFallbackActiveGroupId();
        if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
                && BluetoothUtils.getGroupId(targetCachedDevice) == fallbackActiveGroupId) {
            Log.d(
                    TAG,
                    "Skip updateFallbackActiveDeviceIfNeeded, already is fallback: "
                            + fallbackActiveGroupId);
        CachedBluetoothDevice targetCachedDevice = getMainDevice(
                deviceGroupsInBroadcast.get(targetGroupId));
        if (targetCachedDevice == null) {
            Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded, fail to find main device");
            return;
        }
        Log.d(
                TAG,
                "updateFallbackActiveDeviceIfNeeded, set active device: "
                        + targetDevice.getAnonymizedAddress());
                        + targetCachedDevice.getDevice());
        targetCachedDevice.setActive();
    }

    private List<BluetoothDevice> getDevicesInBroadcast() {
    @NonNull
    private Map<Integer, List<BluetoothDevice>> getDeviceGroupsInBroadcast() {
        boolean hysteresisModeFixEnabled = Flags.audioSharingHysteresisModeFix();
        List<BluetoothDevice> connectedDevices = mServiceBroadcastAssistant.getConnectedDevices();
        return connectedDevices.stream()
                .filter(
                        bluetoothDevice -> {
                        device -> {
                            List<BluetoothLeBroadcastReceiveState> sourceList =
                                    mServiceBroadcastAssistant.getAllSources(
                                            bluetoothDevice);
                                    mServiceBroadcastAssistant.getAllSources(device);
                            return !sourceList.isEmpty() && sourceList.stream().anyMatch(
                                    source -> hysteresisModeFixEnabled
                                            ? BluetoothUtils.isSourceMatched(source, mBroadcastId)
                                            : BluetoothUtils.isConnected(source));
                        })
                .collect(Collectors.toList());
                .collect(Collectors.groupingBy(
                        device -> BluetoothUtils.getGroupId(mDeviceManager.findDevice(device))));
    }

    private int getFallbackActiveGroupId() {
    private int getEarliestConnectedDeviceGroup(
            @NonNull Map<Integer, List<BluetoothDevice>> deviceGroups) {
        List<BluetoothDevice> devices =
                BluetoothAdapter.getDefaultAdapter().getMostRecentlyConnectedDevices();
        // Find the earliest connected device in sharing session.
        int targetDeviceIdx = -1;
        int targetGroupId = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
        for (Map.Entry<Integer, List<BluetoothDevice>> entry : deviceGroups.entrySet()) {
            for (BluetoothDevice device : entry.getValue()) {
                if (devices.contains(device)) {
                    int idx = devices.indexOf(device);
                    if (idx > targetDeviceIdx) {
                        targetDeviceIdx = idx;
                        targetGroupId = entry.getKey();
                    }
                }
            }
        }
        return targetGroupId;
    }

    @Nullable
    private CachedBluetoothDevice getMainDevice(@Nullable List<BluetoothDevice> devices) {
        if (devices == null || devices.size() == 1) return null;
        List<CachedBluetoothDevice> cachedDevices =
                devices.stream()
                        .map(device -> mDeviceManager.findDevice(device))
                        .filter(Objects::nonNull)
                        .collect(toList());
        for (CachedBluetoothDevice cachedDevice : cachedDevices) {
            if (!cachedDevice.getMemberDevice().isEmpty()) {
                return cachedDevice;
            }
        }
        CachedBluetoothDevice mainDevice = cachedDevices.isEmpty() ? null : cachedDevices.get(0);
        return mainDevice;
    }

    private int getUserPreferredPrimaryGroupId() {
        // TODO: use real key name in SettingsProvider
        return Settings.Secure.getInt(
                mContext.getContentResolver(),
                "bluetooth_le_broadcast_fallback_active_group_id",
                mContentResolver,
                BLUETOOTH_LE_BROADCAST_PRIMARY_DEVICE_GROUP_ID,
                BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
    }

+61 −84
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -36,6 +37,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.TelephonyManager;

@@ -96,6 +98,8 @@ public class BluetoothEventManagerTest {
    private LocalBluetoothProfileManager mLocalProfileManager;
    @Mock
    private BluetoothUtils.ErrorListener mErrorListener;
    @Mock
    private LocalBluetoothLeBroadcast mBroadcast;

    private Context mContext;
    private Intent mIntent;
@@ -107,7 +111,7 @@ public class BluetoothEventManagerTest {
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mContext = RuntimeEnvironment.application;
        mContext = spy(RuntimeEnvironment.application);

        mBluetoothEventManager =
                new BluetoothEventManager(
@@ -208,28 +212,14 @@ public class BluetoothEventManagerTest {
     */
    @Test
    public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
        ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
        when(broadcast.isProfileReady()).thenReturn(true);
        LocalBluetoothLeBroadcastAssistant assistant =
                mock(LocalBluetoothLeBroadcastAssistant.class);
        when(assistant.isProfileReady()).thenReturn(true);
        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
        when(mBtManager.getProfileManager()).thenReturn(profileManager);
        setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
                true, /* workProfile= */ false);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
    }

    /**
@@ -239,28 +229,14 @@ public class BluetoothEventManagerTest {
     */
    @Test
    public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
        ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
        when(broadcast.isProfileReady()).thenReturn(true);
        LocalBluetoothLeBroadcastAssistant assistant =
                mock(LocalBluetoothLeBroadcastAssistant.class);
        when(assistant.isProfileReady()).thenReturn(true);
        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
        when(mBtManager.getProfileManager()).thenReturn(profileManager);
        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
                true, /* workProfile= */ false);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
    }

    /**
@@ -270,28 +246,14 @@ public class BluetoothEventManagerTest {
     */
    @Test
    public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
        ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
        when(broadcast.isProfileReady()).thenReturn(false);
        LocalBluetoothLeBroadcastAssistant assistant =
                mock(LocalBluetoothLeBroadcastAssistant.class);
        when(assistant.isProfileReady()).thenReturn(true);
        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
        when(mBtManager.getProfileManager()).thenReturn(profileManager);
        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                false, /* workProfile= */ false);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
    }

    /**
@@ -301,28 +263,31 @@ public class BluetoothEventManagerTest {
     */
    @Test
    public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
        ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
        when(broadcast.isProfileReady()).thenReturn(true);
        LocalBluetoothLeBroadcastAssistant assistant =
                mock(LocalBluetoothLeBroadcastAssistant.class);
        when(assistant.isProfileReady()).thenReturn(true);
        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
        when(mBtManager.getProfileManager()).thenReturn(profileManager);
        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                true, /* workProfile= */ false);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO);

        verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
        verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
    }

    /**
     * dispatchProfileConnectionStateChanged should not call {@link
     * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
     * work profile.
     */
    @Test
    public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                true, /* workProfile= */ true);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
    }

    /**
@@ -332,28 +297,40 @@ public class BluetoothEventManagerTest {
     */
    @Test
    public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
        setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
                true, /* workProfile= */ false);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
    }

    private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
            boolean enableProfile, boolean workProfile) {
        if (enableFlag) {
            mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        } else {
            mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        }
        ShadowBluetoothAdapter shadowBluetoothAdapter =
                Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class);
        when(broadcast.isProfileReady()).thenReturn(true);
        int code = enableFeature ? BluetoothStatusCodes.FEATURE_SUPPORTED
                : BluetoothStatusCodes.FEATURE_NOT_SUPPORTED;
        shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(code);
        shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(code);
        when(mBroadcast.isProfileReady()).thenReturn(enableProfile);
        LocalBluetoothLeBroadcastAssistant assistant =
                mock(LocalBluetoothLeBroadcastAssistant.class);
        when(assistant.isProfileReady()).thenReturn(true);
        when(assistant.isProfileReady()).thenReturn(enableProfile);
        LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast);
        when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
        when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
        when(mBtManager.getProfileManager()).thenReturn(profileManager);
        mBluetoothEventManager.dispatchProfileConnectionStateChanged(
                mCachedBluetoothDevice,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);

        verify(broadcast).updateFallbackActiveDeviceIfNeeded();
        UserManager userManager = mock(UserManager.class);
        when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
        when(userManager.isManagedProfile()).thenReturn(workProfile);
    }

    @Test