Loading android/app/src/com/android/bluetooth/bass_client/BassClientService.java +59 −0 Original line number Diff line number Diff line Loading @@ -3378,6 +3378,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: " Loading Loading @@ -3788,6 +3816,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 = Loading Loading @@ -3840,8 +3869,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) { Loading Loading @@ -4100,6 +4154,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 Loading android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java +16 −7 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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() Loading @@ -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(); Loading android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -6659,4 +6659,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"); } } } } android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java +34 −0 Original line number Diff line number Diff line Loading @@ -2619,6 +2619,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); Loading Loading @@ -2727,6 +2760,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); Loading Loading
android/app/src/com/android/bluetooth/bass_client/BassClientService.java +59 −0 Original line number Diff line number Diff line Loading @@ -3378,6 +3378,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: " Loading Loading @@ -3788,6 +3816,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 = Loading Loading @@ -3840,8 +3869,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) { Loading Loading @@ -4100,6 +4154,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 Loading
android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java +16 −7 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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() Loading @@ -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(); Loading
android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java +54 −0 Original line number Diff line number Diff line Loading @@ -6659,4 +6659,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"); } } } }
android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java +34 −0 Original line number Diff line number Diff line Loading @@ -2619,6 +2619,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); Loading Loading @@ -2727,6 +2760,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); Loading