Loading android/app/src/com/android/bluetooth/bass_client/BassClientService.java +26 −1 Original line number Diff line number Diff line Loading @@ -2236,7 +2236,8 @@ public class BassClientService extends ProfileService { for (int i = 0; i < receiveState.getNumSubgroups(); i++) { Long syncState = receiveState.getBisSyncState().get(i); /* Not synced to BIS of failed to sync to BIG */ if (syncState == 0x00000000 || syncState == 0xFFFFFFFF) { if (syncState == BassConstants.BIS_SYNC_NOT_SYNC_TO_BIS || syncState == BassConstants.BIS_SYNC_FAILED_SYNC_TO_BIG) { continue; } Loading @@ -2248,6 +2249,30 @@ public class BassClientService extends ProfileService { return false; } /** Get the active broadcast sink devices receiving broadcast stream */ public List<BluetoothDevice> getActiveBroadcastSinks() { List<BluetoothDevice> activeSinks = new ArrayList<>(); for (BluetoothDevice device : getConnectedDevices()) { // Check if any device's source in active sync state if (getAllSources(device).stream() .anyMatch( receiveState -> (receiveState.getBisSyncState().stream() .anyMatch( syncState -> syncState != BassConstants .BIS_SYNC_NOT_SYNC_TO_BIS && syncState != BassConstants .BIS_SYNC_FAILED_SYNC_TO_BIG)))) { activeSinks.add(device); } } return activeSinks; } /** Handle broadcast state changed */ public void notifyBroadcastStateChanged(int state, int broadcastId) { switch (state) { Loading android/app/src/com/android/bluetooth/bass_client/BassConstants.java +3 −0 Original line number Diff line number Diff line Loading @@ -81,4 +81,7 @@ public class BassConstants { public static final int PA_SYNC_DO_NOT_SYNC = 0x00; public static final int PA_SYNC_PAST_AVAILABLE = 0x01; public static final int PA_SYNC_PAST_NOT_AVAILABLE = 0x02; // BIS_Sync parameter value public static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L; public static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL; } android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +32 −3 Original line number Diff line number Diff line Loading @@ -99,6 +99,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; Loading Loading @@ -3823,16 +3824,44 @@ public class LeAudioService extends ProfileService { Log.d(TAG, "SetVolume " + volume); int currentlyActiveGroupId = getActiveGroupId(); List<BluetoothDevice> activeBroadcastSinks = new ArrayList<>(); if (currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID) { if (!Flags.leaudioBroadcastVolumeControlWithSetVolume()) { Log.e(TAG, "There is no active group "); return; } BassClientService bassClientService = getBassClientService(); if (bassClientService != null) { activeBroadcastSinks = bassClientService.getActiveBroadcastSinks(); } if (activeBroadcastSinks.isEmpty()) { Log.e(TAG, "There is no active streaming group or broadcast sinks"); return; } } VolumeControlService volumeControlService = getVolumeControlService(); if (volumeControlService != null) { if (Flags.leaudioBroadcastVolumeControlWithSetVolume() && currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID && !activeBroadcastSinks.isEmpty()) { Set<Integer> broadcastGroups = activeBroadcastSinks.stream() .map(dev -> getGroupId(dev)) .filter(id -> id != IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) .collect(Collectors.toSet()); Log.d(TAG, "Setting volume for broadcast sink groups: " + broadcastGroups); broadcastGroups.forEach( groupId -> volumeControlService.setGroupVolume(groupId, volume)); } else { volumeControlService.setGroupVolume(currentlyActiveGroupId, volume); } } } TbsService getTbsService() { if (mTbsService != null) { Loading android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java +92 −0 Original line number Diff line number Diff line Loading @@ -1863,6 +1863,98 @@ public class BassClientServiceTest { assertThat(mBassClientService.isAnyReceiverReceivingBroadcast(devices)).isTrue(); } @Test public void testGetActiveBroadcastSinks() { prepareConnectedDeviceGroup(); 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); injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000000); } 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); injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID + 1, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000000); } } // Verify isAnyReceiverReceivingBroadcast returns empty device list if no BIS synced assertThat(mBassClientService.getActiveBroadcastSinks().isEmpty()).isTrue(); // Update receiver state with lost BIS sync for (BassClientStateMachine sm : mStateMachines.values()) { if (sm.getDevice().equals(mCurrentDevice)) { injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000001); } else if (sm.getDevice().equals(mCurrentDevice1)) { injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID + 1, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000002); } } List<BluetoothDevice> activeSinks = mBassClientService.getActiveBroadcastSinks(); // Verify isAnyReceiverReceivingBroadcast returns correct device list if BIS synced assertThat(activeSinks.size()).isEqualTo(2); assertThat(activeSinks.contains(mCurrentDevice)).isTrue(); assertThat(activeSinks.contains(mCurrentDevice1)).isTrue(); } private void prepareTwoSynchronizedDevices() { BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID); Loading android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java +52 −0 Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; Loading Loading @@ -2255,6 +2256,57 @@ public class LeAudioServiceTest { assertThat(profileInfo.getValue().getVolume()).isEqualTo(volume); } /** Test volume setting for broadcast sink devices */ @Test public void testSetVolumeForBroadcastSinks() { mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_VOLUME_CONTROL_WITH_SET_VOLUME); int groupId = 1; int volume = 100; int newVolume = 120; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; int availableContexts = 4; doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); assertThat(mService.setActiveDevice(mLeftDevice)).isFalse(); ArgumentCaptor<BluetoothProfileConnectionInfo> profileInfo = ArgumentCaptor.forClass(BluetoothProfileConnectionInfo.class); // Add location support. injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); doReturn(volume).when(mVolumeControlService).getAudioDeviceGroupVolume(groupId); // Set group and device as active. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE); verify(mAudioManager, times(1)) .handleBluetoothActiveDeviceChanged(any(), eq(null), profileInfo.capture()); assertThat(profileInfo.getValue().getVolume()).isEqualTo(volume); // Set group to inactive, only keep them connected as broadcast sink devices. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE); verify(mAudioManager, times(1)) .handleBluetoothActiveDeviceChanged( eq(null), any(), any(BluetoothProfileConnectionInfo.class)); // Verify setGroupVolume will not be called if no active sinks doReturn(new ArrayList<>()).when(mBassClientService).getActiveBroadcastSinks(); mService.setVolume(newVolume); verify(mVolumeControlService, times(0)).setGroupVolume(groupId, newVolume); // Verify setGroupVolume will be called if active sinks doReturn(List.of(mLeftDevice, mRightDevice)) .when(mBassClientService) .getActiveBroadcastSinks(); mService.setVolume(newVolume); verify(mVolumeControlService, times(1)).setGroupVolume(groupId, newVolume); } @Test public void testGetAudioDeviceGroupVolume_whenVolumeControlServiceIsNull() { mService.mVolumeControlService = null; Loading Loading
android/app/src/com/android/bluetooth/bass_client/BassClientService.java +26 −1 Original line number Diff line number Diff line Loading @@ -2236,7 +2236,8 @@ public class BassClientService extends ProfileService { for (int i = 0; i < receiveState.getNumSubgroups(); i++) { Long syncState = receiveState.getBisSyncState().get(i); /* Not synced to BIS of failed to sync to BIG */ if (syncState == 0x00000000 || syncState == 0xFFFFFFFF) { if (syncState == BassConstants.BIS_SYNC_NOT_SYNC_TO_BIS || syncState == BassConstants.BIS_SYNC_FAILED_SYNC_TO_BIG) { continue; } Loading @@ -2248,6 +2249,30 @@ public class BassClientService extends ProfileService { return false; } /** Get the active broadcast sink devices receiving broadcast stream */ public List<BluetoothDevice> getActiveBroadcastSinks() { List<BluetoothDevice> activeSinks = new ArrayList<>(); for (BluetoothDevice device : getConnectedDevices()) { // Check if any device's source in active sync state if (getAllSources(device).stream() .anyMatch( receiveState -> (receiveState.getBisSyncState().stream() .anyMatch( syncState -> syncState != BassConstants .BIS_SYNC_NOT_SYNC_TO_BIS && syncState != BassConstants .BIS_SYNC_FAILED_SYNC_TO_BIG)))) { activeSinks.add(device); } } return activeSinks; } /** Handle broadcast state changed */ public void notifyBroadcastStateChanged(int state, int broadcastId) { switch (state) { Loading
android/app/src/com/android/bluetooth/bass_client/BassConstants.java +3 −0 Original line number Diff line number Diff line Loading @@ -81,4 +81,7 @@ public class BassConstants { public static final int PA_SYNC_DO_NOT_SYNC = 0x00; public static final int PA_SYNC_PAST_AVAILABLE = 0x01; public static final int PA_SYNC_PAST_NOT_AVAILABLE = 0x02; // BIS_Sync parameter value public static final long BIS_SYNC_NOT_SYNC_TO_BIS = 0x00000000L; public static final long BIS_SYNC_FAILED_SYNC_TO_BIG = 0xFFFFFFFFL; }
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +32 −3 Original line number Diff line number Diff line Loading @@ -99,6 +99,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; Loading Loading @@ -3823,16 +3824,44 @@ public class LeAudioService extends ProfileService { Log.d(TAG, "SetVolume " + volume); int currentlyActiveGroupId = getActiveGroupId(); List<BluetoothDevice> activeBroadcastSinks = new ArrayList<>(); if (currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID) { if (!Flags.leaudioBroadcastVolumeControlWithSetVolume()) { Log.e(TAG, "There is no active group "); return; } BassClientService bassClientService = getBassClientService(); if (bassClientService != null) { activeBroadcastSinks = bassClientService.getActiveBroadcastSinks(); } if (activeBroadcastSinks.isEmpty()) { Log.e(TAG, "There is no active streaming group or broadcast sinks"); return; } } VolumeControlService volumeControlService = getVolumeControlService(); if (volumeControlService != null) { if (Flags.leaudioBroadcastVolumeControlWithSetVolume() && currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID && !activeBroadcastSinks.isEmpty()) { Set<Integer> broadcastGroups = activeBroadcastSinks.stream() .map(dev -> getGroupId(dev)) .filter(id -> id != IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID) .collect(Collectors.toSet()); Log.d(TAG, "Setting volume for broadcast sink groups: " + broadcastGroups); broadcastGroups.forEach( groupId -> volumeControlService.setGroupVolume(groupId, volume)); } else { volumeControlService.setGroupVolume(currentlyActiveGroupId, volume); } } } TbsService getTbsService() { if (mTbsService != null) { Loading
android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java +92 −0 Original line number Diff line number Diff line Loading @@ -1863,6 +1863,98 @@ public class BassClientServiceTest { assertThat(mBassClientService.isAnyReceiverReceivingBroadcast(devices)).isTrue(); } @Test public void testGetActiveBroadcastSinks() { prepareConnectedDeviceGroup(); 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); injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000000); } 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); injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID + 1, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000000); } } // Verify isAnyReceiverReceivingBroadcast returns empty device list if no BIS synced assertThat(mBassClientService.getActiveBroadcastSinks().isEmpty()).isTrue(); // Update receiver state with lost BIS sync for (BassClientStateMachine sm : mStateMachines.values()) { if (sm.getDevice().equals(mCurrentDevice)) { injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000001); } else if (sm.getDevice().equals(mCurrentDevice1)) { injectRemoteSourceStateChanged( sm, meta, TEST_SOURCE_ID + 1, BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED, meta.isEncrypted() ? BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_DECRYPTING : BluetoothLeBroadcastReceiveState .BIG_ENCRYPTION_STATE_NOT_ENCRYPTED, null, (long) 0x00000002); } } List<BluetoothDevice> activeSinks = mBassClientService.getActiveBroadcastSinks(); // Verify isAnyReceiverReceivingBroadcast returns correct device list if BIS synced assertThat(activeSinks.size()).isEqualTo(2); assertThat(activeSinks.contains(mCurrentDevice)).isTrue(); assertThat(activeSinks.contains(mCurrentDevice1)).isTrue(); } private void prepareTwoSynchronizedDevices() { BluetoothLeBroadcastMetadata meta = createBroadcastMetadata(TEST_BROADCAST_ID); Loading
android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java +52 −0 Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; Loading Loading @@ -2255,6 +2256,57 @@ public class LeAudioServiceTest { assertThat(profileInfo.getValue().getVolume()).isEqualTo(volume); } /** Test volume setting for broadcast sink devices */ @Test public void testSetVolumeForBroadcastSinks() { mSetFlagsRule.enableFlags(Flags.FLAG_LEAUDIO_BROADCAST_VOLUME_CONTROL_WITH_SET_VOLUME); int groupId = 1; int volume = 100; int newVolume = 120; /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */ int direction = 1; int availableContexts = 4; doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class)); connectTestDevice(mLeftDevice, groupId); connectTestDevice(mRightDevice, groupId); assertThat(mService.setActiveDevice(mLeftDevice)).isFalse(); ArgumentCaptor<BluetoothProfileConnectionInfo> profileInfo = ArgumentCaptor.forClass(BluetoothProfileConnectionInfo.class); // Add location support. injectAudioConfChanged(groupId, availableContexts, direction); assertThat(mService.setActiveDevice(mLeftDevice)).isTrue(); doReturn(volume).when(mVolumeControlService).getAudioDeviceGroupVolume(groupId); // Set group and device as active. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE); verify(mAudioManager, times(1)) .handleBluetoothActiveDeviceChanged(any(), eq(null), profileInfo.capture()); assertThat(profileInfo.getValue().getVolume()).isEqualTo(volume); // Set group to inactive, only keep them connected as broadcast sink devices. injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE); verify(mAudioManager, times(1)) .handleBluetoothActiveDeviceChanged( eq(null), any(), any(BluetoothProfileConnectionInfo.class)); // Verify setGroupVolume will not be called if no active sinks doReturn(new ArrayList<>()).when(mBassClientService).getActiveBroadcastSinks(); mService.setVolume(newVolume); verify(mVolumeControlService, times(0)).setGroupVolume(groupId, newVolume); // Verify setGroupVolume will be called if active sinks doReturn(List.of(mLeftDevice, mRightDevice)) .when(mBassClientService) .getActiveBroadcastSinks(); mService.setVolume(newVolume); verify(mVolumeControlService, times(1)).setGroupVolume(groupId, newVolume); } @Test public void testGetAudioDeviceGroupVolume_whenVolumeControlServiceIsNull() { mService.mVolumeControlService = null; Loading