1 /* 2 * Copyright (C) 2008 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.BluetoothAdapter; 20 import android.bluetooth.BluetoothCsipSetCoordinator; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.bluetooth.le.ScanFilter; 24 import android.content.Context; 25 import android.util.Log; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.sql.Timestamp; 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Set; 35 36 /** 37 * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices. 38 */ 39 public class CachedBluetoothDeviceManager { 40 private static final String TAG = "CachedBluetoothDeviceManager"; 41 private static final boolean DEBUG = BluetoothUtils.D; 42 43 @VisibleForTesting static int sLateBondingTimeoutMillis = 5000; // 5s 44 45 private Context mContext; 46 private final LocalBluetoothManager mBtManager; 47 48 @VisibleForTesting 49 final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); 50 @VisibleForTesting 51 HearingAidDeviceManager mHearingAidDeviceManager; 52 @VisibleForTesting 53 CsipDeviceManager mCsipDeviceManager; 54 BluetoothDevice mOngoingSetMemberPair; 55 boolean mIsLateBonding; 56 int mGroupIdOfLateBonding; 57 CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager)58 public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { 59 mContext = context; 60 mBtManager = localBtManager; 61 mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager, 62 mCachedDevices); 63 mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices); 64 } 65 getCachedDevicesCopy()66 public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() { 67 return new ArrayList<>(mCachedDevices); 68 } 69 onDeviceDisappeared(CachedBluetoothDevice cachedDevice)70 public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) { 71 cachedDevice.setJustDiscovered(false); 72 return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE; 73 } 74 onDeviceNameUpdated(BluetoothDevice device)75 public void onDeviceNameUpdated(BluetoothDevice device) { 76 CachedBluetoothDevice cachedDevice = findDevice(device); 77 if (cachedDevice != null) { 78 cachedDevice.refreshName(); 79 } 80 } 81 82 /** 83 * Search for existing {@link CachedBluetoothDevice} or return null 84 * if this device isn't in the cache. Use {@link #addDevice} 85 * to create and return a new {@link CachedBluetoothDevice} for 86 * a newly discovered {@link BluetoothDevice}. 87 * 88 * @param device the address of the Bluetooth device 89 * @return the cached device object for this device, or null if it has 90 * not been previously seen 91 */ findDevice(BluetoothDevice device)92 public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) { 93 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 94 if (cachedDevice.getDevice().equals(device)) { 95 return cachedDevice; 96 } 97 // Check the member devices for the coordinated set if it exists 98 final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice(); 99 if (!memberDevices.isEmpty()) { 100 for (CachedBluetoothDevice memberDevice : memberDevices) { 101 if (memberDevice.getDevice().equals(device)) { 102 return memberDevice; 103 } 104 } 105 } 106 // Check sub devices for hearing aid if it exists 107 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 108 if (subDevice != null && subDevice.getDevice().equals(device)) { 109 return subDevice; 110 } 111 } 112 113 return null; 114 } 115 116 /** 117 * Create and return a new {@link CachedBluetoothDevice}. This assumes 118 * that {@link #findDevice} has already been called and returned null. 119 * @param device the new Bluetooth device 120 * @return the newly created CachedBluetoothDevice object 121 */ addDevice(BluetoothDevice device)122 public CachedBluetoothDevice addDevice(BluetoothDevice device) { 123 return addDevice(device, /*leScanFilters=*/null); 124 } 125 126 /** 127 * Create and return a new {@link CachedBluetoothDevice}. This assumes 128 * that {@link #findDevice} has already been called and returned null. 129 * @param device the new Bluetooth device 130 * @param leScanFilters the BLE scan filters which the device matched 131 * @return the newly created CachedBluetoothDevice object 132 */ addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters)133 public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) { 134 CachedBluetoothDevice newDevice; 135 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 136 synchronized (this) { 137 newDevice = findDevice(device); 138 if (newDevice == null) { 139 newDevice = new CachedBluetoothDevice(mContext, profileManager, device); 140 mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice); 141 mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters); 142 if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice) 143 && !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) { 144 mCachedDevices.add(newDevice); 145 mBtManager.getEventManager().dispatchDeviceAdded(newDevice); 146 } 147 } 148 } 149 150 return newDevice; 151 } 152 153 /** 154 * Returns device summary of the pair of the hearing aid / CSIP passed as the parameter. 155 * 156 * @param CachedBluetoothDevice device 157 * @return Device summary, or if the pair does not exist or if it is not a hearing aid or 158 * a CSIP set member, then {@code null}. 159 */ getSubDeviceSummary(CachedBluetoothDevice device)160 public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) { 161 final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice(); 162 // TODO: check the CSIP group size instead of the real member device set size, and adjust 163 // the size restriction. 164 if (!memberDevices.isEmpty()) { 165 for (CachedBluetoothDevice memberDevice : memberDevices) { 166 if (memberDevice.isConnected()) { 167 return memberDevice.getConnectionSummary(); 168 } 169 } 170 } 171 CachedBluetoothDevice subDevice = device.getSubDevice(); 172 if (subDevice != null && subDevice.isConnected()) { 173 return subDevice.getConnectionSummary(); 174 } 175 return null; 176 } 177 178 /** 179 * Search for existing sub device {@link CachedBluetoothDevice}. 180 * 181 * @param device the address of the Bluetooth device 182 * @return true for found sub / member device or false. 183 */ isSubDevice(BluetoothDevice device)184 public synchronized boolean isSubDevice(BluetoothDevice device) { 185 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 186 if (!cachedDevice.getDevice().equals(device)) { 187 // Check the member devices of the coordinated set if it exists 188 Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice(); 189 if (!memberDevices.isEmpty()) { 190 for (CachedBluetoothDevice memberDevice : memberDevices) { 191 if (memberDevice.getDevice().equals(device)) { 192 return true; 193 } 194 } 195 continue; 196 } 197 // Check sub devices of hearing aid if it exists 198 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 199 if (subDevice != null && subDevice.getDevice().equals(device)) { 200 return true; 201 } 202 } 203 } 204 return false; 205 } 206 207 /** 208 * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the 209 * Hearing Aid Service is connected and the HiSyncId's are now available. 210 */ updateHearingAidsDevices()211 public synchronized void updateHearingAidsDevices() { 212 mHearingAidDeviceManager.updateHearingAidsDevices(); 213 } 214 215 /** 216 * Updates the Csip devices; specifically the GroupId's. This routine is called when the 217 * CSIS is connected and the GroupId's are now available. 218 */ updateCsipDevices()219 public synchronized void updateCsipDevices() { 220 mCsipDeviceManager.updateCsipDevices(); 221 } 222 223 /** 224 * Attempts to get the name of a remote device, otherwise returns the address. 225 * 226 * @param device The remote device. 227 * @return The name, or if unavailable, the address. 228 */ getName(BluetoothDevice device)229 public String getName(BluetoothDevice device) { 230 if (isOngoingPairByCsip(device)) { 231 CachedBluetoothDevice firstDevice = 232 mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding); 233 if (firstDevice != null && firstDevice.getName() != null) { 234 return firstDevice.getName(); 235 } 236 } 237 238 CachedBluetoothDevice cachedDevice = findDevice(device); 239 if (cachedDevice != null && cachedDevice.getName() != null) { 240 return cachedDevice.getName(); 241 } 242 243 String name = device.getAlias(); 244 if (name != null) { 245 return name; 246 } 247 248 return device.getAddress(); 249 } 250 clearNonBondedDevices()251 public synchronized void clearNonBondedDevices() { 252 clearNonBondedSubDevices(); 253 final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>(); 254 mCachedDevices.stream() 255 .filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE) 256 .forEach(cachedDevice -> { 257 cachedDevice.release(); 258 removedCachedDevice.add(cachedDevice); 259 }); 260 mCachedDevices.removeAll(removedCachedDevice); 261 } 262 clearNonBondedSubDevices()263 private void clearNonBondedSubDevices() { 264 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 265 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 266 Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice(); 267 if (!memberDevices.isEmpty()) { 268 for (Object it : memberDevices.toArray()) { 269 CachedBluetoothDevice memberDevice = (CachedBluetoothDevice) it; 270 // Member device exists and it is not bonded 271 if (memberDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) { 272 cachedDevice.removeMemberDevice(memberDevice); 273 } 274 } 275 return; 276 } 277 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 278 if (subDevice != null 279 && subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) { 280 // Sub device exists and it is not bonded 281 subDevice.release(); 282 cachedDevice.setSubDevice(null); 283 } 284 } 285 } 286 onScanningStateChanged(boolean started)287 public synchronized void onScanningStateChanged(boolean started) { 288 if (!started) return; 289 // If starting a new scan, clear old visibility 290 // Iterate in reverse order since devices may be removed. 291 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 292 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 293 cachedDevice.setJustDiscovered(false); 294 final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice(); 295 if (!memberDevices.isEmpty()) { 296 for (CachedBluetoothDevice memberDevice : memberDevices) { 297 memberDevice.setJustDiscovered(false); 298 } 299 return; 300 } 301 final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 302 if (subDevice != null) { 303 subDevice.setJustDiscovered(false); 304 } 305 } 306 } 307 onBluetoothStateChanged(int bluetoothState)308 public synchronized void onBluetoothStateChanged(int bluetoothState) { 309 // When Bluetooth is turning off, we need to clear the non-bonded devices 310 // Otherwise, they end up showing up on the next BT enable 311 if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) { 312 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 313 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 314 final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice(); 315 if (!memberDevices.isEmpty()) { 316 for (CachedBluetoothDevice memberDevice : memberDevices) { 317 if (memberDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 318 cachedDevice.removeMemberDevice(memberDevice); 319 } 320 } 321 } else { 322 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); 323 if (subDevice != null) { 324 if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 325 cachedDevice.setSubDevice(null); 326 } 327 } 328 } 329 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 330 cachedDevice.setJustDiscovered(false); 331 cachedDevice.release(); 332 mCachedDevices.remove(i); 333 } 334 } 335 336 // To clear the SetMemberPair flag when the Bluetooth is turning off. 337 mOngoingSetMemberPair = null; 338 mIsLateBonding = false; 339 mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 340 } 341 } 342 onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state, int profileId)343 public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice 344 cachedDevice, int state, int profileId) { 345 if (profileId == BluetoothProfile.HEARING_AID) { 346 return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, 347 state); 348 } 349 if (profileId == BluetoothProfile.HEADSET 350 || profileId == BluetoothProfile.A2DP 351 || profileId == BluetoothProfile.LE_AUDIO 352 || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { 353 return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, 354 state); 355 } 356 return false; 357 } 358 359 /** Handles when the device been set as active/inactive. */ onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice)360 public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) { 361 if (cachedBluetoothDevice.isHearingAidDevice()) { 362 mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice); 363 } 364 } 365 onDeviceUnpaired(CachedBluetoothDevice device)366 public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) { 367 device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); 368 CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device); 369 // Should iterate through the cloned set to avoid ConcurrentModificationException 370 final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice()); 371 if (!memberDevices.isEmpty()) { 372 // Main device is unpaired, also unpair the member devices 373 for (CachedBluetoothDevice memberDevice : memberDevices) { 374 memberDevice.unpair(); 375 memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID); 376 device.removeMemberDevice(memberDevice); 377 } 378 } else if (mainDevice != null) { 379 // Member device is unpaired, also unpair the main device 380 mainDevice.unpair(); 381 } 382 mainDevice = mHearingAidDeviceManager.findMainDevice(device); 383 CachedBluetoothDevice subDevice = device.getSubDevice(); 384 if (subDevice != null) { 385 // Main device is unpaired, to unpair sub device 386 subDevice.unpair(); 387 device.setSubDevice(null); 388 } else if (mainDevice != null) { 389 // Sub device unpaired, to unpair main device 390 mainDevice.unpair(); 391 mainDevice.setSubDevice(null); 392 } 393 } 394 395 /** 396 * Called when we found a set member of a group. The function will check the {@code groupId} if 397 * it exists and the bond state of the device is BOND_NOE, and if there isn't any ongoing pair 398 * , and then return {@code true} to pair the device automatically. 399 * 400 * @param device The found device 401 * @param groupId The group id of the found device 402 * 403 * @return {@code true}, if the device should pair automatically; Otherwise, return 404 * {@code false}. 405 */ shouldPairByCsip(BluetoothDevice device, int groupId)406 private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) { 407 boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null; 408 int bondState = device.getBondState(); 409 boolean groupExists = mCsipDeviceManager.isExistedGroupId(groupId); 410 Log.d(TAG, 411 "isOngoingSetMemberPair=" + isOngoingSetMemberPair + ", bondState=" + bondState 412 + ", groupExists=" + groupExists + ", groupId=" + groupId); 413 414 if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE || !groupExists) { 415 return false; 416 } 417 return true; 418 } 419 checkLateBonding(int groupId)420 private synchronized boolean checkLateBonding(int groupId) { 421 CachedBluetoothDevice firstDevice = mCsipDeviceManager.getFirstMemberDevice(groupId); 422 if (firstDevice == null) { 423 Log.d(TAG, "No first device in group: " + groupId); 424 return false; 425 } 426 427 Timestamp then = firstDevice.getBondTimestamp(); 428 if (then == null) { 429 Log.d(TAG, "No bond timestamp"); 430 return true; 431 } 432 433 Timestamp now = new Timestamp(System.currentTimeMillis()); 434 435 long diff = (now.getTime() - then.getTime()); 436 Log.d(TAG, "Time difference to first bonding: " + diff + "ms"); 437 438 return diff > sLateBondingTimeoutMillis; 439 } 440 441 /** 442 * Called to check if there is an ongoing bonding for the device and it is late bonding. 443 * If the device is not matching the ongoing bonding device then false will be returned. 444 * 445 * @param device The device to check. 446 */ isLateBonding(BluetoothDevice device)447 public synchronized boolean isLateBonding(BluetoothDevice device) { 448 if (!isOngoingPairByCsip(device)) { 449 Log.d(TAG, "isLateBonding: pair not ongoing or not matching device"); 450 return false; 451 } 452 453 Log.d(TAG, "isLateBonding: " + mIsLateBonding); 454 return mIsLateBonding; 455 } 456 457 /** 458 * Called when we found a set member of a group. The function will check the {@code groupId} if 459 * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair 460 * , and then pair the device automatically. 461 * 462 * @param device The found device 463 * @param groupId The group id of the found device 464 */ pairDeviceByCsip(BluetoothDevice device, int groupId)465 public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) { 466 if (!shouldPairByCsip(device, groupId)) { 467 return; 468 } 469 Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP "); 470 mOngoingSetMemberPair = device; 471 mIsLateBonding = checkLateBonding(groupId); 472 mGroupIdOfLateBonding = groupId; 473 syncConfigFromMainDevice(device, groupId); 474 if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) { 475 Log.d(TAG, "Bonding could not be started"); 476 mOngoingSetMemberPair = null; 477 mIsLateBonding = false; 478 mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 479 } 480 } 481 syncConfigFromMainDevice(BluetoothDevice device, int groupId)482 private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) { 483 if (!isOngoingPairByCsip(device)) { 484 return; 485 } 486 CachedBluetoothDevice memberDevice = findDevice(device); 487 CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice); 488 if (mainDevice == null) { 489 mainDevice = mCsipDeviceManager.getCachedDevice(groupId); 490 } 491 492 if (mainDevice == null || mainDevice.equals(memberDevice)) { 493 Log.d(TAG, "no mainDevice"); 494 return; 495 } 496 497 // The memberDevice set PhonebookAccessPermission 498 device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission()); 499 } 500 501 /** 502 * Called when the bond state change. If the bond state change is related with the 503 * ongoing set member pair, the cachedBluetoothDevice will be created but the UI 504 * would not be updated. For the other case, return {@code false} to go through the normal 505 * flow. 506 * 507 * @param device The device 508 * @param bondState The new bond state 509 * 510 * @return {@code true}, if the bond state change for the device is handled inside this 511 * function, and would not like to update the UI. If not, return {@code false}. 512 */ onBondStateChangedIfProcess(BluetoothDevice device, int bondState)513 public synchronized boolean onBondStateChangedIfProcess(BluetoothDevice device, int bondState) { 514 if (!isOngoingPairByCsip(device)) { 515 return false; 516 } 517 518 if (bondState == BluetoothDevice.BOND_BONDING) { 519 return true; 520 } 521 522 mOngoingSetMemberPair = null; 523 mIsLateBonding = false; 524 mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID; 525 if (bondState != BluetoothDevice.BOND_NONE) { 526 if (findDevice(device) == null) { 527 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); 528 CachedBluetoothDevice newDevice = 529 new CachedBluetoothDevice(mContext, profileManager, device); 530 mCachedDevices.add(newDevice); 531 findDevice(device).connect(); 532 } 533 } 534 535 return true; 536 } 537 538 /** 539 * Check if the device is the one which is initial paired locally by CSIP. The setting 540 * would depned on it to accept the pairing request automatically 541 * 542 * @param device The device 543 * 544 * @return {@code true}, if the device is ongoing pair by CSIP. Otherwise, return 545 * {@code false}. 546 */ isOngoingPairByCsip(BluetoothDevice device)547 public boolean isOngoingPairByCsip(BluetoothDevice device) { 548 return mOngoingSetMemberPair != null && mOngoingSetMemberPair.equals(device); 549 } 550 log(String msg)551 private void log(String msg) { 552 if (DEBUG) { 553 Log.d(TAG, msg); 554 } 555 } 556 } 557