HearingAidService.java revision fc72b6542b8170ae77f89dcae60be2640ce0e990
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
17package com.android.bluetooth.hearingaid;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothHearingAid;
22import android.bluetooth.BluetoothProfile;
23import android.bluetooth.BluetoothUuid;
24import android.bluetooth.IBluetoothHearingAid;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.Intent;
28import android.content.IntentFilter;
29import android.os.HandlerThread;
30import android.os.ParcelUuid;
31import android.provider.Settings;
32import android.support.annotation.VisibleForTesting;
33import android.util.Log;
34
35import com.android.bluetooth.Utils;
36import com.android.bluetooth.btservice.AdapterService;
37import com.android.bluetooth.btservice.ProfileService;
38
39import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.List;
42import java.util.Map;
43import java.util.Objects;
44import java.util.Set;
45
46/**
47 * Provides Bluetooth HearingAid profile, as a service in the Bluetooth application.
48 * @hide
49 */
50public class HearingAidService extends ProfileService {
51    private static final boolean DBG = false;
52    private static final String TAG = "HearingAidService";
53
54    private static HearingAidService sHearingAidService;
55
56    private BluetoothAdapter mAdapter;
57    private AdapterService mAdapterService;
58    private HandlerThread mStateMachinesThread;
59
60    private BluetoothDevice mActiveDevice;
61
62    private final Map<BluetoothDevice, HearingAidStateMachine> mStateMachines =
63            new HashMap<>();
64    private final Map<BluetoothDevice, Integer> mDeviceMap = new HashMap<>();
65
66    // Upper limit of all HearingAid devices: Bonded or Connected
67    private static final int MAX_HEARING_AID_STATE_MACHINES = 10;
68
69    private BroadcastReceiver mBondStateChangedReceiver;
70    private BroadcastReceiver mConnectionStateChangedReceiver;
71
72    @Override
73    protected IProfileServiceBinder initBinder() {
74        return new BluetoothHearingAidBinder(this);
75    }
76
77    @Override
78    protected void create() {
79        if (DBG) {
80            Log.d(TAG, "create()");
81        }
82    }
83
84    @Override
85    protected boolean start() {
86        if (DBG) {
87            Log.d(TAG, "start()");
88        }
89        if (sHearingAidService != null) {
90            throw new IllegalStateException("start() called twice");
91        }
92
93        // Get BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager.
94        // None of them can be null.
95        mAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter(),
96                "BluetoothAdapter cannot be null when HearingAidService starts");
97        mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
98                "AdapterService cannot be null when HearingAidService starts");
99        // TODO: Add native interface
100
101        // Start handler thread for state machines
102        mStateMachines.clear();
103        mStateMachinesThread = new HandlerThread("HearingAidService.StateMachines");
104        mStateMachinesThread.start();
105
106        // Initialize native interface
107        // TODO: Init native interface
108
109        // Setup broadcast receivers
110        IntentFilter filter = new IntentFilter();
111        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
112        mBondStateChangedReceiver = new BondStateChangedReceiver();
113        registerReceiver(mBondStateChangedReceiver, filter);
114        filter = new IntentFilter();
115        filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
116        mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
117        registerReceiver(mConnectionStateChangedReceiver, filter);
118
119        // Mark service as started
120        setHearingAidService(this);
121
122        // Clear active device
123        setActiveDevice(null);
124
125        return true;
126    }
127
128    @Override
129    protected boolean stop() {
130        if (DBG) {
131            Log.d(TAG, "stop()");
132        }
133        if (sHearingAidService == null) {
134            Log.w(TAG, "stop() called before start()");
135            return true;
136        }
137
138        // Clear active device
139        setActiveDevice(null);
140
141        // Mark service as stopped
142        setHearingAidService(null);
143
144        // Unregister broadcast receivers
145        unregisterReceiver(mBondStateChangedReceiver);
146        mBondStateChangedReceiver = null;
147        unregisterReceiver(mConnectionStateChangedReceiver);
148        mConnectionStateChangedReceiver = null;
149
150        // Cleanup native interface
151        // TODO: Cleanup native interface
152
153        // Destroy state machines and stop handler thread
154        synchronized (mStateMachines) {
155            for (HearingAidStateMachine sm : mStateMachines.values()) {
156                sm.doQuit();
157                sm.cleanup();
158            }
159            mStateMachines.clear();
160        }
161
162        if (mStateMachinesThread != null) {
163            mStateMachinesThread.quitSafely();
164            mStateMachinesThread = null;
165        }
166
167        // Clear BluetoothAdapter, AdapterService, HearingAidNativeInterface
168        // TODO: Set native interface to null
169        mAdapterService = null;
170        mAdapter = null;
171
172        return true;
173    }
174
175    @Override
176    protected void cleanup() {
177        if (DBG) {
178            Log.d(TAG, "cleanup()");
179        }
180    }
181
182    /**
183     * Get the HearingAidService instance
184     * @return HearingAidService instance
185     */
186    public static synchronized HearingAidService getHearingAidService() {
187        if (sHearingAidService == null) {
188            Log.w(TAG, "getHearingAidService(): service is NULL");
189            return null;
190        }
191
192        if (!sHearingAidService.isAvailable()) {
193            Log.w(TAG, "getHearingAidService(): service is not available");
194            return null;
195        }
196        return sHearingAidService;
197    }
198
199    private static synchronized void setHearingAidService(HearingAidService instance) {
200        if (DBG) {
201            Log.d(TAG, "setHearingAidService(): set to: " + instance);
202        }
203        sHearingAidService = instance;
204    }
205
206    boolean connect(BluetoothDevice device) {
207        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
208        if (DBG) {
209            Log.d(TAG, "connect(): " + device);
210        }
211
212        if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) {
213            return false;
214        }
215        ParcelUuid[] featureUuids = device.getUuids();
216        if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
217            Log.e(TAG, "Cannot connect to " + device + " : Remote does not have HearingAid UUID");
218            return false;
219        }
220
221        synchronized (mStateMachines) {
222            HearingAidStateMachine smConnect = getOrCreateStateMachine(device);
223            if (smConnect == null) {
224                Log.e(TAG, "Cannot connect to " + device + " : no state machine");
225                return false;
226            }
227            smConnect.sendMessage(HearingAidStateMachine.CONNECT);
228            return true;
229        }
230    }
231
232    boolean disconnect(BluetoothDevice device) {
233        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
234        if (DBG) {
235            Log.d(TAG, "disconnect(): " + device);
236        }
237
238        int hiSyncId = mDeviceMap.get(device);
239        for (BluetoothDevice storedDevice : mDeviceMap.keySet()) {
240            if (mDeviceMap.get(storedDevice) != hiSyncId) {
241                continue;
242            }
243            synchronized (mStateMachines) {
244                HearingAidStateMachine sm = mStateMachines.get(device);
245                if (sm == null) {
246                    Log.e(TAG, "Ignored disconnect request for " + device + " : no state machine");
247                    continue;
248                }
249                sm.sendMessage(HearingAidStateMachine.DISCONNECT);
250            }
251        }
252        return true;
253    }
254
255    List<BluetoothDevice> getConnectedDevices() {
256        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
257        synchronized (mStateMachines) {
258            List<BluetoothDevice> devices = new ArrayList<>();
259            for (HearingAidStateMachine sm : mStateMachines.values()) {
260                if (sm.isConnected()) {
261                    devices.add(sm.getDevice());
262                }
263            }
264            return devices;
265        }
266    }
267
268    /**
269     * Check whether can connect to a peer device.
270     * The check considers a number of factors during the evaluation.
271     *
272     * @param device the peer device to connect to
273     * @return true if connection is allowed, otherwise false
274     */
275    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
276    public boolean okToConnect(BluetoothDevice device) {
277        // Check if this is an incoming connection in Quiet mode.
278        if (mAdapterService.isQuietModeEnabled()) {
279            Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
280            return false;
281        }
282        // Check priority and accept or reject the connection
283        int priority = getPriority(device);
284        int bondState = mAdapterService.getBondState(device);
285        // Allow the connection only if the device is bonded or bonding.
286        if ((priority == BluetoothProfile.PRIORITY_UNDEFINED)
287                && (bondState == BluetoothDevice.BOND_NONE)) {
288            Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority
289                    + " bondState=" + bondState);
290            return false;
291        }
292        if (priority <= BluetoothProfile.PRIORITY_OFF) {
293            Log.e(TAG, "okToConnect: cannot connect to " + device + " : priority=" + priority);
294            return false;
295        }
296        return true;
297    }
298
299    List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
300        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
301        synchronized (mStateMachines) {
302            List<BluetoothDevice> devices = new ArrayList<>();
303            Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices();
304
305            for (BluetoothDevice device : bondedDevices) {
306                ParcelUuid[] featureUuids = device.getUuids();
307                if (!BluetoothUuid.isUuidPresent(featureUuids, BluetoothUuid.HearingAid)) {
308                    continue;
309                }
310                int connectionState = BluetoothProfile.STATE_DISCONNECTED;
311                HearingAidStateMachine sm = mStateMachines.get(device);
312                if (sm != null) {
313                    connectionState = sm.getConnectionState();
314                }
315                for (int state : states) {
316                    if (connectionState == state) {
317                        devices.add(device);
318                    }
319                }
320            }
321            return devices;
322        }
323    }
324
325    /**
326     * Get the list of devices that have state machines.
327     *
328     * @return the list of devices that have state machines
329     */
330    @VisibleForTesting
331    List<BluetoothDevice> getDevices() {
332        List<BluetoothDevice> devices = new ArrayList<>();
333        synchronized (mStateMachines) {
334            for (HearingAidStateMachine sm : mStateMachines.values()) {
335                devices.add(sm.getDevice());
336            }
337            return devices;
338        }
339    }
340
341    int getConnectionState(BluetoothDevice device) {
342        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
343        synchronized (mStateMachines) {
344            HearingAidStateMachine sm = mStateMachines.get(device);
345            if (sm == null) {
346                return BluetoothProfile.STATE_DISCONNECTED;
347            }
348            return sm.getConnectionState();
349        }
350    }
351
352    /**
353     * Set the active device.
354     *
355     * @param device the active device
356     * @return true on success, otherwise false
357     */
358    public synchronized boolean setActiveDevice(BluetoothDevice device) {
359        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission");
360
361        return false;
362    }
363
364    /**
365     * Get the active device.
366     *
367     * @return the active device or null if no device is active
368     */
369    public synchronized BluetoothDevice getActiveDevice() {
370        enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");
371        synchronized (mStateMachines) {
372            return mActiveDevice;
373        }
374    }
375
376    private synchronized boolean isActiveDevice(BluetoothDevice device) {
377        synchronized (mStateMachines) {
378            return (device != null) && Objects.equals(device, mActiveDevice);
379        }
380    }
381
382    /**
383     * Set the priority of the Hearing Aid profile.
384     *
385     * @param device the remote device
386     * @param priority the priority of the profile
387     * @return true on success, otherwise false
388     */
389    public boolean setPriority(BluetoothDevice device, int priority) {
390        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
391        Settings.Global.putInt(getContentResolver(),
392                Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()), priority);
393        if (DBG) {
394            Log.d(TAG, "Saved priority " + device + " = " + priority);
395        }
396        return true;
397    }
398
399    public int getPriority(BluetoothDevice device) {
400        enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission");
401        int priority = Settings.Global.getInt(getContentResolver(),
402                Settings.Global.getBluetoothHearingAidPriorityKey(device.getAddress()),
403                BluetoothProfile.PRIORITY_UNDEFINED);
404        return priority;
405    }
406
407    private HearingAidStateMachine getOrCreateStateMachine(BluetoothDevice device) {
408        if (device == null) {
409            Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
410            return null;
411        }
412        synchronized (mStateMachines) {
413            HearingAidStateMachine sm = mStateMachines.get(device);
414            if (sm != null) {
415                return sm;
416            }
417            // Limit the maximum number of state machines to avoid DoS attack
418            if (mStateMachines.size() >= MAX_HEARING_AID_STATE_MACHINES) {
419                Log.e(TAG, "Maximum number of HearingAid state machines reached: "
420                        + MAX_HEARING_AID_STATE_MACHINES);
421                return null;
422            }
423            if (DBG) {
424                Log.d(TAG, "Creating a new state machine for " + device);
425            }
426            sm = HearingAidStateMachine.make(device, this, mStateMachinesThread.getLooper());
427            mStateMachines.put(device, sm);
428            return sm;
429        }
430    }
431
432    private void broadcastActiveDevice(BluetoothDevice device) {
433        if (DBG) {
434            Log.d(TAG, "broadcastActiveDevice(" + device + ")");
435        }
436
437        Intent intent = new Intent(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED);
438        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
439        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
440                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
441        sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
442    }
443
444    // Remove state machine if the bonding for a device is removed
445    private class BondStateChangedReceiver extends BroadcastReceiver {
446        @Override
447        public void onReceive(Context context, Intent intent) {
448            if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
449                return;
450            }
451            int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
452                                           BluetoothDevice.ERROR);
453            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
454            Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
455            bondStateChanged(device, state);
456        }
457    }
458
459    /**
460     * Process a change in the bonding state for a device.
461     *
462     * @param device the device whose bonding state has changed
463     * @param bondState the new bond state for the device. Possible values are:
464     * {@link BluetoothDevice#BOND_NONE},
465     * {@link BluetoothDevice#BOND_BONDING},
466     * {@link BluetoothDevice#BOND_BONDED}.
467     */
468    @VisibleForTesting
469    void bondStateChanged(BluetoothDevice device, int bondState) {
470        if (DBG) {
471            Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
472        }
473        // Remove state machine if the bonding for a device is removed
474        if (bondState != BluetoothDevice.BOND_NONE) {
475            return;
476        }
477        synchronized (mStateMachines) {
478            HearingAidStateMachine sm = mStateMachines.get(device);
479            if (sm == null) {
480                return;
481            }
482            if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
483                return;
484            }
485            removeStateMachine(device);
486        }
487    }
488
489    private void removeStateMachine(BluetoothDevice device) {
490        synchronized (mStateMachines) {
491            HearingAidStateMachine sm = mStateMachines.get(device);
492            if (sm == null) {
493                Log.w(TAG, "removeStateMachine: device " + device
494                        + " does not have a state machine");
495                return;
496            }
497            Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
498            sm.doQuit();
499            sm.cleanup();
500            mStateMachines.remove(device);
501        }
502    }
503
504    private synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
505                                                     int toState) {
506        if ((device == null) || (fromState == toState)) {
507            return;
508        }
509        // Check if the device is disconnected - if unbond, remove the state machine
510        if (toState == BluetoothProfile.STATE_DISCONNECTED) {
511            int bondState = mAdapterService.getBondState(device);
512            if (bondState == BluetoothDevice.BOND_NONE) {
513                removeStateMachine(device);
514            }
515        }
516    }
517
518    private class ConnectionStateChangedReceiver extends BroadcastReceiver {
519        @Override
520        public void onReceive(Context context, Intent intent) {
521            if (!BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
522                return;
523            }
524            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
525            int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
526            int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
527            connectionStateChanged(device, fromState, toState);
528        }
529    }
530
531    /**
532     * Binder object: must be a static class or memory leak may occur
533     */
534    @VisibleForTesting
535    static class BluetoothHearingAidBinder extends IBluetoothHearingAid.Stub
536            implements IProfileServiceBinder {
537        private HearingAidService mService;
538
539        private HearingAidService getService() {
540            if (!Utils.checkCaller()) {
541                Log.w(TAG, "HearingAid call not allowed for non-active user");
542                return null;
543            }
544
545            if (mService != null && mService.isAvailable()) {
546                return mService;
547            }
548            return null;
549        }
550
551        @VisibleForTesting
552        HearingAidService getServiceForTesting() {
553            if (mService != null && mService.isAvailable()) {
554                return mService;
555            }
556            return null;
557        }
558
559        BluetoothHearingAidBinder(HearingAidService svc) {
560            mService = svc;
561        }
562
563        @Override
564        public void cleanup() {
565            mService = null;
566        }
567
568        @Override
569        public boolean connect(BluetoothDevice device) {
570            HearingAidService service = getService();
571            if (service == null) {
572                return false;
573            }
574            return service.connect(device);
575        }
576
577        @Override
578        public boolean disconnect(BluetoothDevice device) {
579            HearingAidService service = getService();
580            if (service == null) {
581                return false;
582            }
583            return service.disconnect(device);
584        }
585
586        @Override
587        public List<BluetoothDevice> getConnectedDevices() {
588            HearingAidService service = getService();
589            if (service == null) {
590                return new ArrayList<>(0);
591            }
592            return service.getConnectedDevices();
593        }
594
595        @Override
596        public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
597            HearingAidService service = getService();
598            if (service == null) {
599                return new ArrayList<>(0);
600            }
601            return service.getDevicesMatchingConnectionStates(states);
602        }
603
604        @Override
605        public int getConnectionState(BluetoothDevice device) {
606            HearingAidService service = getService();
607            if (service == null) {
608                return BluetoothProfile.STATE_DISCONNECTED;
609            }
610            return service.getConnectionState(device);
611        }
612
613        @Override
614        public boolean setPriority(BluetoothDevice device, int priority) {
615            HearingAidService service = getService();
616            if (service == null) {
617                return false;
618            }
619            return service.setPriority(device, priority);
620        }
621
622        @Override
623        public int getPriority(BluetoothDevice device) {
624            HearingAidService service = getService();
625            if (service == null) {
626                return BluetoothProfile.PRIORITY_UNDEFINED;
627            }
628            return service.getPriority(device);
629        }
630
631        @Override
632        public void setVolume(int volume) {
633        }
634
635        @Override
636        public void adjustVolume(int direction) {
637        }
638
639        @Override
640        public int getVolume() {
641            return 0;
642        }
643
644        @Override
645        public long getHiSyncId(BluetoothDevice device) {
646            return 0;
647        }
648
649        @Override
650        public int getDeviceSide(BluetoothDevice device) {
651            return 0;
652        }
653
654        @Override
655        public int getDeviceMode(BluetoothDevice device) {
656            return 0;
657        }
658    }
659
660    @Override
661    public void dump(StringBuilder sb) {
662        super.dump(sb);
663        ProfileService.println(sb, "mActiveDevice: " + mActiveDevice);
664        for (HearingAidStateMachine sm : mStateMachines.values()) {
665            sm.dump(sb);
666        }
667    }
668}
669