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

Commit 23d827db authored by Rongxuan Liu's avatar Rongxuan Liu
Browse files

[le audio] Resync device to its peer active broadcast source

When remote device reconnected and its peer is still syncing to the
source, we should assist this device to resync to the same source.
Also this CL implements the queuing add source if the BASS state is not
ready.
With this, stack will handle the case if adding source happened
before BASS state is ready.

Bug: 364114064
Bug: 363168099
Test: atest BassClientServiceTest BassClientStateMachineTest
Change-Id: Ie8876be3fc3393d720239b67671e2f26512ca3c2
parent 1faa169b
Loading
Loading
Loading
Loading
+59 −0
Original line number Diff line number Diff line
@@ -3386,6 +3386,34 @@ public class BassClientService extends ProfileService {
        }
    }

    /** Handle device newly connected and its peer device still has active source */
    private void checkAndResumeBroadcast(BluetoothDevice sink) {
        BluetoothLeBroadcastMetadata metadata = mBroadcastMetadataMap.get(sink);
        if (metadata == null) {
            Log.d(TAG, "checkAndResumeBroadcast: no metadata available");
            return;
        }
        for (BluetoothDevice groupDevice : getTargetDeviceList(sink, true)) {
            if (groupDevice.equals(sink)) {
                continue;
            }
            // Check peer device
            Optional<BluetoothLeBroadcastReceiveState> receiver =
                    getOrCreateStateMachine(groupDevice).getAllSources().stream()
                            .filter(e -> e.getBroadcastId() == metadata.getBroadcastId())
                            .findAny();
            if (receiver.isPresent()
                    && !getAllSources(sink).stream()
                            .anyMatch(
                                    rs ->
                                            (rs.getBroadcastId()
                                                    == receiver.get().getBroadcastId()))) {
                Log.d(TAG, "checkAndResumeBroadcast: restore the source for device: " + sink);
                addSource(sink, metadata, false);
            }
        }
    }

    private void logPausedBroadcastsAndSinks() {
        log(
                "mPausedBroadcastIds: "
@@ -3796,6 +3824,7 @@ public class BassClientService extends ProfileService {
        private static final int MSG_SOURCE_REMOVED_FAILED = 11;
        private static final int MSG_RECEIVESTATE_CHANGED = 12;
        private static final int MSG_SOURCE_LOST = 13;
        private static final int MSG_BASS_STATE_READY = 14;

        @GuardedBy("mCallbacksList")
        private final RemoteCallbackList<IBluetoothLeBroadcastAssistantCallback> mCallbacksList =
@@ -3848,8 +3877,33 @@ public class BassClientService extends ProfileService {
            }
        }

        private boolean handleServiceInternalMessage(Message msg) {
            boolean isMsgHandled = false;
            if (sService == null) {
                Log.e(TAG, "Service is null");
                return isMsgHandled;
            }
            BluetoothDevice sink;

            switch (msg.what) {
                case MSG_BASS_STATE_READY:
                    sink = (BluetoothDevice) msg.obj;
                    sService.checkAndResumeBroadcast(sink);
                    isMsgHandled = true;
                    break;
                default:
                    break;
            }
            return isMsgHandled;
        }

        @Override
        public void handleMessage(Message msg) {
            if (handleServiceInternalMessage(msg)) {
                log("Handled internal message: " + msg.what);
                return;
            }

            checkForPendingGroupOpRequest(msg);

            synchronized (mCallbacksList) {
@@ -4108,6 +4162,11 @@ public class BassClientService extends ProfileService {
            sEventLogger.logd(TAG, "notifySourceLost: broadcastId: " + broadcastId);
            obtainMessage(MSG_SOURCE_LOST, 0, broadcastId).sendToTarget();
        }

        void notifyBassStateReady(BluetoothDevice sink) {
            sEventLogger.logd(TAG, "notifyBassStateReady: sink: " + sink);
            obtainMessage(MSG_BASS_STATE_READY, sink).sendToTarget();
        }
    }

    @Override
+16 −7
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;

import static com.android.bluetooth.flags.Flags.leaudioBigDependsOnAudioState;
import static com.android.bluetooth.flags.Flags.leaudioBroadcastResyncHelper;

import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -968,6 +969,11 @@ public class BassClientStateMachine extends StateMachine {
        mBroadcastSyncStats.clear();
    }

    private boolean isSourceAbsent(BluetoothLeBroadcastReceiveState recvState) {
        return recvState.getSourceDevice() == null
                || recvState.getSourceDevice().getAddress().equals("00:00:00:00:00:00");
    }

    private void checkAndUpdateBroadcastCode(BluetoothLeBroadcastReceiveState recvState) {
        log("checkAndUpdateBroadcastCode");
        // Whenever receive state indicated code requested, assistant should set the broadcast code
@@ -1149,14 +1155,18 @@ public class BassClientStateMachine extends StateMachine {
                return;
            }
            mBluetoothLeBroadcastReceiveStates.put(characteristic.getInstanceId(), recvState);
            if (!isSourceAbsent(recvState)) {
                checkAndUpdateBroadcastCode(recvState);
                processPASyncState(recvState);
            }
            if (leaudioBroadcastResyncHelper()) {
                // Notify service BASS state ready for operations
                mService.getCallbacks().notifyBassStateReady(mDevice);
            }
        } else {
            log("Updated receiver state: " + recvState);
            mBluetoothLeBroadcastReceiveStates.replace(characteristic.getInstanceId(), recvState);
            String emptyBluetoothDevice = "00:00:00:00:00:00";
            if (oldRecvState.getSourceDevice() == null
                    || oldRecvState.getSourceDevice().getAddress().equals(emptyBluetoothDevice)) {
            if (isSourceAbsent(oldRecvState)) {
                log("New Source Addition");
                removeMessages(CANCEL_PENDING_SOURCE_OPERATION);
                mService.getCallbacks()
@@ -1170,8 +1180,7 @@ public class BassClientStateMachine extends StateMachine {
                processPASyncState(recvState);
                processSyncStateChangeStats(recvState);
            } else {
                if (recvState.getSourceDevice() == null
                        || recvState.getSourceDevice().getAddress().equals(emptyBluetoothDevice)) {
                if (isSourceAbsent(recvState)) {
                    BluetoothDevice removedDevice = oldRecvState.getSourceDevice();
                    log("sourceInfo removal " + removedDevice);
                    int prevSourceId = oldRecvState.getSourceId();
+54 −0
Original line number Diff line number Diff line
@@ -6661,4 +6661,58 @@ public class BassClientServiceTest {
                .periodicAdvertisingManagerRegisterSync(
                        any(), any(), anyInt(), anyInt(), any(), any());
    }

    /**
     * Test add source will be triggered if new device connected and its peer is synced to broadcast
     * source
     */
    @Test
    @EnableFlags({
        Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
        Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE
    })
    public void sinkBassStateReady_addSourceIfPeerDeviceSynced() {
        // Imitate broadcast being active
        doReturn(true).when(mLeAudioService).isPlaying(TEST_BROADCAST_ID);
        prepareTwoSynchronizedDevicesForLocalBroadcast();

        mBassClientService.getCallbacks().notifyBassStateReady(mCurrentDevice);
        TestUtils.waitForLooperToFinishScheduledTask(mBassClientService.getCallbacks().getLooper());

        assertThat(mStateMachines.size()).isEqualTo(2);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            // No adding source if device remain synced
            verify(sm, never()).sendMessage(any());
        }

        // Remove source on the mCurrentDevice
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateRemoval(sm, TEST_SOURCE_ID);
            }
        }

        mBassClientService.getCallbacks().notifyBassStateReady(mCurrentDevice);
        TestUtils.waitForLooperToFinishScheduledTask(mBassClientService.getCallbacks().getLooper());

        for (BassClientStateMachine sm : mStateMachines.values()) {
            // Verify mCurrentDevice is resuming the broadcast
            if (sm.getDevice().equals(mCurrentDevice1)) {
                verify(sm, never()).sendMessage(any());
            } else if (sm.getDevice().equals(mCurrentDevice)) {
                ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
                verify(sm, atLeast(1)).sendMessage(messageCaptor.capture());

                Message msg =
                        messageCaptor.getAllValues().stream()
                                .filter(m -> (m.what == BassClientStateMachine.ADD_BCAST_SOURCE))
                                .findFirst()
                                .orElse(null);
                assertThat(msg).isNotNull();
                clearInvocations(sm);
            } else {
                throw new AssertionError("Unexpected device");
            }
        }
    }
}
+34 −0
Original line number Diff line number Diff line
@@ -2519,6 +2519,39 @@ public class BassClientStateMachineTest {
                        eq(0x3)); // STATS_SYNC_AUDIO_SYNC_SUCCESS
    }

    @Test
    @EnableFlags({
        Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
        Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE
    })
    public void sinkConnected_queueAddingSourceForReceiveStateReady() {
        mBassClientStateMachine.connectGatt(true);
        BluetoothGattCallback cb = mBassClientStateMachine.mGattCallback;
        cb.onMtuChanged(null, 23, GATT_SUCCESS);
        initToConnectedState();

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

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

        // Initial receive state with empty source device
        generateBroadcastReceiveStatesAndVerify(
                mEmptyTestDevice,
                TEST_SOURCE_ID,
                BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                0x0L);
        // Verify notifyBassStateReady is called
        verify(callbacks).notifyBassStateReady(eq(mTestDevice));
    }

    private void initToConnectingState() {
        allowConnection(true);
        allowConnectGatt(true);
@@ -2627,6 +2660,7 @@ public class BassClientStateMachineTest {
    private void prepareInitialReceiveStateForGatt() {
        initToConnectedState();
        mBassClientStateMachine.connectGatt(true);

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