blob: 4f6a82ae2b0468f7817df4f9a7f91ca8d0631c12 [file] [log] [blame]
Sal Savage703c46f2019-04-15 08:39:25 -07001/*
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
17package com.android.car;
18
19import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_A2DP_SINK_DEVICES;
20import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_HFP_CLIENT_DEVICES;
21import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_MAP_CLIENT_DEVICES;
22import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PAN_DEVICES;
23import static android.car.settings.CarSettings.Secure.KEY_BLUETOOTH_PBAP_CLIENT_DEVICES;
24
25import android.bluetooth.BluetoothA2dpSink;
26import android.bluetooth.BluetoothAdapter;
27import android.bluetooth.BluetoothDevice;
28import android.bluetooth.BluetoothHeadsetClient;
29import android.bluetooth.BluetoothMapClient;
30import android.bluetooth.BluetoothPan;
31import android.bluetooth.BluetoothPbapClient;
32import android.bluetooth.BluetoothProfile;
33import android.bluetooth.BluetoothUuid;
34import android.car.ICarBluetoothUserService;
35import android.content.BroadcastReceiver;
36import android.content.Context;
37import android.content.Intent;
38import android.content.IntentFilter;
39import android.os.Handler;
40import android.os.Looper;
41import android.os.ParcelUuid;
42import android.os.Parcelable;
43import android.os.RemoteException;
44import android.os.UserHandle;
45import android.provider.Settings;
46import android.util.Log;
47import android.util.SparseArray;
48
49import java.io.PrintWriter;
50import java.util.ArrayList;
51import java.util.Arrays;
52import java.util.List;
53import java.util.Objects;
54import 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 */
61public 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
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 {
89 sProfileActions.put(BluetoothProfile.A2DP_SINK,
90 new BluetoothProfileInfo(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED,
91 KEY_BLUETOOTH_A2DP_SINK_DEVICES, new ParcelUuid[] {
Rahul Sabnisb9679f92019-12-03 15:37:51 -080092 BluetoothUuid.A2DP_SOURCE
Sal Savage703c46f2019-04-15 08:39:25 -070093 }, new int[] {}));
94 sProfileActions.put(BluetoothProfile.HEADSET_CLIENT,
95 new BluetoothProfileInfo(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED,
96 KEY_BLUETOOTH_HFP_CLIENT_DEVICES, new ParcelUuid[] {
Rahul Sabnisb9679f92019-12-03 15:37:51 -080097 BluetoothUuid.HFP_AG,
Sal Savage703c46f2019-04-15 08:39:25 -070098 BluetoothUuid.HSP_AG
99 }, new int[] {BluetoothProfile.MAP_CLIENT, BluetoothProfile.PBAP_CLIENT}));
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[] {}));
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[] {}));
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
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 */
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 }
Sal Savage703c46f2019-04-15 08:39:25 -0700191 }
Sal Savage619b04b2019-05-10 10:45:27 -0700192 // 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.
Sal Savage703c46f2019-04-15 08:39:25 -0700195 }
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
526 private boolean connect(BluetoothDevice device) {
527 logd("Connecting " + device);
528 try {
Sal Savage619b04b2019-05-10 10:45:27 -0700529 return mBluetoothUserProxies.bluetoothConnectToProfile(mProfileId, device);
Sal Savage703c46f2019-04-15 08:39:25 -0700530 } catch (RemoteException e) {
531 logw("Failed to connect " + device + ", Reason: " + e);
Sal Savage703c46f2019-04-15 08:39:25 -0700532 }
Sal Savage619b04b2019-05-10 10:45:27 -0700533 return false;
Sal Savage703c46f2019-04-15 08:39:25 -0700534 }
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 */
542 private boolean disconnect(BluetoothDevice device) {
543 logd("Disconnecting " + device);
544 try {
Sal Savage619b04b2019-05-10 10:45:27 -0700545 return mBluetoothUserProxies.bluetoothDisconnectFromProfile(mProfileId, device);
Sal Savage703c46f2019-04-15 08:39:25 -0700546 } catch (RemoteException e) {
547 logw("Failed to disconnect " + device + ", Reason: " + e);
Sal Savage703c46f2019-04-15 08:39:25 -0700548 }
Sal Savage619b04b2019-05-10 10:45:27 -0700549 return false;
Sal Savage703c46f2019-04-15 08:39:25 -0700550 }
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 */
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 */
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 */
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 */
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(() -> {
Sal Savage619b04b2019-05-10 10:45:27 -0700631 boolean connectStatus = connect(device);
632 if (!connectStatus) {
633 logw("Connection attempt immediately failed, moving to the next device");
634 continueAutoConnecting();
635 }
Sal Savage703c46f2019-04-15 08:39:25 -0700636 });
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
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 */
803 private void logw(String msg) {
804 Log.w(TAG, "[" + Utils.getProfileName(mProfileId) + " - User: " + mUserId + "] " + msg);
805 }
806}