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

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

[le audio] Avoid clearing cached sinks with multiple stream requests

Refactoring the usage of mPausedBroadcastSink, and add some fixes.
If the device is in handover state due to previous local stream request,
we don't need to clear the cached in following requests.

This is also to avoid the race condition that some sinks are removing
the source and we've cleared the previous cached sinks.

Bug: 355479593
Test: atest BassClientSericeTest
Test: manual test private broadcast with multiple unicast stream
requests
Flag: Exempt; trivial change covered with unit and manual test

Change-Id: Ie1ca9689f7155684de511a7c2692ef528e2e47d9
parent f94401ae
Loading
Loading
Loading
Loading
+23 −22
Original line number Diff line number Diff line
@@ -92,7 +92,6 @@ import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -148,7 +147,7 @@ public class BassClientService extends ProfileService {
    private final Map<BluetoothDevice, List<Integer>> mActiveSourceMap = new ConcurrentHashMap<>();
    private final Map<BluetoothDevice, BluetoothLeBroadcastMetadata> mBroadcastMetadataMap =
            new ConcurrentHashMap<>();
    private final LinkedList<BluetoothDevice> mPausedBroadcastSinks = new LinkedList<>();
    private final HashSet<BluetoothDevice> mPausedBroadcastSinks = new HashSet<>();
    private final Deque<AddSourceData> mPendingAddSources = new ArrayDeque<>();
    private final Map<Integer, HashSet<BluetoothDevice>> mLocalBroadcastReceivers =
            new ConcurrentHashMap<>();
@@ -704,6 +703,7 @@ public class BassClientService extends ProfileService {
            mLocalBroadcastReceivers.clear();
            mPendingGroupOp.clear();
            mBroadcastMetadataMap.clear();
            mPausedBroadcastSinks.clear();
        }
    }

@@ -1394,6 +1394,7 @@ public class BassClientService extends ProfileService {
        // Check if the device is disconnected - if unbond, remove the state machine
        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
            mPendingGroupOp.remove(device);
            mPausedBroadcastSinks.remove(device);

            int bondState = mAdapterService.getBondState(device);
            if (bondState == BluetoothDevice.BOND_NONE) {
@@ -2958,15 +2959,14 @@ public class BassClientService extends ProfileService {
    private void stopSourceReceivers(int broadcastId, boolean store) {
        Log.d(TAG, "stopSourceReceivers(), broadcastId: " + broadcastId + ", store: " + store);

        if (store && !mPausedBroadcastSinks.isEmpty()) {
            Log.w(TAG, "stopSourceReceivers(), paused broadcast sinks are replaced");
            sEventLogger.logd(TAG, "Clear broadcast sinks paused cache");
            mPausedBroadcastSinks.clear();
        }

        Map<BluetoothDevice, Integer> sourcesToRemove = new HashMap<>();

        for (BluetoothDevice device : getConnectedDevices()) {
            if (mPausedBroadcastSinks.contains(device)) {
                // Skip this device if it has been paused
                continue;
            }

            for (BluetoothLeBroadcastReceiveState receiveState : getAllSources(device)) {
                /* Check if local/last broadcast is the synced one. Invalid broadcast ID means
                 * that all receivers should be considered.
@@ -2976,7 +2976,7 @@ public class BassClientService extends ProfileService {
                    continue;
                }

                if (store && !mPausedBroadcastSinks.contains(device)) {
                if (store) {
                    sEventLogger.logd(TAG, "Add broadcast sink to paused cache: " + device);
                    mPausedBroadcastSinks.add(device);
                }
@@ -3130,18 +3130,12 @@ public class BassClientService extends ProfileService {
        }
    }

    /** Cache suspending sources */
    /** Cache suspending sources when broadcast paused */
    public void cacheSuspendingSources(int broadcastId) {
        sEventLogger.logd(TAG, "Cache suspending sources: " + broadcastId);
        List<Pair<BluetoothLeBroadcastReceiveState, BluetoothDevice>> sourcesToCache =
                getReceiveStateDevicePairs(broadcastId);

        if (!mPausedBroadcastSinks.isEmpty()) {
            Log.w(TAG, "cacheSuspendingSources(), paused broadcast sinks are replaced");
            sEventLogger.logd(TAG, "Clear broadcast sinks paused cache");
            mPausedBroadcastSinks.clear();
        }

        for (Pair<BluetoothLeBroadcastReceiveState, BluetoothDevice> pair : sourcesToCache) {
            mPausedBroadcastSinks.add(pair.second);
        }
@@ -3173,8 +3167,9 @@ public class BassClientService extends ProfileService {
    public void resumeReceiversSourceSynchronization() {
        sEventLogger.logd(TAG, "Resume receivers source synchronization");

        while (!mPausedBroadcastSinks.isEmpty()) {
            BluetoothDevice sink = mPausedBroadcastSinks.remove();
        Iterator<BluetoothDevice> iterator = mPausedBroadcastSinks.iterator();
        while (iterator.hasNext()) {
            BluetoothDevice sink = iterator.next();
            sEventLogger.logd(TAG, "Remove broadcast sink from paused cache: " + sink);
            BluetoothLeBroadcastMetadata metadata = mBroadcastMetadataMap.get(sink);

@@ -3182,9 +3177,11 @@ public class BassClientService extends ProfileService {
                if (metadata == null) {
                    Log.w(
                            TAG,
                            "resumeReceiversSourceSynchronization: failed to get metadata to resume"
                                    + " sink: "
                            "resumeReceiversSourceSynchronization: failed to get metadata to"
                                    + " resume sink: "
                                    + sink);
                    // remove the device from mPausedBroadcastSinks
                    iterator.remove();
                    continue;
                }

@@ -3207,6 +3204,8 @@ public class BassClientService extends ProfileService {

                    if (statusCode != BluetoothStatusCodes.SUCCESS) {
                        mCallbacks.notifySourceModifyFailed(sink, sourceId, statusCode);
                        // remove the device from mPausedBroadcastSinks
                        iterator.remove();
                        continue;
                    }

@@ -3238,11 +3237,13 @@ public class BassClientService extends ProfileService {
                } else {
                    Log.w(
                            TAG,
                            "resumeReceiversSourceSynchronization: failed to get metadata to resume"
                                    + " sink: "
                            "resumeReceiversSourceSynchronization: failed to get metadata to"
                                    + " resume sink: "
                                    + sink);
                }
            }
            // remove the device from mPausedBroadcastSinks
            iterator.remove();
        }
    }

+163 −177
Original line number Diff line number Diff line
@@ -1096,6 +1096,66 @@ public class BassClientServiceTest {
        TestUtils.waitForLooperToFinishScheduledTask(mBassClientService.getCallbacks().getLooper());
    }

    private void prepareRemoteSourceState(BluetoothLeBroadcastMetadata meta, boolean isBisSynced) {
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);

                if (isBisSynced) {
                    // Update receiver state
                    injectRemoteSourceStateChanged(
                            sm,
                            meta,
                            TEST_SOURCE_ID,
                            BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                            meta.isEncrypted()
                                    ? BluetoothLeBroadcastReceiveState
                                            .BIG_ENCRYPTION_STATE_DECRYPTING
                                    : BluetoothLeBroadcastReceiveState
                                            .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                            null,
                            (long) 0x00000001);
                }
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);

                if (isBisSynced) {
                    // Update receiver state
                    injectRemoteSourceStateChanged(
                            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,
                            (long) 0x00000002);
                }
            }
        }
    }

    /**
     * Test whether service.addSource() does send proper messages to all the state machines within
     * the Csip coordinated group
@@ -1178,31 +1238,7 @@ public class BassClientServiceTest {
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);
            }
        }
        prepareRemoteSourceState(meta, false);

        // Update broadcast source using other member of the same group
        BluetoothLeBroadcastMetadata metaUpdate =
@@ -1245,31 +1281,7 @@ public class BassClientServiceTest {
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);
            }
        }
        prepareRemoteSourceState(meta, false);

        // Remove broadcast source using other member of the same group
        mBassClientService.removeSource(mCurrentDevice1, TEST_SOURCE_ID + 1);
@@ -1390,32 +1402,7 @@ public class BassClientServiceTest {
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        verifyAddSourceForGroup(meta);
        // Inject source added
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);
            }
        }
        prepareRemoteSourceState(meta, false);

        // Remove broadcast source
        mBassClientService.removeSource(mCurrentDevice, TEST_SOURCE_ID);
@@ -1747,31 +1734,7 @@ public class BassClientServiceTest {
        // Prepare valid source for group
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);
            }
        }
        prepareRemoteSourceState(meta, false);

        // Verify errors are reported for the entire group
        mBassClientService.modifySource(mCurrentDevice, TEST_SOURCE_ID, null);
@@ -3277,31 +3240,7 @@ public class BassClientServiceTest {
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);
        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);
            }
        }
        prepareRemoteSourceState(meta, false);

        if (Flags.leaudioBroadcastAssistantPeripheralEntrustment()) {
            for (BassClientStateMachine sm : mStateMachines.values()) {
@@ -3463,58 +3402,9 @@ public class BassClientServiceTest {
                .getAllBroadcastMetadata();

        verifyAddSourceForGroup(meta);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            if (sm.getDevice().equals(mCurrentDevice)) {
                injectRemoteSourceStateSourceAdded(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null);
        prepareRemoteSourceState(meta, true);

                // Update receiver state
                injectRemoteSourceStateChanged(
                        sm,
                        meta,
                        TEST_SOURCE_ID,
                        BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_IDLE,
                        meta.isEncrypted()
                                ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING
                                : BluetoothLeBroadcastReceiveState
                                        .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED,
                        null,
                        (long) 0x00000001);
        verify(mLeAudioService).activeBroadcastAssistantNotification(eq(true));
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                injectRemoteSourceStateSourceAdded(
                        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);

                // Update receiver state
                injectRemoteSourceStateChanged(
                        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,
                        (long) 0x00000002);
            }
        }

        /* Unicast would like to stream */
        mBassClientService.handleUnicastSourceStreamStatusChange(
@@ -3660,6 +3550,102 @@ public class BassClientServiceTest {
        }
    }

    @Test
    public void testHandleUnicastSourceStreamStatusChange_MultipleRequests() {
        mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_ASSISTANT_PERIPHERAL_ENTRUSTMENT);

        prepareConnectedDeviceGroup();
        startSearchingForSources();
        onScanResult(mSourceDevice, TEST_BROADCAST_ID);
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);
        BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID);

        /* Fake external broadcast - no Broadcast Metadata from LE Audio service */
        doReturn(new ArrayList<BluetoothLeBroadcastMetadata>())
                .when(mLeAudioService)
                .getAllBroadcastMetadata();

        verifyAddSourceForGroup(meta);
        prepareRemoteSourceState(meta, true);

        verify(mLeAudioService).activeBroadcastAssistantNotification(eq(true));

        /* Unicast would like to stream */
        mBassClientService.handleUnicastSourceStreamStatusChange(
                3 /* STATUS_LOCAL_STREAM_REQUESTED_NO_CONTEXT_VALIDATE */);

        /* Imitate broadcast source stop, sink notify about loosing BIS sync */
        for (BassClientStateMachine sm : mStateMachines.values()) {
            // Inject source removed
            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.REMOVE_BCAST_SOURCE)
                            .findFirst();
            assertThat(msg.isPresent()).isEqualTo(true);

            if (sm.getDevice().equals(mCurrentDevice)) {
                assertThat(msg.get().arg1).isEqualTo(TEST_SOURCE_ID);
                injectRemoteSourceStateRemoval(sm, TEST_SOURCE_ID);
            } else if (sm.getDevice().equals(mCurrentDevice1)) {
                assertThat(msg.get().arg1).isEqualTo(TEST_SOURCE_ID + 1);
                injectRemoteSourceStateRemoval(sm, TEST_SOURCE_ID + 1);
            }
        }

        assertThat(mStateMachines.size()).isEqualTo(2);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            Mockito.clearInvocations(sm);
        }
        // Make another stream request with no context validate
        // and verify sm didn't get REMOVE_BCAST_SOURCE
        mBassClientService.handleUnicastSourceStreamStatusChange(
                3 /* STATUS_LOCAL_STREAM_REQUESTED_NO_CONTEXT_VALIDATE */);

        // Make another stream request
        // and verify sinks to resume remain unchanged later
        mBassClientService.handleUnicastSourceStreamStatusChange(
                0 /* STATUS_LOCAL_STREAM_REQUESTED */);

        assertThat(mStateMachines.size()).isEqualTo(2);
        for (BassClientStateMachine sm : mStateMachines.values()) {
            ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
            verify(sm, never()).sendMessage(messageCaptor.capture());

            Message msg =
                    messageCaptor.getAllValues().stream()
                            .filter(
                                    m ->
                                            (m.what == BassClientStateMachine.REMOVE_BCAST_SOURCE)
                                                    && (m.arg1 == TEST_SOURCE_ID))
                            .findFirst()
                            .orElse(null);
            assertThat(msg).isNull();
        }

        /* Unicast finished streaming */
        mBassClientService.handleUnicastSourceStreamStatusChange(
                2 /* STATUS_LOCAL_STREAM_SUSPENDED */);

        // Verify all group members resume with the previous cached source
        for (BassClientStateMachine sm : mStateMachines.values()) {
            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)
                                                    && (m.obj == meta))
                            .findFirst()
                            .orElse(null);
            assertThat(msg).isNotNull();
        }
    }

    @Test
    public void testIsAnyReceiverReceivingBroadcast() {
        prepareConnectedDeviceGroup();