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