1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.bluetooth; 18 19 import android.bluetooth.BluetoothCsipSetCoordinator; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothProfile; 22 import android.bluetooth.BluetoothUuid; 23 import android.os.Build; 24 import android.os.ParcelUuid; 25 import android.util.Log; 26 27 import androidx.annotation.ChecksSdkIntAtLeast; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.util.ArrayList; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.stream.Collectors; 37 38 /** 39 * CsipDeviceManager manages the set of remote CSIP Bluetooth devices. 40 */ 41 public class CsipDeviceManager { 42 private static final String TAG = "CsipDeviceManager"; 43 private static final boolean DEBUG = BluetoothUtils.D; 44 45 private final LocalBluetoothManager mBtManager; 46 private final List<CachedBluetoothDevice> mCachedDevices; 47 CsipDeviceManager(LocalBluetoothManager localBtManager, List<CachedBluetoothDevice> cachedDevices)48 CsipDeviceManager(LocalBluetoothManager localBtManager, 49 List<CachedBluetoothDevice> cachedDevices) { 50 mBtManager = localBtManager; 51 mCachedDevices = cachedDevices; 52 } 53 initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice)54 void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) { 55 // Current it only supports the base uuid for CSIP and group this set in UI. 56 final int groupId = getBaseGroupId(newDevice.getDevice()); 57 if (isValidGroupId(groupId)) { 58 log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")"); 59 // Once groupId is valid, assign groupId 60 newDevice.setGroupId(groupId); 61 } 62 } 63 getBaseGroupId(BluetoothDevice device)64 private int getBaseGroupId(BluetoothDevice device) { 65 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 66 final CsipSetCoordinatorProfile profileProxy = profileManager 67 .getCsipSetCoordinatorProfile(); 68 if (profileProxy != null) { 69 final Map<Integer, ParcelUuid> groupIdMap = profileProxy 70 .getGroupUuidMapByDevice(device); 71 if (groupIdMap == null) { 72 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 73 } 74 75 for (Map.Entry<Integer, ParcelUuid> entry : groupIdMap.entrySet()) { 76 if (entry.getValue().equals(BluetoothUuid.CAP)) { 77 return entry.getKey(); 78 } 79 } 80 } 81 return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 82 } 83 setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice)84 boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) { 85 final int groupId = newDevice.getGroupId(); 86 if (isValidGroupId(groupId)) { 87 final CachedBluetoothDevice mainDevice = getCachedDevice(groupId); 88 log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice); 89 // Just add one of the coordinated set from a pair in the list that is shown in the UI. 90 // Once there is other devices with the same groupId, to add new device as member 91 // devices. 92 if (mainDevice != null) { 93 mainDevice.addMemberDevice(newDevice); 94 newDevice.setName(mainDevice.getName()); 95 return true; 96 } 97 } 98 return false; 99 } 100 isValidGroupId(int groupId)101 private boolean isValidGroupId(int groupId) { 102 return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 103 } 104 105 /** 106 * To find the device with {@code groupId}. 107 * 108 * @param groupId The group id 109 * @return if we could find a device with this {@code groupId} return this device. Otherwise, 110 * return null. 111 */ getCachedDevice(int groupId)112 public CachedBluetoothDevice getCachedDevice(int groupId) { 113 log("getCachedDevice: groupId: " + groupId); 114 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 115 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 116 if (cachedDevice.getGroupId() == groupId) { 117 log("getCachedDevice: found cachedDevice with the groupId: " 118 + cachedDevice.getDevice().getAnonymizedAddress()); 119 return cachedDevice; 120 } 121 } 122 return null; 123 } 124 125 // To collect all set member devices and call #onGroupIdChanged to group device by GroupId updateCsipDevices()126 void updateCsipDevices() { 127 final Set<Integer> newGroupIdSet = new HashSet<Integer>(); 128 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 129 // Do nothing if GroupId has been assigned 130 if (!isValidGroupId(cachedDevice.getGroupId())) { 131 final int newGroupId = getBaseGroupId(cachedDevice.getDevice()); 132 // Do nothing if there is no GroupId on Bluetooth device 133 if (isValidGroupId(newGroupId)) { 134 cachedDevice.setGroupId(newGroupId); 135 newGroupIdSet.add(newGroupId); 136 } 137 } 138 } 139 for (int groupId : newGroupIdSet) { 140 onGroupIdChanged(groupId); 141 } 142 } 143 144 @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) isAtLeastT()145 private static boolean isAtLeastT() { 146 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; 147 } 148 149 // Group devices by groupId 150 @VisibleForTesting onGroupIdChanged(int groupId)151 void onGroupIdChanged(int groupId) { 152 if (!isValidGroupId(groupId)) { 153 log("onGroupIdChanged: groupId is invalid"); 154 return; 155 } 156 updateRelationshipOfGroupDevices(groupId); 157 } 158 159 // @return {@code true}, the event is processed inside the method. It is for updating 160 // le audio device on group relationship when receiving connected or disconnected. 161 // @return {@code false}, it is not le audio device or to process it same as other profiles onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state)162 boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, 163 int state) { 164 log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state); 165 166 if (state != BluetoothProfile.STATE_CONNECTED 167 && state != BluetoothProfile.STATE_DISCONNECTED) { 168 return false; 169 } 170 return updateRelationshipOfGroupDevices(cachedDevice.getGroupId()); 171 } 172 173 @VisibleForTesting updateRelationshipOfGroupDevices(int groupId)174 boolean updateRelationshipOfGroupDevices(int groupId) { 175 if (!isValidGroupId(groupId)) { 176 log("The device is not group."); 177 return false; 178 } 179 log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString()); 180 181 // Get the preferred main device by getPreferredMainDeviceWithoutConectionState 182 List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId); 183 CachedBluetoothDevice preferredMainDevice = 184 getPreferredMainDevice(groupId, groupDevicesList); 185 log("The preferredMainDevice= " + preferredMainDevice 186 + " and the groupDevicesList of groupId= " + groupId 187 + " =" + groupDevicesList); 188 return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice); 189 } 190 findMainDevice(CachedBluetoothDevice device)191 CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { 192 if (device == null || mCachedDevices == null) { 193 return null; 194 } 195 196 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 197 if (isValidGroupId(cachedDevice.getGroupId())) { 198 Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); 199 if (memberSet.isEmpty()) { 200 continue; 201 } 202 203 for (CachedBluetoothDevice memberDevice : memberSet) { 204 if (memberDevice != null && memberDevice.equals(device)) { 205 return cachedDevice; 206 } 207 } 208 } 209 } 210 return null; 211 } 212 213 /** 214 * Check if the {@code groupId} is existed. 215 * 216 * @param groupId The group id 217 * @return {@code true}, if we could find a device with this {@code groupId}; Otherwise, 218 * return {@code false}. 219 */ isExistedGroupId(int groupId)220 public boolean isExistedGroupId(int groupId) { 221 return getCachedDevice(groupId) != null; 222 } 223 224 @VisibleForTesting getGroupDevicesFromAllOfDevicesList(int groupId)225 List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) { 226 List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>(); 227 if (!isValidGroupId(groupId)) { 228 return groupDevicesList; 229 } 230 for (CachedBluetoothDevice item : mCachedDevices) { 231 if (groupId != item.getGroupId()) { 232 continue; 233 } 234 groupDevicesList.add(item); 235 groupDevicesList.addAll(item.getMemberDevice()); 236 } 237 return groupDevicesList; 238 } 239 getFirstMemberDevice(int groupId)240 public CachedBluetoothDevice getFirstMemberDevice(int groupId) { 241 List<CachedBluetoothDevice> members = getGroupDevicesFromAllOfDevicesList(groupId); 242 if (members.isEmpty()) 243 return null; 244 245 CachedBluetoothDevice firstMember = members.get(0); 246 log("getFirstMemberDevice: groupId=" + groupId 247 + " address=" + firstMember.getDevice().getAnonymizedAddress()); 248 return firstMember; 249 } 250 251 @VisibleForTesting getPreferredMainDevice(int groupId, List<CachedBluetoothDevice> groupDevicesList)252 CachedBluetoothDevice getPreferredMainDevice(int groupId, 253 List<CachedBluetoothDevice> groupDevicesList) { 254 // How to select the preferred main device? 255 // 1. The DUAL mode connected device which has A2DP/HFP and LE audio. 256 // 2. One of connected LE device in the list. Default is the lead device from LE profile. 257 // 3. If there is no connected device, then reset the relationship. Set the DUAL mode 258 // deviced as the main device. Otherwise, set any one of the device. 259 if (groupDevicesList == null || groupDevicesList.isEmpty()) { 260 return null; 261 } 262 263 CachedBluetoothDevice dualModeDevice = groupDevicesList.stream() 264 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() 265 .anyMatch(profile -> profile instanceof LeAudioProfile)) 266 .filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream() 267 .anyMatch(profile -> profile instanceof A2dpProfile 268 || profile instanceof HeadsetProfile)) 269 .findFirst().orElse(null); 270 if (isDeviceConnected(dualModeDevice)) { 271 log("getPreferredMainDevice: The connected DUAL mode device"); 272 return dualModeDevice; 273 } 274 275 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 276 final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); 277 final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); 278 final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT()) 279 ? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null; 280 281 if (leAudioLeadDevice != null) { 282 log("getPreferredMainDevice: The LeadDevice from LE profile is " 283 + leAudioLeadDevice.getAnonymizedAddress()); 284 } 285 CachedBluetoothDevice leAudioLeadCachedDevice = 286 leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null; 287 if (leAudioLeadCachedDevice == null) { 288 log("getPreferredMainDevice: The LeadDevice is not in the all of devices list"); 289 } else if (isDeviceConnected(leAudioLeadCachedDevice)) { 290 log("getPreferredMainDevice: The connected LeadDevice from LE profile"); 291 return leAudioLeadCachedDevice; 292 } 293 CachedBluetoothDevice oneOfConnectedDevices = 294 groupDevicesList.stream() 295 .filter(cachedDevice -> isDeviceConnected(cachedDevice)) 296 .findFirst() 297 .orElse(null); 298 if (oneOfConnectedDevices != null) { 299 log("getPreferredMainDevice: One of the connected devices."); 300 return oneOfConnectedDevices; 301 } 302 303 if (dualModeDevice != null) { 304 log("getPreferredMainDevice: The DUAL mode device."); 305 return dualModeDevice; 306 } 307 // last 308 if (!groupDevicesList.isEmpty()) { 309 log("getPreferredMainDevice: One of the group devices."); 310 return groupDevicesList.get(0); 311 } 312 return null; 313 } 314 315 @VisibleForTesting addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice)316 boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) { 317 boolean hasChanged = false; 318 if (preferredMainDevice == null) { 319 log("addMemberDevicesIntoMainDevice: No main device. Do nothing."); 320 return hasChanged; 321 } 322 323 // If the current main device is not preferred main device, then set it as new main device. 324 // Otherwise, do nothing. 325 BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice(); 326 CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice); 327 boolean hasPreferredMainDeviceAlreadyBeenMainDevice = 328 mainDeviceOfPreferredMainDevice == null; 329 330 if (!hasPreferredMainDeviceAlreadyBeenMainDevice) { 331 // preferredMainDevice has not been the main device. 332 // switch relationship between the mainDeviceOfPreferredMainDevice and 333 // PreferredMainDevice 334 335 log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. " 336 + "Do switch relationship between the mainDeviceOfPreferredMainDevice and " 337 + "PreferredMainDevice"); 338 // To switch content and dispatch to notify UI change 339 mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice); 340 mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice); 341 mainDeviceOfPreferredMainDevice.refresh(); 342 // It is necessary to do remove and add for updating the mapping on 343 // preference and device 344 mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice); 345 hasChanged = true; 346 } 347 348 // If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are 349 // the same groupId, then combine them and also keep the preferred main device as main 350 // device. 351 List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream() 352 .filter(device -> device.getGroupId() == groupId) 353 .collect(Collectors.toList()); 354 boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1; 355 // Update the new main of CachedBluetoothDevice, since it may be changed in above step. 356 final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager(); 357 preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice); 358 if (haveMultiMainDevicesInAllOfDevicesList) { 359 // put another devices into main device. 360 for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) { 361 if (deviceItem.getDevice() == null || deviceItem.getDevice().equals( 362 bluetoothDeviceOfPreferredMainDevice)) { 363 continue; 364 } 365 366 Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice(); 367 for (CachedBluetoothDevice memberSetItem : memberSet) { 368 if (!memberSetItem.equals(preferredMainDevice)) { 369 preferredMainDevice.addMemberDevice(memberSetItem); 370 } 371 } 372 memberSet.clear(); 373 preferredMainDevice.addMemberDevice(deviceItem); 374 mCachedDevices.remove(deviceItem); 375 mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem); 376 hasChanged = true; 377 } 378 } 379 if (hasChanged) { 380 log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " 381 + mCachedDevices); 382 } 383 return hasChanged; 384 } 385 log(String msg)386 private void log(String msg) { 387 if (DEBUG) { 388 Log.d(TAG, msg); 389 } 390 } 391 isDeviceConnected(CachedBluetoothDevice cachedDevice)392 private boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) { 393 if (cachedDevice == null) { 394 return false; 395 } 396 final BluetoothDevice device = cachedDevice.getDevice(); 397 return cachedDevice.isConnected() 398 && device.getBondState() == BluetoothDevice.BOND_BONDED 399 && device.isConnected(); 400 } 401 } 402