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

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

[Audiosharing] Sync sink source when they are combined in CSIP

When users share then pair, we will add source to ther first paired
sink. Once the other sink is grouped with the first one in CSIP, we
should auto sync the source the other sink.

Test: atest
Flag: com.android.settingslib.flags.enable_le_audio_sharing
Bug: 362858921
Change-Id: I599f8cca8eab664b203079081b8d5ea6414aac2a
parent 20c56e7b
Loading
Loading
Loading
Loading
+39 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.settingslib.bluetooth;

import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.os.Build;
@@ -356,6 +357,8 @@ public class CsipDeviceManager {
        final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
        preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
        if (haveMultiMainDevicesInAllOfDevicesList) {
            log("addMemberDevicesIntoMainDevice: haveMultiMainDevicesInAllOfDevicesList. "
                    + "Combine them and also keep the preferred main device as main device.");
            // put another devices into main device.
            for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
                if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
@@ -376,6 +379,7 @@ public class CsipDeviceManager {
                preferredMainDevice.refresh();
                hasChanged = true;
            }
            syncAudioSharingSourceIfNeeded(preferredMainDevice);
        }
        if (hasChanged) {
            log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
@@ -384,6 +388,41 @@ public class CsipDeviceManager {
        return hasChanged;
    }

    private void syncAudioSharingSourceIfNeeded(CachedBluetoothDevice mainDevice) {
        boolean isAudioSharingEnabled = BluetoothUtils.isAudioSharingEnabled();
        if (isAudioSharingEnabled) {
            boolean hasBroadcastSource = BluetoothUtils.isBroadcasting(mBtManager)
                    && BluetoothUtils.hasConnectedBroadcastSource(
                    mainDevice, mBtManager);
            if (hasBroadcastSource) {
                LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null
                        : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
                BluetoothLeBroadcastMetadata metadata = broadcast == null ? null :
                        broadcast.getLatestBluetoothLeBroadcastMetadata();
                LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null
                        : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
                if (metadata != null && assistant != null) {
                    log("addMemberDevicesIntoMainDevice: sync audio sharing source after "
                            + "combining the top level devices.");
                    Set<CachedBluetoothDevice> deviceSet = new HashSet<>();
                    deviceSet.add(mainDevice);
                    deviceSet.addAll(mainDevice.getMemberDevice());
                    Set<BluetoothDevice> sinksToSync = deviceSet.stream()
                            .map(CachedBluetoothDevice::getDevice)
                            .filter(device ->
                                    !BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(
                                            device, mBtManager))
                            .collect(Collectors.toSet());
                    for (BluetoothDevice device : sinksToSync) {
                        log("addMemberDevicesIntoMainDevice: sync audio sharing source to "
                                + device.getAnonymizedAddress());
                        assistant.addSource(device, metadata, /* isGroupOp= */ false);
                    }
                }
            }
        }
    }

    private void log(String msg) {
        if (DEBUG) {
            Log.d(TAG, msg);
+101 −0
Original line number Diff line number Diff line
@@ -18,31 +18,51 @@ package com.android.settingslib.bluetooth;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.os.Looper;
import android.os.Parcel;
import android.platform.test.flag.junit.SetFlagsRule;

import com.android.settingslib.flags.Flags;
import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;

import com.google.common.collect.ImmutableList;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;

import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
public class CsipDeviceManagerTest {
    private final static String DEVICE_NAME_1 = "TestName_1";
    private final static String DEVICE_NAME_2 = "TestName_2";
@@ -59,6 +79,9 @@ public class CsipDeviceManagerTest {
    private final BluetoothClass DEVICE_CLASS_2 =
            createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE);

    @Rule
    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Mock
    private LocalBluetoothManager mLocalBluetoothManager;
    @Mock
@@ -77,7 +100,12 @@ public class CsipDeviceManagerTest {
    private A2dpProfile mA2dpProfile;
    @Mock
    private LeAudioProfile mLeAudioProfile;
    @Mock
    private LocalBluetoothLeBroadcast mBroadcast;
    @Mock
    private LocalBluetoothLeBroadcastAssistant mAssistant;

    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
    private CachedBluetoothDevice mCachedDevice1;
    private CachedBluetoothDevice mCachedDevice2;
    private CachedBluetoothDevice mCachedDevice3;
@@ -101,6 +129,12 @@ public class CsipDeviceManagerTest {
        MockitoAnnotations.initMocks(this);

        mContext = RuntimeEnvironment.application;
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
        mShadowBluetoothAdapter.setEnabled(true);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                BluetoothStatusCodes.FEATURE_SUPPORTED);
        when(mDevice1.getAddress()).thenReturn(DEVICE_ADDRESS_1);
        when(mDevice2.getAddress()).thenReturn(DEVICE_ADDRESS_2);
        when(mDevice3.getAddress()).thenReturn(DEVICE_ADDRESS_3);
@@ -124,6 +158,8 @@ public class CsipDeviceManagerTest {
        when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
        when(mLocalProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile);
        when(mLocalProfileManager.getHeadsetProfile()).thenReturn(mHfpProfile);
        when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
        when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);

        when(mLeAudioProfile.getConnectedGroupLeadDevice(anyInt())).thenReturn(null);
        mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, mLocalBluetoothManager);
@@ -307,6 +343,7 @@ public class CsipDeviceManagerTest {
        mCachedDevices.add(preferredDevice);
        mCachedDevices.add(mCachedDevice2);
        mCachedDevices.add(mCachedDevice3);
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);

        assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                .isTrue();
@@ -314,6 +351,36 @@ public class CsipDeviceManagerTest {
        assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse();
        assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
        assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
        verify(mAssistant, never()).addSource(any(BluetoothDevice.class),
                any(BluetoothLeBroadcastMetadata.class), anyBoolean());
    }

    @Test
    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMainAndTwoMain_syncSource() {
        // Condition: The preferredDevice is main and there is another main device in top list
        // Expected Result: return true and there is the preferredDevice in top list
        CachedBluetoothDevice preferredDevice = mCachedDevice1;
        mCachedDevice1.getMemberDevice().clear();
        mCachedDevices.clear();
        mCachedDevices.add(preferredDevice);
        mCachedDevices.add(mCachedDevice2);
        mCachedDevices.add(mCachedDevice3);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        when(mBroadcast.isEnabled(null)).thenReturn(true);
        BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
        BluetoothLeBroadcastReceiveState state = Mockito.mock(
                BluetoothLeBroadcastReceiveState.class);
        when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
        when(mAssistant.getAllSources(mDevice2)).thenReturn(ImmutableList.of(state));

        assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                .isTrue();
        assertThat(mCachedDevices.contains(preferredDevice)).isTrue();
        assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse();
        assertThat(mCachedDevices.contains(mCachedDevice3)).isTrue();
        assertThat(preferredDevice.getMemberDevice()).contains(mCachedDevice2);
        verify(mAssistant).addSource(mDevice1, metadata, /* isGroupOp= */ false);
    }

    @Test
@@ -341,6 +408,8 @@ public class CsipDeviceManagerTest {
        CachedBluetoothDevice preferredDevice = mCachedDevice2;
        BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
        mCachedDevice3.setGroupId(GROUP1);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        when(mBroadcast.isEnabled(null)).thenReturn(false);

        assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                .isTrue();
@@ -351,8 +420,40 @@ public class CsipDeviceManagerTest {
        assertThat(mCachedDevices.contains(mCachedDevice3)).isFalse();
        assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2);
        assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3);
        assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
        verify(mAssistant, never()).addSource(any(BluetoothDevice.class),
                any(BluetoothLeBroadcastMetadata.class), anyBoolean());
    }

    @Test
    public void addMemberDevicesIntoMainDevice_preferredDeviceIsMemberAndTwoMain_syncSource() {
        // Condition: The preferredDevice is member and there are two main device in top list
        // Expected Result: return true and there is the preferredDevice in top list
        CachedBluetoothDevice preferredDevice = mCachedDevice2;
        BluetoothDevice expectedMainBluetoothDevice = preferredDevice.getDevice();
        mCachedDevice3.setGroupId(GROUP1);
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
        when(mBroadcast.isEnabled(null)).thenReturn(true);
        BluetoothLeBroadcastMetadata metadata = Mockito.mock(BluetoothLeBroadcastMetadata.class);
        when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(metadata);
        BluetoothLeBroadcastReceiveState state = Mockito.mock(
                BluetoothLeBroadcastReceiveState.class);
        when(state.getBisSyncState()).thenReturn(ImmutableList.of(1L));
        when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of(state));

        assertThat(mCsipDeviceManager.addMemberDevicesIntoMainDevice(GROUP1, preferredDevice))
                .isTrue();
        shadowOf(Looper.getMainLooper()).idle();
        // expected main is mCachedDevice1 which is the main of preferredDevice, since system
        // switch the relationship between preferredDevice and the main of preferredDevice
        assertThat(mCachedDevices.contains(mCachedDevice1)).isTrue();
        assertThat(mCachedDevices.contains(mCachedDevice2)).isFalse();
        assertThat(mCachedDevices.contains(mCachedDevice3)).isFalse();
        assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice2);
        assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3);
        assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
        verify(mAssistant).addSource(mDevice2, metadata, /* isGroupOp= */ false);
        verify(mAssistant).addSource(mDevice3, metadata, /* isGroupOp= */ false);
    }

    @Test