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

Commit b190fe32 authored by Rongxuan Liu's avatar Rongxuan Liu Committed by Gerrit Code Review
Browse files

Merge changes I64c507bc,I51cbe48b,I139f792c into main

* changes:
  [le audio] Introduce the source sync queue to avoid race conditions
  [le audio] BassClient adding support for switching broadcast source
  [le audio] BassClient check and reset mPendingMetadata
parents 676a0a01 7e97b876
Loading
Loading
Loading
Loading
+85 −15
Original line number Diff line number Diff line
@@ -200,10 +200,10 @@ public class BassClientService extends ProfileService {
                }
                if (syncHandle != BassConstants.INVALID_SYNC_HANDLE) {
                    paRes.updateSyncHandle(syncHandle);
                    if (mSyncHandleToBroadcastIdMap != null
                            && paRes.getBroadcastId() != BassConstants.INVALID_BROADCAST_ID) {
                        // broadcast successfully synced, update the map
                        mSyncHandleToBroadcastIdMap.put(syncHandle, paRes.getBroadcastId());
                    if (paRes.getBroadcastId() != BassConstants.INVALID_BROADCAST_ID) {
                        // broadcast successfully synced
                        // update the sync handle for the broadcast source
                        updateSyncHandleForBroadcastId(syncHandle, paRes.getBroadcastId());
                    }
                }
                if (addressType != BassConstants.INVALID_ADV_ADDRESS_TYPE) {
@@ -473,6 +473,17 @@ public class BassClientService extends ProfileService {
        return BassConstants.INVALID_BROADCAST_ID;
    }

    void updateSyncHandleForBroadcastId(int syncHandle, int broadcastId) {
        if (mSyncHandleToBroadcastIdMap == null) {
            Log.e(TAG, "mSyncHandleToBroadcastIdMap is null");
            return;
        }

        mSyncHandleToBroadcastIdMap.entrySet().removeIf(entry -> entry.getValue() == broadcastId);
        mSyncHandleToBroadcastIdMap.put(syncHandle, broadcastId);
        log("Updated mSyncHandleToBroadcastIdMap: " + mSyncHandleToBroadcastIdMap);
    }

    private static synchronized void setBassClientService(BassClientService instance) {
        if (DBG) {
            Log.d(TAG, "setBassClientService(): set to: " + instance);
@@ -670,6 +681,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");
@@ -1153,11 +1199,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");
@@ -1235,8 +1276,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)) {
@@ -1287,13 +1358,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;
+126 −27
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ import android.os.Message;
import android.os.ParcelUuid;
import android.provider.DeviceConfig;
import android.util.Log;
import android.util.Pair;

import com.android.bluetooth.BluetoothMethodProxy;
import com.android.bluetooth.Utils;
@@ -106,6 +107,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
@@ -129,6 +131,8 @@ public class BassClientStateMachine extends StateMachine {
    private final Connecting mConnecting = new Connecting();
    private final ConnectedProcessing mConnectedProcessing = new ConnectedProcessing();
    private final FeatureFlags mFeatureFlags;
    private final List<Pair<ScanResult, Integer>> mSourceSyncRequestsQueue =
            new ArrayList<Pair<ScanResult, Integer>>();

    @VisibleForTesting
    final List<BluetoothGattCharacteristic> mBroadcastCharacteristics =
@@ -181,6 +185,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,9 +262,11 @@ public class BassClientStateMachine extends StateMachine {
        mPendingSourceId = -1;
        mPendingMetadata = null;
        mPendingSourceToAdd = null;
        mPendingSourceToSwitch = null;
        mCurrentMetadata.clear();
        mPendingRemove.clear();
        mPeriodicAdvCallbacksMap.clear();
        mSourceSyncRequestsQueue.clear();
    }

    Boolean hasPendingSourceOperation() {
@@ -424,8 +431,8 @@ public class BassClientStateMachine extends StateMachine {
            // null if no name present
            String broadcastName = checkAndParseBroadcastName(scanRecord);

            // Avoid duplicated sync requests for the same broadcast BIG
            if (isDuplicatedSyncRequest(broadcastId)) {
            // Avoid duplicated sync request if the same broadcast BIG is synced
            if (isSourceSynced(broadcastId)) {
                log("Skip duplicated sync request to broadcast id: " + broadcastId);
                return false;
            }
@@ -459,22 +466,18 @@ public class BassClientStateMachine extends StateMachine {
        return true;
    }

    private boolean isDuplicatedSyncRequest(int broadcastId) {
        // Either there is active sync to the same broadcast id or pending operation
        // This is required because selectSource can be triggered from both scanning(user)
        // and adding inactive source(auto)
    private boolean isSourceSynced(int broadcastId) {
        List<Integer> activeSyncedSrc = mService.getActiveSyncedSources(mDevice);
        if ((mPendingSourceToAdd != null && broadcastId == mPendingSourceToAdd.getBroadcastId())
                || (activeSyncedSrc != null
                        && activeSyncedSrc.contains(
                                mService.getSyncHandleForBroadcastId(broadcastId)))) {
            return true;
        }
        return false;
        return (activeSyncedSrc != null
                && activeSyncedSrc.contains(mService.getSyncHandleForBroadcastId(broadcastId)));
    }

    private void cancelActiveSync(Integer syncHandle) {
        log("cancelActiveSync: syncHandle = " + syncHandle);
        if (syncHandle == null) {
            // clean up the pending sync request if syncHandle is null
            mPeriodicAdvCallbacksMap.remove(BassConstants.INVALID_SYNC_HANDLE);
        }
        List<Integer> activeSyncedSrc = mService.getActiveSyncedSources(mDevice);

        /* Stop sync if there is some running */
@@ -511,6 +514,7 @@ public class BassClientStateMachine extends StateMachine {
                Log.w(TAG, "unregisterSync:IllegalArgumentException");
                return false;
            }
            mPeriodicAdvCallbacksMap.remove(syncHandle);
        } else {
            log("calling unregisterSync, not found syncHandle: " + syncHandle);
        }
@@ -665,12 +669,15 @@ 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 {
                if (mService.isLocalBroadcast(mPendingMetadata)) {
                    int advHandle = mPendingMetadata.getSourceAdvertisingSid();
                BluetoothLeBroadcastMetadata currentMetadata =
                        getCurrentBroadcastMetadata(recvState.getSourceId());
                if (mService.isLocalBroadcast(currentMetadata)) {
                    int advHandle = currentMetadata.getSourceAdvertisingSid();
                    serviceData = 0x000000FF & recvState.getSourceId();
                    serviceData = serviceData << 8;
                    // Address we set in the Source Address can differ from the address in the air
@@ -873,10 +880,12 @@ 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;
                }
                checkAndUpdateBroadcastCode(recvState);
                processPASyncState(recvState);
@@ -888,12 +897,31 @@ 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) {
                        setCurrentBroadcastMetadata(recvState.getSourceId(), mPendingMetadata);
                        mPendingMetadata = null;
                    }
                    mService.getCallbacks().notifySourceModified(mDevice,
                            recvState.getSourceId(), BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
                    checkAndUpdateBroadcastCode(recvState);
@@ -1103,6 +1131,14 @@ public class BassClientStateMachine extends StateMachine {
                mPeriodicAdvCallbacksMap.remove(BassConstants.INVALID_SYNC_HANDLE);
            }
            mPendingSourceToAdd = null;
            if (!mSourceSyncRequestsQueue.isEmpty()) {
                log("Processing the next source to sync");
                Pair<ScanResult, Integer> queuedSourceToSync = mSourceSyncRequestsQueue.remove(0);
                Message msg = obtainMessage(SELECT_BCAST_SOURCE);
                msg.obj = queuedSourceToSync.first;
                msg.arg1 = queuedSourceToSync.second;
                sendMessage(msg);
            }
        }

        @Override
@@ -1131,8 +1167,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.");
@@ -1697,12 +1736,49 @@ public class BassClientStateMachine extends StateMachine {
                case SELECT_BCAST_SOURCE:
                    ScanResult scanRes = (ScanResult) message.obj;
                    boolean auto = ((int) message.arg1) == BassConstants.AUTO;
                    // check if invalid sync handle exists indicating a pending sync request
                    if (mPeriodicAdvCallbacksMap.containsKey(BassConstants.INVALID_SYNC_HANDLE)) {
                        log(
                                "SELECT_BCAST_SOURCE queued due to waiting for a previous sync"
                                        + " response");
                        mSourceSyncRequestsQueue.add(
                                new Pair<ScanResult, Integer>(scanRes, message.arg1));
                    } else {
                        selectSource(scanRes, auto);
                    }
                    break;
                case REACHED_MAX_SOURCE_LIMIT:
                    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;

@@ -1719,7 +1795,10 @@ public class BassClientStateMachine extends StateMachine {
                                && mService.getCachedBroadcast(broadcastId) != null) {
                            // If the source has been synced before, try to re-sync(auto/true)
                            // with the source by previously cached scan result
                            selectSource(mService.getCachedBroadcast(broadcastId), true);
                            Message msg = obtainMessage(SELECT_BCAST_SOURCE);
                            msg.obj = mService.getCachedBroadcast(broadcastId);
                            msg.arg1 = BassConstants.AUTO;
                            sendMessage(msg);
                            mPendingSourceToAdd = metaData;
                        } else {
                            mService.getCallbacks().notifySourceAddFailed(mDevice, metaData,
@@ -1831,6 +1910,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:
@@ -1895,6 +1984,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:
@@ -1999,6 +2095,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)
@@ -2091,6 +2188,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
+33 −14

File changed.

Preview size limit exceeded, changes collapsed.