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