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

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

le_audio: Avoid sound effects while adding/switching broadcast source

When listening to the external broadcast, we should not play sound
effects for handover. This is a follow-up improvement change with aosp/2985512 to cover more corner cases.

We should block sound effect once started to add source or perform
source switching, otherwise
we might facing some race condition that we're resuming CIS for sound
effects and buds are in transition for external broadcast.

If adding source failed or source got removed, the original logic will
restore the context mask.

Tag: #bug
Bug: 351095373
Bug: 336468573
Test: atest BassClientServiceTest BassClientStateMachineTest
Test: manually verified by QA
Change-Id: Ibe1726515be51b261ea6985559a374103d29b5ac
parent 1a7bcca7
Loading
Loading
Loading
Loading
+43 −12
Original line number Diff line number Diff line
@@ -902,6 +902,40 @@ public class BassClientService extends ProfileService {
        return false;
    }

    private boolean isAnyConnectedDeviceSwitchingSource() {
        for (BluetoothDevice device : getConnectedDevices()) {
            synchronized (mStateMachines) {
                BassClientStateMachine sm = getOrCreateStateMachine(device);
                // Need to check both mPendingSourceToSwitch and mPendingMetadata
                // to guard the whole source switching flow
                if (sm != null
                        && (sm.hasPendingSwitchingSourceOperation()
                                || sm.hasPendingSourceOperation())) {
                    return true;
                }
            }
        }
        return false;
    }

    private void checkAndSetGroupAllowedContextMask(BluetoothDevice sink) {
        LeAudioService leAudioService = mServiceFactory.getLeAudioService();
        if (leAudioService == null) {
            return;
        }

        if (leaudioAllowedContextMask()) {
            /* Don't bother active group (external broadcaster scenario) with SOUND EFFECTS */
            if (!mIsAllowedContextOfActiveGroupModified && isDevicePartOfActiveUnicastGroup(sink)) {
                leAudioService.setActiveGroupAllowedContextMask(
                        BluetoothLeAudio.CONTEXTS_ALL
                                & ~BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS,
                        BluetoothLeAudio.CONTEXTS_ALL);
                mIsAllowedContextOfActiveGroupModified = true;
            }
        }
    }

    private void checkAndResetGroupAllowedContextMask() {
        LeAudioService leAudioService = mServiceFactory.getLeAudioService();
        if (leAudioService == null) {
@@ -911,7 +945,8 @@ public class BassClientService extends ProfileService {
        if (leaudioAllowedContextMask()) {
            /* Restore allowed context mask for Unicast */
            if (mIsAllowedContextOfActiveGroupModified
                    && !hasAnyConnectedDeviceExternalBroadcastSource()) {
                    && !hasAnyConnectedDeviceExternalBroadcastSource()
                    && !isAnyConnectedDeviceSwitchingSource()) {
                leAudioService.setActiveGroupAllowedContextMask(
                        BluetoothLeAudio.CONTEXTS_ALL, BluetoothLeAudio.CONTEXTS_ALL);
                mIsAllowedContextOfActiveGroupModified = false;
@@ -935,17 +970,7 @@ public class BassClientService extends ProfileService {
                leAudioService.activeBroadcastAssistantNotification(true);
            }

            if (leaudioAllowedContextMask()) {
                /* Don't bother active group (external broadcaster scenario) with SOUND EFFECTS */
                if (!mIsAllowedContextOfActiveGroupModified
                        && isDevicePartOfActiveUnicastGroup(sink)) {
                    leAudioService.setActiveGroupAllowedContextMask(
                            BluetoothLeAudio.CONTEXTS_ALL
                                    & ~BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS,
                            BluetoothLeAudio.CONTEXTS_ALL);
                    mIsAllowedContextOfActiveGroupModified = true;
                }
            }
            checkAndSetGroupAllowedContextMask(sink);
        } else {
            /* Assistant become inactive */
            if (mIsAssistantActive && mPausedBroadcastSinks.isEmpty()) {
@@ -2608,6 +2633,10 @@ public class BassClientService extends ProfileService {
                        device, BassClientStateMachine.ADD_BCAST_SOURCE, sourceMetadata);
            }

            if (!isLocalBroadcast(sourceMetadata)) {
                checkAndSetGroupAllowedContextMask(device);
            }

            sEventLogger.logd(
                    TAG,
                    "Add Broadcast Source: "
@@ -3552,6 +3581,8 @@ public class BassClientService extends ProfileService {

        void notifySourceAddFailed(
                BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
            sService.checkAndResetGroupAllowedContextMask();

            sEventLogger.loge(
                    TAG,
                    "notifySourceAddFailed: sink: "
+10 −1
Original line number Diff line number Diff line
@@ -269,6 +269,10 @@ public class BassClientStateMachine extends StateMachine {
        }
    }

    Boolean hasPendingSwitchingSourceOperation() {
        return mPendingSourceToSwitch != null;
    }

    BluetoothLeBroadcastMetadata getCurrentBroadcastMetadata(Integer sourceId) {
        return mCurrentMetadata.getOrDefault(sourceId, null);
    }
@@ -995,7 +999,6 @@ public class BassClientStateMachine extends StateMachine {
                        Message message = obtainMessage(ADD_BCAST_SOURCE);
                        message.obj = mPendingSourceToSwitch;
                        sendMessage(message);
                        mPendingSourceToSwitch = null;
                    } else {
                        mService.getCallbacks()
                                .notifySourceRemoved(
@@ -2003,6 +2006,12 @@ public class BassClientStateMachine extends StateMachine {
                                .notifySourceAddFailed(
                                        mDevice, metaData, BluetoothStatusCodes.ERROR_UNKNOWN);
                    }
                    if (mPendingSourceToSwitch != null
                            && mPendingSourceToSwitch.getBroadcastId()
                                    == metaData.getBroadcastId()) {
                        // Clear pending source to switch when starting to add this new source
                        mPendingSourceToSwitch = null;
                    }
                    break;
                case UPDATE_BCAST_SOURCE:
                    metaData = (BluetoothLeBroadcastMetadata) message.obj;
+83 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeAudioCodecConfigMetadata;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastAssistant;
@@ -2624,6 +2625,88 @@ public class BassClientServiceTest {
        }
    }

    @Test
    public void testAddSourceForExternalBroadcast_triggerSetContextMask() {
        mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_ALLOWED_CONTEXT_MASK);
        mSetFlagsRule.enableFlags(
                Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE);
        mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_ASSISTANT_PERIPHERAL_ENTRUSTMENT);

        final int testGroupId = 1;
        prepareConnectedDeviceGroup();
        startSearchingForSources();
        onScanResult(mSourceDevice, TEST_BROADCAST_ID);
        onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE);

        /* Fake external broadcast - no Broadcast Metadata from LE Audio service */
        doReturn(new ArrayList<BluetoothLeBroadcastMetadata>())
                .when(mLeAudioService)
                .getAllBroadcastMetadata();
        doReturn(testGroupId).when(mLeAudioService).getActiveGroupId();
        doReturn(new ArrayList<BluetoothDevice>(Arrays.asList(mCurrentDevice)))
                .when(mLeAudioService)
                .getActiveDevices();

        assertThat(mBassClientService.getActiveSyncedSources().size()).isEqualTo(1);
        assertThat(mBassClientService.getActiveSyncedSources().contains(TEST_SYNC_HANDLE)).isTrue();
        assertThat(mBassClientService.getDeviceForSyncHandle(TEST_SYNC_HANDLE))
                .isEqualTo(mSourceDevice);
        assertThat(mBassClientService.getBroadcastIdForSyncHandle(TEST_SYNC_HANDLE))
                .isEqualTo(TEST_BROADCAST_ID);

        BluetoothLeBroadcastMetadata.Builder builder =
                new BluetoothLeBroadcastMetadata.Builder()
                        .setEncrypted(false)
                        .setSourceDevice(mSourceDevice, BluetoothDevice.ADDRESS_TYPE_RANDOM)
                        .setSourceAdvertisingSid(TEST_ADVERTISER_SID)
                        .setBroadcastId(TEST_BROADCAST_ID)
                        .setBroadcastCode(null)
                        .setPaSyncInterval(TEST_PA_SYNC_INTERVAL)
                        .setPresentationDelayMicros(TEST_PRESENTATION_DELAY_MS);
        // builder expect at least one subgroup
        builder.addSubgroup(createBroadcastSubgroup());
        BluetoothLeBroadcastMetadata meta = builder.build();

        // Add source to unsynced broadcast, causes synchronization first
        mBassClientService.addSource(mCurrentDevice, meta, true);

        // Verify setting allowed context mask is triggered
        verify(mLeAudioService)
                .setActiveGroupAllowedContextMask(
                        eq(
                                BluetoothLeAudio.CONTEXTS_ALL
                                        & ~BluetoothLeAudio.CONTEXT_TYPE_SOUND_EFFECTS),
                        eq(BluetoothLeAudio.CONTEXTS_ALL));
        handleHandoverSupport();

        // Verify all group members getting ADD_BCAST_SOURCE message
        assertThat(mStateMachines.size()).isEqualTo(2);
        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();
        }

        mBassClientService
                .getCallbacks()
                .notifySourceAddFailed(mCurrentDevice, meta, BluetoothStatusCodes.ERROR_UNKNOWN);
        TestUtils.waitForLooperToFinishScheduledTask(mBassClientService.getCallbacks().getLooper());

        // Verify resetting allowed context mask is triggered when switching source failed
        verify(mLeAudioService)
                .setActiveGroupAllowedContextMask(
                        eq(BluetoothLeAudio.CONTEXTS_ALL), eq(BluetoothLeAudio.CONTEXTS_ALL));
    }

    @Test
    public void testSelectSource_orderOfSyncRegistering() {
        mSetFlagsRule.enableFlags(
+5 −1
Original line number Diff line number Diff line
@@ -907,7 +907,6 @@ public class BassClientStateMachineTest {
        verify(callbacks)
                .notifyReceiveStateChanged(any(), eq(sourceId), receiveStateCaptor.capture());
        Assert.assertEquals(receiveStateCaptor.getValue().getSourceDevice(), mEmptyTestDevice);
        assertThat(mBassClientStateMachine.mPendingSourceToSwitch).isEqualTo(null);
    }

    @Test
@@ -1364,6 +1363,9 @@ public class BassClientStateMachineTest {

    @Test
    public void sendAddBcastSourceMessage_inConnectedState() {
        mSetFlagsRule.enableFlags(
                Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE);

        initToConnectedState();

        BassClientService.Callbacks callbacks = Mockito.mock(BassClientService.Callbacks.class);
@@ -1386,11 +1388,13 @@ public class BassClientStateMachineTest {
                Mockito.mock(BluetoothGattCharacteristic.class);
        mBassClientStateMachine.mBroadcastScanControlPoint = scanControlPoint;

        mBassClientStateMachine.mPendingSourceToSwitch = metadata;
        sendMessageAndVerifyTransition(
                mBassClientStateMachine.obtainMessage(ADD_BCAST_SOURCE, metadata),
                BassClientStateMachine.ConnectedProcessing.class);
        verify(scanControlPoint).setValue(any(byte[].class));
        verify(btGatt).writeCharacteristic(any());
        assertThat(mBassClientStateMachine.mPendingSourceToSwitch).isEqualTo(null);
    }

    @Test