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

Commit e8f981a3 authored by Rongxuan Liu's avatar Rongxuan Liu
Browse files

[le audio] BassClient enhancement to allow re-sync with known inactive source

In current design, assistant will not be able to add source for inactive source. This commit implemented a solution to allow re-sync to previously cached source.

When syncing with a new source, the assistant will cache the scan result. Then if later the device stoped the sync or user chose to remove the source, adding source operation will check the previous cached scan results. If the source has been found, the assistant will try to resync to this known remote source.

No need to clean up the cached broadcast when stopping the scan since
starting scan will do the cleanup.

Bug: 299422016
Test: atest BassClientStateMachineTest
Test: manual test with broadcast

Change-Id: I09f0c261fd5c937fca75f84046199d6aa6d2c768
parent 2278a1d3
Loading
Loading
Loading
Loading
+12 −5
Original line number Diff line number Diff line
@@ -83,7 +83,7 @@ public class BassClientService extends ProfileService {

    private final Map<BluetoothDevice, BassClientStateMachine> mStateMachines = new HashMap<>();
    private final Object mSearchScanCallbackLock = new Object();
    private final Map<Integer, ScanResult> mScanBroadcasts = new HashMap<>();
    private final Map<Integer, ScanResult> mCachedBroadcasts = new HashMap<>();

    private final Map<BluetoothDevice, List<Pair<Integer, Object>>> mPendingGroupOp =
            new ConcurrentHashMap<>();
@@ -280,6 +280,10 @@ public class BassClientService extends ProfileService {
        return currentSources;
    }

    ScanResult getCachedBroadcast(int broadcastId) {
        return mCachedBroadcasts.get(broadcastId);
    }

    public Callbacks getCallbacks() {
        return mCallbacks;
    }
@@ -392,6 +396,9 @@ public class BassClientService extends ProfileService {
        if (mPendingGroupOp != null) {
            mPendingGroupOp.clear();
        }
        if (mCachedBroadcasts != null) {
            mCachedBroadcasts.clear();
        }
        return true;
    }

@@ -1009,9 +1016,9 @@ public class BassClientService extends ProfileService {
                    sEventLogger.logd(DBG, TAG, "Broadcast Source Found: Broadcast ID: "
                            + broadcastId);

                    if (mScanBroadcasts.get(broadcastId) == null) {
                    if (mCachedBroadcasts.get(broadcastId) == null) {
                        log("selectBroadcastSource: broadcastId " + broadcastId);
                        mScanBroadcasts.put(broadcastId, result);
                        mCachedBroadcasts.put(broadcastId, result);
                        synchronized (mStateMachines) {
                            for (BassClientStateMachine sm : mStateMachines.values()) {
                                if (sm.isConnected()) {
@@ -1026,7 +1033,8 @@ public class BassClientService extends ProfileService {
                    Log.e(TAG, "Scan Failure:" + errorCode);
                }
            };
            mScanBroadcasts.clear();
            // when starting scan, clear the previously cached broadcast scan results
            mCachedBroadcasts.clear();
            // clear previous sources notify flag before scanning new result
            // this is to make sure the active sources are notified even if already synced
            if (mPeriodicAdvertisementResultMap != null) {
@@ -1079,7 +1087,6 @@ public class BassClientService extends ProfileService {
            mSearchScanCallback = null;
            sEventLogger.logd(DBG, TAG, "stopSearchingForSources");
            mCallbacks.notifySearchStopped(BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST);
            mScanBroadcasts.clear();
        }
    }

+45 −17
Original line number Diff line number Diff line
@@ -164,6 +164,8 @@ public class BassClientStateMachine extends StateMachine {
    boolean mAutoTriggered = false;
    private boolean mDefNoPAS = false;
    private boolean mForceSB = false;
    @VisibleForTesting
    BluetoothLeBroadcastMetadata mPendingSourceToAdd = null;
    private int mBroadcastSourceIdLength = 3;
    @VisibleForTesting
    byte mNextSourceId = 0;
@@ -238,6 +240,7 @@ public class BassClientStateMachine extends StateMachine {
        mPendingOperation = -1;
        mPendingSourceId = -1;
        mPendingMetadata = null;
        mPendingSourceToAdd = null;
        mCurrentMetadata.clear();
        mPendingRemove.clear();
    }
@@ -383,18 +386,7 @@ public class BassClientStateMachine extends StateMachine {
        log("selectSource: ScanResult " + scanRes);
        mAutoTriggered = autoTriggered;
        mPASyncRetryCounter = 1;
        // Cache Scan res for Retrys
        mScanRes = scanRes;
        try {
            BluetoothMethodProxy.getInstance().periodicAdvertisingManagerRegisterSync(
                    mPeriodicAdvManager, scanRes, 0, BassConstants.PSYNC_TIMEOUT,
                    mPeriodicAdvCallback, null);
        } catch (IllegalArgumentException ex) {
            Log.w(TAG, "registerSync:IllegalArgumentException");
            Message message = obtainMessage(STOP_SCAN_OFFLOAD);
            sendMessage(message);
            return false;
        }

        // updating mainly for Address type and PA Interval here
        // extract BroadcastId from ScanResult
        ScanRecord scanRecord = scanRes.getScanRecord();
@@ -417,6 +409,27 @@ public class BassClientStateMachine extends StateMachine {
            // null if no name present
            String broadcastName = checkAndParseBroadcastName(scanRecord);

            // Avoid duplicated sync requests for the same broadcast BIG
            // This is required because selectSource can be triggered from both scanning(user)
            // and adding inactive source(auto)
            if (broadcastId != BassConstants.INVALID_BROADCAST_ID
                    && mPendingSourceToAdd != null
                    && broadcastId == mPendingSourceToAdd.getBroadcastId()) {
                log("Skip duplicated sync request to broadcast id: " + broadcastId);
                return false;
            }

            try {
                BluetoothMethodProxy.getInstance().periodicAdvertisingManagerRegisterSync(
                        mPeriodicAdvManager, scanRes, 0, BassConstants.PSYNC_TIMEOUT,
                        mPeriodicAdvCallback, null);
            } catch (IllegalArgumentException ex) {
                Log.w(TAG, "registerSync:IllegalArgumentException");
                Message message = obtainMessage(STOP_SCAN_OFFLOAD);
                sendMessage(message);
                return false;
            }

            mService.updatePeriodicAdvertisementResultMap(
                    scanRes.getDevice(),
                    scanRes.getDevice().getAddressType(),
@@ -592,15 +605,20 @@ public class BassClientStateMachine extends StateMachine {
                                PSYNC_ACTIVE_TIMEOUT, BassConstants.PSYNC_ACTIVE_TIMEOUT_MS);
                        mService.addActiveSyncedSource(mDevice, device);
                        mFirstTimeBisDiscoveryMap.put(syncHandle, true);
                        if (mPendingSourceToAdd != null) {
                            Message message = obtainMessage(ADD_BCAST_SOURCE);
                            message.obj = mPendingSourceToAdd;
                            sendMessage(message);
                        }
                    } else {
                        log("failed to sync to PA: " + mPASyncRetryCounter);
                        mScanRes = null;
                        if (!mAutoTriggered) {
                            Message message = obtainMessage(STOP_SCAN_OFFLOAD);
                            sendMessage(message);
                        }
                        mAutoTriggered = false;
                    }
                    mPendingSourceToAdd = null;
                }

                @Override
@@ -1605,12 +1623,22 @@ public class BassClientStateMachine extends StateMachine {

                    HashSet<BluetoothDevice> activeSyncedSrc =
                            mService.getActiveSyncedSources(mDevice);
                    BluetoothDevice sourceDevice = metaData.getSourceDevice();
                    if (!mService.isLocalBroadcast(metaData)
                            && (activeSyncedSrc == null
                                    || !activeSyncedSrc.contains(metaData.getSourceDevice()))) {
                        log("Adding non-active synced source: " + metaData.getSourceDevice());
                                    || !activeSyncedSrc.contains(sourceDevice))) {
                        log("Adding inactive source: " + sourceDevice);
                        int broadcastId = metaData.getBroadcastId();
                        if (broadcastId != BassConstants.INVALID_BROADCAST_ID
                                && 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);
                            mPendingSourceToAdd = metaData;
                        } else {
                            mService.getCallbacks().notifySourceAddFailed(mDevice, metaData,
                                    BluetoothStatusCodes.ERROR_UNKNOWN);
                        }
                        break;
                    }

+84 −0
Original line number Diff line number Diff line
@@ -1661,6 +1661,90 @@ public class BassClientStateMachineTest {
        verify(btGatt).writeCharacteristic(any());
    }

    @Test
    public void selectBcastSource_withSameBroadcastId() {
        initToConnectedState();

        byte[] scanRecord = new byte[]{
                0x02, 0x01, 0x1a, // advertising flags
                0x05, 0x02, 0x52, 0x18, 0x0a, 0x11, // 16 bit service uuids
                0x04, 0x09, 0x50, 0x65, 0x64, // name
                0x02, 0x0A, (byte) 0xec, // tx power level
                0x05, 0x30, 0x54, 0x65, 0x73, 0x74, // broadcast name: Test
                0x06, 0x16, 0x52, 0x18, 0x2A, 0x00, 0x00, // service data, broadcast id 42
                0x08, 0x16, 0x56, 0x18, 0x07, 0x03, 0x06, 0x07, 0x08,
                // service data - public broadcast,
                // feature - 0x7, metadata len - 0x3, metadata - 0x6, 0x7, 0x8
                0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // manufacturer specific data
                0x03, 0x50, 0x01, 0x02, // an unknown data type won't cause trouble
        };
        ScanRecord record = ScanRecord.parseFromBytes(scanRecord);
        ScanResult scanResult = new ScanResult(mTestDevice, 0, 0, 0, 0, 0, 0, 0, record, 0);
        mBassClientStateMachine.mPendingSourceToAdd = createBroadcastMetadata();

        doNothing().when(mMethodProxy).periodicAdvertisingManagerRegisterSync(
                any(), any(), anyInt(), anyInt(), any(), any());
        mBassClientStateMachine.sendMessage(
                SELECT_BCAST_SOURCE, BassConstants.AUTO, 0, scanResult);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
        // validate syncing to the same broadcast id will be skipped
        verify(mBassClientService, never()).updatePeriodicAdvertisementResultMap(
                any(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(),
                any(), any());

        mBassClientStateMachine.mPendingSourceToAdd = null;
        mBassClientStateMachine.sendMessage(
                SELECT_BCAST_SOURCE, BassConstants.AUTO, 0, scanResult);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
        verify(mBassClientService).updatePeriodicAdvertisementResultMap(
                any(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(),
                any(), eq(TEST_BROADCAST_NAME));
    }

    @Test
    public void addBcastSource_withCachedScanResults() {
        initToConnectedState();

        BassClientService.Callbacks callbacks = Mockito.mock(BassClientService.Callbacks.class);
        when(mBassClientService.getCallbacks()).thenReturn(callbacks);

        BluetoothLeBroadcastMetadata metadata = createBroadcastMetadata();
        when(mBassClientService.isLocalBroadcast(any())).thenReturn(false);
        when(mBassClientService.getActiveSyncedSources(any())).thenReturn(null);
        when(mBassClientService.getCachedBroadcast(anyInt())).thenReturn(null);
        mBassClientStateMachine.sendMessage(ADD_BCAST_SOURCE, metadata);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());

        verify(mBassClientService).getCallbacks();
        verify(callbacks).notifySourceAddFailed(any(), any(), anyInt());

        byte[] scanRecord = new byte[]{
                0x02, 0x01, 0x1a, // advertising flags
                0x05, 0x02, 0x52, 0x18, 0x0a, 0x11, // 16 bit service uuids
                0x04, 0x09, 0x50, 0x65, 0x64, // name
                0x02, 0x0A, (byte) 0xec, // tx power level
                0x05, 0x30, 0x54, 0x65, 0x73, 0x74, // broadcast name: Test
                0x06, 0x16, 0x52, 0x18, 0x2A, 0x00, 0x00, // service data, broadcast id 42
                0x08, 0x16, 0x56, 0x18, 0x07, 0x03, 0x06, 0x07, 0x08,
                // service data - public broadcast,
                // feature - 0x7, metadata len - 0x3, metadata - 0x6, 0x7, 0x8
                0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // manufacturer specific data
                0x03, 0x50, 0x01, 0x02, // an unknown data type won't cause trouble
        };
        ScanRecord record = ScanRecord.parseFromBytes(scanRecord);
        ScanResult scanResult = new ScanResult(mAdapter.getRemoteLeDevice("00:11:22:33:44:55",
                        BluetoothDevice.ADDRESS_TYPE_RANDOM), 0, 0, 0, 0, 0, 0, 0, record, 0);
        when(mBassClientService.getCachedBroadcast(anyInt())).thenReturn(scanResult);
        doNothing().when(mMethodProxy).periodicAdvertisingManagerRegisterSync(
                any(), any(), anyInt(), anyInt(), any(), any());
        // validate add source will trigger select source and update mPendingSourceToAdd
        mBassClientStateMachine.sendMessage(ADD_BCAST_SOURCE, metadata);
        TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());

        Assert.assertEquals(mBassClientStateMachine.mPendingSourceToAdd, metadata);
        verify(mBassClientService, never()).sendBroadcast(any(Intent.class), anyString(), any());
    }

    private void initToDisconnectedState() {
        allowConnection(true);
        allowConnectGatt(true);