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