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