1 /* 2 * Copyright (C) 2019 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.car; 18 19 import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_A2DP_SINK_DEVICES; 20 import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_HFP_CLIENT_DEVICES; 21 import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_MAP_CLIENT_DEVICES; 22 import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PAN_DEVICES; 23 import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PBAP_CLIENT_DEVICES; 24 25 import android.bluetooth.BluetoothA2dpSink; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothHeadsetClient; 29 import android.bluetooth.BluetoothMapClient; 30 import android.bluetooth.BluetoothPan; 31 import android.bluetooth.BluetoothPbapClient; 32 import android.bluetooth.BluetoothProfile; 33 import android.bluetooth.BluetoothUuid; 34 import android.car.ICarBluetoothUserService; 35 import android.content.BroadcastReceiver; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.os.ParcelUuid; 42 import android.os.Parcelable; 43 import android.os.RemoteException; 44 import android.os.UserHandle; 45 import android.provider.Settings; 46 import android.util.Log; 47 import android.util.SparseArray; 48 49 import com.android.internal.annotations.GuardedBy; 50 51 import java.io.PrintWriter; 52 import java.util.ArrayList; 53 import java.util.Arrays; 54 import java.util.List; 55 import java.util.Objects; 56 import java.util.Set; 57 58 /** 59 * BluetoothProfileDeviceManager - Manages a list of devices, sorted by connection attempt priority. 60 * Provides a means for other applications to request connection events and adjust the device 61 * connection priorities. Access to these functions is provided through CarBluetoothManager. 62 */ 63 public class BluetoothProfileDeviceManager { 64 private static final String TAG = "BluetoothProfileDeviceManager"; 65 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 66 private final Context mContext; 67 private final int mUserId; 68 69 private static final String SETTINGS_DELIMITER = ","; 70 71 private static final int AUTO_CONNECT_TIMEOUT_MS = 8000; 72 private static final Object AUTO_CONNECT_TOKEN = new Object(); 73 74 private static class BluetoothProfileInfo { 75 final String mSettingsKey; 76 final String mConnectionAction; 77 final ParcelUuid[] mUuids; 78 final int[] mProfileTriggers; 79 BluetoothProfileInfo(String action, String settingsKey, ParcelUuid[] uuids, int[] profileTriggers)80 private BluetoothProfileInfo(String action, String settingsKey, ParcelUuid[] uuids, 81 int[] profileTriggers) { 82 mSettingsKey = settingsKey; 83 mConnectionAction = action; 84 mUuids = uuids; 85 mProfileTriggers = profileTriggers; 86 } 87 } 88 89 private static final SparseArray<BluetoothProfileInfo> sProfileActions = new SparseArray(); 90 static { sProfileActions.put(BluetoothProfile.A2DP_SINK, new BluetoothProfileInfo(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED, KEY_BLUETOOTH_A2DP_SINK_DEVICES, new ParcelUuid[] { BluetoothUuid.A2DP_SOURCE }, new int[] {}))91 sProfileActions.put(BluetoothProfile.A2DP_SINK, 92 new BluetoothProfileInfo(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED, 93 KEY_BLUETOOTH_A2DP_SINK_DEVICES, new ParcelUuid[] { 94 BluetoothUuid.A2DP_SOURCE 95 }, new int[] {})); sProfileActions.put(BluetoothProfile.HEADSET_CLIENT, new BluetoothProfileInfo(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED, KEY_BLUETOOTH_HFP_CLIENT_DEVICES, new ParcelUuid[] { BluetoothUuid.HFP_AG, BluetoothUuid.HSP_AG }, new int[] {BluetoothProfile.MAP_CLIENT, BluetoothProfile.PBAP_CLIENT}))96 sProfileActions.put(BluetoothProfile.HEADSET_CLIENT, 97 new BluetoothProfileInfo(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED, 98 KEY_BLUETOOTH_HFP_CLIENT_DEVICES, new ParcelUuid[] { 99 BluetoothUuid.HFP_AG, 100 BluetoothUuid.HSP_AG 101 }, new int[] {BluetoothProfile.MAP_CLIENT, BluetoothProfile.PBAP_CLIENT})); sProfileActions.put(BluetoothProfile.MAP_CLIENT, new BluetoothProfileInfo(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED, KEY_BLUETOOTH_MAP_CLIENT_DEVICES, new ParcelUuid[] { BluetoothUuid.MAS }, new int[] {}))102 sProfileActions.put(BluetoothProfile.MAP_CLIENT, 103 new BluetoothProfileInfo(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED, 104 KEY_BLUETOOTH_MAP_CLIENT_DEVICES, new ParcelUuid[] { 105 BluetoothUuid.MAS 106 }, new int[] {})); sProfileActions.put(BluetoothProfile.PAN, new BluetoothProfileInfo(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED, KEY_BLUETOOTH_PAN_DEVICES, new ParcelUuid[] { BluetoothUuid.PANU }, new int[] {}))107 sProfileActions.put(BluetoothProfile.PAN, 108 new BluetoothProfileInfo(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED, 109 KEY_BLUETOOTH_PAN_DEVICES, new ParcelUuid[] { 110 BluetoothUuid.PANU 111 }, new int[] {})); sProfileActions.put(BluetoothProfile.PBAP_CLIENT, new BluetoothProfileInfo(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED, KEY_BLUETOOTH_PBAP_CLIENT_DEVICES, new ParcelUuid[] { BluetoothUuid.PBAP_PSE }, new int[] {}))112 sProfileActions.put(BluetoothProfile.PBAP_CLIENT, 113 new BluetoothProfileInfo(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED, 114 KEY_BLUETOOTH_PBAP_CLIENT_DEVICES, new ParcelUuid[] { 115 BluetoothUuid.PBAP_PSE 116 }, new int[] {})); 117 } 118 119 // Fixed per-profile information for the profile this object manages 120 private final int mProfileId; 121 private final String mSettingsKey; 122 private final String mProfileConnectionAction; 123 private final ParcelUuid[] mProfileUuids; 124 private final int[] mProfileTriggers; 125 126 // Central priority list of devices 127 private final Object mPrioritizedDevicesLock = new Object(); 128 @GuardedBy("mPrioritizedDevicesLock") 129 private ArrayList<BluetoothDevice> mPrioritizedDevices; 130 131 // Auto connection process state 132 private final Object mAutoConnectLock = new Object(); 133 @GuardedBy("mAutoConnectLock") 134 private boolean mConnecting = false; 135 @GuardedBy("mAutoConnectLock") 136 private int mAutoConnectPriority; 137 @GuardedBy("mAutoConnectLock") 138 private ArrayList<BluetoothDevice> mAutoConnectingDevices; 139 140 private final BluetoothAdapter mBluetoothAdapter; 141 private final BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; 142 private final ICarBluetoothUserService mBluetoothUserProxies; 143 private final Handler mHandler = new Handler(Looper.getMainLooper()); 144 145 /** 146 * A BroadcastReceiver that listens specifically for actions related to the profile we're 147 * tracking and uses them to update the status. 148 */ 149 private class BluetoothBroadcastReceiver extends BroadcastReceiver { 150 @Override onReceive(Context context, Intent intent)151 public void onReceive(Context context, Intent intent) { 152 String action = intent.getAction(); 153 if (mProfileConnectionAction.equals(action)) { 154 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 155 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 156 BluetoothProfile.STATE_DISCONNECTED); 157 handleDeviceConnectionStateChange(device, state); 158 } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 159 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 160 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 161 BluetoothDevice.ERROR); 162 handleDeviceBondStateChange(device, state); 163 } else if (BluetoothDevice.ACTION_UUID.equals(action)) { 164 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 165 Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID); 166 handleDeviceUuidEvent(device, uuids); 167 } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { 168 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); 169 handleAdapterStateChange(state); 170 } 171 } 172 } 173 174 /** 175 * Handles an incoming Profile-Device connection event. 176 * 177 * On <BluetoothProfile>.ACTION_CONNECTION_STATE_CHANGED coming from the BroadcastReceiver: 178 * On connected, if we're auto connecting and this is the current device we're managing, then 179 * see if we can move on to the next device in the list. Otherwise, If the device connected 180 * then add it to our priority list if it's not on their already. 181 * 182 * On disconnected, if the device that disconnected also has had its profile priority set to 183 * PRIORITY_OFF, then remove it from our list. 184 * 185 * @param device - The Bluetooth device the state change is for 186 * @param state - The new profile connection state of the device 187 */ handleDeviceConnectionStateChange(BluetoothDevice device, int state)188 private void handleDeviceConnectionStateChange(BluetoothDevice device, int state) { 189 logd("Connection state changed [device: " + device + ", state: " 190 + Utils.getConnectionStateName(state) + "]"); 191 if (state == BluetoothProfile.STATE_CONNECTED) { 192 if (isAutoConnecting() && isAutoConnectingDevice(device)) { 193 continueAutoConnecting(); 194 } else { 195 if (getProfilePriority(device) >= BluetoothProfile.PRIORITY_ON) { 196 addDevice(device); // No-op if device is in the list. 197 } 198 triggerConnections(device); 199 } 200 } 201 // NOTE: We wanted check on disconnect if a device is priority off and use that as an 202 // indicator to remove a device from the list, but priority reporting can be flaky and 203 // was leading to us removing devices when we didn't want to. 204 } 205 206 /** 207 * Handles an incoming device bond status event. 208 * 209 * On BluetoothDevice.ACTION_BOND_STATE_CHANGED: 210 * - If a device becomes unbonded, remove it from our list if it's there. 211 * - If it's bonded, then add it to our list if the UUID set says it supports us. 212 * 213 * @param device - The Bluetooth device the state change is for 214 * @param state - The new bond state of the device 215 */ handleDeviceBondStateChange(BluetoothDevice device, int state)216 private void handleDeviceBondStateChange(BluetoothDevice device, int state) { 217 logd("Bond state has changed [device: " + device + ", state: " 218 + Utils.getBondStateName(state) + "]"); 219 if (state == BluetoothDevice.BOND_NONE) { 220 // Note: We have seen cases of unbonding events being sent without actually 221 // unbonding the device. 222 removeDevice(device); 223 } else if (state == BluetoothDevice.BOND_BONDED) { 224 addBondedDeviceIfSupported(device); 225 } 226 } 227 228 /** 229 * Handles an incoming device UUID set update event. 230 * 231 * On BluetoothDevice.ACTION_UUID: 232 * If the UUID is one this profile cares about, set the profile priority for the device that 233 * the UUID was found on to PRIORITY_ON if its not PRIORITY_OFF already (meaning inhibited or 234 * disabled by the user through settings). 235 * 236 * @param device - The Bluetooth device the UUID event is for 237 * @param uuids - The incoming set of supported UUIDs for the device 238 */ handleDeviceUuidEvent(BluetoothDevice device, Parcelable[] uuids)239 private void handleDeviceUuidEvent(BluetoothDevice device, Parcelable[] uuids) { 240 logd("UUIDs found, device: " + device); 241 if (uuids != null) { 242 ParcelUuid[] uuidsToSend = new ParcelUuid[uuids.length]; 243 for (int i = 0; i < uuidsToSend.length; i++) { 244 uuidsToSend[i] = (ParcelUuid) uuids[i]; 245 } 246 provisionDeviceIfSupported(device, uuidsToSend); 247 } 248 } 249 250 /** 251 * Handle an adapter state change event. 252 * 253 * On BluetoothAdapter.ACTION_STATE_CHANGED: 254 * If the adapter is going into the OFF state, then cancel any auto connecting, commit our 255 * priority list and go idle. 256 * 257 * @param state - The new state of the Bluetooth adapter 258 */ handleAdapterStateChange(int state)259 private void handleAdapterStateChange(int state) { 260 logd("Bluetooth Adapter state changed: " + Utils.getAdapterStateName(state)); 261 // Crashes of the BT stack mean we're not promised to see all the state changes we 262 // might want to see. In order to be a bit more robust to crashes, we'll treat any 263 // non-ON state as a time to cancel auto-connect. This gives us a better chance of 264 // seeing a cancel state before a crash, as well as makes sure we're "cancelled" 265 // before we see an ON. 266 if (state != BluetoothAdapter.STATE_ON) { 267 cancelAutoConnecting(); 268 } 269 // To reduce how many times we're committing the list, we'll only write back on off 270 if (state == BluetoothAdapter.STATE_OFF) { 271 commit(); 272 } 273 } 274 275 /** 276 * Creates an instance of BluetoothProfileDeviceManager that will manage devices 277 * for the given profile ID. 278 * 279 * @param context - context of calling code 280 * @param userId - ID of user we want to manage devices for 281 * @param bluetoothUserProxies - Set of per-user bluetooth proxies for calling into the 282 * bluetooth stack as the current user. 283 * @param profileId - BluetoothProfile integer that represents the profile we're managing 284 * @return A new instance of a BluetoothProfileDeviceManager, or null on any error 285 */ create(Context context, int userId, ICarBluetoothUserService bluetoothUserProxies, int profileId)286 public static BluetoothProfileDeviceManager create(Context context, int userId, 287 ICarBluetoothUserService bluetoothUserProxies, int profileId) { 288 try { 289 return new BluetoothProfileDeviceManager(context, userId, bluetoothUserProxies, 290 profileId); 291 } catch (NullPointerException | IllegalArgumentException e) { 292 return null; 293 } 294 } 295 296 /** 297 * Creates an instance of BluetoothProfileDeviceManager that will manage devices 298 * for the given profile ID. 299 * 300 * @param context - context of calling code 301 * @param userId - ID of user we want to manage devices for 302 * @param bluetoothUserProxies - Set of per-user bluetooth proxies for calling into the 303 * bluetooth stack as the current user. 304 * @param profileId - BluetoothProfile integer that represents the profile we're managing 305 * @return A new instance of a BluetoothProfileDeviceManager 306 */ BluetoothProfileDeviceManager(Context context, int userId, ICarBluetoothUserService bluetoothUserProxies, int profileId)307 private BluetoothProfileDeviceManager(Context context, int userId, 308 ICarBluetoothUserService bluetoothUserProxies, int profileId) { 309 mContext = Objects.requireNonNull(context); 310 mUserId = userId; 311 mBluetoothUserProxies = bluetoothUserProxies; 312 313 mPrioritizedDevices = new ArrayList<>(); 314 BluetoothProfileInfo bpi = sProfileActions.get(profileId); 315 if (bpi == null) { 316 throw new IllegalArgumentException("Provided profile " + Utils.getProfileName(profileId) 317 + " is unrecognized"); 318 } 319 mProfileId = profileId; 320 mSettingsKey = bpi.mSettingsKey; 321 mProfileConnectionAction = bpi.mConnectionAction; 322 mProfileUuids = bpi.mUuids; 323 mProfileTriggers = bpi.mProfileTriggers; 324 325 mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); 326 mBluetoothAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter()); 327 } 328 329 /** 330 * Begin managing devices for this profile. Sets the start state from persistent memory. 331 */ start()332 public void start() { 333 logd("Starting device management"); 334 load(); 335 synchronized (mAutoConnectLock) { 336 mConnecting = false; 337 mAutoConnectPriority = -1; 338 mAutoConnectingDevices = null; 339 } 340 341 IntentFilter profileFilter = new IntentFilter(); 342 profileFilter.addAction(mProfileConnectionAction); 343 profileFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 344 profileFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 345 profileFilter.addAction(BluetoothDevice.ACTION_UUID); 346 mContext.registerReceiverAsUser(mBluetoothBroadcastReceiver, UserHandle.CURRENT, 347 profileFilter, null, null); 348 } 349 350 /** 351 * Stop managing devices for this profile. Commits the final priority list to persistent memory 352 * and cleans up local resources. 353 */ stop()354 public void stop() { 355 logd("Stopping device management"); 356 if (mBluetoothBroadcastReceiver != null) { 357 if (mContext != null) { 358 mContext.unregisterReceiver(mBluetoothBroadcastReceiver); 359 } 360 } 361 cancelAutoConnecting(); 362 commit(); 363 return; 364 } 365 366 /** 367 * Loads the current device priority list from persistent memory in {@link Settings.Secure}. 368 * 369 * This will overwrite the contents of the local priority list. It does not attempt to take the 370 * union of the file and existing set. As such, you likely do not want to load after starting. 371 * Failed attempts to load leave the prioritized device list unchanged. 372 * 373 * @return true on success, false otherwise 374 */ load()375 private boolean load() { 376 logd("Loading device priority list snapshot using key '" + mSettingsKey + "'"); 377 378 // Read from Settings.Secure for our profile, as the current user. 379 String devicesStr = Settings.Secure.getStringForUser(mContext.getContentResolver(), 380 mSettingsKey, mUserId); 381 logd("Found Device String: '" + devicesStr + "'"); 382 if (devicesStr == null || "".equals(devicesStr)) { 383 return false; 384 } 385 386 // Split string into list of device MAC addresses 387 List<String> deviceList = Arrays.asList(devicesStr.split(SETTINGS_DELIMITER)); 388 if (deviceList == null) { 389 return false; 390 } 391 392 // Turn the strings into full blown Bluetooth devices 393 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 394 for (String address : deviceList) { 395 try { 396 BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); 397 devices.add(device); 398 } catch (IllegalArgumentException e) { 399 logw("Unable to parse address '" + address + "' to a device"); 400 continue; 401 } 402 } 403 404 synchronized (mPrioritizedDevicesLock) { 405 mPrioritizedDevices = devices; 406 } 407 408 logd("Loaded Priority list: " + devices); 409 return true; 410 } 411 412 /** 413 * Commits the current device priority list to persistent memory in {@link Settings.Secure}. 414 * 415 * @return true on success, false otherwise 416 */ commit()417 private boolean commit() { 418 StringBuilder sb = new StringBuilder(); 419 String delimiter = ""; 420 synchronized (mPrioritizedDevicesLock) { 421 for (BluetoothDevice device : mPrioritizedDevices) { 422 sb.append(delimiter); 423 sb.append(device.getAddress()); 424 delimiter = SETTINGS_DELIMITER; 425 } 426 } 427 428 String devicesStr = sb.toString(); 429 Settings.Secure.putStringForUser(mContext.getContentResolver(), mSettingsKey, devicesStr, 430 mUserId); 431 logd("Committed key: " + mSettingsKey + ", value: '" + devicesStr + "'"); 432 return true; 433 } 434 435 /** 436 * Syncs the current priority list against the list of bonded devices from the adapter so that 437 * we can make sure things haven't changed on us between the last two times we've ran. 438 */ sync()439 private void sync() { 440 logd("Syncing the priority list with the adapter's list of bonded devices"); 441 Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices(); 442 for (BluetoothDevice device : bondedDevices) { 443 addDevice(device); // No-op if device is already in the priority list 444 } 445 446 synchronized (mPrioritizedDevicesLock) { 447 ArrayList<BluetoothDevice> devices = getDeviceListSnapshot(); 448 for (BluetoothDevice device : devices) { 449 if (!bondedDevices.contains(device)) { 450 removeDevice(device); 451 } 452 } 453 } 454 } 455 456 /** 457 * Makes a clone of the current prioritized device list in a synchronized fashion 458 * 459 * @return A clone of the most up to date prioritized device list 460 */ getDeviceListSnapshot()461 public ArrayList<BluetoothDevice> getDeviceListSnapshot() { 462 ArrayList<BluetoothDevice> devices = new ArrayList<>(); 463 synchronized (mPrioritizedDevicesLock) { 464 devices = (ArrayList) mPrioritizedDevices.clone(); 465 } 466 return devices; 467 } 468 469 /** 470 * Adds a device to the end of the priority list. 471 * 472 * @param device - The device you wish to add 473 */ addDevice(BluetoothDevice device)474 public void addDevice(BluetoothDevice device) { 475 if (device == null) return; 476 synchronized (mPrioritizedDevicesLock) { 477 if (mPrioritizedDevices.contains(device)) return; 478 logd("Add device " + device); 479 mPrioritizedDevices.add(device); 480 commit(); 481 } 482 } 483 484 /** 485 * Removes a device from the priority list. 486 * 487 * @param device - The device you wish to remove 488 */ removeDevice(BluetoothDevice device)489 public void removeDevice(BluetoothDevice device) { 490 if (device == null) return; 491 synchronized (mPrioritizedDevicesLock) { 492 if (!mPrioritizedDevices.contains(device)) return; 493 logd("Remove device " + device); 494 mPrioritizedDevices.remove(device); 495 commit(); 496 } 497 } 498 499 /** 500 * Get the connection priority of a device. 501 * 502 * @param device - The device you want the priority of 503 * @return The priority of the device, or -1 if the device is not in the list 504 */ getDeviceConnectionPriority(BluetoothDevice device)505 public int getDeviceConnectionPriority(BluetoothDevice device) { 506 if (device == null) return -1; 507 logd("Get connection priority of " + device); 508 synchronized (mPrioritizedDevicesLock) { 509 return mPrioritizedDevices.indexOf(device); 510 } 511 } 512 513 /** 514 * Set the connection priority of a device. 515 * 516 * If the devide does not exist, it will be added. If the priority is less than zero, 517 * no priority will be set. If the priority exceeds the bounds of the list, no priority will be 518 * set. 519 * 520 * @param device - The device you want to set the priority of 521 * @param priority - The priority you want to the device to have 522 */ setDeviceConnectionPriority(BluetoothDevice device, int priority)523 public void setDeviceConnectionPriority(BluetoothDevice device, int priority) { 524 synchronized (mPrioritizedDevicesLock) { 525 if (device == null || priority < 0 || priority > mPrioritizedDevices.size() 526 || getDeviceConnectionPriority(device) == priority) return; 527 if (mPrioritizedDevices.contains(device)) { 528 mPrioritizedDevices.remove(device); 529 if (priority > mPrioritizedDevices.size()) priority = mPrioritizedDevices.size(); 530 } 531 logd("Set connection priority of " + device + " to " + priority); 532 mPrioritizedDevices.add(priority, device); 533 commit(); 534 } 535 } 536 537 /** 538 * Connect a specific device on this profile. 539 * 540 * @param device - The device to connect 541 * @return true on success, false otherwise 542 */ connect(BluetoothDevice device)543 private boolean connect(BluetoothDevice device) { 544 logd("Connecting " + device); 545 try { 546 return mBluetoothUserProxies.bluetoothConnectToProfile(mProfileId, device); 547 } catch (RemoteException e) { 548 logw("Failed to connect " + device + ", Reason: " + e); 549 } 550 return false; 551 } 552 553 /** 554 * Disconnect a specific device from this profile. 555 * 556 * @param device - The device to disconnect 557 * @return true on success, false otherwise 558 */ disconnect(BluetoothDevice device)559 private boolean disconnect(BluetoothDevice device) { 560 logd("Disconnecting " + device); 561 try { 562 return mBluetoothUserProxies.bluetoothDisconnectFromProfile(mProfileId, device); 563 } catch (RemoteException e) { 564 logw("Failed to disconnect " + device + ", Reason: " + e); 565 } 566 return false; 567 } 568 569 /** 570 * Gets the Bluetooth stack priority on this profile for a specific device. 571 * 572 * @param device - The device to get the Bluetooth stack priority of 573 * @return The Bluetooth stack priority on this profile for the given device 574 */ getProfilePriority(BluetoothDevice device)575 private int getProfilePriority(BluetoothDevice device) { 576 try { 577 return mBluetoothUserProxies.getProfilePriority(mProfileId, device); 578 } catch (RemoteException e) { 579 logw("Failed to get bluetooth stack priority for " + device + ", Reason: " + e); 580 } 581 return BluetoothProfile.PRIORITY_UNDEFINED; 582 } 583 584 /** 585 * Gets the Bluetooth stack priority on this profile for a specific device. 586 * 587 * @param device - The device to set the Bluetooth stack priority of 588 * @return true on success, false otherwise 589 */ setProfilePriority(BluetoothDevice device, int priority)590 private boolean setProfilePriority(BluetoothDevice device, int priority) { 591 logd("Set " + device + " stack priority to " + Utils.getProfilePriorityName(priority)); 592 try { 593 mBluetoothUserProxies.setProfilePriority(mProfileId, device, priority); 594 } catch (RemoteException e) { 595 logw("Failed to set bluetooth stack priority for " + device + ", Reason: " + e); 596 return false; 597 } 598 return true; 599 } 600 601 /** 602 * Begins the process of connecting to devices, one by one, in the order that the priority 603 * list currently specifies. 604 * 605 * If we are already connecting, or no devices are present, then no work is done. 606 */ beginAutoConnecting()607 public void beginAutoConnecting() { 608 logd("Request to begin auto connection process"); 609 synchronized (mAutoConnectLock) { 610 if (isAutoConnecting()) { 611 logd("Auto connect requested while we are already auto connecting."); 612 return; 613 } 614 if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { 615 logd("Bluetooth Adapter is not on, cannot connect devices"); 616 return; 617 } 618 mAutoConnectingDevices = getDeviceListSnapshot(); 619 if (mAutoConnectingDevices.size() == 0) { 620 logd("No saved devices to auto-connect to."); 621 cancelAutoConnecting(); 622 return; 623 } 624 mConnecting = true; 625 mAutoConnectPriority = 0; 626 } 627 autoConnectWithTimeout(); 628 } 629 630 /** 631 * Connects the current priority device and sets a timeout timer to indicate when to give up and 632 * move on to the next one. 633 */ autoConnectWithTimeout()634 private void autoConnectWithTimeout() { 635 synchronized (mAutoConnectLock) { 636 if (!isAutoConnecting()) { 637 logd("Autoconnect process was cancelled, skipping connecting next device."); 638 return; 639 } 640 if (mAutoConnectPriority < 0 || mAutoConnectPriority >= mAutoConnectingDevices.size()) { 641 return; 642 } 643 644 BluetoothDevice device = mAutoConnectingDevices.get(mAutoConnectPriority); 645 logd("Auto connecting (" + mAutoConnectPriority + ") device: " + device); 646 647 mHandler.post(() -> { 648 boolean connectStatus = connect(device); 649 if (!connectStatus) { 650 logw("Connection attempt immediately failed, moving to the next device"); 651 continueAutoConnecting(); 652 } 653 }); 654 mHandler.postDelayed(() -> { 655 logw("Auto connect process has timed out connecting to " + device); 656 continueAutoConnecting(); 657 }, AUTO_CONNECT_TOKEN, AUTO_CONNECT_TIMEOUT_MS); 658 } 659 } 660 661 /** 662 * Will forcibly move the auto connect process to the next device, or finish it if no more 663 * devices are available. 664 */ continueAutoConnecting()665 private void continueAutoConnecting() { 666 logd("Continue auto-connect process on next device"); 667 synchronized (mAutoConnectLock) { 668 if (!isAutoConnecting()) { 669 logd("Autoconnect process was cancelled, no need to continue."); 670 return; 671 } 672 mHandler.removeCallbacksAndMessages(AUTO_CONNECT_TOKEN); 673 mAutoConnectPriority++; 674 if (mAutoConnectPriority >= mAutoConnectingDevices.size()) { 675 logd("No more devices to connect to"); 676 cancelAutoConnecting(); 677 return; 678 } 679 } 680 autoConnectWithTimeout(); 681 } 682 683 /** 684 * Cancels the auto-connection process. Any in-flight connection attempts will still be tried. 685 * 686 * Canceling is defined as deleting the snapshot of devices, resetting the device to connect 687 * index, setting the connecting boolean to null, and removing any pending timeouts if they 688 * exist. 689 * 690 * If there are no auto-connects in process this will do nothing. 691 */ cancelAutoConnecting()692 private void cancelAutoConnecting() { 693 logd("Cleaning up any auto-connect process"); 694 synchronized (mAutoConnectLock) { 695 if (!isAutoConnecting()) return; 696 mHandler.removeCallbacksAndMessages(AUTO_CONNECT_TOKEN); 697 mConnecting = false; 698 mAutoConnectPriority = -1; 699 mAutoConnectingDevices = null; 700 } 701 } 702 703 /** 704 * Get the auto-connect status of thie profile device manager 705 * 706 * @return true on success, false otherwise 707 */ isAutoConnecting()708 public boolean isAutoConnecting() { 709 synchronized (mAutoConnectLock) { 710 return mConnecting; 711 } 712 } 713 714 /** 715 * Determine if a device is the currently auto-connecting device 716 * 717 * @param device - A BluetoothDevice object to compare against any know auto connecting device 718 * @return true if the input device is the device we're currently connecting, false otherwise 719 */ isAutoConnectingDevice(BluetoothDevice device)720 private boolean isAutoConnectingDevice(BluetoothDevice device) { 721 synchronized (mAutoConnectLock) { 722 if (mAutoConnectingDevices == null) return false; 723 return mAutoConnectingDevices.get(mAutoConnectPriority).equals(device); 724 } 725 } 726 727 /** 728 * Given a device, will check the cached UUID set and see if it supports this profile. If it 729 * does then we will add it to the end of our prioritized set and attempt a connection if and 730 * only if the Bluetooth device priority allows a connection. 731 * 732 * Will do nothing if the device isn't bonded. 733 */ addBondedDeviceIfSupported(BluetoothDevice device)734 private void addBondedDeviceIfSupported(BluetoothDevice device) { 735 logd("Add device " + device + " if it is supported"); 736 if (device.getBondState() != BluetoothDevice.BOND_BONDED) return; 737 if (BluetoothUuid.containsAnyUuid(device.getUuids(), mProfileUuids) 738 && getProfilePriority(device) >= BluetoothProfile.PRIORITY_ON) { 739 addDevice(device); 740 } 741 } 742 743 /** 744 * Checks the reported UUIDs for a device to see if the device supports this profile. If it does 745 * then it will update the underlying Bluetooth stack with PRIORITY_ON so long as the device 746 * doesn't have a PRIORITY_OFF value set. 747 * 748 * @param device - The device that may support our profile 749 * @param uuids - The set of UUIDs for the device, which may include our profile 750 */ provisionDeviceIfSupported(BluetoothDevice device, ParcelUuid[] uuids)751 private void provisionDeviceIfSupported(BluetoothDevice device, ParcelUuid[] uuids) { 752 logd("Checking UUIDs for device: " + device); 753 if (BluetoothUuid.containsAnyUuid(uuids, mProfileUuids)) { 754 int devicePriority = getProfilePriority(device); 755 logd("Device " + device + " supports this profile. Priority: " 756 + Utils.getProfilePriorityName(devicePriority)); 757 // Transition from PRIORITY_OFF to any other Bluetooth stack priority value is supposed 758 // to be a user choice, enabled through the Settings applications. That's why we don't 759 // do it here for them. 760 if (devicePriority == BluetoothProfile.PRIORITY_UNDEFINED) { 761 // As a note, UUID updates happen during pairing, as well as each time the adapter 762 // turns on. Initiating connections to bonded device following UUID verification 763 // would defeat the purpose of the priority list. They don't arrive in a predictable 764 // order either. Since we call this function on UUID discovery, don't connect here! 765 setProfilePriority(device, BluetoothProfile.PRIORITY_ON); 766 return; 767 } 768 } 769 logd("Provisioning of " + device + " has ended without priority being set"); 770 } 771 772 /** 773 * Trigger connections of related Bluetooth profiles on a device 774 * 775 * @param device - The Bluetooth device you would like to connect to 776 */ triggerConnections(BluetoothDevice device)777 private void triggerConnections(BluetoothDevice device) { 778 for (int profile : mProfileTriggers) { 779 logd("Trigger connection to " + Utils.getProfileName(profile) + "on " + device); 780 try { 781 mBluetoothUserProxies.bluetoothConnectToProfile(profile, device); 782 } catch (RemoteException e) { 783 logw("Failed to connect " + device + ", Reason: " + e); 784 } 785 } 786 } 787 788 /** 789 * Writes the verbose current state of the object to the PrintWriter 790 * 791 * @param writer PrintWriter object to write lines to 792 */ dump(PrintWriter writer, String indent)793 public void dump(PrintWriter writer, String indent) { 794 writer.println(indent + "BluetoothProfileDeviceManager [" + Utils.getProfileName(mProfileId) 795 + "]"); 796 writer.println(indent + "\tUser: " + mUserId); 797 writer.println(indent + "\tSettings Location: " + mSettingsKey); 798 writer.println(indent + "\tUser Proxies Exist: " 799 + (mBluetoothUserProxies != null ? "Yes" : "No")); 800 writer.println(indent + "\tAuto-Connecting: " + (isAutoConnecting() ? "Yes" : "No")); 801 writer.println(indent + "\tPriority List:"); 802 ArrayList<BluetoothDevice> devices = getDeviceListSnapshot(); 803 for (BluetoothDevice device : devices) { 804 writer.println(indent + "\t\t" + device.getAddress() + " - " + device.getName()); 805 } 806 } 807 808 /** 809 * Log a message to DEBUG 810 */ logd(String msg)811 private void logd(String msg) { 812 if (DBG) { 813 Log.d(TAG, "[" + Utils.getProfileName(mProfileId) + " - User: " + mUserId + "] " + msg); 814 } 815 } 816 817 /** 818 * Log a message to WARN 819 */ logw(String msg)820 private void logw(String msg) { 821 Log.w(TAG, "[" + Utils.getProfileName(mProfileId) + " - User: " + mUserId + "] " + msg); 822 } 823 } 824