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