• 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 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 import android.util.StatsLog;
33 
34 import com.android.bluetooth.BluetoothMetricsProto;
35 import com.android.bluetooth.Utils;
36 import com.android.bluetooth.a2dp.A2dpService;
37 import com.android.bluetooth.btservice.AdapterService;
38 import com.android.bluetooth.btservice.MetricsLogger;
39 import com.android.bluetooth.btservice.ProfileService;
40 import com.android.bluetooth.btservice.ServiceFactory;
41 import com.android.internal.annotations.VisibleForTesting;
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 
connect(BluetoothDevice device)224     boolean connect(BluetoothDevice device) {
225         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
226         if (DBG) {
227             Log.d(TAG, "connect(): " + device);
228         }
229         if (device == null) {
230             return false;
231         }
232 
233         if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
234             return false;
235         }
236         ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
237         if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
238             Log.e(TAG, "Cannot connect to " + device + " : Remote does not have Hearing Aid UUID");
239             return false;
240         }
241 
242         long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
243                 BluetoothHearingAid.HI_SYNC_ID_INVALID);
244 
245         if (hiSyncId != mActiveDeviceHiSyncId
246                 && hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID
247                 && mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
248             for (BluetoothDevice connectedDevice : getConnectedDevices()) {
249                 disconnect(connectedDevice);
250             }
251         }
252 
253         synchronized (mStateMachines) {
254             HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
255             if (smConnect == null) {
256                 Log.e(TAG, "Cannot connect to " + device + " : no state machine");
257             }
258             smConnect.sendMessage(HearingAidStateMachine.CONNECT);
259         }
260 
261         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
262             if (device.equals(storedDevice)) {
263                 continue;
264             }
265             if (mDeviceHiSyncIdMap.getOrDefault(storedDevice,
266                     BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) {
267                 synchronized (mStateMachines) {
268                     HearingAidStateMachine sm = getOrCreateStateMachine(storedDevice);
269                     if (sm == null) {
270                         Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
271                         continue;
272                     }
273                     sm.sendMessage(HearingAidStateMachine.CONNECT);
274                 }
275                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
276                         && !device.equals(storedDevice)) {
277                     break;
278                 }
279             }
280         }
281         return true;
282     }
283 
disconnect(BluetoothDevice device)284     boolean disconnect(BluetoothDevice device) {
285         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
286         if (DBG) {
287             Log.d(TAG, "disconnect(): " + device);
288         }
289         if (device == null) {
290             return false;
291         }
292         long hiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
293                 BluetoothHearingAid.HI_SYNC_ID_INVALID);
294 
295         for (BluetoothDevice storedDevice : mDeviceHiSyncIdMap.keySet()) {
296             if (mDeviceHiSyncIdMap.getOrDefault(storedDevice,
297                     BluetoothHearingAid.HI_SYNC_ID_INVALID) == hiSyncId) {
298                 synchronized (mStateMachines) {
299                     HearingAidStateMachine sm = mStateMachines.get(storedDevice);
300                     if (sm == null) {
301                         Log.e(TAG, "Ignored disconnect request for " + device
302                                 + " : no state machine");
303                         continue;
304                     }
305                     sm.sendMessage(HearingAidStateMachine.DISCONNECT);
306                 }
307                 if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
308                         && !device.equals(storedDevice)) {
309                     break;
310                 }
311             }
312         }
313         return true;
314     }
315 
getConnectedDevices()316     List<BluetoothDevice> getConnectedDevices() {
317         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
318         synchronized (mStateMachines) {
319             List<BluetoothDevice> devices = new ArrayList<>();
320             for (HearingAidStateMachine sm : mStateMachines.values()) {
321                 if (sm.isConnected()) {
322                     devices.add(sm.getDevice());
323                 }
324             }
325             return devices;
326         }
327     }
328 
329     /**
330      * Check any peer device is connected.
331      * The check considers any peer device is connected.
332      *
333      * @param device the peer device to connect to
334      * @return true if there are any peer device connected.
335      */
isConnectedPeerDevices(BluetoothDevice device)336     public boolean isConnectedPeerDevices(BluetoothDevice device) {
337         long hiSyncId = getHiSyncId(device);
338         if (getConnectedPeerDevices(hiSyncId).isEmpty()) {
339             return false;
340         }
341         return true;
342     }
343 
344     /**
345      * Check whether can connect to a peer device.
346      * The check considers a number of factors during the evaluation.
347      *
348      * @param device the peer device to connect to
349      * @return true if connection is allowed, otherwise false
350      */
351     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
okToConnect(BluetoothDevice device)352     public boolean okToConnect(BluetoothDevice device) {
353         // Check if this is an incoming connection in Quiet mode.
354         if (mAdapterService.isQuietModeEnabled()) {
355             Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
356             return false;
357         }
358         // Check priority and accept or reject the connection.
359         int priority = getPriority(device);
360         int bondState = mAdapterService.getBondState(device);
361         // Allow this connection only if the device is bonded. Any attempt to connect while
362         // bonding would potentially lead to an unauthorized connection.
363         if (bondState != BluetoothDevice.BOND_BONDED) {
364             Log.w(TAG, "okToConnect: return false, bondState=" + bondState);
365             return false;
366         } else if (priority != BluetoothProfile.PRIORITY_UNDEFINED
367                 && priority != BluetoothProfile.PRIORITY_ON
368                 && priority != BluetoothProfile.PRIORITY_AUTO_CONNECT) {
369             // Otherwise, reject the connection if priority is not valid.
370             Log.w(TAG, "okToConnect: return false, priority=" + priority);
371             return false;
372         }
373         return true;
374     }
375 
getDevicesMatchingConnectionStates(int[] states)376     List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
377         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
378         ArrayList<BluetoothDevice> devices = new ArrayList<>();
379         if (states == null) {
380             return devices;
381         }
382         final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
383         if (bondedDevices == null) {
384             return devices;
385         }
386         synchronized (mStateMachines) {
387             for (BluetoothDevice device : bondedDevices) {
388                 final ParcelUuid[] featureUuids = device.getUuids();
389                 if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
390                     continue;
391                 }
392                 int connectionState = BluetoothProfile.STATE_DISCONNECTED;
393                 HearingAidStateMachine sm = mStateMachines.get(device);
394                 if (sm != null) {
395                     connectionState = sm.getConnectionState();
396                 }
397                 for (int state : states) {
398                     if (connectionState == state) {
399                         devices.add(device);
400                         break;
401                     }
402                 }
403             }
404             return devices;
405         }
406     }
407 
408     /**
409      * Get the list of devices that have state machines.
410      *
411      * @return the list of devices that have state machines
412      */
413     @VisibleForTesting
getDevices()414     List<BluetoothDevice> getDevices() {
415         List<BluetoothDevice> devices = new ArrayList<>();
416         synchronized (mStateMachines) {
417             for (HearingAidStateMachine sm : mStateMachines.values()) {
418                 devices.add(sm.getDevice());
419             }
420             return devices;
421         }
422     }
423 
424     /**
425      * Get the HiSyncIdMap for testing
426      *
427      * @return mDeviceHiSyncIdMap
428      */
429     @VisibleForTesting
getHiSyncIdMap()430     Map<BluetoothDevice, Long> getHiSyncIdMap() {
431         return mDeviceHiSyncIdMap;
432     }
433 
getConnectionState(BluetoothDevice device)434     int getConnectionState(BluetoothDevice device) {
435         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
436         synchronized (mStateMachines) {
437             HearingAidStateMachine sm = mStateMachines.get(device);
438             if (sm == null) {
439                 return BluetoothProfile.STATE_DISCONNECTED;
440             }
441             return sm.getConnectionState();
442         }
443     }
444 
445     /**
446      * Set the priority of the Hearing Aid profile.
447      *
448      * @param device the remote device
449      * @param priority the priority of the profile
450      * @return true on success, otherwise false
451      */
setPriority(BluetoothDevice device, int priority)452     public boolean setPriority(BluetoothDevice device, int priority) {
453         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
454         if (DBG) {
455             Log.d(TAG, "Saved priority " + device + " = " + priority);
456         }
457         mAdapterService.getDatabase()
458                 .setProfilePriority(device, BluetoothProfile.HEARING_AID, priority);
459         return true;
460     }
461 
getPriority(BluetoothDevice device)462     public int getPriority(BluetoothDevice device) {
463         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
464         return mAdapterService.getDatabase()
465                 .getProfilePriority(device, BluetoothProfile.HEARING_AID);
466     }
467 
setVolume(int volume)468     void setVolume(int volume) {
469         mHearingAidNativeInterface.setVolume(volume);
470     }
471 
getHiSyncId(BluetoothDevice device)472     long getHiSyncId(BluetoothDevice device) {
473         if (device == null) {
474             return BluetoothHearingAid.HI_SYNC_ID_INVALID;
475         }
476         return mDeviceHiSyncIdMap.getOrDefault(device, BluetoothHearingAid.HI_SYNC_ID_INVALID);
477     }
478 
getCapabilities(BluetoothDevice device)479     int getCapabilities(BluetoothDevice device) {
480         return mDeviceCapabilitiesMap.getOrDefault(device, -1);
481     }
482 
483     /**
484      * Set the active device.
485      * @param device the new active device
486      * @return true on success, otherwise false
487      */
setActiveDevice(BluetoothDevice device)488     public boolean setActiveDevice(BluetoothDevice device) {
489         enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
490         if (DBG) {
491             Log.d(TAG, "setActiveDevice:" + device);
492         }
493         synchronized (mStateMachines) {
494             if (device == null) {
495                 if (mActiveDeviceHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
496                     reportActiveDevice(null);
497                     mActiveDeviceHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
498                 }
499                 return true;
500             }
501             if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
502                 Log.e(TAG, "setActiveDevice(" + device + "): failed because device not connected");
503                 return false;
504             }
505             Long deviceHiSyncId = mDeviceHiSyncIdMap.getOrDefault(device,
506                     BluetoothHearingAid.HI_SYNC_ID_INVALID);
507             if (deviceHiSyncId != mActiveDeviceHiSyncId) {
508                 // Give an early notification to A2DP that active device is being switched
509                 // to Hearing Aids before the Audio Service.
510                 final A2dpService a2dpService = mFactory.getA2dpService();
511                 if (a2dpService != null) {
512                     if (DBG) {
513                         Log.d(TAG, "earlyNotifyHearingAidActive for " + device);
514                     }
515                     a2dpService.earlyNotifyHearingAidActive();
516                 }
517                 mActiveDeviceHiSyncId = deviceHiSyncId;
518                 reportActiveDevice(device);
519             }
520         }
521         return true;
522     }
523 
524     /**
525      * Get the connected physical Hearing Aid devices that are active
526      *
527      * @return the list of active devices. The first element is the left active
528      * device; the second element is the right active device. If either or both side
529      * is not active, it will be null on that position
530      */
getActiveDevices()531     public List<BluetoothDevice> getActiveDevices() {
532         if (DBG) {
533             Log.d(TAG, "getActiveDevices");
534         }
535         ArrayList<BluetoothDevice> activeDevices = new ArrayList<>();
536         activeDevices.add(null);
537         activeDevices.add(null);
538         synchronized (mStateMachines) {
539             if (mActiveDeviceHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
540                 return activeDevices;
541             }
542             for (BluetoothDevice device : mDeviceHiSyncIdMap.keySet()) {
543                 if (getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
544                     continue;
545                 }
546                 if (mDeviceHiSyncIdMap.get(device) == mActiveDeviceHiSyncId) {
547                     int deviceSide = getCapabilities(device) & 1;
548                     if (deviceSide == BluetoothHearingAid.SIDE_RIGHT) {
549                         activeDevices.set(1, device);
550                     } else {
551                         activeDevices.set(0, device);
552                     }
553                 }
554             }
555         }
556         return activeDevices;
557     }
558 
messageFromNative(HearingAidStackEvent stackEvent)559     void messageFromNative(HearingAidStackEvent stackEvent) {
560         Objects.requireNonNull(stackEvent.device,
561                 "Device should never be null, event: " + stackEvent);
562 
563         if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_DEVICE_AVAILABLE) {
564             BluetoothDevice device = stackEvent.device;
565             int capabilities = stackEvent.valueInt1;
566             long hiSyncId = stackEvent.valueLong2;
567             if (DBG) {
568                 Log.d(TAG, "Device available: device=" + device + " capabilities="
569                         + capabilities + " hiSyncId=" + hiSyncId);
570             }
571             mDeviceCapabilitiesMap.put(device, capabilities);
572             mDeviceHiSyncIdMap.put(device, hiSyncId);
573             return;
574         }
575 
576         synchronized (mStateMachines) {
577             BluetoothDevice device = stackEvent.device;
578             HearingAidStateMachine sm = mStateMachines.get(device);
579             if (sm == null) {
580                 if (stackEvent.type == HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
581                     switch (stackEvent.valueInt1) {
582                         case HearingAidStackEvent.CONNECTION_STATE_CONNECTED:
583                         case HearingAidStackEvent.CONNECTION_STATE_CONNECTING:
584                             sm = getOrCreateStateMachine(device);
585                             break;
586                         default:
587                             break;
588                     }
589                 }
590             }
591             if (sm == null) {
592                 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
593                 return;
594             }
595             sm.sendMessage(HearingAidStateMachine.STACK_EVENT, stackEvent);
596         }
597     }
598 
getOrCreateStateMachine(BluetoothDevice device)599     private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
600         if (device == null) {
601             Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
602             return null;
603         }
604         synchronized (mStateMachines) {
605             HearingAidStateMachine sm = mStateMachines.get(device);
606             if (sm != null) {
607                 return sm;
608             }
609             // Limit the maximum number of state machines to avoid DoS attack
610             if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) {
611                 Log.e(TAG, "Maximum number of HearingAid state machines reached: "
612                         + MAX_HEARING_AID_STATE_MACHINES);
613                 return null;
614             }
615             if (DBG) {
616                 Log.d(TAG, "Creating a new state machine for " + device);
617             }
618             sm = HearingAidStateMachine.make(device, this,
619                     mHearingAidNativeInterface, mStateMachinesThread.getLooper());
620             mStateMachines.put(device, sm);
621             return sm;
622         }
623     }
624 
625     /**
626      * Report the active device change to the active device manager and the media framework.
627      * @param device the new active device; or null if no active device
628      */
reportActiveDevice(BluetoothDevice device)629     private void reportActiveDevice(BluetoothDevice device) {
630         if (DBG) {
631             Log.d(TAG, "reportActiveDevice(" + device + ")");
632         }
633 
634         StatsLog.write(StatsLog.BLUETOOTH_ACTIVE_DEVICE_CHANGED, BluetoothProfile.HEARING_AID,
635                 mAdapterService.obfuscateAddress(device));
636 
637         Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
638         intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
639         intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
640                 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
641         sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
642 
643         if (device == null) {
644             if (DBG) {
645                 Log.d(TAG, "Set Hearing Aid audio to disconnected");
646             }
647             boolean suppressNoisyIntent =
648                     (getConnectionState(mPreviousAudioDevice) == BluetoothProfile.STATE_CONNECTED);
649             mAudioManager.setBluetoothHearingAidDeviceConnectionState(
650                     mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED,
651                     suppressNoisyIntent, 0);
652             mPreviousAudioDevice = null;
653         } else {
654             if (DBG) {
655                 Log.d(TAG, "Set Hearing Aid audio to connected");
656             }
657             if (mPreviousAudioDevice != null) {
658                 mAudioManager.setBluetoothHearingAidDeviceConnectionState(
659                         mPreviousAudioDevice, BluetoothProfile.STATE_DISCONNECTED,
660                         true, 0);
661             }
662             mAudioManager.setBluetoothHearingAidDeviceConnectionState(
663                     device, BluetoothProfile.STATE_CONNECTED,
664                     true, 0);
665             mPreviousAudioDevice = device;
666         }
667     }
668 
669     // Remove state machine if the bonding for a device is removed
670     private class BondStateChangedReceiver extends BroadcastReceiver {
671         @Override
onReceive(Context context, Intent intent)672         public void onReceive(Context context, Intent intent) {
673             if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
674                 return;
675             }
676             int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
677                                            BluetoothDevice.ERROR);
678             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
679             Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
680             bondStateChanged(device, state);
681         }
682     }
683 
684     /**
685      * Process a change in the bonding state for a device.
686      *
687      * @param device the device whose bonding state has changed
688      * @param bondState the new bond state for the device. Possible values are:
689      * {@link BluetoothDevice#BOND_NONE},
690      * {@link BluetoothDevice#BOND_BONDING},
691      * {@link BluetoothDevice#BOND_BONDED}.
692      */
693     @VisibleForTesting
bondStateChanged(BluetoothDevice device, int bondState)694     void bondStateChanged(BluetoothDevice device, int bondState) {
695         if (DBG) {
696             Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
697         }
698         // Remove state machine if the bonding for a device is removed
699         if (bondState != BluetoothDevice.BOND_NONE) {
700             return;
701         }
702         mDeviceHiSyncIdMap.remove(device);
703         synchronized (mStateMachines) {
704             HearingAidStateMachine sm = mStateMachines.get(device);
705             if (sm == null) {
706                 return;
707             }
708             if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
709                 return;
710             }
711             removeStateMachine(device);
712         }
713     }
714 
removeStateMachine(BluetoothDevice device)715     private void removeStateMachine(BluetoothDevice device) {
716         synchronized (mStateMachines) {
717             HearingAidStateMachine sm = mStateMachines.get(device);
718             if (sm == null) {
719                 Log.w(TAG, "removeStateMachine: device " + device
720                         + " does not have a state machine");
721                 return;
722             }
723             Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
724             sm.doQuit();
725             sm.cleanup();
726             mStateMachines.remove(device);
727         }
728     }
729 
getConnectedPeerDevices(long hiSyncId)730     private List<BluetoothDevice> getConnectedPeerDevices(long hiSyncId) {
731         List<BluetoothDevice> result = new ArrayList<>();
732         for (BluetoothDevice peerDevice : getConnectedDevices()) {
733             if (getHiSyncId(peerDevice) == hiSyncId) {
734                 result.add(peerDevice);
735             }
736         }
737         return result;
738     }
739 
740     @VisibleForTesting
connectionStateChanged(BluetoothDevice device, int fromState, int toState)741     synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
742                                                      int toState) {
743         if ((device == null) || (fromState == toState)) {
744             Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
745                     + " fromState=" + fromState + " toState=" + toState);
746             return;
747         }
748         if (toState == BluetoothProfile.STATE_CONNECTED) {
749             long myHiSyncId = getHiSyncId(device);
750             if (myHiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID
751                     || getConnectedPeerDevices(myHiSyncId).size() == 1) {
752                 // Log hearing aid connection event if we are the first device in a set
753                 // Or when the hiSyncId has not been found
754                 MetricsLogger.logProfileConnectionEvent(
755                         BluetoothMetricsProto.ProfileId.HEARING_AID);
756             }
757             if (!mHiSyncIdConnectedMap.getOrDefault(myHiSyncId, false)) {
758                 setActiveDevice(device);
759                 mHiSyncIdConnectedMap.put(myHiSyncId, true);
760             }
761         }
762         if (fromState == BluetoothProfile.STATE_CONNECTED && getConnectedDevices().isEmpty()) {
763             setActiveDevice(null);
764             long myHiSyncId = getHiSyncId(device);
765             mHiSyncIdConnectedMap.put(myHiSyncId, false);
766         }
767         // Check if the device is disconnected - if unbond, remove the state machine
768         if (toState == BluetoothProfile.STATE_DISCONNECTED) {
769             int bondState = mAdapterService.getBondState(device);
770             if (bondState == BluetoothDevice.BOND_NONE) {
771                 if (DBG) {
772                     Log.d(TAG, device + " is unbond. Remove state machine");
773                 }
774                 removeStateMachine(device);
775             }
776         }
777     }
778 
779     private class ConnectionStateChangedReceiver extends BroadcastReceiver {
780         @Override
onReceive(Context context, Intent intent)781         public void onReceive(Context context, Intent intent) {
782             if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
783                 return;
784             }
785             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
786             int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
787             int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
788             connectionStateChanged(device, fromState, toState);
789         }
790     }
791 
792     /**
793      * Binder object: must be a static class or memory leak may occur
794      */
795     @VisibleForTesting
796     static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub
797             implements IProfileServiceBinder {
798         private HearingAidService mService;
799 
getService()800         private HearingAidService getService() {
801             if (!Utils.checkCaller()) {
802                 Log.w(TAG, "HearingAid call not allowed for non-active user");
803                 return null;
804             }
805 
806             if (mService != null && mService.isAvailable()) {
807                 return mService;
808             }
809             return null;
810         }
811 
BluetoothHearingAidBinder(HearingAidService svc)812         BluetoothHearingAidBinder(HearingAidService svc) {
813             mService = svc;
814         }
815 
816         @Override
cleanup()817         public void cleanup() {
818             mService = null;
819         }
820 
821         @Override
connect(BluetoothDevice device)822         public boolean connect(BluetoothDevice device) {
823             HearingAidService service = getService();
824             if (service == null) {
825                 return false;
826             }
827             return service.connect(device);
828         }
829 
830         @Override
disconnect(BluetoothDevice device)831         public boolean disconnect(BluetoothDevice device) {
832             HearingAidService service = getService();
833             if (service == null) {
834                 return false;
835             }
836             return service.disconnect(device);
837         }
838 
839         @Override
getConnectedDevices()840         public List<BluetoothDevice> getConnectedDevices() {
841             HearingAidService service = getService();
842             if (service == null) {
843                 return new ArrayList<>();
844             }
845             return service.getConnectedDevices();
846         }
847 
848         @Override
getDevicesMatchingConnectionStates(int[] states)849         public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
850             HearingAidService service = getService();
851             if (service == null) {
852                 return new ArrayList<>();
853             }
854             return service.getDevicesMatchingConnectionStates(states);
855         }
856 
857         @Override
getConnectionState(BluetoothDevice device)858         public int getConnectionState(BluetoothDevice device) {
859             HearingAidService service = getService();
860             if (service == null) {
861                 return BluetoothProfile.STATE_DISCONNECTED;
862             }
863             return service.getConnectionState(device);
864         }
865 
866         @Override
setActiveDevice(BluetoothDevice device)867         public boolean setActiveDevice(BluetoothDevice device) {
868             HearingAidService service = getService();
869             if (service == null) {
870                 return false;
871             }
872             return service.setActiveDevice(device);
873         }
874 
875         @Override
getActiveDevices()876         public List<BluetoothDevice> getActiveDevices() {
877             HearingAidService service = getService();
878             if (service == null) {
879                 return new ArrayList<>();
880             }
881             return service.getActiveDevices();
882         }
883 
884         @Override
setPriority(BluetoothDevice device, int priority)885         public boolean setPriority(BluetoothDevice device, int priority) {
886             HearingAidService service = getService();
887             if (service == null) {
888                 return false;
889             }
890             return service.setPriority(device, priority);
891         }
892 
893         @Override
getPriority(BluetoothDevice device)894         public int getPriority(BluetoothDevice device) {
895             HearingAidService service = getService();
896             if (service == null) {
897                 return BluetoothProfile.PRIORITY_UNDEFINED;
898             }
899             return service.getPriority(device);
900         }
901 
902         @Override
setVolume(int volume)903         public void setVolume(int volume) {
904             HearingAidService service = getService();
905             if (service == null) {
906                 return;
907             }
908             service.setVolume(volume);
909         }
910 
911         @Override
adjustVolume(int direction)912         public void adjustVolume(int direction) {
913             // TODO: Remove me?
914         }
915 
916         @Override
getVolume()917         public int getVolume() {
918             // TODO: Remove me?
919             return 0;
920         }
921 
922         @Override
getHiSyncId(BluetoothDevice device)923         public long getHiSyncId(BluetoothDevice device) {
924             HearingAidService service = getService();
925             if (service == null) {
926                 return BluetoothHearingAid.HI_SYNC_ID_INVALID;
927             }
928             return service.getHiSyncId(device);
929         }
930 
931         @Override
getDeviceSide(BluetoothDevice device)932         public int getDeviceSide(BluetoothDevice device) {
933             HearingAidService service = getService();
934             if (service == null) {
935                 return BluetoothHearingAid.SIDE_RIGHT;
936             }
937             return service.getCapabilities(device) & 1;
938         }
939 
940         @Override
getDeviceMode(BluetoothDevice device)941         public int getDeviceMode(BluetoothDevice device) {
942             HearingAidService service = getService();
943             if (service == null) {
944                 return BluetoothHearingAid.MODE_BINAURAL;
945             }
946             return service.getCapabilities(device) >> 1 & 1;
947         }
948     }
949 
950     @Override
dump(StringBuilder sb)951     public void dump(StringBuilder sb) {
952         super.dump(sb);
953         for (HearingAidStateMachine sm : mStateMachines.values()) {
954             sm.dump(sb);
955         }
956     }
957 }
958