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