Loading android/app/src/com/android/bluetooth/btservice/PhonePolicy.java +22 −3 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ import android.os.Looper; import android.os.Message; import android.os.ParcelUuid; import android.os.SystemProperties; import android.provider.DeviceConfig; import android.util.Log; import com.android.bluetooth.R; Loading Loading @@ -98,6 +99,10 @@ class PhonePolicy { @VisibleForTesting static final String AUTO_CONNECT_PROFILES_PROPERTY = "bluetooth.auto_connect_profiles.enabled"; private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default"; private static boolean sLeAudioEnabledByDefault = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_BLUETOOTH, CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false); // Timeouts @VisibleForTesting static int sConnectOtherProfilesTimeoutMillis = 6000; // 6s Loading Loading @@ -295,7 +300,8 @@ class PhonePolicy { if ((leAudioService != null) && Utils.arrayContains(uuids, BluetoothUuid.LE_AUDIO) && (leAudioService.getConnectionPolicy(device) != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) && mAdapterService.isLeAudioAllowed(device)) { && mAdapterService.isLeAudioAllowed(device) && (sLeAudioEnabledByDefault || isDualModeAudioEnabled())) { isLeAudioProfileAllowed = true; } Loading Loading @@ -353,7 +359,7 @@ class PhonePolicy { } } // CSIP should be connected prior than LE Audio // CSIP should be connected prior to LE Audio if ((csipSetCooridnatorService != null) && (Utils.arrayContains(uuids, BluetoothUuid.COORDINATED_SET)) && (csipSetCooridnatorService.getConnectionPolicy(device) Loading Loading @@ -391,6 +397,10 @@ class PhonePolicy { mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } } else if (!sLeAudioEnabledByDefault) { debugLog("clear LEA profile priority because dual mode is disabled by default"); mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } if ((hearingAidService != null) && Utils.arrayContains(uuids, Loading @@ -415,7 +425,7 @@ class PhonePolicy { if ((volumeControlService != null) && Utils.arrayContains(uuids, BluetoothUuid.VOLUME_CONTROL) && (volumeControlService.getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_UNKNOWN)) { == BluetoothProfile.CONNECTION_POLICY_UNKNOWN) && isLeAudioProfileAllowed) { debugLog("setting volume control profile priority for device " + device); if (mAutoConnectProfilesSupported) { volumeControlService.setConnectionPolicy(device, Loading @@ -425,6 +435,10 @@ class PhonePolicy { BluetoothProfile.VOLUME_CONTROL, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } } else if (!sLeAudioEnabledByDefault) { debugLog("clear VCP priority because dual mode is disabled by default"); mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } if ((hapClientService != null) && Utils.arrayContains(uuids, Loading Loading @@ -838,6 +852,11 @@ class PhonePolicy { } } @VisibleForTesting void setLeAudioEnabledByDefaultForTesting(boolean enabled) { sLeAudioEnabledByDefault = enabled; } private static void debugLog(String msg) { if (DBG) { Log.i(TAG, msg); Loading android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java +71 −2 Original line number Diff line number Diff line Loading @@ -47,16 +47,20 @@ import android.util.Pair; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.le_audio.LeAudioService; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.SynchronousResultReceiver; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; Loading @@ -78,9 +82,11 @@ public class CsipSetCoordinatorService extends ProfileService { private static CsipSetCoordinatorService sCsipSetCoordinatorService; private AdapterService mAdapterService; private LeAudioService mLeAudioService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; private BluetoothDevice mPreviousAudioDevice; @VisibleForTesting ServiceFactory mServiceFactory = new ServiceFactory(); @VisibleForTesting CsipSetCoordinatorNativeInterface mCsipSetCoordinatorNativeInterface; Loading @@ -90,7 +96,10 @@ public class CsipSetCoordinatorService extends ProfileService { private final Map<Integer, ParcelUuid> mGroupIdToUuidMap = new HashMap<>(); private final Map<BluetoothDevice, Map<Integer, Integer>> mDeviceGroupIdRankMap = new ConcurrentHashMap<>(); // Tracks the number of devices in the CSIP group (greater than or equal to available devices) private final Map<Integer, Integer> mGroupIdToGroupSize = new HashMap<>(); // Tracks the number of available devices mapped to the group id private final Map<Integer, Set<BluetoothDevice>> mGroupIdToConnectedDevices = new HashMap<>(); private final Map<ParcelUuid, Map<Executor, IBluetoothCsipSetCoordinatorCallback>> mCallbacks = new HashMap<>(); private final Map<Integer, Pair<UUID, IBluetoothCsipSetCoordinatorLockCallback>> mLocks = Loading Loading @@ -135,6 +144,9 @@ public class CsipSetCoordinatorService extends ProfileService { "CsipSetCoordinatorNativeInterface cannot be null when" .concat("CsipSetCoordinatorService starts")); // Get LE Audio service (can be null) mLeAudioService = mServiceFactory.getLeAudioService(); // Start handler thread for state machines mStateMachines.clear(); mStateMachinesThread = new HandlerThread("CsipSetCoordinatorService.StateMachines"); Loading Loading @@ -210,6 +222,7 @@ public class CsipSetCoordinatorService extends ProfileService { mDeviceGroupIdRankMap.clear(); mCallbacks.clear(); mGroupIdToGroupSize.clear(); mGroupIdToConnectedDevices.clear(); mGroupIdToUuidMap.clear(); mLocks.clear(); Loading Loading @@ -683,7 +696,9 @@ public class CsipSetCoordinatorService extends ProfileService { IBluetoothCsipSetCoordinator.CSIS_GROUP_SIZE_UNKNOWN); } private void handleDeviceAvailable(BluetoothDevice device, int groupId, int rank, UUID uuid) { private void handleDeviceAvailable(BluetoothDevice device, int groupId, int rank, UUID uuid, int groupSize) { mGroupIdToGroupSize.put(groupId, groupSize); ParcelUuid parcel_uuid = new ParcelUuid(uuid); if (!getAllGroupIds(parcel_uuid).contains(groupId)) { mGroupIdToUuidMap.put(groupId, parcel_uuid); Loading @@ -697,6 +712,47 @@ public class CsipSetCoordinatorService extends ProfileService { all_device_groups.put(groupId, rank); } /** * If all the group devices are now available, make sure CSIP connection policy mirrors the LEA * connection policy. * @param groupId is the group that has a new device available */ private void disableCsipIfNeeded(int groupId) { /* Make sure CSIP connection policy mirrors that of LeAudioService once all CSIP characteristic reads have completed (ensures we can pair other set devices) */ if (mLeAudioService == null) { mLeAudioService = mServiceFactory.getLeAudioService(); } if (mLeAudioService != null) { if (!mGroupIdToConnectedDevices.containsKey(groupId)) { Log.w(TAG, "No connected devices for groupId=" + groupId); return; } if (!mGroupIdToGroupSize.containsKey(groupId)) { Log.w(TAG, "No group size stored for groupId=" + groupId); return; } if (mGroupIdToConnectedDevices.get(groupId).size() < mGroupIdToGroupSize.get(groupId)) { Log.d(TAG, "disableCsipIfNeeded: groupId " + groupId + "has " + mGroupIdToConnectedDevices.get(groupId).size() + " connected devices out" + " of a group size of " + mGroupIdToGroupSize.get(groupId)); return; } for (BluetoothDevice groupDevice : mGroupIdToConnectedDevices.get(groupId)) { if (mLeAudioService.getConnectionPolicy(groupDevice) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { Log.i(TAG, "Setting CSIP connection policy to FORBIDDEN for device " + groupDevice + " after all group devices bonded because LEA " + "connection policy is FORBIDDEN"); setConnectionPolicy(groupDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } } } else { Log.w(TAG, "checkIfGroupPaired: LE Audio Service is null"); } } private void executeCallback(Executor exec, IBluetoothCsipSetCoordinatorCallback callback, BluetoothDevice device, int groupId) throws RemoteException { exec.execute(() -> { Loading Loading @@ -793,7 +849,8 @@ public class CsipSetCoordinatorService extends ProfileService { intent.putExtra( BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_TYPE_UUID, stackEvent.valueUuid1); handleDeviceAvailable(device, groupId, stackEvent.valueInt3, stackEvent.valueUuid1); handleDeviceAvailable(device, groupId, stackEvent.valueInt3, stackEvent.valueUuid1, stackEvent.valueInt2); } else if (stackEvent.type == CsipSetCoordinatorStackEvent.EVENT_TYPE_SET_MEMBER_AVAILABLE) { Objects.requireNonNull(device, "Device should never be null, event: " + stackEvent); Loading Loading @@ -896,12 +953,17 @@ public class CsipSetCoordinatorService extends ProfileService { if (DBG) { Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); } // Remove state machine if the bonding for a device is removed if (bondState != BluetoothDevice.BOND_NONE) { return; } mDeviceGroupIdRankMap.remove(device); for (Map.Entry<Integer, Set<BluetoothDevice>> entry: mGroupIdToConnectedDevices.entrySet()) { entry.getValue().remove(device); } synchronized (mStateMachines) { CsipSetCoordinatorStateMachine sm = mStateMachines.get(device); Loading Loading @@ -950,6 +1012,13 @@ public class CsipSetCoordinatorService extends ProfileService { } removeStateMachine(device); } } else if (toState == BluetoothProfile.STATE_CONNECTED) { int groupId = getGroupId(device, BluetoothUuid.CAP); if (!mGroupIdToConnectedDevices.containsKey(groupId)) { mGroupIdToConnectedDevices.put(groupId, new HashSet<>()); } mGroupIdToConnectedDevices.get(groupId).add(device); disableCsipIfNeeded(groupId); } } Loading android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +2 −0 Original line number Diff line number Diff line Loading @@ -2469,6 +2469,8 @@ public class LeAudioService extends ProfileService { if (mCsipSetCoordinatorService == null) { mCsipSetCoordinatorService = mServiceFactory.getCsipSetCoordinatorService(); } // Disallow setting CSIP to forbidden until characteristic reads are complete if (mCsipSetCoordinatorService != null) { mCsipSetCoordinatorService.setConnectionPolicy(device, connectionPolicy); } Loading android/app/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java +56 −6 Original line number Diff line number Diff line Loading @@ -177,12 +177,12 @@ public class PhonePolicyTest { } @Test public void testProcessInitProfilePriorities_LeAudio() { public void testProcessInitProfilePriorities_LeAudioDisabledByDefault() { BluetoothDevice device = getTestDevice(mAdapter, 0); when(mAdapterService.isLeAudioAllowed(device)).thenReturn(true); // Auto connect to LE audio, HFP, A2DP processInitProfilePriorities_LeAudioHelper(true, true); processInitProfilePriorities_LeAudioHelper(true, true, false); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) Loading @@ -191,7 +191,7 @@ public class PhonePolicyTest { .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and allow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(true, false); processInitProfilePriorities_LeAudioHelper(true, false, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); Loading @@ -203,7 +203,56 @@ public class PhonePolicyTest { BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Auto connect to LE audio but disallow HFP and A2DP processInitProfilePriorities_LeAudioHelper(false, true); processInitProfilePriorities_LeAudioHelper(false, true, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and disallow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(false, false, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.A2DP, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.HEADSET, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } @Test public void testProcessInitProfilePriorities_LeAudioEnabledByDefault() { BluetoothDevice device = getTestDevice(mAdapter, 0); when(mAdapterService.isLeAudioAllowed(device)).thenReturn(true); // Auto connect to LE audio, HFP, A2DP processInitProfilePriorities_LeAudioHelper(true, true, true); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and allow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(true, false, true); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.A2DP, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.HEADSET, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Auto connect to LE audio but disallow HFP and A2DP processInitProfilePriorities_LeAudioHelper(false, true, true); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) Loading @@ -214,7 +263,7 @@ public class PhonePolicyTest { BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Does not auto connect and disallow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(false, false); processInitProfilePriorities_LeAudioHelper(false, false, true); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); Loading @@ -227,8 +276,9 @@ public class PhonePolicyTest { } private void processInitProfilePriorities_LeAudioHelper( boolean dualModeEnabled, boolean autoConnect) { boolean dualModeEnabled, boolean autoConnect, boolean leAudioEnabledByDefault) { Utils.setDualModeAudioStateForTesting(dualModeEnabled); mPhonePolicy.setLeAudioEnabledByDefaultForTesting(leAudioEnabledByDefault); mPhonePolicy.mAutoConnectProfilesSupported = autoConnect; BluetoothDevice device = getTestDevice(mAdapter, 0); Loading android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java +58 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package com.android.bluetooth.csip; import static org.mockito.Mockito.*; import android.bluetooth.*; import android.bluetooth.BluetoothUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; Loading @@ -37,7 +38,9 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.le_audio.LeAudioService; import java.util.HashMap; import java.util.List; Loading @@ -53,6 +56,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @MediumTest @RunWith(AndroidJUnit4.class) Loading @@ -73,6 +77,9 @@ public class CsipSetCoordinatorServiceTest { private static final int TIMEOUT_MS = 1000; @Mock private AdapterService mAdapterService; @Mock private LeAudioService mLeAudioService; @Spy private ServiceFactory mServiceFactory = new ServiceFactory(); @Mock private DatabaseManager mDatabaseManager; @Mock private CsipSetCoordinatorNativeInterface mCsipSetCoordinatorNativeInterface; @Mock private IBluetoothCsipSetCoordinatorLockCallback mCsipSetCoordinatorLockCallback; Loading Loading @@ -100,6 +107,8 @@ public class CsipSetCoordinatorServiceTest { startService(); mService.mCsipSetCoordinatorNativeInterface = mCsipSetCoordinatorNativeInterface; mService.mServiceFactory = mServiceFactory; when(mServiceFactory.getLeAudioService()).thenReturn(mLeAudioService); // Override the timeout value to speed up the test CsipSetCoordinatorStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s Loading Loading @@ -533,6 +542,55 @@ public class CsipSetCoordinatorServiceTest { group_id, intent.getIntExtra(BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_ID, -1)); } /** * Test that we make CSIP FORBIDDEN after all set members are paired if the LE Audio connection * policy is FORBIDDEN. */ @Test public void testDisableCsipAfterConnectingIfLeAudioDisabled() { int group_id = 0x01; int group_size = 0x02; long uuidLsb = BluetoothUuid.CAP.getUuid().getLeastSignificantBits(); long uuidMsb = BluetoothUuid.CAP.getUuid().getMostSignificantBits(); doCallRealMethod() .when(mCsipSetCoordinatorNativeInterface) .onDeviceAvailable(any(byte[].class), anyInt(), anyInt(), anyInt(), anyLong(), anyLong()); when(mLeAudioService.getConnectionPolicy(any())).thenReturn( BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Make first set device available and connected mCsipSetCoordinatorNativeInterface.onDeviceAvailable( getByteAddress(mTestDevice), group_id, group_size, 0x02, uuidLsb, uuidMsb); mService.connectionStateChanged(mTestDevice, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED); // Another device with the highest rank mCsipSetCoordinatorNativeInterface.onDeviceAvailable( getByteAddress(mTestDevice2), group_id, group_size, 0x01, uuidLsb, uuidMsb); // When LEA is FORBIDDEN, verify we don't disable CSIP until all set devices are available verify(mDatabaseManager, never()).setProfileConnectionPolicy(mTestDevice, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, never()).setProfileConnectionPolicy(mTestDevice2, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Mark the second device as connected mService.connectionStateChanged(mTestDevice2, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED); // When LEA is FORBIDDEN, verify we disable CSIP once all set devices are available verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mTestDevice, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mTestDevice2, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } @Test public void testDump_doesNotCrash() { // Update the device policy so okToConnect() returns true Loading Loading
android/app/src/com/android/bluetooth/btservice/PhonePolicy.java +22 −3 Original line number Diff line number Diff line Loading @@ -38,6 +38,7 @@ import android.os.Looper; import android.os.Message; import android.os.ParcelUuid; import android.os.SystemProperties; import android.provider.DeviceConfig; import android.util.Log; import com.android.bluetooth.R; Loading Loading @@ -98,6 +99,10 @@ class PhonePolicy { @VisibleForTesting static final String AUTO_CONNECT_PROFILES_PROPERTY = "bluetooth.auto_connect_profiles.enabled"; private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default"; private static boolean sLeAudioEnabledByDefault = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_BLUETOOTH, CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false); // Timeouts @VisibleForTesting static int sConnectOtherProfilesTimeoutMillis = 6000; // 6s Loading Loading @@ -295,7 +300,8 @@ class PhonePolicy { if ((leAudioService != null) && Utils.arrayContains(uuids, BluetoothUuid.LE_AUDIO) && (leAudioService.getConnectionPolicy(device) != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) && mAdapterService.isLeAudioAllowed(device)) { && mAdapterService.isLeAudioAllowed(device) && (sLeAudioEnabledByDefault || isDualModeAudioEnabled())) { isLeAudioProfileAllowed = true; } Loading Loading @@ -353,7 +359,7 @@ class PhonePolicy { } } // CSIP should be connected prior than LE Audio // CSIP should be connected prior to LE Audio if ((csipSetCooridnatorService != null) && (Utils.arrayContains(uuids, BluetoothUuid.COORDINATED_SET)) && (csipSetCooridnatorService.getConnectionPolicy(device) Loading Loading @@ -391,6 +397,10 @@ class PhonePolicy { mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } } else if (!sLeAudioEnabledByDefault) { debugLog("clear LEA profile priority because dual mode is disabled by default"); mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } if ((hearingAidService != null) && Utils.arrayContains(uuids, Loading @@ -415,7 +425,7 @@ class PhonePolicy { if ((volumeControlService != null) && Utils.arrayContains(uuids, BluetoothUuid.VOLUME_CONTROL) && (volumeControlService.getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_UNKNOWN)) { == BluetoothProfile.CONNECTION_POLICY_UNKNOWN) && isLeAudioProfileAllowed) { debugLog("setting volume control profile priority for device " + device); if (mAutoConnectProfilesSupported) { volumeControlService.setConnectionPolicy(device, Loading @@ -425,6 +435,10 @@ class PhonePolicy { BluetoothProfile.VOLUME_CONTROL, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } } else if (!sLeAudioEnabledByDefault) { debugLog("clear VCP priority because dual mode is disabled by default"); mAdapterService.getDatabase().setProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } if ((hapClientService != null) && Utils.arrayContains(uuids, Loading Loading @@ -838,6 +852,11 @@ class PhonePolicy { } } @VisibleForTesting void setLeAudioEnabledByDefaultForTesting(boolean enabled) { sLeAudioEnabledByDefault = enabled; } private static void debugLog(String msg) { if (DBG) { Log.i(TAG, msg); Loading
android/app/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java +71 −2 Original line number Diff line number Diff line Loading @@ -47,16 +47,20 @@ import android.util.Pair; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.le_audio.LeAudioService; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.SynchronousResultReceiver; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; Loading @@ -78,9 +82,11 @@ public class CsipSetCoordinatorService extends ProfileService { private static CsipSetCoordinatorService sCsipSetCoordinatorService; private AdapterService mAdapterService; private LeAudioService mLeAudioService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; private BluetoothDevice mPreviousAudioDevice; @VisibleForTesting ServiceFactory mServiceFactory = new ServiceFactory(); @VisibleForTesting CsipSetCoordinatorNativeInterface mCsipSetCoordinatorNativeInterface; Loading @@ -90,7 +96,10 @@ public class CsipSetCoordinatorService extends ProfileService { private final Map<Integer, ParcelUuid> mGroupIdToUuidMap = new HashMap<>(); private final Map<BluetoothDevice, Map<Integer, Integer>> mDeviceGroupIdRankMap = new ConcurrentHashMap<>(); // Tracks the number of devices in the CSIP group (greater than or equal to available devices) private final Map<Integer, Integer> mGroupIdToGroupSize = new HashMap<>(); // Tracks the number of available devices mapped to the group id private final Map<Integer, Set<BluetoothDevice>> mGroupIdToConnectedDevices = new HashMap<>(); private final Map<ParcelUuid, Map<Executor, IBluetoothCsipSetCoordinatorCallback>> mCallbacks = new HashMap<>(); private final Map<Integer, Pair<UUID, IBluetoothCsipSetCoordinatorLockCallback>> mLocks = Loading Loading @@ -135,6 +144,9 @@ public class CsipSetCoordinatorService extends ProfileService { "CsipSetCoordinatorNativeInterface cannot be null when" .concat("CsipSetCoordinatorService starts")); // Get LE Audio service (can be null) mLeAudioService = mServiceFactory.getLeAudioService(); // Start handler thread for state machines mStateMachines.clear(); mStateMachinesThread = new HandlerThread("CsipSetCoordinatorService.StateMachines"); Loading Loading @@ -210,6 +222,7 @@ public class CsipSetCoordinatorService extends ProfileService { mDeviceGroupIdRankMap.clear(); mCallbacks.clear(); mGroupIdToGroupSize.clear(); mGroupIdToConnectedDevices.clear(); mGroupIdToUuidMap.clear(); mLocks.clear(); Loading Loading @@ -683,7 +696,9 @@ public class CsipSetCoordinatorService extends ProfileService { IBluetoothCsipSetCoordinator.CSIS_GROUP_SIZE_UNKNOWN); } private void handleDeviceAvailable(BluetoothDevice device, int groupId, int rank, UUID uuid) { private void handleDeviceAvailable(BluetoothDevice device, int groupId, int rank, UUID uuid, int groupSize) { mGroupIdToGroupSize.put(groupId, groupSize); ParcelUuid parcel_uuid = new ParcelUuid(uuid); if (!getAllGroupIds(parcel_uuid).contains(groupId)) { mGroupIdToUuidMap.put(groupId, parcel_uuid); Loading @@ -697,6 +712,47 @@ public class CsipSetCoordinatorService extends ProfileService { all_device_groups.put(groupId, rank); } /** * If all the group devices are now available, make sure CSIP connection policy mirrors the LEA * connection policy. * @param groupId is the group that has a new device available */ private void disableCsipIfNeeded(int groupId) { /* Make sure CSIP connection policy mirrors that of LeAudioService once all CSIP characteristic reads have completed (ensures we can pair other set devices) */ if (mLeAudioService == null) { mLeAudioService = mServiceFactory.getLeAudioService(); } if (mLeAudioService != null) { if (!mGroupIdToConnectedDevices.containsKey(groupId)) { Log.w(TAG, "No connected devices for groupId=" + groupId); return; } if (!mGroupIdToGroupSize.containsKey(groupId)) { Log.w(TAG, "No group size stored for groupId=" + groupId); return; } if (mGroupIdToConnectedDevices.get(groupId).size() < mGroupIdToGroupSize.get(groupId)) { Log.d(TAG, "disableCsipIfNeeded: groupId " + groupId + "has " + mGroupIdToConnectedDevices.get(groupId).size() + " connected devices out" + " of a group size of " + mGroupIdToGroupSize.get(groupId)); return; } for (BluetoothDevice groupDevice : mGroupIdToConnectedDevices.get(groupId)) { if (mLeAudioService.getConnectionPolicy(groupDevice) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { Log.i(TAG, "Setting CSIP connection policy to FORBIDDEN for device " + groupDevice + " after all group devices bonded because LEA " + "connection policy is FORBIDDEN"); setConnectionPolicy(groupDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } } } else { Log.w(TAG, "checkIfGroupPaired: LE Audio Service is null"); } } private void executeCallback(Executor exec, IBluetoothCsipSetCoordinatorCallback callback, BluetoothDevice device, int groupId) throws RemoteException { exec.execute(() -> { Loading Loading @@ -793,7 +849,8 @@ public class CsipSetCoordinatorService extends ProfileService { intent.putExtra( BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_TYPE_UUID, stackEvent.valueUuid1); handleDeviceAvailable(device, groupId, stackEvent.valueInt3, stackEvent.valueUuid1); handleDeviceAvailable(device, groupId, stackEvent.valueInt3, stackEvent.valueUuid1, stackEvent.valueInt2); } else if (stackEvent.type == CsipSetCoordinatorStackEvent.EVENT_TYPE_SET_MEMBER_AVAILABLE) { Objects.requireNonNull(device, "Device should never be null, event: " + stackEvent); Loading Loading @@ -896,12 +953,17 @@ public class CsipSetCoordinatorService extends ProfileService { if (DBG) { Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); } // Remove state machine if the bonding for a device is removed if (bondState != BluetoothDevice.BOND_NONE) { return; } mDeviceGroupIdRankMap.remove(device); for (Map.Entry<Integer, Set<BluetoothDevice>> entry: mGroupIdToConnectedDevices.entrySet()) { entry.getValue().remove(device); } synchronized (mStateMachines) { CsipSetCoordinatorStateMachine sm = mStateMachines.get(device); Loading Loading @@ -950,6 +1012,13 @@ public class CsipSetCoordinatorService extends ProfileService { } removeStateMachine(device); } } else if (toState == BluetoothProfile.STATE_CONNECTED) { int groupId = getGroupId(device, BluetoothUuid.CAP); if (!mGroupIdToConnectedDevices.containsKey(groupId)) { mGroupIdToConnectedDevices.put(groupId, new HashSet<>()); } mGroupIdToConnectedDevices.get(groupId).add(device); disableCsipIfNeeded(groupId); } } Loading
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +2 −0 Original line number Diff line number Diff line Loading @@ -2469,6 +2469,8 @@ public class LeAudioService extends ProfileService { if (mCsipSetCoordinatorService == null) { mCsipSetCoordinatorService = mServiceFactory.getCsipSetCoordinatorService(); } // Disallow setting CSIP to forbidden until characteristic reads are complete if (mCsipSetCoordinatorService != null) { mCsipSetCoordinatorService.setConnectionPolicy(device, connectionPolicy); } Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/PhonePolicyTest.java +56 −6 Original line number Diff line number Diff line Loading @@ -177,12 +177,12 @@ public class PhonePolicyTest { } @Test public void testProcessInitProfilePriorities_LeAudio() { public void testProcessInitProfilePriorities_LeAudioDisabledByDefault() { BluetoothDevice device = getTestDevice(mAdapter, 0); when(mAdapterService.isLeAudioAllowed(device)).thenReturn(true); // Auto connect to LE audio, HFP, A2DP processInitProfilePriorities_LeAudioHelper(true, true); processInitProfilePriorities_LeAudioHelper(true, true, false); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) Loading @@ -191,7 +191,7 @@ public class PhonePolicyTest { .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and allow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(true, false); processInitProfilePriorities_LeAudioHelper(true, false, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); Loading @@ -203,7 +203,56 @@ public class PhonePolicyTest { BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Auto connect to LE audio but disallow HFP and A2DP processInitProfilePriorities_LeAudioHelper(false, true); processInitProfilePriorities_LeAudioHelper(false, true, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and disallow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(false, false, false); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.A2DP, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.HEADSET, BluetoothProfile.CONNECTION_POLICY_ALLOWED); } @Test public void testProcessInitProfilePriorities_LeAudioEnabledByDefault() { BluetoothDevice device = getTestDevice(mAdapter, 0); when(mAdapterService.isLeAudioAllowed(device)).thenReturn(true); // Auto connect to LE audio, HFP, A2DP processInitProfilePriorities_LeAudioHelper(true, true, true); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mA2dpService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mHeadsetService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Does not auto connect and allow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(true, false, true); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.A2DP, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) .setProfileConnectionPolicy(device, BluetoothProfile.HEADSET, BluetoothProfile.CONNECTION_POLICY_ALLOWED); // Auto connect to LE audio but disallow HFP and A2DP processInitProfilePriorities_LeAudioHelper(false, true, true); verify(mLeAudioService, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(1)) Loading @@ -214,7 +263,7 @@ public class PhonePolicyTest { BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Does not auto connect and disallow HFP and A2DP to be connected processInitProfilePriorities_LeAudioHelper(false, false); processInitProfilePriorities_LeAudioHelper(false, false, true); verify(mDatabaseManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)) .setProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO, BluetoothProfile.CONNECTION_POLICY_ALLOWED); Loading @@ -227,8 +276,9 @@ public class PhonePolicyTest { } private void processInitProfilePriorities_LeAudioHelper( boolean dualModeEnabled, boolean autoConnect) { boolean dualModeEnabled, boolean autoConnect, boolean leAudioEnabledByDefault) { Utils.setDualModeAudioStateForTesting(dualModeEnabled); mPhonePolicy.setLeAudioEnabledByDefaultForTesting(leAudioEnabledByDefault); mPhonePolicy.mAutoConnectProfilesSupported = autoConnect; BluetoothDevice device = getTestDevice(mAdapter, 0); Loading
android/app/tests/unit/src/com/android/bluetooth/csip/CsipSetCoordinatorServiceTest.java +58 −0 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ package com.android.bluetooth.csip; import static org.mockito.Mockito.*; import android.bluetooth.*; import android.bluetooth.BluetoothUuid; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; Loading @@ -37,7 +38,9 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.le_audio.LeAudioService; import java.util.HashMap; import java.util.List; Loading @@ -53,6 +56,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; @MediumTest @RunWith(AndroidJUnit4.class) Loading @@ -73,6 +77,9 @@ public class CsipSetCoordinatorServiceTest { private static final int TIMEOUT_MS = 1000; @Mock private AdapterService mAdapterService; @Mock private LeAudioService mLeAudioService; @Spy private ServiceFactory mServiceFactory = new ServiceFactory(); @Mock private DatabaseManager mDatabaseManager; @Mock private CsipSetCoordinatorNativeInterface mCsipSetCoordinatorNativeInterface; @Mock private IBluetoothCsipSetCoordinatorLockCallback mCsipSetCoordinatorLockCallback; Loading Loading @@ -100,6 +107,8 @@ public class CsipSetCoordinatorServiceTest { startService(); mService.mCsipSetCoordinatorNativeInterface = mCsipSetCoordinatorNativeInterface; mService.mServiceFactory = mServiceFactory; when(mServiceFactory.getLeAudioService()).thenReturn(mLeAudioService); // Override the timeout value to speed up the test CsipSetCoordinatorStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s Loading Loading @@ -533,6 +542,55 @@ public class CsipSetCoordinatorServiceTest { group_id, intent.getIntExtra(BluetoothCsipSetCoordinator.EXTRA_CSIS_GROUP_ID, -1)); } /** * Test that we make CSIP FORBIDDEN after all set members are paired if the LE Audio connection * policy is FORBIDDEN. */ @Test public void testDisableCsipAfterConnectingIfLeAudioDisabled() { int group_id = 0x01; int group_size = 0x02; long uuidLsb = BluetoothUuid.CAP.getUuid().getLeastSignificantBits(); long uuidMsb = BluetoothUuid.CAP.getUuid().getMostSignificantBits(); doCallRealMethod() .when(mCsipSetCoordinatorNativeInterface) .onDeviceAvailable(any(byte[].class), anyInt(), anyInt(), anyInt(), anyLong(), anyLong()); when(mLeAudioService.getConnectionPolicy(any())).thenReturn( BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Make first set device available and connected mCsipSetCoordinatorNativeInterface.onDeviceAvailable( getByteAddress(mTestDevice), group_id, group_size, 0x02, uuidLsb, uuidMsb); mService.connectionStateChanged(mTestDevice, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED); // Another device with the highest rank mCsipSetCoordinatorNativeInterface.onDeviceAvailable( getByteAddress(mTestDevice2), group_id, group_size, 0x01, uuidLsb, uuidMsb); // When LEA is FORBIDDEN, verify we don't disable CSIP until all set devices are available verify(mDatabaseManager, never()).setProfileConnectionPolicy(mTestDevice, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, never()).setProfileConnectionPolicy(mTestDevice2, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); // Mark the second device as connected mService.connectionStateChanged(mTestDevice2, BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED); // When LEA is FORBIDDEN, verify we disable CSIP once all set devices are available verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mTestDevice, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mTestDevice2, BluetoothProfile.CSIP_SET_COORDINATOR, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); } @Test public void testDump_doesNotCrash() { // Update the device policy so okToConnect() returns true Loading