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