1 /*
2  * Copyright 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 android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothHearingAid;
21 import android.bluetooth.BluetoothProfile;
22 import android.bluetooth.BluetoothUuid;
23 import android.bluetooth.IBluetoothHearingAid;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.media.AudioManager;
29 import android.os.HandlerThread;
30 import android.os.ParcelUuid;
31 import android.util.Log;
32 
33 import com.android.bluetooth.BluetoothMetricsProto;
34 import com.android.bluetooth.BluetoothStatsLog;
35 import com.android.bluetooth.Utils;
36 import com.android.bluetooth.btservice.AdapterService;
37 import com.android.bluetooth.btservice.MetricsLogger;
38 import com.android.bluetooth.btservice.ProfileService;
39 import com.android.bluetooth.btservice.ServiceFactory;
40 import com.android.bluetooth.btservice.storage.DatabaseManager;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.util.ArrayUtils;
43 
44 import java.util.ArrayList;
45 import java.util.HashMap;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.concurrent.ConcurrentHashMap;
50 
51 /**
52  * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application.
53  * @hide
54  */
55 public class HearingAidService extends ProfileService {
56     private static final boolean DBG = true;
57     private static final String TAG = "HearingAidService";
58 
59     // Upper limit of all HearingAid devices: Bonded or Connected
60     private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
61     private static HearingAidService sHearingAidService;
62 
63     private AdapterService mAdapterService;
64     private DatabaseManager mDatabaseManager;
65     private HandlerThread mStateMachinesThread;
66     private BluetoothDevice mPreviousAudioDevice;
67 
68     @VisibleForTesting
69     HearingAidNativeInterface mHearingAidNativeInterface;
70     @VisibleForTesting
71     AudioManager mAudioManager;
72 
73     private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines =
74             new HashMap<>();
75     private final Map<BluetoothDevice, Long> mDeviceHiSyncIdMap = new ConcurrentHashMap<>();
76     private final Map<BluetoothDevice, Integer> mDeviceCapabilitiesMap = new HashMap<>();
77     private final Map<Long, Boolean> mHiSyncIdConnectedMap = new HashMap<>();
78     private long mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
79 
80     private BroadcastReceiver mBondStateChangedReceiver;
81     private BroadcastReceiver mConnectionStateChangedReceiver;
82 
83     private final ServiceFactory mFactory = new ServiceFactory();
84 
85     @Override
initBinder()86     protected IProfileServiceBinder initBinder() {
87         return new BluetoothHearingAidBinder(this);
88     }
89 
90     @Override
create()91     protected void create() {
92         if (DBG) {
93             Log.d(TAG, "create()");
94         }
95     }
96 
97     @Override
start()98     protected boolean start() {
99         if (DBG) {
100             Log.d(TAG, "start()");
101         }
102         if (sHearingAidService != null) {
103             throw new IllegalStateException("start() called twice");
104         }
105 
106         mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
107                 "AdapterService cannot be null when HearingAidService starts");
108         mHearingAidNativeInterface = Objects.requireNonNull(HearingAidNativeInterface.getInstance(),
109                 "HearingAidNativeInterface cannot be null when HearingAidService starts");
110         mDatabaseManager = Objects.requireNonNull(mAdapterService.getDatabase(),
111                 "DatabaseManager cannot be null when HearingAidService starts");
112         mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
113         Objects.requireNonNull(mAudioManager,
114                 "AudioManager cannot be null when HearingAidService starts");
115 
116         // Start handler thread for state machines
117         mStateMachines.clear();
118         mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
119         mStateMachinesThread.start();
120 
121         // Clear HiSyncId map, capabilities map and HiSyncId Connected map
122         mDeviceHiSyncIdMap.clear();
123         mDeviceCapabilitiesMap.clear();
124         mHiSyncIdConnectedMap.clear();
125 
126         // Setup broadcast receivers
127         IntentFilter filter = new IntentFilter();
128         filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
129         mBondStateChangedReceiver = new BondStateChangedReceiver();
130         registerReceiver(mBondStateChangedReceiver, filter);
131         filter = new IntentFilter();
132         filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
133         mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
134         registerReceiver(mConnectionStateChangedReceiver, filter);
135 
136         // Mark service as started
137         setHearingAidService(this);
138 
139         // Initialize native interface
140         mHearingAidNativeInterface.init();
141 
142         return true;
143     }
144 
145     @Override
stop()146     protected boolean stop() {
147         if (DBG) {
148             Log.d(TAG, "stop()");
149         }
150         if (sHearingAidService == null) {
151             Log.w(TAG, "stop() called before start()");
152             return true;
153         }
154 
155         // Cleanup native interface
156         mHearingAidNativeInterface.cleanup();
157         mHearingAidNativeInterface = null;
158 
159         // Mark service as stopped
160         setHearingAidService(null);
161 
162         // Unregister broadcast receivers
163         unregisterReceiver(mBondStateChangedReceiver);
164         mBondStateChangedReceiver = null;
165         unregisterReceiver(mConnectionStateChangedReceiver);
166         mConnectionStateChangedReceiver = null;
167 
168         // Destroy state machines and stop handler thread
169         synchronized (mStateMachines) {
170             for (HearingAidStateMachine sm : mStateMachines.values()) {
171                 sm.doQuit();
172                 sm.cleanup();
173             }
174             mStateMachines.clear();
175         }
176 
177         // Clear HiSyncId map, capabilities map and HiSyncId Connected map
178         mDeviceHiSyncIdMap.clear();
179         mDeviceCapabilitiesMap.clear();
180         mHiSyncIdConnectedMap.clear();
181 
182         if (mStateMachinesThread != null) {
183             mStateMachinesThread.quitSafely();
184             mStateMachinesThread = null;
185         }
186 
187         // Clear AdapterService, HearingAidNativeInterface
188         mAudioManager = null;
189         mHearingAidNativeInterface = null;
190         mAdapterService = null;
191 
192         return true;
193     }
194 
195     @Override
cleanup()196     protected void cleanup() {
197         if (DBG) {
198             Log.d(TAG, "cleanup()");
199         }
200     }
201 
202     /**
203      * Get the HearingAidService instance
204      * @return HearingAidService instance
205      */
getHearingAidService()206     public static synchronized HearingAidService getHearingAidService() {
207         if (sHearingAidService == null) {
208             Log.w(TAG, "getHearingAidService(): service is NULL");
209             return null;
210         }
211 
212         if (!sHearingAidService.isAvailable()) {
213             Log.w(TAG, "getHearingAidService(): service is not available");
214             return null;
215         }
216         return sHearingAidService;
217     }
218 
setHearingAidService(HearingAidService instance)219     private static synchronized void setHearingAidService(HearingAidService instance) {
220         if (DBG) {
221             Log.d(TAG, "setHearingAidService(): set to: " + instance);
222         }
223         sHearingAidService = instance;
224     }
225 
226     /**
227      * Connects the hearing aid profile to the passed in device
228      *
229      * @param device is the device with which we will connect the hearing aid profile
230      * @return true if hearing aid profile successfully connected, false otherwise
231      */
connect(BluetoothDevice device)232     public boolean connect(BluetoothDevice device) {
233         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
234                 "Need BLUETOOTH_PRIVILEGED permission");
235         if (DBG) {
236             Log.d(TAG, "connect(): " + device);
237         }
238         if (device == null) {
239             return false;
240         }
241 
242         if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
243             return false;
244         }
245         ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
246         if (!ArrayUtils.contains(featureUuids, BluetoothUuid.HEARING_AID)) {
247             Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Hearing Aid UUID");
248             return false;
249         }
250 
251         long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
252                 BluetoothHearingAid.HI_SYNC_ID_INVALID);
253 
254         if (hiSyncId != mActiveDeviceHiSyncId
255                 && hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID
256                 && mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
257             for (BluetoothDevice connectedDevice : getConnectedDevices()) {
258                 disconnect(connectedDevice);
259             }
260         }
261 
262         synchronized (mStateMachines) {
263             HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
264             if (smConnect == null) {
265                 Log.e(TAG, "Cannot connect to " + device + " : no state machine");
266             }
267             smConnect.sendMessage(HearingAidStateMachine.CONNECT);
268         }
269 
270         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
271             if (device.equals(storedDevice)) {
272                 continue;
273             }
274             if (mDeviceHiSyncIdMap.getOrDefault(storedDevice,
275                     BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) {
276                 synchronized (mStateMachines) {
277                     HearingAidStateMachine sm = getOrCreateStateMachine(storedDevice);
278                     if (sm == null) {
279                         Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
280                         continue;
281                     }
282                     sm.sendMessage(HearingAidStateMachine.CONNECT);
283                 }
284                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
285                         && !device.equals(storedDevice)) {
286                     break;
287                 }
288             }
289         }
290         return true;
291     }
292 
293     /**
294      * Disconnects hearing aid profile for the passed in device
295      *
296      * @param device is the device with which we want to disconnected the hearing aid profile
297      * @return true if hearing aid profile successfully disconnected, false otherwise
298      */
disconnect(BluetoothDevice device)299     public boolean disconnect(BluetoothDevice device) {
300         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
301                 "Need BLUETOOTH_PRIVILEGED permission");
302         if (DBG) {
303             Log.d(TAG, "disconnect(): " + device);
304         }
305         if (device == null) {
306             return false;
307         }
308         long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
309                 BluetoothHearingAid.HI_SYNC_ID_INVALID);
310 
311         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
312             if (mDeviceHiSyncIdMap.getOrDefault(storedDevice,
313                     BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) {
314                 synchronized (mStateMachines) {
315                     HearingAidStateMachine sm = mStateMachines.get(storedDevice);
316                     if (sm == null) {
317                         Log.e(TAG, "Ignored disconnect request for " + device
318                                 + " : no state machine");
319                         continue;
320                     }
321                     sm.sendMessage(HearingAidStateMachine.DISCONNECT);
322                 }
323                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
324                         && !device.equals(storedDevice)) {
325                     break;
326                 }
327             }
328         }
329         return true;
330     }
331 
getConnectedDevices()332     List<BluetoothDevice> getConnectedDevices() {
333         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
334         synchronized (mStateMachines) {
335             List<BluetoothDevice> devices = new ArrayList<>();
336             for (HearingAidStateMachine sm : mStateMachines.values()) {
337                 if (sm.isConnected()) {
338                     devices.add(sm.getDevice());
339                 }
340             }
341             return devices;
342         }
343     }
344 
345     /**
346      * Check any peer device is connected.
347      * The check considers any peer device is connected.
348      *
349      * @param device the peer device to connect to
350      * @return true if there are any peer device connected.
351      */
isConnectedPeerDevices(BluetoothDevice device)352     public boolean isConnectedPeerDevices(BluetoothDevice device) {
353         long hiSyncId = getHiSyncId(device);
354         if (getConnectedPeerDevices(hiSyncId).isEmpty()) {
355             return false;
356         }
357         return true;
358     }
359 
360     /**
361      * Check whether can connect to a peer device.
362      * The check considers a number of factors during the evaluation.
363      *
364      * @param device the peer device to connect to
365      * @return true if connection is allowed, otherwise false
366      */
367     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
okToConnect(BluetoothDevice device)368     public boolean okToConnect(BluetoothDevice device) {
369         // Check if this is an incoming connection in Quiet mode.
370         if (mAdapterService.isQuietModeEnabled()) {
371             Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
372             return false;
373         }
374         // Check connection policy and accept or reject the connection.
375         int connectionPolicy = getConnectionPolicy(device);
376         int bondState = mAdapterService.getBondState(device);
377         // Allow this connection only if the device is bonded. Any attempt to connect while
378         // bonding would potentially lead to an unauthorized connection.
379         if (bondState != BluetoothDevice.BOND_BONDED) {
380             Log.w(TAG, "okToConnect: return false, bondState=" + bondState);
381             return false;
382         } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
383                 && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
384             // Otherwise, reject the connection if connectionPolicy is not valid.
385             Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy);
386             return false;
387         }
388         return true;
389     }
390 
getDevicesMatchingConnectionStates(int[] states)391     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
392         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
393         ArrayList<BluetoothDevice> devices = new ArrayList<>();
394         if (states == null) {
395             return devices;
396         }
397         final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
398         if (bondedDevices == null) {
399             return devices;
400         }
401         synchronized (mStateMachines) {
402             for (BluetoothDevice device : bondedDevices) {
403                 final ParcelUuid[] featureUuids = device.getUuids();
404                 if (!ArrayUtils.contains(featureUuids, BluetoothUuid.HEARING_AID)) {
405                     continue;
406                 }
407                 int connectionState = BluetoothProfile.STATE_DISCONNECTED;
408                 HearingAidStateMachine sm = mStateMachines.get(device);
409                 if (sm != null) {
410                     connectionState = sm.getConnectionState();
411                 }
412                 for (int state : states) {
413                     if (connectionState == state) {
414                         devices.add(device);
415                         break;
416                     }
417                 }
418             }
419             return devices;
420         }
421     }
422 
423     /**
424      * Get the list of devices that have state machines.
425      *
426      * @return the list of devices that have state machines
427      */
428     @VisibleForTesting
getDevices()429     List<BluetoothDevice> getDevices() {
430         List<BluetoothDevice> devices = new ArrayList<>();
431         synchronized (mStateMachines) {
432             for (HearingAidStateMachine sm : mStateMachines.values()) {
433                 devices.add(sm.getDevice());
434             }
435             return devices;
436         }
437     }
438 
439     /**
440      * Get the HiSyncIdMap for testing
441      *
442      * @return mDeviceHiSyncIdMap
443      */
444     @VisibleForTesting
getHiSyncIdMap()445     Map<BluetoothDevice, Long> getHiSyncIdMap() {
446         return mDeviceHiSyncIdMap;
447     }
448 
449     /**
450      * Get the current connection state of the profile
451      *
452      * @param device is the remote bluetooth device
453      * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
454      * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
455      * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
456      * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
457      */
getConnectionState(BluetoothDevice device)458     public int getConnectionState(BluetoothDevice device) {
459         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
460         synchronized (mStateMachines) {
461             HearingAidStateMachine sm = mStateMachines.get(device);
462             if (sm == null) {
463                 return BluetoothProfile.STATE_DISCONNECTED;
464             }
465             return sm.getConnectionState();
466         }
467     }
468 
469     /**
470      * Set connection policy of the profile and connects it if connectionPolicy is
471      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
472      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
473      *
474      * <p> The device should already be paired.
475      * Connection policy can be one of:
476      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
477      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
478      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
479      *
480      * @param device Paired bluetooth device
481      * @param connectionPolicy is the connection policy to set to for this profile
482      * @return true if connectionPolicy is set, false on error
483      */
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)484     public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
485         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
486                 "Need BLUETOOTH_PRIVILEGED permission");
487         if (DBG) {
488             Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
489         }
490 
491         if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID,
492                   connectionPolicy)) {
493             return false;
494         }
495         if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
496             connect(device);
497         } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
498             disconnect(device);
499         }
500         return true;
501     }
502 
503     /**
504      * Get the connection policy of the profile.
505      *
506      * <p> The connection policy can be any of:
507      * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
508      * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
509      * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
510      *
511      * @param device Bluetooth device
512      * @return connection policy of the device
513      * @hide
514      */
getConnectionPolicy(BluetoothDevice device)515     public int getConnectionPolicy(BluetoothDevice device) {
516         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
517                 "Need BLUETOOTH_PRIVILEGED permission");
518         return mDatabaseManager
519                 .getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID);
520     }
521 
setVolume(int volume)522     void setVolume(int volume) {
523         mHearingAidNativeInterface.setVolume(volume);
524     }
525 
getHiSyncId(BluetoothDevice device)526     long getHiSyncId(BluetoothDevice device) {
527         enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
528                 "Need BLUETOOTH_PRIVILEGED permission");
529         if (device == null) {
530             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
531         }
532         return mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
533     }
534 
getCapabilities(BluetoothDevice device)535     int getCapabilities(BluetoothDevice device) {
536         return mDeviceCapabilitiesMap.getOrDefault(device, -1);
537     }
538 
539     /**
540      * Set the active device.
541      * @param device the new active device
542      * @return true on success, otherwise false
543      */
setActiveDevice(BluetoothDevice device)544     public boolean setActiveDevice(BluetoothDevice device) {
545         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
546         if (DBG) {
547             Log.d(TAG, "setActiveDevice:" + device);
548         }
549         synchronized (mStateMachines) {
550             if (device == null) {
551                 if (mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
552                     reportActiveDevice(null);
553                     mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
554                 }
555                 return true;
556             }
557             if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
558                 Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected");
559                 return false;
560             }
561             Long deviceHiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
562                     BluetoothHearingAid.HI_SYNC_ID_INVALID);
563             if (deviceHiSyncId != mActiveDeviceHiSyncId) {
564                 mActiveDeviceHiSyncId = deviceHiSyncId;
565                 reportActiveDevice(device);
566             }
567         }
568         return true;
569     }
570 
571     /**
572      * Get the connected physical Hearing Aid devices that are active
573      *
574      * @return the list of active devices. The first element is the left active
575      * device; the second element is the right active device. If either or both side
576      * is not active, it will be null on that position
577      */
getActiveDevices()578     public List<BluetoothDevice> getActiveDevices() {
579         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
580         if (DBG) {
581             Log.d(TAG, "getActiveDevices");
582         }
583         ArrayList<BluetoothDevice> activeDevices = new ArrayList<>();
584         activeDevices.add(null);
585         activeDevices.add(null);
586         synchronized (mStateMachines) {
587             if (mActiveDeviceHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
588                 return activeDevices;
589             }
590             for (BluetoothDevice device : mDeviceHiSyncIdMap.keySet()) {
591                 if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
592                     continue;
593                 }
594                 if (mDeviceHiSyncIdMap.get(device) == mActiveDeviceHiSyncId) {
595                     int deviceSide = getCapabilities(device) & 1;
596                     if (deviceSide == BluetoothHearingAid.SIDE_RIGHT) {
597                         activeDevices.set(1, device);
598                     } else {
599                         activeDevices.set(0, device);
600                     }
601                 }
602             }
603         }
604         return activeDevices;
605     }
606 
messageFromNative(HearingAidStackEvent stackEvent)607     void messageFromNative(HearingAidStackEvent stackEvent) {
608         Objects.requireNonNull(stackEvent.device,
609                 "Device should never be null, event: " + stackEvent);
610 
611         if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) {
612             BluetoothDevice device = stackEvent.device;
613             int capabilities = stackEvent.valueInt1;
614             long hiSyncId = stackEvent.valueLong2;
615             if (DBG) {
616                 Log.d(TAG, "Device available: device=" + device + " capabilities="
617                         + capabilities + " hiSyncId=" + hiSyncId);
618             }
619             mDeviceCapabilitiesMap.put(device, capabilities);
620             mDeviceHiSyncIdMap.put(device, hiSyncId);
621             return;
622         }
623 
624         synchronized (mStateMachines) {
625             BluetoothDevice device = stackEvent.device;
626             HearingAidStateMachine sm = mStateMachines.get(device);
627             if (sm == null) {
628                 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
629                     switch (stackEvent.valueInt1) {
630                         case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
631                         case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
632                             sm = getOrCreateStateMachine(device);
633                             break;
634                         default:
635                             break;
636                     }
637                 }
638             }
639             if (sm == null) {
640                 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
641                 return;
642             }
643             sm.sendMessage(HearingAidStateMachine.STACK_EVENT, stackEvent);
644         }
645     }
646 
getOrCreateStateMachine(BluetoothDevice device)647     private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
648         if (device == null) {
649             Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
650             return null;
651         }
652         synchronized (mStateMachines) {
653             HearingAidStateMachine sm = mStateMachines.get(device);
654             if (sm != null) {
655                 return sm;
656             }
657             // Limit the maximum number of state machines to avoid DoS attack
658             if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) {
659                 Log.e(TAG, "Maximum number of HearingAid state machines reached: "
660                         + MAX_HEARING_AID_STATE_MACHINES);
661                 return null;
662             }
663             if (DBG) {
664                 Log.d(TAG, "Creating a new state machine for " + device);
665             }
666             sm = HearingAidStateMachine.make(device, this,
667                     mHearingAidNativeInterface, mStateMachinesThread.getLooper());
668             mStateMachines.put(device, sm);
669             return sm;
670         }
671     }
672 
673     /**
674      * Report the active device change to the active device manager and the media framework.
675      * @param device the new active device; or null if no active device
676      */
reportActiveDevice(BluetoothDevice device)677     private void reportActiveDevice(BluetoothDevice device) {
678         if (DBG) {
679             Log.d(TAG, "reportActiveDevice(" + device + ")");
680         }
681 
682         BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED,
683                 BluetoothProfile.HEARING_AID, mAdapterService.obfuscateAddress(device),
684                 mAdapterService.getMetricId(device));
685 
686         Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
687         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
688         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
689                 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
690         sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
691 
692         if (device == null) {
693             if (DBG) {
694                 Log.d(TAG, "Set Hearing Aid audio to disconnected");
695             }
696             boolean suppressNoisyIntent =
697                     (getConnectionState(mPreviousAudioDevice) == BluetoothProfile.STATE_CONNECTED);
698             mAudioManager.setBluetoothHearingAidDeviceConnectionState(
699                     mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED,
700                     suppressNoisyIntent, 0);
701             mPreviousAudioDevice = null;
702         } else {
703             if (DBG) {
704                 Log.d(TAG, "Set Hearing Aid audio to connected");
705             }
706             if (mPreviousAudioDevice != null) {
707                 mAudioManager.setBluetoothHearingAidDeviceConnectionState(
708                         mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED,
709                         true, 0);
710             }
711             mAudioManager.setBluetoothHearingAidDeviceConnectionState(
712                     device, BluetoothProfile.STATE_CONNECTED,
713                     true, 0);
714             mPreviousAudioDevice = device;
715         }
716     }
717 
718     // Remove state machine if the bonding for a device is removed
719     private class BondStateChangedReceiver extends BroadcastReceiver {
720         @Override
onReceive(Context context, Intent intent)721         public void onReceive(Context context, Intent intent) {
722             if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
723                 return;
724             }
725             int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
726                                            BluetoothDevice.ERROR);
727             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
728             Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
729             bondStateChanged(device, state);
730         }
731     }
732 
733     /**
734      * Process a change in the bonding state for a device.
735      *
736      * @param device the device whose bonding state has changed
737      * @param bondState the new bond state for the device. Possible values are:
738      * {@link BluetoothDevice#BOND_NONE},
739      * {@link BluetoothDevice#BOND_BONDING},
740      * {@link BluetoothDevice#BOND_BONDED}.
741      */
742     @VisibleForTesting
bondStateChanged(BluetoothDevice device, int bondState)743     void bondStateChanged(BluetoothDevice device, int bondState) {
744         if (DBG) {
745             Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
746         }
747         // Remove state machine if the bonding for a device is removed
748         if (bondState != BluetoothDevice.BOND_NONE) {
749             return;
750         }
751         mDeviceHiSyncIdMap.remove(device);
752         synchronized (mStateMachines) {
753             HearingAidStateMachine sm = mStateMachines.get(device);
754             if (sm == null) {
755                 return;
756             }
757             if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
758                 return;
759             }
760             removeStateMachine(device);
761         }
762     }
763 
removeStateMachine(BluetoothDevice device)764     private void removeStateMachine(BluetoothDevice device) {
765         synchronized (mStateMachines) {
766             HearingAidStateMachine sm = mStateMachines.get(device);
767             if (sm == null) {
768                 Log.w(TAG, "removeStateMachine: device " + device
769                         + " does not have a state machine");
770                 return;
771             }
772             Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
773             sm.doQuit();
774             sm.cleanup();
775             mStateMachines.remove(device);
776         }
777     }
778 
getConnectedPeerDevices(long hiSyncId)779     private List<BluetoothDevice> getConnectedPeerDevices(long hiSyncId) {
780         List<BluetoothDevice> result = new ArrayList<>();
781         for (BluetoothDevice peerDevice : getConnectedDevices()) {
782             if (getHiSyncId(peerDevice) == hiSyncId) {
783                 result.add(peerDevice);
784             }
785         }
786         return result;
787     }
788 
789     @VisibleForTesting
connectionStateChanged(BluetoothDevice device, int fromState, int toState)790     synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
791                                                      int toState) {
792         if ((device == null) || (fromState == toState)) {
793             Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
794                     + " fromState=" + fromState + " toState=" + toState);
795             return;
796         }
797         if (toState == BluetoothProfile.STATE_CONNECTED) {
798             long myHiSyncId = getHiSyncId(device);
799             if (myHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
800                     || getConnectedPeerDevices(myHiSyncId).size() == 1) {
801                 // Log hearing aid connection event if we are the first device in a set
802                 // Or when the hiSyncId has not been found
803                 MetricsLogger.logProfileConnectionEvent(
804                         BluetoothMetricsProto.ProfileId.HEARING_AID);
805             }
806             if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) {
807                 setActiveDevice(device);
808                 mHiSyncIdConnectedMap.put(myHiSyncId, true);
809             }
810         }
811         if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) {
812             setActiveDevice(null);
813             long myHiSyncId = getHiSyncId(device);
814             mHiSyncIdConnectedMap.put(myHiSyncId, false);
815         }
816         // Check if the device is disconnected - if unbond, remove the state machine
817         if (toState == BluetoothProfile.STATE_DISCONNECTED) {
818             int bondState = mAdapterService.getBondState(device);
819             if (bondState == BluetoothDevice.BOND_NONE) {
820                 if (DBG) {
821                     Log.d(TAG, device + " is unbond. Remove state machine");
822                 }
823                 removeStateMachine(device);
824             }
825         }
826     }
827 
828     private class ConnectionStateChangedReceiver extends BroadcastReceiver {
829         @Override
onReceive(Context context, Intent intent)830         public void onReceive(Context context, Intent intent) {
831             if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
832                 return;
833             }
834             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
835             int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
836             int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
837             connectionStateChanged(device, fromState, toState);
838         }
839     }
840 
841     /**
842      * Binder object: must be a static class or memory leak may occur
843      */
844     @VisibleForTesting
845     static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub
846             implements IProfileServiceBinder {
847         private HearingAidService mService;
848 
getService()849         private HearingAidService getService() {
850             if (!Utils.checkCaller()) {
851                 Log.w(TAG, "HearingAid call not allowed for non-active user");
852                 return null;
853             }
854 
855             if (mService != null && mService.isAvailable()) {
856                 return mService;
857             }
858             return null;
859         }
860 
BluetoothHearingAidBinder(HearingAidService svc)861         BluetoothHearingAidBinder(HearingAidService svc) {
862             mService = svc;
863         }
864 
865         @Override
cleanup()866         public void cleanup() {
867             mService = null;
868         }
869 
870         @Override
connect(BluetoothDevice device)871         public boolean connect(BluetoothDevice device) {
872             HearingAidService service = getService();
873             if (service == null) {
874                 return false;
875             }
876             return service.connect(device);
877         }
878 
879         @Override
disconnect(BluetoothDevice device)880         public boolean disconnect(BluetoothDevice device) {
881             HearingAidService service = getService();
882             if (service == null) {
883                 return false;
884             }
885             return service.disconnect(device);
886         }
887 
888         @Override
getConnectedDevices()889         public List<BluetoothDevice> getConnectedDevices() {
890             HearingAidService service = getService();
891             if (service == null) {
892                 return new ArrayList<>();
893             }
894             return service.getConnectedDevices();
895         }
896 
897         @Override
getDevicesMatchingConnectionStates(int[] states)898         public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
899             HearingAidService service = getService();
900             if (service == null) {
901                 return new ArrayList<>();
902             }
903             return service.getDevicesMatchingConnectionStates(states);
904         }
905 
906         @Override
getConnectionState(BluetoothDevice device)907         public int getConnectionState(BluetoothDevice device) {
908             HearingAidService service = getService();
909             if (service == null) {
910                 return BluetoothProfile.STATE_DISCONNECTED;
911             }
912             return service.getConnectionState(device);
913         }
914 
915         @Override
setActiveDevice(BluetoothDevice device)916         public boolean setActiveDevice(BluetoothDevice device) {
917             HearingAidService service = getService();
918             if (service == null) {
919                 return false;
920             }
921             return service.setActiveDevice(device);
922         }
923 
924         @Override
getActiveDevices()925         public List<BluetoothDevice> getActiveDevices() {
926             HearingAidService service = getService();
927             if (service == null) {
928                 return new ArrayList<>();
929             }
930             return service.getActiveDevices();
931         }
932 
933         @Override
setConnectionPolicy(BluetoothDevice device, int connectionPolicy)934         public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
935             HearingAidService service = getService();
936             if (service == null) {
937                 return false;
938             }
939             return service.setConnectionPolicy(device, connectionPolicy);
940         }
941 
942         @Override
getConnectionPolicy(BluetoothDevice device)943         public int getConnectionPolicy(BluetoothDevice device) {
944             HearingAidService service = getService();
945             if (service == null) {
946                 return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
947             }
948             return service.getConnectionPolicy(device);
949         }
950 
951         @Override
setVolume(int volume)952         public void setVolume(int volume) {
953             HearingAidService service = getService();
954             if (service == null) {
955                 return;
956             }
957             service.setVolume(volume);
958         }
959 
960         @Override
getHiSyncId(BluetoothDevice device)961         public long getHiSyncId(BluetoothDevice device) {
962             HearingAidService service = getService();
963             if (service == null) {
964                 return BluetoothHearingAid.HI_SYNC_ID_INVALID;
965             }
966             return service.getHiSyncId(device);
967         }
968 
969         @Override
getDeviceSide(BluetoothDevice device)970         public int getDeviceSide(BluetoothDevice device) {
971             HearingAidService service = getService();
972             if (service == null) {
973                 return BluetoothHearingAid.SIDE_RIGHT;
974             }
975             return service.getCapabilities(device) & 1;
976         }
977 
978         @Override
getDeviceMode(BluetoothDevice device)979         public int getDeviceMode(BluetoothDevice device) {
980             HearingAidService service = getService();
981             if (service == null) {
982                 return BluetoothHearingAid.MODE_BINAURAL;
983             }
984             return service.getCapabilities(device) >> 1 & 1;
985         }
986     }
987 
988     @Override
dump(StringBuilder sb)989     public void dump(StringBuilder sb) {
990         super.dump(sb);
991         for (HearingAidStateMachine sm : mStateMachines.values()) {
992             sm.dump(sb);
993         }
994     }
995 }
996