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 android.bluetooth;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SdkConstant;
24 import android.annotation.SdkConstant.SdkConstantType;
25 import android.annotation.SystemApi;
26 import android.compat.annotation.UnsupportedAppUsage;
27 import android.content.Context;
28 import android.os.Binder;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.Log;
32 
33 import java.util.ArrayList;
34 import java.util.List;
35 
36 /**
37  * This class provides the public APIs to control the Hearing Aid profile.
38  *
39  * <p>BluetoothHearingAid is a proxy object for controlling the Bluetooth Hearing Aid
40  * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
41  * the BluetoothHearingAid proxy object.
42  *
43  * <p> Android only supports one set of connected Bluetooth Hearing Aid device at a time. Each
44  * method is protected with its appropriate permission.
45  */
46 public final class BluetoothHearingAid implements BluetoothProfile {
47     private static final String TAG = "BluetoothHearingAid";
48     private static final boolean DBG = true;
49     private static final boolean VDBG = false;
50 
51     /**
52      * Intent used to broadcast the change in connection state of the Hearing Aid
53      * profile. Please note that in the binaural case, there will be two different LE devices for
54      * the left and right side and each device will have their own connection state changes.S
55      *
56      * <p>This intent will have 3 extras:
57      * <ul>
58      * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
59      * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
60      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
61      * </ul>
62      *
63      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
64      * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
65      * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
66      *
67      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
68      * receive.
69      */
70     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
71     public static final String ACTION_CONNECTION_STATE_CHANGED =
72             "android.bluetooth.hearingaid.profile.action.CONNECTION_STATE_CHANGED";
73 
74     /**
75      * Intent used to broadcast the selection of a connected device as active.
76      *
77      * <p>This intent will have one extra:
78      * <ul>
79      * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
80      * be null if no device is active. </li>
81      * </ul>
82      *
83      * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
84      * receive.
85      *
86      * @hide
87      */
88     @UnsupportedAppUsage
89     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
90     public static final String ACTION_ACTIVE_DEVICE_CHANGED =
91             "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED";
92 
93     /**
94      * This device represents Left Hearing Aid.
95      *
96      * @hide
97      */
98     public static final int SIDE_LEFT = IBluetoothHearingAid.SIDE_LEFT;
99 
100     /**
101      * This device represents Right Hearing Aid.
102      *
103      * @hide
104      */
105     public static final int SIDE_RIGHT = IBluetoothHearingAid.SIDE_RIGHT;
106 
107     /**
108      * This device is Monaural.
109      *
110      * @hide
111      */
112     public static final int MODE_MONAURAL = IBluetoothHearingAid.MODE_MONAURAL;
113 
114     /**
115      * This device is Binaural (should receive only left or right audio).
116      *
117      * @hide
118      */
119     public static final int MODE_BINAURAL = IBluetoothHearingAid.MODE_BINAURAL;
120 
121     /**
122      * Indicates the HiSyncID could not be read and is unavailable.
123      *
124      * @hide
125      */
126     public static final long HI_SYNC_ID_INVALID = IBluetoothHearingAid.HI_SYNC_ID_INVALID;
127 
128     private BluetoothAdapter mAdapter;
129     private final BluetoothProfileConnector<IBluetoothHearingAid> mProfileConnector =
130             new BluetoothProfileConnector(this, BluetoothProfile.HEARING_AID,
131                     "BluetoothHearingAid", IBluetoothHearingAid.class.getName()) {
132                 @Override
133                 public IBluetoothHearingAid getServiceInterface(IBinder service) {
134                     return IBluetoothHearingAid.Stub.asInterface(Binder.allowBlocking(service));
135                 }
136     };
137 
138     /**
139      * Create a BluetoothHearingAid proxy object for interacting with the local
140      * Bluetooth Hearing Aid service.
141      */
BluetoothHearingAid(Context context, ServiceListener listener)142     /*package*/ BluetoothHearingAid(Context context, ServiceListener listener) {
143         mAdapter = BluetoothAdapter.getDefaultAdapter();
144         mProfileConnector.connect(context, listener);
145     }
146 
close()147     /*package*/ void close() {
148         mProfileConnector.disconnect();
149     }
150 
getService()151     private IBluetoothHearingAid getService() {
152         return mProfileConnector.getService();
153     }
154 
155     /**
156      * Initiate connection to a profile of the remote bluetooth device.
157      *
158      * <p> This API returns false in scenarios like the profile on the
159      * device is already connected or Bluetooth is not turned on.
160      * When this API returns true, it is guaranteed that
161      * connection state intent for the profile will be broadcasted with
162      * the state. Users can get the connection state of the profile
163      * from this intent.
164      *
165      * @param device Remote Bluetooth Device
166      * @return false on immediate error, true otherwise
167      * @hide
168      */
169     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
connect(BluetoothDevice device)170     public boolean connect(BluetoothDevice device) {
171         if (DBG) log("connect(" + device + ")");
172         final IBluetoothHearingAid service = getService();
173         try {
174             if (service != null && isEnabled() && isValidDevice(device)) {
175                 return service.connect(device);
176             }
177             if (service == null) Log.w(TAG, "Proxy not attached to service");
178             return false;
179         } catch (RemoteException e) {
180             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
181             return false;
182         }
183     }
184 
185     /**
186      * Initiate disconnection from a profile
187      *
188      * <p> This API will return false in scenarios like the profile on the
189      * Bluetooth device is not in connected state etc. When this API returns,
190      * true, it is guaranteed that the connection state change
191      * intent will be broadcasted with the state. Users can get the
192      * disconnection state of the profile from this intent.
193      *
194      * <p> If the disconnection is initiated by a remote device, the state
195      * will transition from {@link #STATE_CONNECTED} to
196      * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the
197      * host (local) device the state will transition from
198      * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to
199      * state {@link #STATE_DISCONNECTED}. The transition to
200      * {@link #STATE_DISCONNECTING} can be used to distinguish between the
201      * two scenarios.
202      *
203      * @param device Remote Bluetooth Device
204      * @return false on immediate error, true otherwise
205      * @hide
206      */
207     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
disconnect(BluetoothDevice device)208     public boolean disconnect(BluetoothDevice device) {
209         if (DBG) log("disconnect(" + device + ")");
210         final IBluetoothHearingAid service = getService();
211         try {
212             if (service != null && isEnabled() && isValidDevice(device)) {
213                 return service.disconnect(device);
214             }
215             if (service == null) Log.w(TAG, "Proxy not attached to service");
216             return false;
217         } catch (RemoteException e) {
218             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
219             return false;
220         }
221     }
222 
223     /**
224      * {@inheritDoc}
225      */
226     @Override
getConnectedDevices()227     public @NonNull List<BluetoothDevice> getConnectedDevices() {
228         if (VDBG) log("getConnectedDevices()");
229         final IBluetoothHearingAid service = getService();
230         try {
231             if (service != null && isEnabled()) {
232                 return service.getConnectedDevices();
233             }
234             if (service == null) Log.w(TAG, "Proxy not attached to service");
235             return new ArrayList<BluetoothDevice>();
236         } catch (RemoteException e) {
237             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
238             return new ArrayList<BluetoothDevice>();
239         }
240     }
241 
242     /**
243      * {@inheritDoc}
244      */
245     @Override
getDevicesMatchingConnectionStates( @onNull int[] states)246     public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
247     @NonNull int[] states) {
248         if (VDBG) log("getDevicesMatchingStates()");
249         final IBluetoothHearingAid service = getService();
250         try {
251             if (service != null && isEnabled()) {
252                 return service.getDevicesMatchingConnectionStates(states);
253             }
254             if (service == null) Log.w(TAG, "Proxy not attached to service");
255             return new ArrayList<BluetoothDevice>();
256         } catch (RemoteException e) {
257             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
258             return new ArrayList<BluetoothDevice>();
259         }
260     }
261 
262     /**
263      * {@inheritDoc}
264      */
265     @Override
getConnectionState( @onNull BluetoothDevice device)266     public @BluetoothProfile.BtProfileState int getConnectionState(
267     @NonNull BluetoothDevice device) {
268         if (VDBG) log("getState(" + device + ")");
269         final IBluetoothHearingAid service = getService();
270         try {
271             if (service != null && isEnabled()
272                     && isValidDevice(device)) {
273                 return service.getConnectionState(device);
274             }
275             if (service == null) Log.w(TAG, "Proxy not attached to service");
276             return BluetoothProfile.STATE_DISCONNECTED;
277         } catch (RemoteException e) {
278             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
279             return BluetoothProfile.STATE_DISCONNECTED;
280         }
281     }
282 
283     /**
284      * Select a connected device as active.
285      *
286      * The active device selection is per profile. An active device's
287      * purpose is profile-specific. For example, Hearing Aid audio
288      * streaming is to the active Hearing Aid device. If a remote device
289      * is not connected, it cannot be selected as active.
290      *
291      * <p> This API returns false in scenarios like the profile on the
292      * device is not connected or Bluetooth is not turned on.
293      * When this API returns true, it is guaranteed that the
294      * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
295      * with the active device.
296      *
297      * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
298      * permission.
299      *
300      * @param device the remote Bluetooth device. Could be null to clear
301      * the active device and stop streaming audio to a Bluetooth device.
302      * @return false on immediate error, true otherwise
303      * @hide
304      */
305     @UnsupportedAppUsage
setActiveDevice(@ullable BluetoothDevice device)306     public boolean setActiveDevice(@Nullable BluetoothDevice device) {
307         if (DBG) log("setActiveDevice(" + device + ")");
308         final IBluetoothHearingAid service = getService();
309         try {
310             if (service != null && isEnabled()
311                     && ((device == null) || isValidDevice(device))) {
312                 service.setActiveDevice(device);
313                 return true;
314             }
315             if (service == null) Log.w(TAG, "Proxy not attached to service");
316             return false;
317         } catch (RemoteException e) {
318             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
319             return false;
320         }
321     }
322 
323     /**
324      * Get the connected physical Hearing Aid devices that are active
325      *
326      * @return the list of active devices. The first element is the left active
327      * device; the second element is the right active device. If either or both side
328      * is not active, it will be null on that position. Returns empty list on error.
329      * @hide
330      */
331     @UnsupportedAppUsage
332     @RequiresPermission(Manifest.permission.BLUETOOTH)
getActiveDevices()333     public @NonNull List<BluetoothDevice> getActiveDevices() {
334         if (VDBG) log("getActiveDevices()");
335         final IBluetoothHearingAid service = getService();
336         try {
337             if (service != null && isEnabled()) {
338                 return service.getActiveDevices();
339             }
340             if (service == null) Log.w(TAG, "Proxy not attached to service");
341             return new ArrayList<>();
342         } catch (RemoteException e) {
343             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
344             return new ArrayList<>();
345         }
346     }
347 
348     /**
349      * Set priority of the profile
350      *
351      * <p> The device should already be paired.
352      * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF},
353      *
354      * @param device Paired bluetooth device
355      * @param priority
356      * @return true if priority is set, false on error
357      * @hide
358      */
359     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
setPriority(BluetoothDevice device, int priority)360     public boolean setPriority(BluetoothDevice device, int priority) {
361         if (DBG) log("setPriority(" + device + ", " + priority + ")");
362         return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority));
363     }
364 
365     /**
366      * Set connection policy of the profile
367      *
368      * <p> The device should already be paired.
369      * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
370      * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
371      *
372      * @param device Paired bluetooth device
373      * @param connectionPolicy is the connection policy to set to for this profile
374      * @return true if connectionPolicy is set, false on error
375      * @hide
376      */
377     @SystemApi
378     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
setConnectionPolicy(@onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)379     public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
380             @ConnectionPolicy int connectionPolicy) {
381         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
382         verifyDeviceNotNull(device, "setConnectionPolicy");
383         final IBluetoothHearingAid service = getService();
384         try {
385             if (service != null && isEnabled()
386                     && isValidDevice(device)) {
387                 if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
388                         && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
389                     return false;
390                 }
391                 return service.setConnectionPolicy(device, connectionPolicy);
392             }
393             if (service == null) Log.w(TAG, "Proxy not attached to service");
394             return false;
395         } catch (RemoteException e) {
396             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
397             return false;
398         }
399     }
400 
401     /**
402      * Get the priority of the profile.
403      *
404      * <p> The priority can be any of:
405      * {@link #PRIORITY_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
406      *
407      * @param device Bluetooth device
408      * @return priority of the device
409      * @hide
410      */
411     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getPriority(BluetoothDevice device)412     public int getPriority(BluetoothDevice device) {
413         if (VDBG) log("getPriority(" + device + ")");
414         return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
415     }
416 
417     /**
418      * Get the connection policy of the profile.
419      *
420      * <p> The connection policy can be any of:
421      * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
422      * {@link #CONNECTION_POLICY_UNKNOWN}
423      *
424      * @param device Bluetooth device
425      * @return connection policy of the device
426      * @hide
427      */
428     @SystemApi
429     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getConnectionPolicy(@onNull BluetoothDevice device)430     public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
431         if (VDBG) log("getConnectionPolicy(" + device + ")");
432         verifyDeviceNotNull(device, "getConnectionPolicy");
433         final IBluetoothHearingAid service = getService();
434         try {
435             if (service != null && isEnabled()
436                     && isValidDevice(device)) {
437                 return service.getConnectionPolicy(device);
438             }
439             if (service == null) Log.w(TAG, "Proxy not attached to service");
440             return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
441         } catch (RemoteException e) {
442             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
443             return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
444         }
445     }
446 
447     /**
448      * Helper for converting a state to a string.
449      *
450      * For debug use only - strings are not internationalized.
451      *
452      * @hide
453      */
stateToString(int state)454     public static String stateToString(int state) {
455         switch (state) {
456             case STATE_DISCONNECTED:
457                 return "disconnected";
458             case STATE_CONNECTING:
459                 return "connecting";
460             case STATE_CONNECTED:
461                 return "connected";
462             case STATE_DISCONNECTING:
463                 return "disconnecting";
464             default:
465                 return "<unknown state " + state + ">";
466         }
467     }
468 
469     /**
470      * Tells remote device to set an absolute volume.
471      *
472      * @param volume Absolute volume to be set on remote
473      * @hide
474      */
setVolume(int volume)475     public void setVolume(int volume) {
476         if (DBG) Log.d(TAG, "setVolume(" + volume + ")");
477 
478         final IBluetoothHearingAid service = getService();
479         try {
480             if (service == null) {
481                 Log.w(TAG, "Proxy not attached to service");
482                 return;
483             }
484 
485             if (!isEnabled()) return;
486 
487             service.setVolume(volume);
488         } catch (RemoteException e) {
489             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
490         }
491     }
492 
493     /**
494      * Get the HiSyncId (unique hearing aid device identifier) of the device.
495      *
496      * <a href=https://source.android.com/devices/bluetooth/asha#hisyncid>HiSyncId documentation
497      * can be found here</a>
498      *
499      * @param device Bluetooth device
500      * @return the HiSyncId of the device
501      * @hide
502      */
503     @SystemApi
504     @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
getHiSyncId(@onNull BluetoothDevice device)505     public long getHiSyncId(@NonNull BluetoothDevice device) {
506         if (VDBG) {
507             log("getHiSyncId(" + device + ")");
508         }
509         verifyDeviceNotNull(device, "getConnectionPolicy");
510         final IBluetoothHearingAid service = getService();
511         try {
512             if (service == null) {
513                 Log.w(TAG, "Proxy not attached to service");
514                 return HI_SYNC_ID_INVALID;
515             }
516 
517             if (!isEnabled() || !isValidDevice(device)) return HI_SYNC_ID_INVALID;
518 
519             return service.getHiSyncId(device);
520         } catch (RemoteException e) {
521             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
522             return HI_SYNC_ID_INVALID;
523         }
524     }
525 
526     /**
527      * Get the side of the device.
528      *
529      * @param device Bluetooth device.
530      * @return SIDE_LEFT or SIDE_RIGHT
531      * @hide
532      */
533     @RequiresPermission(Manifest.permission.BLUETOOTH)
getDeviceSide(BluetoothDevice device)534     public int getDeviceSide(BluetoothDevice device) {
535         if (VDBG) {
536             log("getDeviceSide(" + device + ")");
537         }
538         final IBluetoothHearingAid service = getService();
539         try {
540             if (service != null && isEnabled()
541                     && isValidDevice(device)) {
542                 return service.getDeviceSide(device);
543             }
544             if (service == null) Log.w(TAG, "Proxy not attached to service");
545             return SIDE_LEFT;
546         } catch (RemoteException e) {
547             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
548             return SIDE_LEFT;
549         }
550     }
551 
552     /**
553      * Get the mode of the device.
554      *
555      * @param device Bluetooth device
556      * @return MODE_MONAURAL or MODE_BINAURAL
557      * @hide
558      */
559     @RequiresPermission(Manifest.permission.BLUETOOTH)
getDeviceMode(BluetoothDevice device)560     public int getDeviceMode(BluetoothDevice device) {
561         if (VDBG) {
562             log("getDeviceMode(" + device + ")");
563         }
564         final IBluetoothHearingAid service = getService();
565         try {
566             if (service != null && isEnabled()
567                     && isValidDevice(device)) {
568                 return service.getDeviceMode(device);
569             }
570             if (service == null) Log.w(TAG, "Proxy not attached to service");
571             return MODE_MONAURAL;
572         } catch (RemoteException e) {
573             Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
574             return MODE_MONAURAL;
575         }
576     }
577 
isEnabled()578     private boolean isEnabled() {
579         if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
580         return false;
581     }
582 
verifyDeviceNotNull(BluetoothDevice device, String methodName)583     private void verifyDeviceNotNull(BluetoothDevice device, String methodName) {
584         if (device == null) {
585             Log.e(TAG, methodName + ": device param is null");
586             throw new IllegalArgumentException("Device cannot be null");
587         }
588     }
589 
isValidDevice(BluetoothDevice device)590     private boolean isValidDevice(BluetoothDevice device) {
591         if (device == null) return false;
592 
593         if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
594         return false;
595     }
596 
log(String msg)597     private static void log(String msg) {
598         Log.d(TAG, msg);
599     }
600 }
601