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

Commit 64e4bd9e authored by Rongxuan Liu's avatar Rongxuan Liu
Browse files

[le audio] BassClient adding support for switching broadcast source

Currently if the sinks have active source, assistant won't proceed with
syncing or adding with new source.

This commit adds the changes for
1. Search and show remote source should not depend on whether remote
   buds are having source(either active or not).
2. Upon adding a source, stack will try to automatically remove any
   source(either active or not) to make a room first, and then proceed
   with adding the new source.

Bug: 316423019
Bug: 316005152
Test: atest BassClientServiceTest BassClientStateMachineTest
Test: manual test with le audio broadcast QR code path
Change-Id: I51cbe48b442cad6bddd2671d8fcdc590391d69d9
parent bfe04c4b
Loading
Loading
Loading
Loading
+70 −11
Original line number Diff line number Diff line
@@ -668,6 +668,41 @@ public class BassClientService extends ProfileService {
        return isRoomAvailable;
    }

    private Integer getSourceIdToRemove(BluetoothDevice device) {
        BassClientStateMachine stateMachine = null;

        synchronized (mStateMachines) {
            stateMachine = getOrCreateStateMachine(device);
        }
        if (stateMachine == null) {
            log("stateMachine is null");
            return BassConstants.INVALID_SOURCE_ID;
        }
        List<BluetoothLeBroadcastReceiveState> sources = stateMachine.getAllSources();
        if (sources.isEmpty()) {
            log("sources is empty");
            return BassConstants.INVALID_SOURCE_ID;
        }

        Integer sourceId = BassConstants.INVALID_SOURCE_ID;
        // Select the source by checking if there is one with PA not synced
        Optional<BluetoothLeBroadcastReceiveState> receiver =
                sources.stream()
                        .filter(
                                e ->
                                        (e.getPaSyncState()
                                                != BluetoothLeBroadcastReceiveState
                                                        .PA_SYNC_STATE_SYNCHRONIZED))
                        .findAny();
        if (receiver.isPresent()) {
            sourceId = receiver.get().getSourceId();
        } else {
            // If all sources are synced, continue to pick the 1st source
            sourceId = sources.get(0).getSourceId();
        }
        return sourceId;
    }

    private BassClientStateMachine getOrCreateStateMachine(BluetoothDevice device) {
        if (device == null) {
            Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
@@ -1151,11 +1186,6 @@ public class BassClientService extends ProfileService {
    }

    void selectSource(BluetoothDevice sink, ScanResult result, boolean autoTrigger) {
        if (!hasRoomForBroadcastSourceAddition(sink)) {
            log("selectSource: No more slot");
            return;
        }

        List<Integer> activeSyncedSrc = getActiveSyncedSources(sink);
        if (activeSyncedSrc != null && activeSyncedSrc.size() >= MAX_ACTIVE_SYNCED_SOURCES_NUM) {
            log("selectSource : reached max allowed active source");
@@ -1233,8 +1263,38 @@ public class BassClientService extends ProfileService {
            }
            if (!hasRoomForBroadcastSourceAddition(device)) {
                log("addSource: device has no room");
                mCallbacks.notifySourceAddFailed(device, sourceMetadata,
                Integer sourceId = getSourceIdToRemove(device);
                if (sourceId != BassConstants.INVALID_SOURCE_ID) {
                    sEventLogger.logd(
                            DBG,
                            TAG,
                            "Switch Broadcast Source: device: "
                                    + device
                                    + ", old SourceId: "
                                    + sourceId
                                    + ", new SourceMetadata: "
                                    + sourceMetadata);

                    // new source will be added once the existing source got removed
                    if (isGroupOp) {
                        // mark group op for both remove and add source
                        // so setSourceGroupManaged will be updated accordingly in callbacks
                        enqueueSourceGroupOp(
                                device, BassClientStateMachine.REMOVE_BCAST_SOURCE, sourceId);
                        enqueueSourceGroupOp(
                                device, BassClientStateMachine.ADD_BCAST_SOURCE, sourceMetadata);
                    }
                    Message message =
                            stateMachine.obtainMessage(BassClientStateMachine.SWITH_BCAST_SOURCE);
                    message.obj = sourceMetadata;
                    message.arg1 = sourceId;
                    stateMachine.sendMessage(message);
                } else {
                    mCallbacks.notifySourceAddFailed(
                            device,
                            sourceMetadata,
                            BluetoothStatusCodes.ERROR_REMOTE_NOT_ENOUGH_RESOURCES);
                }
                continue;
            }
            if (!isValidBroadcastSourceAddition(device, sourceMetadata)) {
@@ -1285,13 +1345,12 @@ public class BassClientService extends ProfileService {
    /**
     * Modify the Broadcast Source information on a Broadcast Sink
     *
     * @param sink representing the Broadcast Sink to which the Broadcast
     *               Source should be updated
     * @param sink representing the Broadcast Sink to which the Broadcast Source should be updated
     * @param sourceId source ID as delivered in onSourceAdded
     * @param updatedMetadata updated Broadcast Source metadata to be updated on the Broadcast Sink
     */
    public void modifySource(BluetoothDevice sink, int sourceId,
            BluetoothLeBroadcastMetadata updatedMetadata) {
    public void modifySource(
            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastMetadata updatedMetadata) {
        log("modifySource: device: " + sink + " sourceId " + sourceId);

        Map<BluetoothDevice, Integer> devices = getGroupManagedDeviceSources(sink, sourceId).second;
+82 −10
Original line number Diff line number Diff line
@@ -106,6 +106,7 @@ public class BassClientStateMachine extends StateMachine {
    static final int PSYNC_ACTIVE_TIMEOUT = 14;
    static final int CONNECT_TIMEOUT = 15;
    static final int REACHED_MAX_SOURCE_LIMIT = 16;
    static final int SWITH_BCAST_SOURCE = 17;

    // NOTE: the value is not "final" - it is modified in the unit tests
    @VisibleForTesting
@@ -181,6 +182,7 @@ public class BassClientStateMachine extends StateMachine {
    BluetoothGattCallback mGattCallback = null;
    @VisibleForTesting PeriodicAdvertisingCallback mLocalPeriodicAdvCallback = new PACallback();
    int mMaxSingleAttributeWriteValueLen = 0;
    @VisibleForTesting BluetoothLeBroadcastMetadata mPendingSourceToSwitch = null;

    BassClientStateMachine(
            BluetoothDevice device,
@@ -257,6 +259,7 @@ public class BassClientStateMachine extends StateMachine {
        mPendingSourceId = -1;
        mPendingMetadata = null;
        mPendingSourceToAdd = null;
        mPendingSourceToSwitch = null;
        mCurrentMetadata.clear();
        mPendingRemove.clear();
        mPeriodicAdvCallbacksMap.clear();
@@ -665,7 +668,8 @@ public class BassClientStateMachine extends StateMachine {
                            & (~BassConstants.ADV_ADDRESS_DONT_MATCHES_SOURCE_ADV_ADDRESS);
                    log("Initiate PAST for: " + mDevice + ", syncHandle: " +  syncHandle
                            + "serviceData" + serviceData);
                    BluetoothMethodProxy.getInstance().periodicAdvertisingManagerTransferSync(
                    BluetoothMethodProxy.getInstance()
                            .periodicAdvertisingManagerTransferSync(
                                    mPeriodicAdvManager, mDevice, serviceData, syncHandle);
                }
            } else {
@@ -875,8 +879,9 @@ public class BassClientStateMachine extends StateMachine {
            if (oldRecvState.getSourceDevice() == null
                    || oldRecvState.getSourceDevice().getAddress().equals(emptyBluetoothDevice)) {
                log("New Source Addition");
                mService.getCallbacks().notifySourceAdded(mDevice, recvState,
                        BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                mService.getCallbacks()
                        .notifySourceAdded(
                                mDevice, recvState, BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                if (mPendingMetadata != null) {
                    setCurrentBroadcastMetadata(recvState.getSourceId(), mPendingMetadata);
                    mPendingMetadata = null;
@@ -891,9 +896,25 @@ public class BassClientStateMachine extends StateMachine {
                    cancelActiveSync(
                            mService.getSyncHandleForBroadcastId(recvState.getBroadcastId()));
                    setCurrentBroadcastMetadata(oldRecvState.getSourceId(), null);
                    mService.getCallbacks().notifySourceRemoved(mDevice,
                    if (mPendingSourceToSwitch != null) {
                        // Source remove is triggered by switch source request
                        mService.getCallbacks()
                                .notifySourceRemoved(
                                        mDevice,
                                        oldRecvState.getSourceId(),
                                        BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST);
                        log("Switching to new source");
                        Message message = obtainMessage(ADD_BCAST_SOURCE);
                        message.obj = mPendingSourceToSwitch;
                        sendMessage(message);
                        mPendingSourceToSwitch = null;
                    } else {
                        mService.getCallbacks()
                                .notifySourceRemoved(
                                        mDevice,
                                        oldRecvState.getSourceId(),
                                        BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                    }
                } else {
                    log("update to an existing recvState");
                    if (mPendingMetadata != null) {
@@ -1137,8 +1158,11 @@ public class BassClientStateMachine extends StateMachine {

        @Override
        public void onBigInfoAdvertisingReport(int syncHandle, boolean encrypted) {
            log("onBIGInfoAdvertisingReport: syncHandle=" + syncHandle +
                    " ,encrypted =" + encrypted);
            log(
                    "onBIGInfoAdvertisingReport: syncHandle="
                            + syncHandle
                            + " ,encrypted ="
                            + encrypted);
            BluetoothDevice srcDevice = mService.getDeviceForSyncHandle(syncHandle);
            if (srcDevice == null) {
                log("No device found.");
@@ -1709,6 +1733,34 @@ public class BassClientStateMachine extends StateMachine {
                    int handle = message.arg1;
                    cancelActiveSync(handle);
                    break;
                case SWITH_BCAST_SOURCE:
                    metaData = (BluetoothLeBroadcastMetadata) message.obj;
                    int sourceIdToRemove = message.arg1;
                    // Save pending source to be added once existing source got removed
                    mPendingSourceToSwitch = metaData;
                    // Remove the source first
                    BluetoothLeBroadcastReceiveState recvStateToUpdate =
                            getBroadcastReceiveStateForSourceId(sourceIdToRemove);
                    BluetoothLeBroadcastMetadata metaDataToUpdate =
                            getCurrentBroadcastMetadata(sourceIdToRemove);
                    if (metaDataToUpdate != null
                            && recvStateToUpdate != null
                            && recvStateToUpdate.getPaSyncState()
                                    == BluetoothLeBroadcastReceiveState
                                            .PA_SYNC_STATE_SYNCHRONIZED) {
                        log("SWITH_BCAST_SOURCE force source to lost PA sync");
                        Message msg = obtainMessage(UPDATE_BCAST_SOURCE);
                        msg.arg1 = sourceIdToRemove;
                        msg.arg2 = BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE;
                        msg.obj = metaDataToUpdate;
                        /* Pending remove set. Remove source once not synchronized to PA */
                        sendMessage(msg);
                    } else {
                        Message msg = obtainMessage(REMOVE_BCAST_SOURCE);
                        msg.arg1 = sourceIdToRemove;
                        sendMessage(msg);
                    }
                    break;
                case ADD_BCAST_SOURCE:
                    metaData = (BluetoothLeBroadcastMetadata) message.obj;

@@ -1837,6 +1889,16 @@ public class BassClientStateMachine extends StateMachine {
                        Log.e(TAG, "REMOVE_BCAST_SOURCE: no Bluetooth Gatt handle, Fatal");
                        mService.getCallbacks().notifySourceRemoveFailed(mDevice,
                                sid, BluetoothStatusCodes.ERROR_UNKNOWN);
                        if (mPendingSourceToSwitch != null) {
                            // Switching source failed
                            // Need to notify add source failure for service to cleanup
                            mService.getCallbacks()
                                    .notifySourceAddFailed(
                                            mDevice,
                                            mPendingSourceToSwitch,
                                            BluetoothStatusCodes.ERROR_UNKNOWN);
                            mPendingSourceToSwitch = null;
                        }
                    }
                    break;
                case PSYNC_ACTIVE_TIMEOUT:
@@ -1901,6 +1963,13 @@ public class BassClientStateMachine extends StateMachine {
                if (!isSuccess(status)) {
                    mService.getCallbacks().notifySourceRemoveFailed(mDevice,
                            mPendingSourceId, status);
                    if (mPendingSourceToSwitch != null) {
                        // Switching source failed
                        // Need to notify add source failure for service to cleanup
                        mService.getCallbacks()
                                .notifySourceAddFailed(mDevice, mPendingSourceToSwitch, status);
                        mPendingSourceToSwitch = null;
                    }
                }
                break;
            case SET_BCAST_CODE:
@@ -2005,6 +2074,7 @@ public class BassClientStateMachine extends StateMachine {
                case SET_BCAST_CODE:
                case REMOVE_BCAST_SOURCE:
                case REACHED_MAX_SOURCE_LIMIT:
                case SWITH_BCAST_SOURCE:
                case PSYNC_ACTIVE_TIMEOUT:
                    log("defer the message: "
                            + messageWhatToString(message.what)
@@ -2097,6 +2167,8 @@ public class BassClientStateMachine extends StateMachine {
                return "REMOVE_BCAST_SOURCE";
            case REACHED_MAX_SOURCE_LIMIT:
                return "REACHED_MAX_SOURCE_LIMIT";
            case SWITH_BCAST_SOURCE:
                return "SWITH_BCAST_SOURCE";
            case PSYNC_ACTIVE_TIMEOUT:
                return "PSYNC_ACTIVE_TIMEOUT";
            case CONNECT_TIMEOUT:
+51 −0
Original line number Diff line number Diff line
@@ -792,6 +792,57 @@ public class BassClientServiceTest {
        assertThat(msg.isPresent()).isFalse();
    }

    /** Test switch source will be triggered if adding new source when sink has source */
    @Test
    public void testSwitchSourceAfterSourceAdded() {
        prepareConnectedDeviceGroup();
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        BluetoothLeBroadcastMetadata newMeta = createBroadcastMetadata(TEST_BROADCAST_ID + 1);
        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            injectRemoteSourceState(
                    sm,
                    meta,
                    TEST_SOURCE_ID,
                    BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                    meta.isEncrypted()
                            ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                            : BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                    null);
            injectRemoteSourceState(
                    sm,
                    meta,
                    TEST_SOURCE_ID + 1,
                    BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                    meta.isEncrypted()
                            ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                            : BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                    null);
        }

        // Add another new broadcast source
        mBassClientService.addSource(mCurrentDevice, newMeta, true);

        // Verify all group members getting SWITH_BCAST_SOURCE message and first source got selected
        // to remove
        assertThat(mStateMachines.size()).isEqualTo(2);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
            verify(sm, atLeast(1)).sendMessage(messageCaptor.capture());

            Optional<Message> msg =
                    messageCaptor.getAllValues().stream()
                            .filter(
                                    m ->
                                            (m.what == BassClientStateMachine.SWITH_BCAST_SOURCE)
                                                    && (m.obj == newMeta)
                                                    && (m.arg1 == TEST_SOURCE_ID))
                            .findFirst();
            assertThat(msg.isPresent()).isTrue();
            assertThat(msg.orElse(null)).isNotNull();
        }
    }

    /**
     * Test that after multiple calls to service.addSource() with a group operation flag set,
     * there are two call to service.removeSource() needed to clear the flag
+29 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import static com.android.bluetooth.bass_client.BassClientStateMachine.SELECT_BC
import static com.android.bluetooth.bass_client.BassClientStateMachine.SET_BCAST_CODE;
import static com.android.bluetooth.bass_client.BassClientStateMachine.START_SCAN_OFFLOAD;
import static com.android.bluetooth.bass_client.BassClientStateMachine.STOP_SCAN_OFFLOAD;
import static com.android.bluetooth.bass_client.BassClientStateMachine.SWITH_BCAST_SOURCE;
import static com.android.bluetooth.bass_client.BassClientStateMachine.UPDATE_BCAST_SOURCE;
import static com.android.bluetooth.bass_client.BassConstants.CLIENT_CHARACTERISTIC_CONFIG;

@@ -70,6 +71,7 @@ import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.le.PeriodicAdvertisingCallback;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
@@ -763,13 +765,17 @@ public class BassClientStateMachineTest {
        when(mBassClientService.getPeriodicAdvertisementResult(any(), anyInt())).thenReturn(null);
        when(mBassClientService.isLocalBroadcast(any())).thenReturn(true);
        when(characteristic.getValue()).thenReturn(value);
        mBassClientStateMachine.mPendingSourceToSwitch = mBassClientStateMachine.mPendingMetadata;

        Mockito.clearInvocations(callbacks);
        cb.onCharacteristicRead(null, characteristic, GATT_SUCCESS);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());

        verify(callbacks).notifySourceRemoved(any(), anyInt(), anyInt());
        verify(callbacks)
                .notifySourceRemoved(
                        any(), anyInt(), eq(BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST));
        verify(callbacks).notifyReceiveStateChanged(any(), anyInt(), any());
        assertThat(mBassClientStateMachine.mPendingSourceToSwitch).isEqualTo(null);
    }

    @Test
@@ -1206,6 +1212,24 @@ public class BassClientStateMachineTest {
        verify(mBassClientService).getActiveSyncedSources(any());
    }

    @Test
    public void sendSwitchSourceMessage_inConnectedState() {
        initToConnectedState();
        BluetoothLeBroadcastMetadata metadata = createBroadcastMetadata();
        Integer sourceId = 1;

        BassClientStateMachine.BluetoothGattTestableWrapper btGatt =
                Mockito.mock(BassClientStateMachine.BluetoothGattTestableWrapper.class);
        mBassClientStateMachine.mBluetoothGatt = btGatt;
        BluetoothGattCharacteristic scanControlPoint =
                Mockito.mock(BluetoothGattCharacteristic.class);
        mBassClientStateMachine.mBroadcastScanControlPoint = scanControlPoint;

        mBassClientStateMachine.sendMessage(SWITH_BCAST_SOURCE, sourceId, 0, metadata);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
        assertThat(mBassClientStateMachine.mPendingSourceToSwitch).isEqualTo(metadata);
    }

    @Test
    public void sendUpdateBcastSourceMessage_inConnectedState() {
        initToConnectedState();
@@ -1609,6 +1633,10 @@ public class BassClientStateMachineTest {
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
        assertThat(mBassClientStateMachine.hasDeferredMessagesSuper(REACHED_MAX_SOURCE_LIMIT))
                .isTrue();

        mBassClientStateMachine.sendMessage(SWITH_BCAST_SOURCE);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
        assertThat(mBassClientStateMachine.hasDeferredMessagesSuper(SWITH_BCAST_SOURCE)).isTrue();
    }

    @Test