• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.bluetooth.hearingaid;
18 
19 import static android.Manifest.permission.BLUETOOTH_CONNECT;
20 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
21 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
22 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
23 import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
24 import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
25 import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
26 
27 import static java.util.Objects.requireNonNull;
28 
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothHearingAid;
31 import android.bluetooth.BluetoothHearingAid.AdvertisementServiceData;
32 import android.bluetooth.BluetoothProfile;
33 import android.bluetooth.BluetoothUuid;
34 import android.content.Intent;
35 import android.media.AudioDeviceCallback;
36 import android.media.AudioDeviceInfo;
37 import android.media.AudioManager;
38 import android.media.BluetoothProfileConnectionInfo;
39 import android.os.Handler;
40 import android.os.HandlerThread;
41 import android.os.Looper;
42 import android.os.ParcelUuid;
43 import android.os.UserHandle;
44 import android.sysprop.BluetoothProperties;
45 import android.util.Log;
46 
47 import com.android.bluetooth.BluetoothStatsLog;
48 import com.android.bluetooth.Utils;
49 import com.android.bluetooth.btservice.AdapterService;
50 import com.android.bluetooth.btservice.ProfileService;
51 import com.android.bluetooth.btservice.storage.DatabaseManager;
52 import com.android.bluetooth.flags.Flags;
53 import com.android.internal.annotations.VisibleForTesting;
54 
55 import java.util.ArrayList;
56 import java.util.HashMap;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.concurrent.ConcurrentHashMap;
60 
61 /** Provides Bluetooth HearingAid profile, as a service in the Bluetooth application. */
62 public class HearingAidService extends ProfileService {
63     private static final String TAG = HearingAidService.class.getSimpleName();
64 
65     private static final int SM_THREAD_JOIN_TIMEOUT_MS = 1000;
66 
67     // Upper limit of all HearingAid devices: Bonded or Connected
68     private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
69 
70     private static HearingAidService sHearingAidService;
71 
72     private final AdapterService mAdapterService;
73     private final DatabaseManager mDatabaseManager;
74     private final HearingAidNativeInterface mNativeInterface;
75     private final AudioManager mAudioManager;
76     private final HandlerThread mStateMachinesThread;
77     private final Looper mStateMachinesLooper;
78     private final Handler mHandler;
79 
80     private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines = new HashMap<>();
81     private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>();
82     private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>();
83     private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>();
84     private final AudioManagerOnAudioDevicesAddedCallback mAudioManagerOnAudioDevicesAddedCallback =
85             new AudioManagerOnAudioDevicesAddedCallback();
86     private final AudioManagerOnAudioDevicesRemovedCallback
87             mAudioManagerOnAudioDevicesRemovedCallback =
88                     new AudioManagerOnAudioDevicesRemovedCallback();
89 
90     private BluetoothDevice mActiveDevice;
91     private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
92 
HearingAidService(AdapterService adapterService)93     public HearingAidService(AdapterService adapterService) {
94         this(adapterService, null, HearingAidNativeInterface.getInstance());
95     }
96 
97     @VisibleForTesting
HearingAidService( AdapterService adapterService, Looper looper, HearingAidNativeInterface nativeInterface)98     HearingAidService(
99             AdapterService adapterService,
100             Looper looper,
101             HearingAidNativeInterface nativeInterface) {
102         super(requireNonNull(adapterService));
103         mAdapterService = adapterService;
104         mDatabaseManager = requireNonNull(mAdapterService.getDatabase());
105         if (looper == null) {
106             mHandler = new Handler(requireNonNull(Looper.getMainLooper()));
107             mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
108             mStateMachinesThread.start();
109             mStateMachinesLooper = mStateMachinesThread.getLooper();
110         } else {
111             mHandler = new Handler(looper);
112             mStateMachinesThread = null;
113             mStateMachinesLooper = looper;
114         }
115         mNativeInterface = requireNonNull(nativeInterface);
116         mAudioManager = requireNonNull(getSystemService(AudioManager.class));
117 
118         setHearingAidService(this);
119         mNativeInterface.init();
120     }
121 
isEnabled()122     public static boolean isEnabled() {
123         return BluetoothProperties.isProfileAshaCentralEnabled().orElse(true);
124     }
125 
126     @Override
initBinder()127     protected IProfileServiceBinder initBinder() {
128         return new HearingAidServiceBinder(this);
129     }
130 
131     @Override
cleanup()132     public void cleanup() {
133         Log.i(TAG, "Cleanup HearingAid Service");
134 
135         // Cleanup native interface
136         mNativeInterface.cleanup();
137 
138         // Mark service as stopped
139         setHearingAidService(null);
140 
141         // Destroy state machines and stop handler thread
142         synchronized (mStateMachines) {
143             for (HearingAidStateMachine sm : mStateMachines.values()) {
144                 sm.doQuit();
145             }
146             mStateMachines.clear();
147         }
148 
149         // Clear HiSyncId map, capabilities map and HiSyncId Connected map
150         mDeviceHiSyncIdMap.clear();
151         mDeviceCapabilitiesMap.clear();
152         mHiSyncIdConnectedMap.clear();
153 
154         if (mStateMachinesThread != null) {
155             try {
156                 mStateMachinesThread.quitSafely();
157                 mStateMachinesThread.join(SM_THREAD_JOIN_TIMEOUT_MS);
158             } catch (InterruptedException e) {
159                 // Do not rethrow as we are shutting down anyway
160             }
161         }
162 
163         mHandler.removeCallbacksAndMessages(null);
164 
165         mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesAddedCallback);
166         mAudioManager.unregisterAudioDeviceCallback(mAudioManagerOnAudioDevicesRemovedCallback);
167     }
168 
169     /**
170      * Get the HearingAidService instance
171      *
172      * @return HearingAidService instance
173      */
getHearingAidService()174     public static synchronized HearingAidService getHearingAidService() {
175         if (sHearingAidService == null) {
176             Log.w(TAG, "getHearingAidService(): service is NULL");
177             return null;
178         }
179 
180         if (!sHearingAidService.isAvailable()) {
181             Log.w(TAG, "getHearingAidService(): service is not available");
182             return null;
183         }
184         return sHearingAidService;
185     }
186 
187     @VisibleForTesting
setHearingAidService(HearingAidService instance)188     static synchronized void setHearingAidService(HearingAidService instance) {
189         Log.d(TAG, "setHearingAidService(): set to: " + instance);
190         sHearingAidService = instance;
191     }
192 
193     /**
194      * Connects the hearing aid profile to the passed in device
195      *
196      * @param device is the device with which we will connect the hearing aid profile
197      * @return true if hearing aid profile successfully connected, false otherwise
198      */
connect(BluetoothDevice device)199     public boolean connect(BluetoothDevice device) {
200         Log.d(TAG, "connect(): " + device);
201         if (device == null) {
202             return false;
203         }
204 
205         if (getConnectionPolicy(device) == CONNECTION_POLICY_FORBIDDEN) {
206             return false;
207         }
208         final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
209         if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) {
210             Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Hearing Aid UUID");
211             return false;
212         }
213 
214         long hiSyncId =
215                 mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
216 
217         if (hiSyncId != mActiveDeviceHiSyncId
218                 && hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID
219                 && mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
220             for (BluetoothDevice connectedDevice : getConnectedDevices()) {
221                 disconnect(connectedDevice);
222             }
223         }
224 
225         synchronized (mStateMachines) {
226             HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
227             if (smConnect == null) {
228                 Log.e(TAG, "Cannot connect to " + device + " : no state machine");
229             }
230             smConnect.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT);
231         }
232 
233         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
234             if (device.equals(storedDevice)) {
235                 continue;
236             }
237             if (mDeviceHiSyncIdMap.getOrDefault(
238                             storedDevice, BluetoothHearingAid.HI_SYNC_ID_INVALID)
239                     == hiSyncId) {
240                 synchronized (mStateMachines) {
241                     HearingAidStateMachine sm = getOrCreateStateMachine(storedDevice);
242                     if (sm == null) {
243                         Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
244                         continue;
245                     }
246                     sm.sendMessage(HearingAidStateMachine.MESSAGE_CONNECT);
247                 }
248                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
249                         && !device.equals(storedDevice)) {
250                     break;
251                 }
252             }
253         }
254         return true;
255     }
256 
257     /**
258      * Disconnects hearing aid profile for the passed in device
259      *
260      * @param device is the device with which we want to disconnected the hearing aid profile
261      * @return true if hearing aid profile successfully disconnected, false otherwise
262      */
disconnect(BluetoothDevice device)263     public boolean disconnect(BluetoothDevice device) {
264         Log.d(TAG, "disconnect(): " + device);
265         if (device == null) {
266             return false;
267         }
268         long hiSyncId =
269                 mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
270 
271         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
272             if (mDeviceHiSyncIdMap.getOrDefault(
273                             storedDevice, BluetoothHearingAid.HI_SYNC_ID_INVALID)
274                     == hiSyncId) {
275                 synchronized (mStateMachines) {
276                     HearingAidStateMachine sm = mStateMachines.get(storedDevice);
277                     if (sm == null) {
278                         Log.e(
279                                 TAG,
280                                 "Ignored disconnect request for " + device + " : no state machine");
281                         continue;
282                     }
283                     sm.sendMessage(HearingAidStateMachine.MESSAGE_DISCONNECT);
284                 }
285                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
286                         && !device.equals(storedDevice)) {
287                     break;
288                 }
289             }
290         }
291         return true;
292     }
293 
getConnectedDevices()294     public List<BluetoothDevice> getConnectedDevices() {
295         synchronized (mStateMachines) {
296             List<BluetoothDevice> devices = new ArrayList<>();
297             for (HearingAidStateMachine sm : mStateMachines.values()) {
298                 if (sm.isConnected()) {
299                     devices.add(sm.getDevice());
300                 }
301             }
302             return devices;
303         }
304     }
305 
306     /**
307      * Check any peer device is connected. The check considers any peer device is connected.
308      *
309      * @param device the peer device to connect to
310      * @return true if there are any peer device connected.
311      */
isConnectedPeerDevices(BluetoothDevice device)312     public boolean isConnectedPeerDevices(BluetoothDevice device) {
313         long hiSyncId = getHiSyncId(device);
314         if (getConnectedPeerDevices(hiSyncId).isEmpty()) {
315             return false;
316         }
317         return true;
318     }
319 
320     /**
321      * Check whether can connect to a peer device. The check considers a number of factors during
322      * the evaluation.
323      *
324      * @param device the peer device to connect to
325      * @return true if connection is allowed, otherwise false
326      */
327     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
okToConnect(BluetoothDevice device)328     public boolean okToConnect(BluetoothDevice device) {
329         // Check if this is an incoming connection in Quiet mode.
330         if (mAdapterService.isQuietModeEnabled()) {
331             Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
332             return false;
333         }
334         // Check connection policy and accept or reject the connection.
335         int connectionPolicy = getConnectionPolicy(device);
336         if (!Flags.donotValidateBondStateFromProfiles()) {
337             int bondState = mAdapterService.getBondState(device);
338             // Allow this connection only if the device is bonded. Any attempt to connect while
339             // bonding would potentially lead to an unauthorized connection.
340             if (bondState != BluetoothDevice.BOND_BONDED) {
341                 Log.w(TAG, "okToConnect: return false, bondState=" + bondState);
342                 return false;
343             }
344         }
345         if (connectionPolicy != CONNECTION_POLICY_UNKNOWN
346                 && connectionPolicy != CONNECTION_POLICY_ALLOWED) {
347             // Otherwise, reject the connection if connectionPolicy is not valid.
348             Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy);
349             return false;
350         }
351         return true;
352     }
353 
getDevicesMatchingConnectionStates(int[] states)354     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
355         ArrayList<BluetoothDevice> devices = new ArrayList<>();
356         if (states == null) {
357             return devices;
358         }
359         final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
360         if (bondedDevices == null) {
361             return devices;
362         }
363         synchronized (mStateMachines) {
364             for (BluetoothDevice device : bondedDevices) {
365                 final ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
366                 if (!Utils.arrayContains(featureUuids, BluetoothUuid.HEARING_AID)) {
367                     continue;
368                 }
369                 int connectionState = STATE_DISCONNECTED;
370                 HearingAidStateMachine sm = mStateMachines.get(device);
371                 if (sm != null) {
372                     connectionState = sm.getConnectionState();
373                 }
374                 for (int state : states) {
375                     if (connectionState == state) {
376                         devices.add(device);
377                         break;
378                     }
379                 }
380             }
381             return devices;
382         }
383     }
384 
385     /**
386      * Get the list of devices that have state machines.
387      *
388      * @return the list of devices that have state machines
389      */
390     @VisibleForTesting
getDevices()391     List<BluetoothDevice> getDevices() {
392         List<BluetoothDevice> devices = new ArrayList<>();
393         synchronized (mStateMachines) {
394             for (HearingAidStateMachine sm : mStateMachines.values()) {
395                 devices.add(sm.getDevice());
396             }
397             return devices;
398         }
399     }
400 
401     /**
402      * Get the HiSyncIdMap for testing
403      *
404      * @return mDeviceHiSyncIdMap
405      */
406     @VisibleForTesting
getHiSyncIdMap()407     Map<BluetoothDevice, Long> getHiSyncIdMap() {
408         return mDeviceHiSyncIdMap;
409     }
410 
411     /**
412      * Get the current connection state of the profile
413      *
414      * @param device is the remote bluetooth device
415      * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, {@link
416      *     BluetoothProfile#STATE_CONNECTING} if this profile is being connected, {@link
417      *     BluetoothProfile#STATE_CONNECTED} if this profile is connected, or {@link
418      *     BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
419      */
getConnectionState(BluetoothDevice device)420     public int getConnectionState(BluetoothDevice device) {
421         synchronized (mStateMachines) {
422             HearingAidStateMachine sm = mStateMachines.get(device);
423             if (sm == null) {
424                 return STATE_DISCONNECTED;
425             }
426             return sm.getConnectionState();
427         }
428     }
429 
430     /**
431      * Set connection policy of the profile and connects it if connectionPolicy is {@link
432      * BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is {@link
433      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
434      *
435      * <p>The device should already be paired. Connection policy can be one of: {@link
436      * BluetoothProfile#CONNECTION_POLICY_ALLOWED}, {@link
437      * BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
438      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
439      *
440      * @param device Paired bluetooth device
441      * @param connectionPolicy is the connection policy to set to for this profile
442      * @return true if connectionPolicy is set, false on error
443      */
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)444     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
445         Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
446 
447         if (!mDatabaseManager.setProfileConnectionPolicy(
448                 device, BluetoothProfile.HEARING_AID, connectionPolicy)) {
449             return false;
450         }
451         if (connectionPolicy == CONNECTION_POLICY_ALLOWED) {
452             connect(device);
453         } else if (connectionPolicy == CONNECTION_POLICY_FORBIDDEN) {
454             disconnect(device);
455         }
456         return true;
457     }
458 
459     /**
460      * Get the connection policy of the profile.
461      *
462      * <p>The connection policy can be any of: {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
463      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, {@link
464      * BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
465      *
466      * @param device Bluetooth device
467      * @return connection policy of the device
468      */
getConnectionPolicy(BluetoothDevice device)469     public int getConnectionPolicy(BluetoothDevice device) {
470         return mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID);
471     }
472 
setVolume(int volume)473     void setVolume(int volume) {
474         mNativeInterface.setVolume(volume);
475     }
476 
getHiSyncId(BluetoothDevice device)477     public long getHiSyncId(BluetoothDevice device) {
478         if (device == null) {
479             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
480         }
481         return mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
482     }
483 
getCapabilities(BluetoothDevice device)484     int getCapabilities(BluetoothDevice device) {
485         return mDeviceCapabilitiesMap.getOrDefault(device, -1);
486     }
487 
getAdvertisementServiceData(BluetoothDevice device)488     AdvertisementServiceData getAdvertisementServiceData(BluetoothDevice device) {
489         int capability = mAdapterService.getAshaCapability(device);
490         int id = mAdapterService.getAshaTruncatedHiSyncId(device);
491         if (capability < 0) {
492             Log.i(TAG, "device does not have AdvertisementServiceData");
493             return null;
494         }
495         return new AdvertisementServiceData(capability, id);
496     }
497 
498     /**
499      * Remove the active device.
500      *
501      * @param stopAudio whether to stop current media playback.
502      * @return true on success, otherwise false
503      */
removeActiveDevice(boolean stopAudio)504     public boolean removeActiveDevice(boolean stopAudio) {
505         Log.d(TAG, "removeActiveDevice: stopAudio=" + stopAudio);
506         synchronized (mStateMachines) {
507             if (mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
508                 reportActiveDevice(null, stopAudio);
509                 mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
510             }
511         }
512         return true;
513     }
514 
515     /**
516      * Set the active device.
517      *
518      * @param device the new active device. Should not be null.
519      * @return true on success, otherwise false
520      */
setActiveDevice(BluetoothDevice device)521     public boolean setActiveDevice(BluetoothDevice device) {
522         if (device == null) {
523             Log.e(TAG, "setActiveDevice: device should not be null!");
524             return removeActiveDevice(true);
525         }
526         Log.d(TAG, "setActiveDevice: " + device);
527         synchronized (mStateMachines) {
528             /* No action needed since this is the same device as previously activated */
529             if (device.equals(mActiveDevice)) {
530                 Log.d(TAG, "setActiveDevice: The device is already active. Ignoring.");
531                 return true;
532             }
533 
534             if (getConnectionState(device) != STATE_CONNECTED) {
535                 Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected");
536                 return false;
537             }
538             Long deviceHiSyncId =
539                     mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
540             if (deviceHiSyncId != mActiveDeviceHiSyncId) {
541                 mActiveDeviceHiSyncId = deviceHiSyncId;
542                 reportActiveDevice(device, false);
543             }
544         }
545         return true;
546     }
547 
548     /**
549      * Get the connected physical Hearing Aid devices that are active
550      *
551      * @return the list of active devices. The first element is the left active device; the second
552      *     element is the right active device. If either or both side is not active, it will be null
553      *     on that position
554      */
getActiveDevices()555     public List<BluetoothDevice> getActiveDevices() {
556         ArrayList<BluetoothDevice> activeDevices = new ArrayList<>();
557         activeDevices.add(null);
558         activeDevices.add(null);
559 
560         synchronized (mStateMachines) {
561             long activeDeviceHiSyncId = mActiveDeviceHiSyncId;
562             if (activeDeviceHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
563                 return activeDevices;
564             }
565 
566             mDeviceHiSyncIdMap.entrySet().stream()
567                     .filter(entry -> activeDeviceHiSyncId == entry.getValue())
568                     .map(Map.Entry::getKey)
569                     .filter(device -> getConnectionState(device) == STATE_CONNECTED)
570                     .forEach(
571                             device -> {
572                                 int deviceSide = getCapabilities(device) & 1;
573                                 if (deviceSide == BluetoothHearingAid.SIDE_RIGHT) {
574                                     activeDevices.set(1, device);
575                                 } else {
576                                     activeDevices.set(0, device);
577                                 }
578                             });
579         }
580 
581         return activeDevices;
582     }
583 
messageFromNative(HearingAidStackEvent stackEvent)584     void messageFromNative(HearingAidStackEvent stackEvent) {
585         requireNonNull(stackEvent.device);
586 
587         if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) {
588             BluetoothDevice device = stackEvent.device;
589             int capabilities = stackEvent.valueInt1;
590             long hiSyncId = stackEvent.valueLong2;
591             Log.d(
592                     TAG,
593                     ("Device available: device=" + device)
594                             + (" capabilities=" + capabilities)
595                             + (" hiSyncId=" + hiSyncId));
596             mDeviceCapabilitiesMap.put(device, capabilities);
597             mDeviceHiSyncIdMap.put(device, hiSyncId);
598             return;
599         }
600 
601         synchronized (mStateMachines) {
602             BluetoothDevice device = stackEvent.device;
603             HearingAidStateMachine sm = mStateMachines.get(device);
604             if (sm == null) {
605                 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
606                     switch (stackEvent.valueInt1) {
607                         case STATE_CONNECTED:
608                         case STATE_CONNECTING:
609                             sm = getOrCreateStateMachine(device);
610                             break;
611                         default:
612                             break;
613                     }
614                 }
615             }
616             if (sm == null) {
617                 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
618                 return;
619             }
620             sm.sendMessage(HearingAidStateMachine.MESSAGE_STACK_EVENT, stackEvent);
621         }
622     }
623 
notifyActiveDeviceChanged()624     private void notifyActiveDeviceChanged() {
625         mAdapterService.handleActiveDeviceChange(BluetoothProfile.HEARING_AID, mActiveDevice);
626         Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
627         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mActiveDevice);
628         intent.addFlags(
629                 Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
630                         | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
631         sendBroadcastAsUser(intent, UserHandle.ALL, BLUETOOTH_CONNECT);
632     }
633 
634     /* Notifications of audio device disconnection events. */
635     private class AudioManagerOnAudioDevicesRemovedCallback extends AudioDeviceCallback {
636         @Override
onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices)637         public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
638             for (AudioDeviceInfo deviceInfo : removedDevices) {
639                 if (deviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
640                     Log.d(TAG, " onAudioDevicesRemoved: device type: " + deviceInfo.getType());
641                     if (mAudioManager != null) {
642                         notifyActiveDeviceChanged();
643                         mAudioManager.unregisterAudioDeviceCallback(this);
644                     } else {
645                         Log.w(TAG, "onAudioDevicesRemoved: mAudioManager is null");
646                     }
647                 }
648             }
649         }
650     }
651 
652     /* Notifications of audio device connection events. */
653     private class AudioManagerOnAudioDevicesAddedCallback extends AudioDeviceCallback {
654         @Override
onAudioDevicesAdded(AudioDeviceInfo[] addedDevices)655         public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
656             for (AudioDeviceInfo deviceInfo : addedDevices) {
657                 if (deviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
658                     Log.d(TAG, " onAudioDevicesAdded: device type: " + deviceInfo.getType());
659                     if (mAudioManager != null) {
660                         notifyActiveDeviceChanged();
661                         mAudioManager.unregisterAudioDeviceCallback(this);
662                     } else {
663                         Log.w(TAG, "onAudioDevicesAdded: mAudioManager is null");
664                     }
665                 }
666             }
667         }
668     }
669 
getOrCreateStateMachine(BluetoothDevice device)670     private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
671         if (device == null) {
672             Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
673             return null;
674         }
675         synchronized (mStateMachines) {
676             HearingAidStateMachine sm = mStateMachines.get(device);
677             if (sm != null) {
678                 return sm;
679             }
680             // Limit the maximum number of state machines to avoid DoS attack
681             if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) {
682                 Log.e(
683                         TAG,
684                         "Maximum number of HearingAid state machines reached: "
685                                 + MAX_HEARING_AID_STATE_MACHINES);
686                 return null;
687             }
688             Log.d(TAG, "Creating a new state machine for " + device);
689             sm = new HearingAidStateMachine(this, device, mNativeInterface, mStateMachinesLooper);
690             sm.start();
691             mStateMachines.put(device, sm);
692             return sm;
693         }
694     }
695 
696     /**
697      * Report the active device change to the active device manager and the media framework.
698      *
699      * @param device the new active device; or null if no active device
700      * @param stopAudio whether to stop audio when device is null.
701      */
reportActiveDevice(BluetoothDevice device, boolean stopAudio)702     private void reportActiveDevice(BluetoothDevice device, boolean stopAudio) {
703         Log.d(TAG, "reportActiveDevice: device=" + device + " stopAudio=" + stopAudio);
704 
705         if (device != null && stopAudio) {
706             Log.e(TAG, "Illegal arguments: stopAudio should be false when device is not null!");
707             stopAudio = false;
708         }
709 
710         // Note: This is just a safety check for handling illegal call - setActiveDevice(null).
711         if (device == null && stopAudio && getConnectionState(mActiveDevice) == STATE_CONNECTED) {
712             Log.e(
713                     TAG,
714                     "Illegal arguments: stopAudio should be false when the active hearing aid "
715                             + "is still connected!");
716             stopAudio = false;
717         }
718 
719         BluetoothDevice previousAudioDevice = mActiveDevice;
720 
721         mActiveDevice = device;
722 
723         BluetoothStatsLog.write(
724                 BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED,
725                 BluetoothProfile.HEARING_AID,
726                 mAdapterService.obfuscateAddress(device),
727                 mAdapterService.getMetricId(device));
728 
729         Log.d(
730                 TAG,
731                 "Hearing Aid audio: "
732                         + previousAudioDevice
733                         + " -> "
734                         + device
735                         + ". Stop audio: "
736                         + stopAudio);
737 
738         if (device != null) {
739             mAudioManager.registerAudioDeviceCallback(
740                     mAudioManagerOnAudioDevicesAddedCallback, mHandler);
741         } else {
742             mAudioManager.registerAudioDeviceCallback(
743                     mAudioManagerOnAudioDevicesRemovedCallback, mHandler);
744         }
745 
746         mAudioManager.handleBluetoothActiveDeviceChanged(
747                 device,
748                 previousAudioDevice,
749                 BluetoothProfileConnectionInfo.createHearingAidInfo(!stopAudio));
750     }
751 
752     /** Process a change in the bonding state for a device */
handleBondStateChanged(BluetoothDevice device, int fromState, int toState)753     public void handleBondStateChanged(BluetoothDevice device, int fromState, int toState) {
754         mHandler.post(() -> bondStateChanged(device, toState));
755     }
756 
757     /**
758      * Remove state machine if the bonding for a device is removed
759      *
760      * @param device the device whose bonding state has changed
761      * @param bondState the new bond state for the device. Possible values are: {@link
762      *     BluetoothDevice#BOND_NONE}, {@link BluetoothDevice#BOND_BONDING}, {@link
763      *     BluetoothDevice#BOND_BONDED}.
764      */
765     @VisibleForTesting
bondStateChanged(BluetoothDevice device, int bondState)766     void bondStateChanged(BluetoothDevice device, int bondState) {
767         Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
768         // Remove state machine if the bonding for a device is removed
769         if (bondState != BluetoothDevice.BOND_NONE) {
770             return;
771         }
772         mDeviceHiSyncIdMap.remove(device);
773         synchronized (mStateMachines) {
774             HearingAidStateMachine sm = mStateMachines.get(device);
775             if (sm == null) {
776                 return;
777             }
778             if (sm.getConnectionState() != STATE_DISCONNECTED) {
779                 Log.i(TAG, "Disconnecting device because it was unbonded.");
780                 disconnect(device);
781                 return;
782             }
783             removeStateMachine(device);
784         }
785     }
786 
removeStateMachine(BluetoothDevice device)787     private void removeStateMachine(BluetoothDevice device) {
788         synchronized (mStateMachines) {
789             HearingAidStateMachine sm = mStateMachines.get(device);
790             if (sm == null) {
791                 Log.w(
792                         TAG,
793                         "removeStateMachine: device " + device + " does not have a state machine");
794                 return;
795             }
796             Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
797             sm.doQuit();
798             mStateMachines.remove(device);
799         }
800     }
801 
getConnectedPeerDevices(long hiSyncId)802     public List<BluetoothDevice> getConnectedPeerDevices(long hiSyncId) {
803         List<BluetoothDevice> result = new ArrayList<>();
804         for (BluetoothDevice peerDevice : getConnectedDevices()) {
805             if (getHiSyncId(peerDevice) == hiSyncId) {
806                 result.add(peerDevice);
807             }
808         }
809         return result;
810     }
811 
connectionStateChanged(BluetoothDevice device, int fromState, int toState)812     synchronized void connectionStateChanged(BluetoothDevice device, int fromState, int toState) {
813         if ((device == null) || (fromState == toState)) {
814             Log.e(
815                     TAG,
816                     "connectionStateChanged: unexpected invocation. device="
817                             + device
818                             + " fromState="
819                             + fromState
820                             + " toState="
821                             + toState);
822             return;
823         }
824         if (toState == STATE_CONNECTED) {
825             long myHiSyncId = getHiSyncId(device);
826             if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) {
827                 mHiSyncIdConnectedMap.put(myHiSyncId, true);
828             }
829         }
830         if (fromState == STATE_CONNECTED && getConnectedDevices().isEmpty()) {
831             long myHiSyncId = getHiSyncId(device);
832             mHiSyncIdConnectedMap.put(myHiSyncId, false);
833             // ActiveDeviceManager will call removeActiveDevice().
834         }
835         // Check if the device is disconnected - if unbond, remove the state machine
836         if (toState == STATE_DISCONNECTED) {
837             int bondState = mAdapterService.getBondState(device);
838             if (bondState == BluetoothDevice.BOND_NONE) {
839                 Log.d(TAG, device + " is unbond. Remove state machine");
840                 removeStateMachine(device);
841             }
842         }
843         mAdapterService.notifyProfileConnectionStateChangeToGatt(
844                 BluetoothProfile.HEARING_AID, fromState, toState);
845         mAdapterService
846                 .getActiveDeviceManager()
847                 .profileConnectionStateChanged(
848                         BluetoothProfile.HEARING_AID, device, fromState, toState);
849         mAdapterService.updateProfileConnectionAdapterProperties(
850                 device, BluetoothProfile.HEARING_AID, toState, fromState);
851     }
852 
853     @Override
dump(StringBuilder sb)854     public void dump(StringBuilder sb) {
855         super.dump(sb);
856         for (HearingAidStateMachine sm : mStateMachines.values()) {
857             sm.dump(sb);
858         }
859     }
860 }
861