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.BluetoothDevice; 21 import android.bluetooth.BluetoothHearingAid; 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.HashMap; 31 import java.util.HashSet; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Set; 35 import java.util.Objects; 36 37 /** 38 * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices. 39 */ 40 public class CachedBluetoothDeviceManager { 41 private static final String TAG = "CachedBluetoothDeviceManager"; 42 private static final boolean DEBUG = Utils.D; 43 44 private Context mContext; 45 private final LocalBluetoothManager mBtManager; 46 47 @VisibleForTesting 48 final List<CachedBluetoothDevice> mCachedDevices = 49 new ArrayList<CachedBluetoothDevice>(); 50 // Contains the list of hearing aid devices that should not be shown in the UI. 51 @VisibleForTesting 52 final List<CachedBluetoothDevice> mHearingAidDevicesNotAddedInCache 53 = new ArrayList<CachedBluetoothDevice>(); 54 // Maintains a list of devices which are added in mCachedDevices and have hiSyncIds. 55 @VisibleForTesting 56 final Map<Long, CachedBluetoothDevice> mCachedDevicesMapForHearingAids 57 = new HashMap<Long, CachedBluetoothDevice>(); 58 CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager)59 CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { 60 mContext = context; 61 mBtManager = localBtManager; 62 } 63 getCachedDevicesCopy()64 public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() { 65 return new ArrayList<CachedBluetoothDevice>(mCachedDevices); 66 } 67 onDeviceDisappeared(CachedBluetoothDevice cachedDevice)68 public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) { 69 cachedDevice.setJustDiscovered(false); 70 return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE; 71 } 72 onDeviceNameUpdated(BluetoothDevice device)73 public void onDeviceNameUpdated(BluetoothDevice device) { 74 CachedBluetoothDevice cachedDevice = findDevice(device); 75 if (cachedDevice != null) { 76 cachedDevice.refreshName(); 77 } 78 } 79 80 /** 81 * Search for existing {@link CachedBluetoothDevice} or return null 82 * if this device isn't in the cache. Use {@link #addDevice} 83 * to create and return a new {@link CachedBluetoothDevice} for 84 * a newly discovered {@link BluetoothDevice}. 85 * 86 * @param device the address of the Bluetooth device 87 * @return the cached device object for this device, or null if it has 88 * not been previously seen 89 */ findDevice(BluetoothDevice device)90 public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) { 91 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 92 if (cachedDevice.getDevice().equals(device)) { 93 return cachedDevice; 94 } 95 } 96 for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) { 97 if (notCachedDevice.getDevice().equals(device)) { 98 return notCachedDevice; 99 } 100 } 101 return null; 102 } 103 104 /** 105 * Create and return a new {@link CachedBluetoothDevice}. This assumes 106 * that {@link #findDevice} has already been called and returned null. 107 * @param device the address of the new Bluetooth device 108 * @return the newly created CachedBluetoothDevice object 109 */ addDevice(LocalBluetoothAdapter adapter, LocalBluetoothProfileManager profileManager, BluetoothDevice device)110 public CachedBluetoothDevice addDevice(LocalBluetoothAdapter adapter, 111 LocalBluetoothProfileManager profileManager, 112 BluetoothDevice device) { 113 CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, adapter, 114 profileManager, device); 115 if (profileManager.getHearingAidProfile() != null 116 && profileManager.getHearingAidProfile().getHiSyncId(newDevice.getDevice()) 117 != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 118 newDevice.setHiSyncId(profileManager.getHearingAidProfile() 119 .getHiSyncId(newDevice.getDevice())); 120 } 121 // Just add one of the hearing aids from a pair in the list that is shown in the UI. 122 if (isPairAddedInCache(newDevice.getHiSyncId())) { 123 synchronized (this) { 124 mHearingAidDevicesNotAddedInCache.add(newDevice); 125 } 126 } else { 127 synchronized (this) { 128 mCachedDevices.add(newDevice); 129 if (newDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID 130 && !mCachedDevicesMapForHearingAids.containsKey(newDevice.getHiSyncId())) { 131 mCachedDevicesMapForHearingAids.put(newDevice.getHiSyncId(), newDevice); 132 } 133 mBtManager.getEventManager().dispatchDeviceAdded(newDevice); 134 } 135 } 136 137 return newDevice; 138 } 139 140 /** 141 * Returns true if the one of the two hearing aid devices is already cached for UI. 142 * 143 * @param long hiSyncId 144 * @return {@code True} if one of the two hearing aid devices is is already cached for UI. 145 */ isPairAddedInCache(long hiSyncId)146 private synchronized boolean isPairAddedInCache(long hiSyncId) { 147 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { 148 return false; 149 } 150 if(mCachedDevicesMapForHearingAids.containsKey(hiSyncId)) { 151 return true; 152 } 153 return false; 154 } 155 156 /** 157 * Returns device summary of the pair of the hearing aid passed as the parameter. 158 * 159 * @param CachedBluetoothDevice device 160 * @return Device summary, or if the pair does not exist or if its not a hearing aid, 161 * then {@code null}. 162 */ getHearingAidPairDeviceSummary(CachedBluetoothDevice device)163 public synchronized String getHearingAidPairDeviceSummary(CachedBluetoothDevice device) { 164 String pairDeviceSummary = null; 165 if (device.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 166 for (CachedBluetoothDevice hearingAidDevice : mHearingAidDevicesNotAddedInCache) { 167 if (hearingAidDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID 168 && hearingAidDevice.getHiSyncId() == device.getHiSyncId()) { 169 pairDeviceSummary = hearingAidDevice.getConnectionSummary(); 170 } 171 } 172 } 173 return pairDeviceSummary; 174 } 175 176 /** 177 * Adds the 2nd hearing aid in a pair in a list that maintains the hearing aids that are 178 * not dispalyed in the UI. 179 * 180 * @param CachedBluetoothDevice device 181 */ addDeviceNotaddedInMap(CachedBluetoothDevice device)182 public synchronized void addDeviceNotaddedInMap(CachedBluetoothDevice device) { 183 mHearingAidDevicesNotAddedInCache.add(device); 184 } 185 186 /** 187 * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the 188 * Hearing Aid Service is connected and the HiSyncId's are now available. 189 * @param LocalBluetoothProfileManager profileManager 190 */ updateHearingAidsDevices(LocalBluetoothProfileManager profileManager)191 public synchronized void updateHearingAidsDevices(LocalBluetoothProfileManager profileManager) { 192 HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); 193 if (profileProxy == null) { 194 log("updateHearingAidsDevices: getHearingAidProfile() is null"); 195 return; 196 } 197 final Set<Long> syncIdChangedSet = new HashSet<Long>(); 198 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 199 if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 200 continue; 201 } 202 203 long newHiSyncId = profileProxy.getHiSyncId(cachedDevice.getDevice()); 204 205 if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { 206 cachedDevice.setHiSyncId(newHiSyncId); 207 syncIdChangedSet.add(newHiSyncId); 208 } 209 } 210 for (Long syncId : syncIdChangedSet) { 211 onHiSyncIdChanged(syncId); 212 } 213 } 214 215 /** 216 * Attempts to get the name of a remote device, otherwise returns the address. 217 * 218 * @param device The remote device. 219 * @return The name, or if unavailable, the address. 220 */ getName(BluetoothDevice device)221 public String getName(BluetoothDevice device) { 222 CachedBluetoothDevice cachedDevice = findDevice(device); 223 if (cachedDevice != null && cachedDevice.getName() != null) { 224 return cachedDevice.getName(); 225 } 226 227 String name = device.getAliasName(); 228 if (name != null) { 229 return name; 230 } 231 232 return device.getAddress(); 233 } 234 clearNonBondedDevices()235 public synchronized void clearNonBondedDevices() { 236 237 mCachedDevicesMapForHearingAids.entrySet().removeIf(entries 238 -> entries.getValue().getBondState() == BluetoothDevice.BOND_NONE); 239 240 mCachedDevices.removeIf(cachedDevice 241 -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE); 242 243 mHearingAidDevicesNotAddedInCache.removeIf(hearingAidDevice 244 -> hearingAidDevice.getBondState() == BluetoothDevice.BOND_NONE); 245 } 246 onScanningStateChanged(boolean started)247 public synchronized void onScanningStateChanged(boolean started) { 248 if (!started) return; 249 // If starting a new scan, clear old visibility 250 // Iterate in reverse order since devices may be removed. 251 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 252 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 253 cachedDevice.setJustDiscovered(false); 254 } 255 for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { 256 CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i); 257 notCachedDevice.setJustDiscovered(false); 258 } 259 } 260 onBtClassChanged(BluetoothDevice device)261 public synchronized void onBtClassChanged(BluetoothDevice device) { 262 CachedBluetoothDevice cachedDevice = findDevice(device); 263 if (cachedDevice != null) { 264 cachedDevice.refreshBtClass(); 265 } 266 } 267 onUuidChanged(BluetoothDevice device)268 public synchronized void onUuidChanged(BluetoothDevice device) { 269 CachedBluetoothDevice cachedDevice = findDevice(device); 270 if (cachedDevice != null) { 271 cachedDevice.onUuidChanged(); 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 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 282 cachedDevice.setJustDiscovered(false); 283 mCachedDevices.remove(i); 284 if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID 285 && mCachedDevicesMapForHearingAids.containsKey(cachedDevice.getHiSyncId())) 286 { 287 mCachedDevicesMapForHearingAids.remove(cachedDevice.getHiSyncId()); 288 } 289 } else { 290 // For bonded devices, we need to clear the connection status so that 291 // when BT is enabled next time, device connection status shall be retrieved 292 // by making a binder call. 293 cachedDevice.clearProfileConnectionState(); 294 } 295 } 296 for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { 297 CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i); 298 if (notCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 299 notCachedDevice.setJustDiscovered(false); 300 mHearingAidDevicesNotAddedInCache.remove(i); 301 } else { 302 // For bonded devices, we need to clear the connection status so that 303 // when BT is enabled next time, device connection status shall be retrieved 304 // by making a binder call. 305 notCachedDevice.clearProfileConnectionState(); 306 } 307 } 308 } 309 } 310 onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile)311 public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, 312 int bluetoothProfile) { 313 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 314 boolean isActive = Objects.equals(cachedDevice, activeDevice); 315 cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile); 316 } 317 } 318 onHiSyncIdChanged(long hiSyncId)319 public synchronized void onHiSyncIdChanged(long hiSyncId) { 320 int firstMatchedIndex = -1; 321 322 for (int i = mCachedDevices.size() - 1; i >= 0; i--) { 323 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); 324 if (cachedDevice.getHiSyncId() == hiSyncId) { 325 if (firstMatchedIndex != -1) { 326 /* Found the second one */ 327 int indexToRemoveFromUi; 328 CachedBluetoothDevice deviceToRemoveFromUi; 329 330 // Since the hiSyncIds have been updated for a connected pair of hearing aids, 331 // we remove the entry of one the hearing aids from the UI. Unless the 332 // hiSyncId get updated, the system does not know it is a hearing aid, so we add 333 // both the hearing aids as separate entries in the UI first, then remove one 334 // of them after the hiSyncId is populated. We will choose the device that 335 // is not connected to be removed. 336 if (cachedDevice.isConnected()) { 337 indexToRemoveFromUi = firstMatchedIndex; 338 deviceToRemoveFromUi = mCachedDevices.get(firstMatchedIndex); 339 mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice); 340 } else { 341 indexToRemoveFromUi = i; 342 deviceToRemoveFromUi = cachedDevice; 343 mCachedDevicesMapForHearingAids.put(hiSyncId, 344 mCachedDevices.get(firstMatchedIndex)); 345 } 346 347 mCachedDevices.remove(indexToRemoveFromUi); 348 mHearingAidDevicesNotAddedInCache.add(deviceToRemoveFromUi); 349 log("onHiSyncIdChanged: removed from UI device=" + deviceToRemoveFromUi 350 + ", with hiSyncId=" + hiSyncId); 351 mBtManager.getEventManager().dispatchDeviceRemoved(deviceToRemoveFromUi); 352 break; 353 } else { 354 mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice); 355 firstMatchedIndex = i; 356 } 357 } 358 } 359 } 360 getHearingAidOtherDevice(CachedBluetoothDevice thisDevice, long hiSyncId)361 private CachedBluetoothDevice getHearingAidOtherDevice(CachedBluetoothDevice thisDevice, 362 long hiSyncId) { 363 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { 364 return null; 365 } 366 367 // Searched the lists for the other side device with the matching hiSyncId. 368 for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) { 369 if ((hiSyncId == notCachedDevice.getHiSyncId()) && 370 (!Objects.equals(notCachedDevice, thisDevice))) { 371 return notCachedDevice; 372 } 373 } 374 375 CachedBluetoothDevice cachedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); 376 if (!Objects.equals(cachedDevice, thisDevice)) { 377 return cachedDevice; 378 } 379 return null; 380 } 381 hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice, CachedBluetoothDevice toHideDevice, long hiSyncId)382 private void hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice, 383 CachedBluetoothDevice toHideDevice, long hiSyncId) 384 { 385 log("hearingAidSwitchDisplayDevice: toDisplayDevice=" + toDisplayDevice 386 + ", toHideDevice=" + toHideDevice); 387 388 // Remove the "toHideDevice" device from the UI. 389 mHearingAidDevicesNotAddedInCache.add(toHideDevice); 390 mCachedDevices.remove(toHideDevice); 391 mBtManager.getEventManager().dispatchDeviceRemoved(toHideDevice); 392 393 // Add the "toDisplayDevice" device to the UI. 394 mHearingAidDevicesNotAddedInCache.remove(toDisplayDevice); 395 mCachedDevices.add(toDisplayDevice); 396 mCachedDevicesMapForHearingAids.put(hiSyncId, toDisplayDevice); 397 mBtManager.getEventManager().dispatchDeviceAdded(toDisplayDevice); 398 } 399 onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)400 public synchronized void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, 401 int state, int bluetoothProfile) { 402 if (bluetoothProfile == BluetoothProfile.HEARING_AID 403 && cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID 404 && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { 405 406 long hiSyncId = cachedDevice.getHiSyncId(); 407 408 CachedBluetoothDevice otherDevice = getHearingAidOtherDevice(cachedDevice, hiSyncId); 409 if (otherDevice == null) { 410 // no other side device. Nothing to do. 411 return; 412 } 413 414 if (state == BluetoothProfile.STATE_CONNECTED && 415 mHearingAidDevicesNotAddedInCache.contains(cachedDevice)) { 416 hearingAidSwitchDisplayDevice(cachedDevice, otherDevice, hiSyncId); 417 } else if (state == BluetoothProfile.STATE_DISCONNECTED 418 && otherDevice.isConnected()) { 419 CachedBluetoothDevice mapDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); 420 if ((mapDevice != null) && (Objects.equals(cachedDevice, mapDevice))) { 421 hearingAidSwitchDisplayDevice(otherDevice, cachedDevice, hiSyncId); 422 } 423 } 424 } 425 } 426 onDeviceUnpaired(CachedBluetoothDevice device)427 public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) { 428 final long hiSyncId = device.getHiSyncId(); 429 430 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) return; 431 432 for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { 433 CachedBluetoothDevice cachedDevice = mHearingAidDevicesNotAddedInCache.get(i); 434 if (cachedDevice.getHiSyncId() == hiSyncId) { 435 // TODO: Look for more cleanups on unpairing the device. 436 mHearingAidDevicesNotAddedInCache.remove(i); 437 if (device == cachedDevice) continue; 438 log("onDeviceUnpaired: Unpair device=" + cachedDevice); 439 cachedDevice.unpair(); 440 } 441 } 442 443 CachedBluetoothDevice mappedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); 444 if ((mappedDevice != null) && (!Objects.equals(device, mappedDevice))) { 445 log("onDeviceUnpaired: Unpair mapped device=" + mappedDevice); 446 mappedDevice.unpair(); 447 } 448 } 449 dispatchAudioModeChanged()450 public synchronized void dispatchAudioModeChanged() { 451 for (CachedBluetoothDevice cachedDevice : mCachedDevices) { 452 cachedDevice.onAudioModeChanged(); 453 } 454 } 455 log(String msg)456 private void log(String msg) { 457 if (DEBUG) { 458 Log.d(TAG, msg); 459 } 460 } 461 } 462