1 /*
2  * Copyright (C) 2008 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.settingslib.bluetooth;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothCsipSetCoordinator;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.bluetooth.le.ScanFilter;
24 import android.content.Context;
25 import android.util.Log;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.sql.Timestamp;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Set;
35 
36 /**
37  * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
38  */
39 public class CachedBluetoothDeviceManager {
40     private static final String TAG = "CachedBluetoothDeviceManager";
41     private static final boolean DEBUG = BluetoothUtils.D;
42 
43     @VisibleForTesting static int sLateBondingTimeoutMillis = 5000; // 5s
44 
45     private Context mContext;
46     private final LocalBluetoothManager mBtManager;
47 
48     @VisibleForTesting
49     final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>();
50     @VisibleForTesting
51     HearingAidDeviceManager mHearingAidDeviceManager;
52     @VisibleForTesting
53     CsipDeviceManager mCsipDeviceManager;
54     BluetoothDevice mOngoingSetMemberPair;
55     boolean mIsLateBonding;
56     int mGroupIdOfLateBonding;
57 
CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager)58     public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
59         mContext = context;
60         mBtManager = localBtManager;
61         mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager,
62                 mCachedDevices);
63         mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
64     }
65 
getCachedDevicesCopy()66     public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
67         return new ArrayList<>(mCachedDevices);
68     }
69 
onDeviceDisappeared(CachedBluetoothDevice cachedDevice)70     public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
71         cachedDevice.setJustDiscovered(false);
72         return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
73     }
74 
onDeviceNameUpdated(BluetoothDevice device)75     public void onDeviceNameUpdated(BluetoothDevice device) {
76         CachedBluetoothDevice cachedDevice = findDevice(device);
77         if (cachedDevice != null) {
78             cachedDevice.refreshName();
79         }
80     }
81 
82     /**
83      * Search for existing {@link CachedBluetoothDevice} or return null
84      * if this device isn't in the cache. Use {@link #addDevice}
85      * to create and return a new {@link CachedBluetoothDevice} for
86      * a newly discovered {@link BluetoothDevice}.
87      *
88      * @param device the address of the Bluetooth device
89      * @return the cached device object for this device, or null if it has
90      *   not been previously seen
91      */
findDevice(BluetoothDevice device)92     public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
93         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
94             if (cachedDevice.getDevice().equals(device)) {
95                 return cachedDevice;
96             }
97             // Check the member devices for the coordinated set if it exists
98             final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
99             if (!memberDevices.isEmpty()) {
100                 for (CachedBluetoothDevice memberDevice : memberDevices) {
101                     if (memberDevice.getDevice().equals(device)) {
102                         return memberDevice;
103                     }
104                 }
105             }
106             // Check sub devices for hearing aid if it exists
107             CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
108             if (subDevice != null && subDevice.getDevice().equals(device)) {
109                 return subDevice;
110             }
111         }
112 
113         return null;
114     }
115 
116     /**
117      * Create and return a new {@link CachedBluetoothDevice}. This assumes
118      * that {@link #findDevice} has already been called and returned null.
119      * @param device the new Bluetooth device
120      * @return the newly created CachedBluetoothDevice object
121      */
addDevice(BluetoothDevice device)122     public CachedBluetoothDevice addDevice(BluetoothDevice device) {
123         return addDevice(device, /*leScanFilters=*/null);
124     }
125 
126     /**
127      * Create and return a new {@link CachedBluetoothDevice}. This assumes
128      * that {@link #findDevice} has already been called and returned null.
129      * @param device the new Bluetooth device
130      * @param leScanFilters the BLE scan filters which the device matched
131      * @return the newly created CachedBluetoothDevice object
132      */
addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters)133     public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) {
134         CachedBluetoothDevice newDevice;
135         final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
136         synchronized (this) {
137             newDevice = findDevice(device);
138             if (newDevice == null) {
139                 newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
140                 mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
141                 mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters);
142                 if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
143                         && !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
144                     mCachedDevices.add(newDevice);
145                     mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
146                 }
147             }
148         }
149 
150         return newDevice;
151     }
152 
153     /**
154      * Returns device summary of the pair of the hearing aid / CSIP passed as the parameter.
155      *
156      * @param CachedBluetoothDevice device
157      * @return Device summary, or if the pair does not exist or if it is not a hearing aid or
158      * a CSIP set member, then {@code null}.
159      */
getSubDeviceSummary(CachedBluetoothDevice device)160     public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) {
161         final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
162         // TODO: check the CSIP group size instead of the real member device set size, and adjust
163         // the size restriction.
164         if (!memberDevices.isEmpty()) {
165             for (CachedBluetoothDevice memberDevice : memberDevices) {
166                 if (memberDevice.isConnected()) {
167                     return memberDevice.getConnectionSummary();
168                 }
169             }
170         }
171         CachedBluetoothDevice subDevice = device.getSubDevice();
172         if (subDevice != null && subDevice.isConnected()) {
173             return subDevice.getConnectionSummary();
174         }
175         return null;
176     }
177 
178     /**
179      * Search for existing sub device {@link CachedBluetoothDevice}.
180      *
181      * @param device the address of the Bluetooth device
182      * @return true for found sub / member device or false.
183      */
isSubDevice(BluetoothDevice device)184     public synchronized boolean isSubDevice(BluetoothDevice device) {
185         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
186             if (!cachedDevice.getDevice().equals(device)) {
187                 // Check the member devices of the coordinated set if it exists
188                 Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
189                 if (!memberDevices.isEmpty()) {
190                     for (CachedBluetoothDevice memberDevice : memberDevices) {
191                         if (memberDevice.getDevice().equals(device)) {
192                             return true;
193                         }
194                     }
195                     continue;
196                 }
197                 // Check sub devices of hearing aid if it exists
198                 CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
199                 if (subDevice != null && subDevice.getDevice().equals(device)) {
200                     return true;
201                 }
202             }
203         }
204         return false;
205     }
206 
207     /**
208      * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
209      * Hearing Aid Service is connected and the HiSyncId's are now available.
210      */
updateHearingAidsDevices()211     public synchronized void updateHearingAidsDevices() {
212         mHearingAidDeviceManager.updateHearingAidsDevices();
213     }
214 
215     /**
216      * Updates the Csip devices; specifically the GroupId's. This routine is called when the
217      * CSIS is connected and the GroupId's are now available.
218      */
updateCsipDevices()219     public synchronized void updateCsipDevices() {
220         mCsipDeviceManager.updateCsipDevices();
221     }
222 
223     /**
224      * Attempts to get the name of a remote device, otherwise returns the address.
225      *
226      * @param device The remote device.
227      * @return The name, or if unavailable, the address.
228      */
getName(BluetoothDevice device)229     public String getName(BluetoothDevice device) {
230         if (isOngoingPairByCsip(device)) {
231             CachedBluetoothDevice firstDevice =
232                     mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding);
233             if (firstDevice != null && firstDevice.getName() != null) {
234                 return firstDevice.getName();
235             }
236         }
237 
238         CachedBluetoothDevice cachedDevice = findDevice(device);
239         if (cachedDevice != null && cachedDevice.getName() != null) {
240             return cachedDevice.getName();
241         }
242 
243         String name = device.getAlias();
244         if (name != null) {
245             return name;
246         }
247 
248         return device.getAddress();
249     }
250 
clearNonBondedDevices()251     public synchronized void clearNonBondedDevices() {
252         clearNonBondedSubDevices();
253         final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
254         mCachedDevices.stream()
255                 .filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
256                 .forEach(cachedDevice -> {
257                     cachedDevice.release();
258                     removedCachedDevice.add(cachedDevice);
259                 });
260         mCachedDevices.removeAll(removedCachedDevice);
261     }
262 
clearNonBondedSubDevices()263     private void clearNonBondedSubDevices() {
264         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
265             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
266             Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
267             if (!memberDevices.isEmpty()) {
268                 for (Object it : memberDevices.toArray()) {
269                     CachedBluetoothDevice memberDevice = (CachedBluetoothDevice) it;
270                     // Member device exists and it is not bonded
271                     if (memberDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
272                         cachedDevice.removeMemberDevice(memberDevice);
273                     }
274                 }
275                 return;
276             }
277             CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
278             if (subDevice != null
279                     && subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
280                 // Sub device exists and it is not bonded
281                 subDevice.release();
282                 cachedDevice.setSubDevice(null);
283             }
284         }
285     }
286 
onScanningStateChanged(boolean started)287     public synchronized void onScanningStateChanged(boolean started) {
288         if (!started) return;
289         // If starting a new scan, clear old visibility
290         // Iterate in reverse order since devices may be removed.
291         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
292             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
293             cachedDevice.setJustDiscovered(false);
294             final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
295             if (!memberDevices.isEmpty()) {
296                 for (CachedBluetoothDevice memberDevice : memberDevices) {
297                     memberDevice.setJustDiscovered(false);
298                 }
299                 return;
300             }
301             final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
302             if (subDevice != null) {
303                 subDevice.setJustDiscovered(false);
304             }
305         }
306     }
307 
onBluetoothStateChanged(int bluetoothState)308     public synchronized void onBluetoothStateChanged(int bluetoothState) {
309         // When Bluetooth is turning off, we need to clear the non-bonded devices
310         // Otherwise, they end up showing up on the next BT enable
311         if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
312             for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
313                 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
314                 final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
315                 if (!memberDevices.isEmpty()) {
316                     for (CachedBluetoothDevice memberDevice : memberDevices) {
317                         if (memberDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
318                             cachedDevice.removeMemberDevice(memberDevice);
319                         }
320                     }
321                 } else {
322                     CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
323                     if (subDevice != null) {
324                         if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
325                             cachedDevice.setSubDevice(null);
326                         }
327                     }
328                 }
329                 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
330                     cachedDevice.setJustDiscovered(false);
331                     cachedDevice.release();
332                     mCachedDevices.remove(i);
333                 }
334             }
335 
336             // To clear the SetMemberPair flag when the Bluetooth is turning off.
337             mOngoingSetMemberPair = null;
338             mIsLateBonding = false;
339             mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
340         }
341     }
342 
onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state, int profileId)343     public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice
344             cachedDevice, int state, int profileId) {
345         if (profileId == BluetoothProfile.HEARING_AID) {
346             return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
347                 state);
348         }
349         if (profileId == BluetoothProfile.HEADSET
350                 || profileId == BluetoothProfile.A2DP
351                 || profileId == BluetoothProfile.LE_AUDIO
352                 || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
353             return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
354                 state);
355         }
356         return false;
357     }
358 
359     /** Handles when the device been set as active/inactive. */
onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice)360     public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) {
361         if (cachedBluetoothDevice.isHearingAidDevice()) {
362             mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice);
363         }
364     }
365 
onDeviceUnpaired(CachedBluetoothDevice device)366     public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
367         device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
368         CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
369         // Should iterate through the cloned set to avoid ConcurrentModificationException
370         final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice());
371         if (!memberDevices.isEmpty()) {
372             // Main device is unpaired, also unpair the member devices
373             for (CachedBluetoothDevice memberDevice : memberDevices) {
374                 memberDevice.unpair();
375                 memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
376                 device.removeMemberDevice(memberDevice);
377             }
378         } else if (mainDevice != null) {
379             // Member device is unpaired, also unpair the main device
380             mainDevice.unpair();
381         }
382         mainDevice = mHearingAidDeviceManager.findMainDevice(device);
383         CachedBluetoothDevice subDevice = device.getSubDevice();
384         if (subDevice != null) {
385             // Main device is unpaired, to unpair sub device
386             subDevice.unpair();
387             device.setSubDevice(null);
388         } else if (mainDevice != null) {
389             // Sub device unpaired, to unpair main device
390             mainDevice.unpair();
391             mainDevice.setSubDevice(null);
392         }
393     }
394 
395     /**
396      * Called when we found a set member of a group. The function will check the {@code groupId} if
397      * it exists and the bond state of the device is BOND_NOE, and if there isn't any ongoing pair
398      * , and then return {@code true} to pair the device automatically.
399      *
400      * @param device The found device
401      * @param groupId The group id of the found device
402      *
403      * @return {@code true}, if the device should pair automatically; Otherwise, return
404      * {@code false}.
405      */
shouldPairByCsip(BluetoothDevice device, int groupId)406     private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
407         boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null;
408         int bondState = device.getBondState();
409         boolean groupExists = mCsipDeviceManager.isExistedGroupId(groupId);
410         Log.d(TAG,
411                 "isOngoingSetMemberPair=" + isOngoingSetMemberPair + ", bondState=" + bondState
412                         + ", groupExists=" + groupExists + ", groupId=" + groupId);
413 
414         if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE || !groupExists) {
415             return false;
416         }
417         return true;
418     }
419 
checkLateBonding(int groupId)420     private synchronized boolean checkLateBonding(int groupId) {
421         CachedBluetoothDevice firstDevice = mCsipDeviceManager.getFirstMemberDevice(groupId);
422         if (firstDevice == null) {
423             Log.d(TAG, "No first device in group: " + groupId);
424             return false;
425         }
426 
427         Timestamp then = firstDevice.getBondTimestamp();
428         if (then == null) {
429             Log.d(TAG, "No bond timestamp");
430             return true;
431         }
432 
433         Timestamp now = new Timestamp(System.currentTimeMillis());
434 
435         long diff = (now.getTime() - then.getTime());
436         Log.d(TAG, "Time difference to first bonding: " + diff + "ms");
437 
438         return diff > sLateBondingTimeoutMillis;
439     }
440 
441     /**
442      * Called to check if there is an ongoing bonding for the device and it is late bonding.
443      * If the device is not matching the ongoing bonding device then false will be returned.
444      *
445      * @param device The device to check.
446      */
isLateBonding(BluetoothDevice device)447     public synchronized boolean isLateBonding(BluetoothDevice device) {
448         if (!isOngoingPairByCsip(device)) {
449             Log.d(TAG, "isLateBonding: pair not ongoing or not matching device");
450             return false;
451         }
452 
453         Log.d(TAG, "isLateBonding: " + mIsLateBonding);
454         return mIsLateBonding;
455     }
456 
457     /**
458      * Called when we found a set member of a group. The function will check the {@code groupId} if
459      * it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair
460      * , and then pair the device automatically.
461      *
462      * @param device The found device
463      * @param groupId The group id of the found device
464      */
pairDeviceByCsip(BluetoothDevice device, int groupId)465     public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) {
466         if (!shouldPairByCsip(device, groupId)) {
467             return;
468         }
469         Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP ");
470         mOngoingSetMemberPair = device;
471         mIsLateBonding = checkLateBonding(groupId);
472         mGroupIdOfLateBonding = groupId;
473         syncConfigFromMainDevice(device, groupId);
474         if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
475             Log.d(TAG, "Bonding could not be started");
476             mOngoingSetMemberPair = null;
477             mIsLateBonding = false;
478             mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
479         }
480     }
481 
syncConfigFromMainDevice(BluetoothDevice device, int groupId)482     private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
483         if (!isOngoingPairByCsip(device)) {
484             return;
485         }
486         CachedBluetoothDevice memberDevice = findDevice(device);
487         CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice);
488         if (mainDevice == null) {
489             mainDevice = mCsipDeviceManager.getCachedDevice(groupId);
490         }
491 
492         if (mainDevice == null || mainDevice.equals(memberDevice)) {
493             Log.d(TAG, "no mainDevice");
494             return;
495         }
496 
497         // The memberDevice set PhonebookAccessPermission
498         device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission());
499     }
500 
501     /**
502      * Called when the bond state change. If the bond state change is related with the
503      * ongoing set member pair, the cachedBluetoothDevice will be created but the UI
504      * would not be updated. For the other case, return {@code false} to go through the normal
505      * flow.
506      *
507      * @param device The device
508      * @param bondState The new bond state
509      *
510      * @return {@code true}, if the bond state change for the device is handled inside this
511      * function, and would not like to update the UI. If not, return {@code false}.
512      */
onBondStateChangedIfProcess(BluetoothDevice device, int bondState)513     public synchronized boolean onBondStateChangedIfProcess(BluetoothDevice device, int bondState) {
514         if (!isOngoingPairByCsip(device)) {
515             return false;
516         }
517 
518         if (bondState == BluetoothDevice.BOND_BONDING) {
519             return true;
520         }
521 
522         mOngoingSetMemberPair = null;
523         mIsLateBonding = false;
524         mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
525         if (bondState != BluetoothDevice.BOND_NONE) {
526             if (findDevice(device) == null) {
527                 final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
528                 CachedBluetoothDevice newDevice =
529                         new CachedBluetoothDevice(mContext, profileManager, device);
530                 mCachedDevices.add(newDevice);
531                 findDevice(device).connect();
532             }
533         }
534 
535         return true;
536     }
537 
538     /**
539      * Check if the device is the one which is initial paired locally by CSIP. The setting
540      * would depned on it to accept the pairing request automatically
541      *
542      * @param device The device
543      *
544      * @return {@code true}, if the device is ongoing pair by CSIP. Otherwise, return
545      * {@code false}.
546      */
isOngoingPairByCsip(BluetoothDevice device)547     public boolean isOngoingPairByCsip(BluetoothDevice device) {
548         return mOngoingSetMemberPair != null && mOngoingSetMemberPair.equals(device);
549     }
550 
log(String msg)551     private void log(String msg) {
552         if (DEBUG) {
553             Log.d(TAG, msg);
554         }
555     }
556 }
557