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