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