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.BluetoothDevice;
21 import android.bluetooth.BluetoothHearingAid;
22 import android.bluetooth.BluetoothProfile;
23 import android.content.Context;
24 import android.util.Log;
25 
26 import com.android.internal.annotations.VisibleForTesting;
27 
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.Objects;
36 
37 /**
38  * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
39  */
40 public class CachedBluetoothDeviceManager {
41     private static final String TAG = "CachedBluetoothDeviceManager";
42     private static final boolean DEBUG = Utils.D;
43 
44     private Context mContext;
45     private final LocalBluetoothManager mBtManager;
46 
47     @VisibleForTesting
48     final List<CachedBluetoothDevice> mCachedDevices =
49         new ArrayList<CachedBluetoothDevice>();
50     // Contains the list of hearing aid devices that should not be shown in the UI.
51     @VisibleForTesting
52     final List<CachedBluetoothDevice> mHearingAidDevicesNotAddedInCache
53         = new ArrayList<CachedBluetoothDevice>();
54     // Maintains a list of devices which are added in mCachedDevices and have hiSyncIds.
55     @VisibleForTesting
56     final Map<Long, CachedBluetoothDevice> mCachedDevicesMapForHearingAids
57         = new HashMap<Long, CachedBluetoothDevice>();
58 
CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager)59     CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
60         mContext = context;
61         mBtManager = localBtManager;
62     }
63 
getCachedDevicesCopy()64     public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
65         return new ArrayList<CachedBluetoothDevice>(mCachedDevices);
66     }
67 
onDeviceDisappeared(CachedBluetoothDevice cachedDevice)68     public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
69         cachedDevice.setJustDiscovered(false);
70         return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
71     }
72 
onDeviceNameUpdated(BluetoothDevice device)73     public void onDeviceNameUpdated(BluetoothDevice device) {
74         CachedBluetoothDevice cachedDevice = findDevice(device);
75         if (cachedDevice != null) {
76             cachedDevice.refreshName();
77         }
78     }
79 
80     /**
81      * Search for existing {@link CachedBluetoothDevice} or return null
82      * if this device isn't in the cache. Use {@link #addDevice}
83      * to create and return a new {@link CachedBluetoothDevice} for
84      * a newly discovered {@link BluetoothDevice}.
85      *
86      * @param device the address of the Bluetooth device
87      * @return the cached device object for this device, or null if it has
88      *   not been previously seen
89      */
findDevice(BluetoothDevice device)90     public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
91         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
92             if (cachedDevice.getDevice().equals(device)) {
93                 return cachedDevice;
94             }
95         }
96         for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
97             if (notCachedDevice.getDevice().equals(device)) {
98                 return notCachedDevice;
99             }
100         }
101         return null;
102     }
103 
104     /**
105      * Create and return a new {@link CachedBluetoothDevice}. This assumes
106      * that {@link #findDevice} has already been called and returned null.
107      * @param device the address of the new Bluetooth device
108      * @return the newly created CachedBluetoothDevice object
109      */
addDevice(LocalBluetoothAdapter adapter, LocalBluetoothProfileManager profileManager, BluetoothDevice device)110     public CachedBluetoothDevice addDevice(LocalBluetoothAdapter adapter,
111             LocalBluetoothProfileManager profileManager,
112             BluetoothDevice device) {
113         CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, adapter,
114             profileManager, device);
115         if (profileManager.getHearingAidProfile() != null
116             && profileManager.getHearingAidProfile().getHiSyncId(newDevice.getDevice())
117                 != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
118             newDevice.setHiSyncId(profileManager.getHearingAidProfile()
119                 .getHiSyncId(newDevice.getDevice()));
120         }
121         // Just add one of the hearing aids from a pair in the list that is shown in the UI.
122         if (isPairAddedInCache(newDevice.getHiSyncId())) {
123             synchronized (this) {
124                 mHearingAidDevicesNotAddedInCache.add(newDevice);
125             }
126         } else {
127             synchronized (this) {
128                 mCachedDevices.add(newDevice);
129                 if (newDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
130                     && !mCachedDevicesMapForHearingAids.containsKey(newDevice.getHiSyncId())) {
131                     mCachedDevicesMapForHearingAids.put(newDevice.getHiSyncId(), newDevice);
132                 }
133                 mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
134             }
135         }
136 
137         return newDevice;
138     }
139 
140     /**
141      * Returns true if the one of the two hearing aid devices is already cached for UI.
142      *
143      * @param long hiSyncId
144      * @return {@code True} if one of the two hearing aid devices is is already cached for UI.
145      */
isPairAddedInCache(long hiSyncId)146     private synchronized boolean isPairAddedInCache(long hiSyncId) {
147         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
148             return false;
149         }
150         if(mCachedDevicesMapForHearingAids.containsKey(hiSyncId)) {
151             return true;
152         }
153         return false;
154     }
155 
156     /**
157      * Returns device summary of the pair of the hearing aid passed as the parameter.
158      *
159      * @param CachedBluetoothDevice device
160      * @return Device summary, or if the pair does not exist or if its not a hearing aid,
161      * then {@code null}.
162      */
getHearingAidPairDeviceSummary(CachedBluetoothDevice device)163     public synchronized String getHearingAidPairDeviceSummary(CachedBluetoothDevice device) {
164         String pairDeviceSummary = null;
165         if (device.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
166             for (CachedBluetoothDevice hearingAidDevice : mHearingAidDevicesNotAddedInCache) {
167                 if (hearingAidDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
168                     && hearingAidDevice.getHiSyncId() == device.getHiSyncId()) {
169                     pairDeviceSummary = hearingAidDevice.getConnectionSummary();
170                 }
171             }
172         }
173         return pairDeviceSummary;
174     }
175 
176     /**
177      * Adds the 2nd hearing aid in a pair in a list that maintains the hearing aids that are
178      * not dispalyed in the UI.
179      *
180      * @param CachedBluetoothDevice device
181      */
addDeviceNotaddedInMap(CachedBluetoothDevice device)182     public synchronized void addDeviceNotaddedInMap(CachedBluetoothDevice device) {
183         mHearingAidDevicesNotAddedInCache.add(device);
184     }
185 
186     /**
187      * Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
188      * Hearing Aid Service is connected and the HiSyncId's are now available.
189      * @param LocalBluetoothProfileManager profileManager
190      */
updateHearingAidsDevices(LocalBluetoothProfileManager profileManager)191     public synchronized void updateHearingAidsDevices(LocalBluetoothProfileManager profileManager) {
192         HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
193         if (profileProxy == null) {
194             log("updateHearingAidsDevices: getHearingAidProfile() is null");
195             return;
196         }
197         final Set<Long> syncIdChangedSet = new HashSet<Long>();
198         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
199             if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
200                 continue;
201             }
202 
203             long newHiSyncId = profileProxy.getHiSyncId(cachedDevice.getDevice());
204 
205             if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
206                 cachedDevice.setHiSyncId(newHiSyncId);
207                 syncIdChangedSet.add(newHiSyncId);
208             }
209         }
210         for (Long syncId : syncIdChangedSet) {
211             onHiSyncIdChanged(syncId);
212         }
213     }
214 
215     /**
216      * Attempts to get the name of a remote device, otherwise returns the address.
217      *
218      * @param device The remote device.
219      * @return The name, or if unavailable, the address.
220      */
getName(BluetoothDevice device)221     public String getName(BluetoothDevice device) {
222         CachedBluetoothDevice cachedDevice = findDevice(device);
223         if (cachedDevice != null && cachedDevice.getName() != null) {
224             return cachedDevice.getName();
225         }
226 
227         String name = device.getAliasName();
228         if (name != null) {
229             return name;
230         }
231 
232         return device.getAddress();
233     }
234 
clearNonBondedDevices()235     public synchronized void clearNonBondedDevices() {
236 
237         mCachedDevicesMapForHearingAids.entrySet().removeIf(entries
238             -> entries.getValue().getBondState() == BluetoothDevice.BOND_NONE);
239 
240         mCachedDevices.removeIf(cachedDevice
241             -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE);
242 
243         mHearingAidDevicesNotAddedInCache.removeIf(hearingAidDevice
244             -> hearingAidDevice.getBondState() == BluetoothDevice.BOND_NONE);
245     }
246 
onScanningStateChanged(boolean started)247     public synchronized void onScanningStateChanged(boolean started) {
248         if (!started) return;
249         // If starting a new scan, clear old visibility
250         // Iterate in reverse order since devices may be removed.
251         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
252             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
253             cachedDevice.setJustDiscovered(false);
254         }
255         for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
256             CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
257             notCachedDevice.setJustDiscovered(false);
258         }
259     }
260 
onBtClassChanged(BluetoothDevice device)261     public synchronized void onBtClassChanged(BluetoothDevice device) {
262         CachedBluetoothDevice cachedDevice = findDevice(device);
263         if (cachedDevice != null) {
264             cachedDevice.refreshBtClass();
265         }
266     }
267 
onUuidChanged(BluetoothDevice device)268     public synchronized void onUuidChanged(BluetoothDevice device) {
269         CachedBluetoothDevice cachedDevice = findDevice(device);
270         if (cachedDevice != null) {
271             cachedDevice.onUuidChanged();
272         }
273     }
274 
onBluetoothStateChanged(int bluetoothState)275     public synchronized void onBluetoothStateChanged(int bluetoothState) {
276         // When Bluetooth is turning off, we need to clear the non-bonded devices
277         // Otherwise, they end up showing up on the next BT enable
278         if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
279             for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
280                 CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
281                 if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
282                     cachedDevice.setJustDiscovered(false);
283                     mCachedDevices.remove(i);
284                     if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
285                         && mCachedDevicesMapForHearingAids.containsKey(cachedDevice.getHiSyncId()))
286                     {
287                         mCachedDevicesMapForHearingAids.remove(cachedDevice.getHiSyncId());
288                     }
289                 } else {
290                     // For bonded devices, we need to clear the connection status so that
291                     // when BT is enabled next time, device connection status shall be retrieved
292                     // by making a binder call.
293                     cachedDevice.clearProfileConnectionState();
294                 }
295             }
296             for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
297                 CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
298                 if (notCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
299                     notCachedDevice.setJustDiscovered(false);
300                     mHearingAidDevicesNotAddedInCache.remove(i);
301                 } else {
302                     // For bonded devices, we need to clear the connection status so that
303                     // when BT is enabled next time, device connection status shall be retrieved
304                     // by making a binder call.
305                     notCachedDevice.clearProfileConnectionState();
306                 }
307             }
308         }
309     }
310 
onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile)311     public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
312                                                    int bluetoothProfile) {
313         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
314             boolean isActive = Objects.equals(cachedDevice, activeDevice);
315             cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile);
316         }
317     }
318 
onHiSyncIdChanged(long hiSyncId)319     public synchronized void onHiSyncIdChanged(long hiSyncId) {
320         int firstMatchedIndex = -1;
321 
322         for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
323             CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
324             if (cachedDevice.getHiSyncId() == hiSyncId) {
325                 if (firstMatchedIndex != -1) {
326                     /* Found the second one */
327                     int indexToRemoveFromUi;
328                     CachedBluetoothDevice deviceToRemoveFromUi;
329 
330                     // Since the hiSyncIds have been updated for a connected pair of hearing aids,
331                     // we remove the entry of one the hearing aids from the UI. Unless the
332                     // hiSyncId get updated, the system does not know it is a hearing aid, so we add
333                     // both the hearing aids as separate entries in the UI first, then remove one
334                     // of them after the hiSyncId is populated. We will choose the device that
335                     // is not connected to be removed.
336                     if (cachedDevice.isConnected()) {
337                         indexToRemoveFromUi = firstMatchedIndex;
338                         deviceToRemoveFromUi = mCachedDevices.get(firstMatchedIndex);
339                         mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
340                     } else {
341                         indexToRemoveFromUi = i;
342                         deviceToRemoveFromUi = cachedDevice;
343                         mCachedDevicesMapForHearingAids.put(hiSyncId,
344                                                             mCachedDevices.get(firstMatchedIndex));
345                     }
346 
347                     mCachedDevices.remove(indexToRemoveFromUi);
348                     mHearingAidDevicesNotAddedInCache.add(deviceToRemoveFromUi);
349                     log("onHiSyncIdChanged: removed from UI device=" + deviceToRemoveFromUi
350                         + ", with hiSyncId=" + hiSyncId);
351                     mBtManager.getEventManager().dispatchDeviceRemoved(deviceToRemoveFromUi);
352                     break;
353                 } else {
354                     mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice);
355                     firstMatchedIndex = i;
356                 }
357             }
358         }
359     }
360 
getHearingAidOtherDevice(CachedBluetoothDevice thisDevice, long hiSyncId)361     private CachedBluetoothDevice getHearingAidOtherDevice(CachedBluetoothDevice thisDevice,
362                                                            long hiSyncId) {
363         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
364             return null;
365         }
366 
367         // Searched the lists for the other side device with the matching hiSyncId.
368         for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) {
369             if ((hiSyncId == notCachedDevice.getHiSyncId()) &&
370                 (!Objects.equals(notCachedDevice, thisDevice))) {
371                 return notCachedDevice;
372             }
373         }
374 
375         CachedBluetoothDevice cachedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
376         if (!Objects.equals(cachedDevice, thisDevice)) {
377             return cachedDevice;
378         }
379         return null;
380     }
381 
hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice, CachedBluetoothDevice toHideDevice, long hiSyncId)382     private void hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice,
383                                            CachedBluetoothDevice toHideDevice, long hiSyncId)
384     {
385         log("hearingAidSwitchDisplayDevice: toDisplayDevice=" + toDisplayDevice
386             + ", toHideDevice=" + toHideDevice);
387 
388         // Remove the "toHideDevice" device from the UI.
389         mHearingAidDevicesNotAddedInCache.add(toHideDevice);
390         mCachedDevices.remove(toHideDevice);
391         mBtManager.getEventManager().dispatchDeviceRemoved(toHideDevice);
392 
393         // Add the "toDisplayDevice" device to the UI.
394         mHearingAidDevicesNotAddedInCache.remove(toDisplayDevice);
395         mCachedDevices.add(toDisplayDevice);
396         mCachedDevicesMapForHearingAids.put(hiSyncId, toDisplayDevice);
397         mBtManager.getEventManager().dispatchDeviceAdded(toDisplayDevice);
398     }
399 
onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)400     public synchronized void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice,
401                                                              int state, int bluetoothProfile) {
402         if (bluetoothProfile == BluetoothProfile.HEARING_AID
403             && cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
404             && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
405 
406             long hiSyncId = cachedDevice.getHiSyncId();
407 
408             CachedBluetoothDevice otherDevice = getHearingAidOtherDevice(cachedDevice, hiSyncId);
409             if (otherDevice == null) {
410                 // no other side device. Nothing to do.
411                 return;
412             }
413 
414             if (state == BluetoothProfile.STATE_CONNECTED &&
415                 mHearingAidDevicesNotAddedInCache.contains(cachedDevice)) {
416                 hearingAidSwitchDisplayDevice(cachedDevice, otherDevice, hiSyncId);
417             } else if (state == BluetoothProfile.STATE_DISCONNECTED
418                        && otherDevice.isConnected()) {
419                 CachedBluetoothDevice mapDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
420                 if ((mapDevice != null) && (Objects.equals(cachedDevice, mapDevice))) {
421                     hearingAidSwitchDisplayDevice(otherDevice, cachedDevice, hiSyncId);
422                 }
423             }
424         }
425     }
426 
onDeviceUnpaired(CachedBluetoothDevice device)427     public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
428         final long hiSyncId = device.getHiSyncId();
429 
430         if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) return;
431 
432         for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) {
433             CachedBluetoothDevice cachedDevice = mHearingAidDevicesNotAddedInCache.get(i);
434             if (cachedDevice.getHiSyncId() == hiSyncId) {
435                 // TODO: Look for more cleanups on unpairing the device.
436                 mHearingAidDevicesNotAddedInCache.remove(i);
437                 if (device == cachedDevice) continue;
438                 log("onDeviceUnpaired: Unpair device=" + cachedDevice);
439                 cachedDevice.unpair();
440             }
441         }
442 
443         CachedBluetoothDevice mappedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId);
444         if ((mappedDevice != null) && (!Objects.equals(device, mappedDevice))) {
445             log("onDeviceUnpaired: Unpair mapped device=" + mappedDevice);
446             mappedDevice.unpair();
447         }
448     }
449 
dispatchAudioModeChanged()450     public synchronized void dispatchAudioModeChanged() {
451         for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
452             cachedDevice.onAudioModeChanged();
453         }
454     }
455 
log(String msg)456     private void log(String msg) {
457         if (DEBUG) {
458             Log.d(TAG, msg);
459         }
460     }
461 }
462