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