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

Commit 6fd6f1ae authored by Jakub Tyszkowski's avatar Jakub Tyszkowski
Browse files

VolumeControl: Set proper volume on joining device

When one of the devices connects to an already streaming group it
needs to have it volume level corrected to the already cached group
volume level.

Additionally on the initial connection when the second device is not yet
recognised by CSIS as a group member but the Volume Control Service
has already been connected and group volume was already set by the
LeAudioService, the newly added group member also needs its volume level
to be corrected to the already set group volume level.

Bug: 248969553
Bug: 248915809
Tag: #feature
Test: atest BluetoothInstrumentationTests
Change-Id: I02e1827c4f652f78ab86365b7b9e72ef0c1c77c2
parent dcb00430
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -597,6 +597,24 @@ public class CsipSetCoordinatorService extends ProfileService {
                .collect(Collectors.toList());
    }

    /**
     * Get group ID for a given device and UUID
     * @param device potential group member
     * @param uuid profile context UUID
     * @return group ID
     */
    public Integer getGroupId(BluetoothDevice device, ParcelUuid uuid) {
        Map<Integer, Integer> device_groups =
                mDeviceGroupIdRankMap.getOrDefault(device, new HashMap<>());
        return mGroupIdToUuidMap.entrySet()
                .stream()
                .filter(e -> (device_groups.containsKey(e.getKey())
                        && e.getValue().equals(uuid)))
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID);
    }

    /**
     * Get device's groups/
     * @param device group member device
+7 −0
Original line number Diff line number Diff line
@@ -1933,6 +1933,13 @@ public class LeAudioService extends ProfileService {
    }

    private void notifyGroupNodeAdded(BluetoothDevice device, int groupId) {
        if (mVolumeControlService == null) {
            mVolumeControlService = mServiceFactory.getVolumeControlService();
        }
        if (mVolumeControlService != null) {
            mVolumeControlService.handleGroupNodeAdded(groupId, device);
        }

        if (mLeAudioCallbacks != null) {
            int n = mLeAudioCallbacks.beginBroadcast();
            for (int i = 0; i < n; i++) {
+38 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
import android.bluetooth.IBluetoothCsipSetCoordinator;
import android.bluetooth.IBluetoothLeAudio;
import android.bluetooth.IBluetoothVolumeControl;
import android.bluetooth.IBluetoothVolumeControlCallback;
@@ -47,6 +48,7 @@ import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
@@ -193,7 +195,8 @@ public class VolumeControlService extends ProfileService {
    private BroadcastReceiver mBondStateChangedReceiver;
    private BroadcastReceiver mConnectionStateChangedReceiver;

    private final ServiceFactory mFactory = new ServiceFactory();
    @VisibleForTesting
    ServiceFactory mFactory = new ServiceFactory();

    public static boolean isEnabled() {
        return BluetoothProperties.isProfileVcpControllerEnabled().orElse(false);
@@ -632,6 +635,29 @@ public class VolumeControlService extends ProfileService {
        mVolumeControlNativeInterface.unmuteGroup(groupId);
    }

    /**
     * {@hide}
     */
    public void handleGroupNodeAdded(int groupId, BluetoothDevice device) {
        // Ignore disconnected device, its volume will be set once it connects
        synchronized (mStateMachines) {
            VolumeControlStateMachine sm = mStateMachines.get(device);
            if (sm == null) {
                return;
            }
            if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
                return;
            }
        }

        // If group volume has already changed, the new group member should set it
        Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
                IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
        if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
            mVolumeControlNativeInterface.setVolume(device, groupVolume);
        }
    }

    void handleVolumeControlChanged(BluetoothDevice device, int groupId,
                                    int volume, boolean mute, boolean isAutonomous) {

@@ -974,6 +1000,17 @@ public class VolumeControlService extends ProfileService {
                }
                removeStateMachine(device);
            }
        } else if (toState == BluetoothProfile.STATE_CONNECTED) {
            // Restore the group volume if it was changed while the device was not yet connected.
            CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService();
            Integer groupId = csipClient.getGroupId(device, BluetoothUuid.CAP);
            if (groupId != IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID) {
                Integer groupVolume = mGroupVolumeCache.getOrDefault(groupId,
                        IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME);
                if (groupVolume != IBluetoothVolumeControl.VOLUME_CONTROL_UNKNOWN_VOLUME) {
                    mVolumeControlNativeInterface.setVolume(device, groupVolume);
                }
            }
        }
    }

+96 −0
Original line number Diff line number Diff line
@@ -41,7 +41,9 @@ import androidx.test.rule.ServiceTestRule;
import androidx.test.runner.AndroidJUnit4;

import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver;
@@ -73,6 +75,7 @@ public class VolumeControlServiceTest {
    private VolumeControlService mService;
    private VolumeControlService.BluetoothVolumeControlBinder mServiceBinder;
    private BluetoothDevice mDevice;
    private BluetoothDevice mDeviceTwo;
    private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap;
    private static final int TIMEOUT_MS = 1000;
    private static final int BT_LE_AUDIO_MAX_VOL = 255;
@@ -87,6 +90,8 @@ public class VolumeControlServiceTest {
    @Mock private DatabaseManager mDatabaseManager;
    @Mock private VolumeControlNativeInterface mNativeInterface;
    @Mock private AudioManager mAudioManager;
    @Mock private ServiceFactory mServiceFactory;
    @Mock private CsipSetCoordinatorService mCsipService;

    @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();

@@ -119,9 +124,12 @@ public class VolumeControlServiceTest {
        startService();
        mService.mVolumeControlNativeInterface = mNativeInterface;
        mService.mAudioManager = mAudioManager;
        mService.mFactory = mServiceFactory;
        mServiceBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder();
        mServiceBinder.mIsTesting = true;

        doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService();

        // Override the timeout value to speed up the test
        VolumeControlStateMachine.sConnectTimeoutMs = TIMEOUT_MS;    // 1s

@@ -134,8 +142,10 @@ public class VolumeControlServiceTest {

        // Get a device for testing
        mDevice = TestUtils.getTestDevice(mAdapter, 0);
        mDeviceTwo = TestUtils.getTestDevice(mAdapter, 1);
        mDeviceQueueMap = new HashMap<>();
        mDeviceQueueMap.put(mDevice, new LinkedBlockingQueue<>());
        mDeviceQueueMap.put(mDeviceTwo, new LinkedBlockingQueue<>());
        doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
                .getBondState(any(BluetoothDevice.class));
        doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService)
@@ -631,6 +641,92 @@ public class VolumeControlServiceTest {
        Assert.assertEquals(volume, mService.getGroupVolume(groupId));
    }

    /**
     * Test setting volume for a group member who connects after the volume level
     * for a group was already changed and cached.
     */
    @Test
    public void testLateConnectingDevice() throws Exception {
        int groupId = 1;
        int groupVolume = 56;

        // Both devices are in the same group
        when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);

        // Update the device policy so okToConnect() returns true
        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
        when(mDatabaseManager
                .getProfileConnectionPolicy(any(BluetoothDevice.class),
                        eq(BluetoothProfile.VOLUME_CONTROL)))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
        doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
        doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));

        generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_DISCONNECTED);
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
                mService.getConnectionState(mDevice));
        Assert.assertTrue(mService.getDevices().contains(mDevice));

        mService.setGroupVolume(groupId, groupVolume);
        verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(groupVolume));
        verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));

        // Verify that second device gets the proper group volume level when connected
        generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_DISCONNECTED);
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
                mService.getConnectionState(mDeviceTwo));
        Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
        verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
    }

    /**
     * Test setting volume for a new group member who is discovered after the volume level
     * for a group was already changed and cached.
     */
    @Test
    public void testLateDiscoveredGroupMember() throws Exception {
        int groupId = 1;
        int groupVolume = 56;

        // For now only one device is in the group
        when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId);
        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1);

        // Update the device policy so okToConnect() returns true
        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
        when(mDatabaseManager
                .getProfileConnectionPolicy(any(BluetoothDevice.class),
                        eq(BluetoothProfile.VOLUME_CONTROL)))
                .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
        doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class));
        doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class));

        generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_DISCONNECTED);
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
                mService.getConnectionState(mDevice));
        Assert.assertTrue(mService.getDevices().contains(mDevice));

        // Set the group volume
        mService.setGroupVolume(groupId, groupVolume);

        // Verify that second device will not get the group volume level if it is not a group member
        generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_DISCONNECTED);
        Assert.assertEquals(BluetoothProfile.STATE_CONNECTED,
                mService.getConnectionState(mDeviceTwo));
        Assert.assertTrue(mService.getDevices().contains(mDeviceTwo));
        verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume));

        // But gets the volume when it becomes the group member
        when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId);
        mService.handleGroupNodeAdded(groupId, mDeviceTwo);
        verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume));
    }

    @Test
    public void testServiceBinderGetDevicesMatchingConnectionStates() throws Exception {
        final SynchronousResultReceiver<List<BluetoothDevice>> recv =