1 /*
2  * Copyright (C) 2008 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.annotation.IntDef;
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.SuppressLint;
26 import android.annotation.SystemApi;
27 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
28 import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
29 import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
30 import android.compat.annotation.UnsupportedAppUsage;
31 import android.content.AttributionSource;
32 import android.content.Context;
33 import android.os.Build;
34 import android.os.IBinder;
35 import android.os.RemoteException;
36 import android.util.Log;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * Public API for controlling the Bluetooth Headset Service. This includes both Bluetooth Headset
46  * and Handsfree (v1.5) profiles.
47  *
48  * <p>BluetoothHeadset is a proxy object for controlling the Bluetooth Headset Service via IPC.
49  *
50  * <p>Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHeadset proxy object. Use
51  * {@link BluetoothAdapter#closeProfileProxy} to close the service connection.
52  *
53  * <p>Android only supports one connected Bluetooth Headset at a time. Each method is protected with
54  * its appropriate permission.
55  */
56 public final class BluetoothHeadset implements BluetoothProfile {
57     private static final String TAG = "BluetoothHeadset";
58     private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
59     private static final boolean VDBG = false;
60 
61     /**
62      * Intent used to broadcast the change in connection state of the Headset profile.
63      *
64      * <p>This intent will have 3 extras:
65      *
66      * <ul>
67      *   <li>{@link #EXTRA_STATE} - The current state of the profile.
68      *   <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
69      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
70      * </ul>
71      *
72      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
73      * #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, {@link #STATE_CONNECTED}, {@link
74      * #STATE_DISCONNECTING}.
75      */
76     @RequiresLegacyBluetoothPermission
77     @RequiresBluetoothConnectPermission
78     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
79     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
80     public static final String ACTION_CONNECTION_STATE_CHANGED =
81             "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED";
82 
83     /**
84      * Intent used to broadcast the change in the Audio Connection state of the HFP profile.
85      *
86      * <p>This intent will have 3 extras:
87      *
88      * <ul>
89      *   <li>{@link #EXTRA_STATE} - The current state of the profile.
90      *   <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
91      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
92      * </ul>
93      *
94      * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of {@link
95      * #STATE_AUDIO_CONNECTED}, {@link #STATE_AUDIO_DISCONNECTED},
96      */
97     @RequiresLegacyBluetoothPermission
98     @RequiresBluetoothConnectPermission
99     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
100     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
101     public static final String ACTION_AUDIO_STATE_CHANGED =
102             "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED";
103 
104     /**
105      * Intent used to broadcast the selection of a connected device as active.
106      *
107      * <p>This intent will have one extra:
108      *
109      * <ul>
110      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can be null if no device
111      *       is active.
112      * </ul>
113      *
114      * @hide
115      */
116     @SystemApi
117     @RequiresLegacyBluetoothPermission
118     @RequiresBluetoothConnectPermission
119     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
120     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
121     @SuppressLint("ActionValue")
122     public static final String ACTION_ACTIVE_DEVICE_CHANGED =
123             "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
124 
125     /**
126      * Intent used to broadcast that the headset has posted a vendor-specific event.
127      *
128      * <p>This intent will have 4 extras and 1 category.
129      *
130      * <ul>
131      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote Bluetooth Device
132      *   <li>{@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD} - The vendor specific command
133      *   <li>{@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} - The AT command type which can
134      *       be one of {@link #AT_CMD_TYPE_READ}, {@link #AT_CMD_TYPE_TEST}, or {@link
135      *       #AT_CMD_TYPE_SET}, {@link #AT_CMD_TYPE_BASIC},{@link #AT_CMD_TYPE_ACTION}.
136      *   <li>{@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS} - Command arguments.
137      * </ul>
138      *
139      * <p>The category is the Company ID of the vendor defining the vendor-specific command. {@link
140      * BluetoothAssignedNumbers}
141      *
142      * <p>For example, for Plantronics specific events Category will be {@link
143      * #VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.55
144      *
145      * <p>For example, an AT+XEVENT=foo,3 will get translated into
146      *
147      * <ul>
148      *   <li>EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = +XEVENT
149      *   <li>EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE = AT_CMD_TYPE_SET
150      *   <li>EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = foo, 3
151      * </ul>
152      */
153     @RequiresLegacyBluetoothPermission
154     @RequiresBluetoothConnectPermission
155     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
156     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
157     public static final String ACTION_VENDOR_SPECIFIC_HEADSET_EVENT =
158             "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT";
159 
160     /**
161      * A String extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains
162      * the name of the vendor-specific command.
163      */
164     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD =
165             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD";
166 
167     /**
168      * An int extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains the
169      * AT command type of the vendor-specific command.
170      */
171     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE =
172             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE";
173 
174     /**
175      * AT command type READ used with {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} For
176      * example, AT+VGM?. There are no arguments for this command type.
177      */
178     public static final int AT_CMD_TYPE_READ = 0;
179 
180     /**
181      * AT command type TEST used with {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} For
182      * example, AT+VGM=?. There are no arguments for this command type.
183      */
184     public static final int AT_CMD_TYPE_TEST = 1;
185 
186     /**
187      * AT command type SET used with {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} For
188      * example, AT+VGM=<args>.
189      */
190     public static final int AT_CMD_TYPE_SET = 2;
191 
192     /**
193      * AT command type BASIC used with {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} For
194      * example, ATD. Single character commands and everything following the character are arguments.
195      */
196     public static final int AT_CMD_TYPE_BASIC = 3;
197 
198     /**
199      * AT command type ACTION used with {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} For
200      * example, AT+CHUP. There are no arguments for action commands.
201      */
202     public static final int AT_CMD_TYPE_ACTION = 4;
203 
204     /**
205      * A Parcelable String array extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT}
206      * intents that contains the arguments to the vendor-specific command.
207      */
208     public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS =
209             "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_ARGS";
210 
211     /**
212      * The intent category to be used with {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} for the
213      * companyId
214      */
215     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY =
216             "android.bluetooth.headset.intent.category.companyid";
217 
218     /** A vendor-specific command for unsolicited result code. */
219     public static final String VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID";
220 
221     /**
222      * A vendor-specific AT command
223      *
224      * @hide
225      */
226     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XAPL = "+XAPL";
227 
228     /**
229      * A vendor-specific AT command
230      *
231      * @hide
232      */
233     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV";
234 
235     /**
236      * Battery level indicator associated with {@link #VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV}
237      *
238      * @hide
239      */
240     public static final int VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1;
241 
242     /**
243      * A vendor-specific AT command
244      *
245      * @hide
246      */
247     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT = "+XEVENT";
248 
249     /**
250      * Battery level indicator associated with {@link #VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT}
251      *
252      * @hide
253      */
254     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL = "BATTERY";
255 
256     /**
257      * A vendor-specific AT command that asks for the information about device manufacturer.
258      *
259      * @hide
260      */
261     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_CGMI = "+CGMI";
262 
263     /**
264      * A vendor-specific AT command that asks for the information about the model of the device.
265      *
266      * @hide
267      */
268     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_CGMM = "+CGMM";
269 
270     /**
271      * A vendor-specific AT command that asks for the revision information, for Android we will
272      * return the OS version and build number.
273      *
274      * @hide
275      */
276     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_CGMR = "+CGMR";
277 
278     /**
279      * A vendor-specific AT command that asks for the device's serial number.
280      *
281      * @hide
282      */
283     public static final String VENDOR_SPECIFIC_HEADSET_EVENT_CGSN = "+CGSN";
284 
285     /**
286      * Headset state when SCO audio is not connected. This state can be one of {@link #EXTRA_STATE}
287      * or {@link #EXTRA_PREVIOUS_STATE} of {@link #ACTION_AUDIO_STATE_CHANGED} intent.
288      */
289     public static final int STATE_AUDIO_DISCONNECTED = 10;
290 
291     /**
292      * Headset state when SCO audio is connecting. This state can be one of {@link #EXTRA_STATE} or
293      * {@link #EXTRA_PREVIOUS_STATE} of {@link #ACTION_AUDIO_STATE_CHANGED} intent.
294      */
295     public static final int STATE_AUDIO_CONNECTING = 11;
296 
297     /**
298      * Headset state when SCO audio is connected. This state can be one of {@link #EXTRA_STATE} or
299      * {@link #EXTRA_PREVIOUS_STATE} of {@link #ACTION_AUDIO_STATE_CHANGED} intent.
300      */
301     public static final int STATE_AUDIO_CONNECTED = 12;
302 
303     /**
304      * Intent used to broadcast the headset's indicator status
305      *
306      * <p>This intent will have 3 extras:
307      *
308      * <ul>
309      *   <li>{@link #EXTRA_HF_INDICATORS_IND_ID} - The Assigned number of headset Indicator which is
310      *       supported by the headset ( as indicated by AT+BIND command in the SLC sequence) or
311      *       whose value is changed (indicated by AT+BIEV command)
312      *   <li>{@link #EXTRA_HF_INDICATORS_IND_VALUE} - Updated value of headset indicator.
313      *   <li>{@link BluetoothDevice#EXTRA_DEVICE} - Remote device.
314      * </ul>
315      *
316      * <p>{@link #EXTRA_HF_INDICATORS_IND_ID} is defined by Bluetooth SIG and each of the indicators
317      * are given an assigned number. Below shows the assigned number of Indicator added so far -
318      * Enhanced Safety - 1, Valid Values: 0 - Disabled, 1 - Enabled - Battery Level - 2, Valid
319      * Values: 0~100 - Remaining level of Battery
320      *
321      * @hide
322      */
323     @RequiresLegacyBluetoothPermission
324     @RequiresBluetoothConnectPermission
325     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
326     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
327     public static final String ACTION_HF_INDICATORS_VALUE_CHANGED =
328             "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED";
329 
330     /**
331      * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED} intents that contains the
332      * assigned number of the headset indicator as defined by Bluetooth SIG that is being sent.
333      * Value range is 0-65535 as defined in HFP 1.7
334      *
335      * @hide
336      */
337     public static final String EXTRA_HF_INDICATORS_IND_ID =
338             "android.bluetooth.headset.extra.HF_INDICATORS_IND_ID";
339 
340     /**
341      * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED} intents that contains the
342      * value of the Headset indicator that is being sent.
343      *
344      * @hide
345      */
346     public static final String EXTRA_HF_INDICATORS_IND_VALUE =
347             "android.bluetooth.headset.extra.HF_INDICATORS_IND_VALUE";
348 
349     private final BluetoothAdapter mAdapter;
350     private final AttributionSource mAttributionSource;
351 
352     private IBluetoothHeadset mService;
353 
354     /** Create a BluetoothHeadset proxy object. */
BluetoothHeadset(Context context, BluetoothAdapter adapter)355     /* package */ BluetoothHeadset(Context context, BluetoothAdapter adapter) {
356         mAdapter = adapter;
357         mAttributionSource = adapter.getAttributionSource();
358         mService = null;
359     }
360 
361     /**
362      * Close the connection to the backing service. Other public functions of BluetoothHeadset will
363      * return default error results once close() has been called. Multiple invocations of close()
364      * are ok.
365      *
366      * @hide
367      */
368     @UnsupportedAppUsage
close()369     public void close() {
370         mAdapter.closeProfileProxy(this);
371     }
372 
373     /** @hide */
374     @Override
onServiceConnected(IBinder service)375     public void onServiceConnected(IBinder service) {
376         mService = IBluetoothHeadset.Stub.asInterface(service);
377     }
378 
379     /** @hide */
380     @Override
onServiceDisconnected()381     public void onServiceDisconnected() {
382         mService = null;
383     }
384 
getService()385     private IBluetoothHeadset getService() {
386         return mService;
387     }
388 
389     /** @hide */
390     @Override
getAdapter()391     public BluetoothAdapter getAdapter() {
392         return mAdapter;
393     }
394 
395     /** @hide */
396     @Override
397     @SuppressWarnings("Finalize") // empty finalize for api signature
finalize()398     protected void finalize() throws Throwable {
399         // The empty finalize needs to be kept or the
400         // cts signature tests would fail.
401     }
402 
403     /**
404      * Initiate connection to a profile of the remote bluetooth device.
405      *
406      * <p>Currently, the system supports only 1 connection to the headset/handsfree profile. The API
407      * will automatically disconnect connected devices before connecting.
408      *
409      * <p>This API returns false in scenarios like the profile on the device is already connected or
410      * Bluetooth is not turned on. When this API returns true, it is guaranteed that connection
411      * state intent for the profile will be broadcasted with the state. Users can get the connection
412      * state of the profile from this intent.
413      *
414      * @param device Remote Bluetooth Device
415      * @return false on immediate error, true otherwise
416      * @hide
417      */
418     @SystemApi
419     @RequiresLegacyBluetoothAdminPermission
420     @RequiresBluetoothConnectPermission
421     @RequiresPermission(
422             allOf = {
423                 android.Manifest.permission.BLUETOOTH_CONNECT,
424                 android.Manifest.permission.MODIFY_PHONE_STATE,
425             })
connect(BluetoothDevice device)426     public boolean connect(BluetoothDevice device) {
427         if (DBG) log("connect(" + device + ")");
428         final IBluetoothHeadset service = getService();
429         if (service == null) {
430             Log.w(TAG, "Proxy not attached to service");
431             if (DBG) log(Log.getStackTraceString(new Throwable()));
432         } else if (isEnabled() && isValidDevice(device)) {
433             try {
434                 return service.connect(device, mAttributionSource);
435             } catch (RemoteException e) {
436                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
437             }
438         }
439         return false;
440     }
441 
442     /**
443      * Initiate disconnection from a profile
444      *
445      * <p>This API will return false in scenarios like the profile on the Bluetooth device is not in
446      * connected state etc. When this API returns, true, it is guaranteed that the connection state
447      * change intent will be broadcasted with the state. Users can get the disconnection state of
448      * the profile from this intent.
449      *
450      * <p>If the disconnection is initiated by a remote device, the state will transition from
451      * {@link #STATE_CONNECTED} to {@link #STATE_DISCONNECTED}. If the disconnect is initiated by
452      * the host (local) device the state will transition from {@link #STATE_CONNECTED} to state
453      * {@link #STATE_DISCONNECTING} to state {@link #STATE_DISCONNECTED}. The transition to {@link
454      * #STATE_DISCONNECTING} can be used to distinguish between the two scenarios.
455      *
456      * @param device Remote Bluetooth Device
457      * @return false on immediate error, true otherwise
458      * @hide
459      */
460     @SystemApi
461     @RequiresLegacyBluetoothAdminPermission
462     @RequiresBluetoothConnectPermission
463     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
disconnect(BluetoothDevice device)464     public boolean disconnect(BluetoothDevice device) {
465         if (DBG) log("disconnect(" + device + ")");
466         final IBluetoothHeadset service = getService();
467         if (service == null) {
468             Log.w(TAG, "Proxy not attached to service");
469             if (DBG) log(Log.getStackTraceString(new Throwable()));
470         } else if (isEnabled() && isValidDevice(device)) {
471             try {
472                 return service.disconnect(device, mAttributionSource);
473             } catch (RemoteException e) {
474                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
475             }
476         }
477         return false;
478     }
479 
480     /** {@inheritDoc} */
481     @Override
482     @RequiresBluetoothConnectPermission
483     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getConnectedDevices()484     public List<BluetoothDevice> getConnectedDevices() {
485         if (VDBG) log("getConnectedDevices()");
486         final IBluetoothHeadset service = getService();
487         if (service == null) {
488             Log.w(TAG, "Proxy not attached to service");
489             if (DBG) log(Log.getStackTraceString(new Throwable()));
490         } else if (isEnabled()) {
491             try {
492                 return Attributable.setAttributionSource(
493                         service.getConnectedDevices(mAttributionSource), mAttributionSource);
494             } catch (RemoteException e) {
495                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
496             }
497         }
498         return Collections.emptyList();
499     }
500 
501     /** {@inheritDoc} */
502     @Override
503     @RequiresBluetoothConnectPermission
504     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getDevicesMatchingConnectionStates(int[] states)505     public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
506         if (VDBG) log("getDevicesMatchingStates()");
507         final IBluetoothHeadset service = getService();
508         if (service == null) {
509             Log.w(TAG, "Proxy not attached to service");
510             if (DBG) log(Log.getStackTraceString(new Throwable()));
511         } else if (isEnabled()) {
512             try {
513                 return Attributable.setAttributionSource(
514                         service.getDevicesMatchingConnectionStates(states, mAttributionSource),
515                         mAttributionSource);
516             } catch (RemoteException e) {
517                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
518             }
519         }
520         return Collections.emptyList();
521     }
522 
523     /** {@inheritDoc} */
524     @Override
525     @RequiresBluetoothConnectPermission
526     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getConnectionState(BluetoothDevice device)527     public int getConnectionState(BluetoothDevice device) {
528         if (VDBG) log("getConnectionState(" + device + ")");
529         final IBluetoothHeadset service = getService();
530         if (service == null) {
531             Log.w(TAG, "Proxy not attached to service");
532             if (DBG) log(Log.getStackTraceString(new Throwable()));
533         } else if (isEnabled() && isValidDevice(device)) {
534             try {
535                 return service.getConnectionState(device, mAttributionSource);
536             } catch (RemoteException e) {
537                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
538             }
539         }
540         return BluetoothProfile.STATE_DISCONNECTED;
541     }
542 
543     /**
544      * Set connection policy of the profile
545      *
546      * <p>The device should already be paired. Connection policy can be one of {@link
547      * #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, {@link
548      * #CONNECTION_POLICY_UNKNOWN}
549      *
550      * @param device Paired bluetooth device
551      * @param connectionPolicy is the connection policy to set to for this profile
552      * @return true if connectionPolicy is set, false on error
553      * @hide
554      */
555     @SystemApi
556     @RequiresBluetoothConnectPermission
557     @RequiresPermission(
558             allOf = {
559                 android.Manifest.permission.BLUETOOTH_CONNECT,
560                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
561                 android.Manifest.permission.MODIFY_PHONE_STATE,
562             })
setConnectionPolicy( @onNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy)563     public boolean setConnectionPolicy(
564             @NonNull BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
565         if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
566         final IBluetoothHeadset service = getService();
567         if (service == null) {
568             Log.w(TAG, "Proxy not attached to service");
569             if (DBG) log(Log.getStackTraceString(new Throwable()));
570         } else if (isEnabled()
571                 && isValidDevice(device)
572                 && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
573                         || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
574             try {
575                 return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
576             } catch (RemoteException e) {
577                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
578             }
579         }
580         return false;
581     }
582 
583     /**
584      * Get the priority of the profile.
585      *
586      * <p>The priority can be any of: {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, {@link
587      * #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
588      *
589      * @param device Bluetooth device
590      * @return priority of the device
591      * @hide
592      */
593     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
594     @RequiresLegacyBluetoothPermission
595     @RequiresBluetoothConnectPermission
596     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getPriority(BluetoothDevice device)597     public int getPriority(BluetoothDevice device) {
598         if (VDBG) log("getPriority(" + device + ")");
599         return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device));
600     }
601 
602     /**
603      * Get the connection policy of the profile.
604      *
605      * <p>The connection policy can be any of: {@link #CONNECTION_POLICY_ALLOWED}, {@link
606      * #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
607      *
608      * @param device Bluetooth device
609      * @return connection policy of the device
610      * @hide
611      */
612     @SystemApi
613     @RequiresBluetoothConnectPermission
614     @RequiresPermission(
615             allOf = {
616                 android.Manifest.permission.BLUETOOTH_CONNECT,
617                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
618             })
getConnectionPolicy(@onNull BluetoothDevice device)619     public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
620         if (VDBG) log("getConnectionPolicy(" + device + ")");
621         final IBluetoothHeadset service = getService();
622         if (service == null) {
623             Log.w(TAG, "Proxy not attached to service");
624             if (DBG) log(Log.getStackTraceString(new Throwable()));
625         } else if (isEnabled() && isValidDevice(device)) {
626             try {
627                 return service.getConnectionPolicy(device, mAttributionSource);
628             } catch (RemoteException e) {
629                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
630             }
631         }
632         return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
633     }
634 
635     /**
636      * Checks whether the headset supports some form of noise reduction
637      *
638      * @param device Bluetooth device
639      * @return true if echo cancellation and/or noise reduction is supported, false otherwise
640      */
641     @RequiresLegacyBluetoothPermission
642     @RequiresBluetoothConnectPermission
643     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
isNoiseReductionSupported(@onNull BluetoothDevice device)644     public boolean isNoiseReductionSupported(@NonNull BluetoothDevice device) {
645         if (DBG) log("isNoiseReductionSupported()");
646         final IBluetoothHeadset service = getService();
647         if (service == null) {
648             Log.w(TAG, "Proxy not attached to service");
649             if (DBG) log(Log.getStackTraceString(new Throwable()));
650         } else if (isEnabled() && isValidDevice(device)) {
651             try {
652                 return service.isNoiseReductionSupported(device, mAttributionSource);
653             } catch (RemoteException e) {
654                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
655             }
656         }
657         return false;
658     }
659 
660     /**
661      * Checks whether the headset supports voice recognition
662      *
663      * @param device Bluetooth device
664      * @return true if voice recognition is supported, false otherwise
665      */
666     @RequiresLegacyBluetoothPermission
667     @RequiresBluetoothConnectPermission
668     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
isVoiceRecognitionSupported(@onNull BluetoothDevice device)669     public boolean isVoiceRecognitionSupported(@NonNull BluetoothDevice device) {
670         if (DBG) log("isVoiceRecognitionSupported()");
671         final IBluetoothHeadset service = getService();
672         if (service == null) {
673             Log.w(TAG, "Proxy not attached to service");
674             if (DBG) log(Log.getStackTraceString(new Throwable()));
675         } else if (isEnabled() && isValidDevice(device)) {
676             try {
677                 return service.isVoiceRecognitionSupported(device, mAttributionSource);
678             } catch (RemoteException e) {
679                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
680             }
681         }
682         return false;
683     }
684 
685     /**
686      * Start Bluetooth voice recognition. This methods sends the voice recognition AT command to the
687      * headset and establishes the audio connection.
688      *
689      * <p>Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. If this function returns true,
690      * this intent will be broadcasted with {@link #EXTRA_STATE} set to {@link
691      * #STATE_AUDIO_CONNECTING}.
692      *
693      * <p>{@link #EXTRA_STATE} will transition from {@link #STATE_AUDIO_CONNECTING} to {@link
694      * #STATE_AUDIO_CONNECTED} when audio connection is established and to {@link
695      * #STATE_AUDIO_DISCONNECTED} in case of failure to establish the audio connection.
696      *
697      * @param device Bluetooth headset
698      * @return false if there is no headset connected, or the connected headset doesn't support
699      *     voice recognition, or voice recognition is already started, or audio channel is occupied,
700      *     or on error, true otherwise
701      */
702     @RequiresLegacyBluetoothPermission
703     @RequiresBluetoothConnectPermission
704     @RequiresPermission(
705             allOf = {
706                 android.Manifest.permission.BLUETOOTH_CONNECT,
707                 android.Manifest.permission.MODIFY_PHONE_STATE,
708             })
startVoiceRecognition(BluetoothDevice device)709     public boolean startVoiceRecognition(BluetoothDevice device) {
710         if (DBG) log("startVoiceRecognition()");
711         final IBluetoothHeadset service = getService();
712         if (service == null) {
713             Log.w(TAG, "Proxy not attached to service");
714             if (DBG) log(Log.getStackTraceString(new Throwable()));
715         } else if (isEnabled() && isValidDevice(device)) {
716             try {
717                 return service.startVoiceRecognition(device, mAttributionSource);
718             } catch (RemoteException e) {
719                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
720             }
721         }
722         return false;
723     }
724 
725     /**
726      * Stop Bluetooth Voice Recognition mode, and shut down the Bluetooth audio path.
727      *
728      * <p>Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. If this function returns true,
729      * this intent will be broadcasted with {@link #EXTRA_STATE} set to {@link
730      * #STATE_AUDIO_DISCONNECTED}.
731      *
732      * @param device Bluetooth headset
733      * @return false if there is no headset connected, or voice recognition has not started, or
734      *     voice recognition has ended on this headset, or on error, true otherwise
735      */
736     @RequiresLegacyBluetoothPermission
737     @RequiresBluetoothConnectPermission
738     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
stopVoiceRecognition(BluetoothDevice device)739     public boolean stopVoiceRecognition(BluetoothDevice device) {
740         if (DBG) log("stopVoiceRecognition()");
741         final IBluetoothHeadset service = getService();
742         if (service == null) {
743             Log.w(TAG, "Proxy not attached to service");
744             if (DBG) log(Log.getStackTraceString(new Throwable()));
745         } else if (isEnabled() && isValidDevice(device)) {
746             try {
747                 return service.stopVoiceRecognition(device, mAttributionSource);
748             } catch (RemoteException e) {
749                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
750             }
751         }
752         return false;
753     }
754 
755     /**
756      * Check if Bluetooth SCO audio is connected.
757      *
758      * @param device Bluetooth headset
759      * @return true if SCO is connected, false otherwise or on error
760      */
761     @RequiresLegacyBluetoothPermission
762     @RequiresBluetoothConnectPermission
763     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
isAudioConnected(BluetoothDevice device)764     public boolean isAudioConnected(BluetoothDevice device) {
765         if (VDBG) log("isAudioConnected()");
766         final IBluetoothHeadset service = getService();
767         if (service == null) {
768             Log.w(TAG, "Proxy not attached to service");
769             if (DBG) log(Log.getStackTraceString(new Throwable()));
770         } else if (isEnabled() && isValidDevice(device)) {
771             try {
772                 return service.isAudioConnected(device, mAttributionSource);
773             } catch (RemoteException e) {
774                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
775             }
776         }
777         return false;
778     }
779 
780     /** @hide */
781     @Retention(RetentionPolicy.SOURCE)
782     @IntDef(
783             value = {
784                 BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
785                 BluetoothHeadset.STATE_AUDIO_CONNECTING,
786                 BluetoothHeadset.STATE_AUDIO_CONNECTED,
787                 BluetoothStatusCodes.ERROR_TIMEOUT
788             })
789     public @interface GetAudioStateReturnValues {}
790 
791     /**
792      * Get the current audio state of the Headset.
793      *
794      * @param device is the Bluetooth device for which the audio state is being queried
795      * @return the audio state of the device or an error code
796      * @throws NullPointerException if the device is null
797      * @hide
798      */
799     @SystemApi
800     @RequiresBluetoothConnectPermission
801     @RequiresPermission(
802             allOf = {
803                 android.Manifest.permission.BLUETOOTH_CONNECT,
804                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
805             })
getAudioState(@onNull BluetoothDevice device)806     public @GetAudioStateReturnValues int getAudioState(@NonNull BluetoothDevice device) {
807         if (VDBG) log("getAudioState");
808         Objects.requireNonNull(device);
809         final IBluetoothHeadset service = getService();
810         if (service == null) {
811             Log.w(TAG, "Proxy not attached to service");
812             if (DBG) log(Log.getStackTraceString(new Throwable()));
813         } else if (!isDisabled()) {
814             try {
815                 return service.getAudioState(device, mAttributionSource);
816             } catch (RemoteException e) {
817                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
818                 throw e.rethrowAsRuntimeException();
819             }
820         }
821         return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
822     }
823 
824     /** @hide */
825     @Retention(RetentionPolicy.SOURCE)
826     @IntDef(
827             value = {
828                 BluetoothStatusCodes.SUCCESS,
829                 BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
830                 BluetoothStatusCodes.ERROR_TIMEOUT,
831                 BluetoothStatusCodes.ERROR_UNKNOWN,
832             })
833     public @interface SetAudioRouteAllowedReturnValues {}
834 
835     /** @hide */
836     @Retention(RetentionPolicy.SOURCE)
837     @IntDef(
838             value = {
839                 BluetoothStatusCodes.ALLOWED,
840                 BluetoothStatusCodes.NOT_ALLOWED,
841                 BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
842                 BluetoothStatusCodes.ERROR_TIMEOUT,
843                 BluetoothStatusCodes.ERROR_UNKNOWN,
844             })
845     public @interface GetAudioRouteAllowedReturnValues {}
846 
847     /**
848      * Sets whether audio routing is allowed. When set to {@code false}, the AG will not route any
849      * audio to the HF unless explicitly told to. This method should be used in cases where the SCO
850      * channel is shared between multiple profiles and must be delegated by a source knowledgeable.
851      *
852      * @param allowed {@code true} if the profile can reroute audio, {@code false} otherwise.
853      * @return {@link BluetoothStatusCodes#SUCCESS} upon successful setting, otherwise an error
854      *     code.
855      * @hide
856      */
857     @SystemApi
858     @RequiresBluetoothConnectPermission
859     @RequiresPermission(
860             allOf = {
861                 android.Manifest.permission.BLUETOOTH_CONNECT,
862                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
863             })
setAudioRouteAllowed(boolean allowed)864     public @SetAudioRouteAllowedReturnValues int setAudioRouteAllowed(boolean allowed) {
865         if (VDBG) log("setAudioRouteAllowed");
866         final IBluetoothHeadset service = getService();
867         if (service == null) {
868             Log.w(TAG, "Proxy not attached to service");
869             if (DBG) log(Log.getStackTraceString(new Throwable()));
870             return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
871         } else if (isEnabled()) {
872             try {
873                 service.setAudioRouteAllowed(allowed, mAttributionSource);
874                 return BluetoothStatusCodes.SUCCESS;
875             } catch (RemoteException e) {
876                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
877                 throw e.rethrowAsRuntimeException();
878             }
879         }
880 
881         Log.e(TAG, "setAudioRouteAllowed: Bluetooth disabled, but profile service still bound");
882         return BluetoothStatusCodes.ERROR_UNKNOWN;
883     }
884 
885     /**
886      * @return {@link BluetoothStatusCodes#ALLOWED} if audio routing is allowed, {@link
887      *     BluetoothStatusCodes#NOT_ALLOWED} if audio routing is not allowed, or an error code if an
888      *     error occurs. see {@link #setAudioRouteAllowed(boolean)}.
889      * @hide
890      */
891     @SystemApi
892     @RequiresBluetoothConnectPermission
893     @RequiresPermission(
894             allOf = {
895                 android.Manifest.permission.BLUETOOTH_CONNECT,
896                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
897             })
getAudioRouteAllowed()898     public @GetAudioRouteAllowedReturnValues int getAudioRouteAllowed() {
899         if (VDBG) log("getAudioRouteAllowed");
900         final IBluetoothHeadset service = getService();
901         if (service == null) {
902             Log.w(TAG, "Proxy not attached to service");
903             if (DBG) log(Log.getStackTraceString(new Throwable()));
904             return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
905         } else if (isEnabled()) {
906             try {
907                 return service.getAudioRouteAllowed(mAttributionSource)
908                         ? BluetoothStatusCodes.ALLOWED
909                         : BluetoothStatusCodes.NOT_ALLOWED;
910             } catch (RemoteException e) {
911                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
912                 throw e.rethrowAsRuntimeException();
913             }
914         }
915 
916         Log.e(TAG, "getAudioRouteAllowed: Bluetooth disabled, but profile service still bound");
917         return BluetoothStatusCodes.ERROR_UNKNOWN;
918     }
919 
920     /**
921      * Force SCO audio to be opened regardless any other restrictions
922      *
923      * @param forced Whether or not SCO audio connection should be forced: True to force SCO audio
924      *     False to use SCO audio in normal manner
925      * @hide
926      */
927     @RequiresBluetoothConnectPermission
928     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
setForceScoAudio(boolean forced)929     public void setForceScoAudio(boolean forced) {
930         if (VDBG) log("setForceScoAudio " + String.valueOf(forced));
931         final IBluetoothHeadset service = getService();
932         if (service == null) {
933             Log.w(TAG, "Proxy not attached to service");
934             if (DBG) log(Log.getStackTraceString(new Throwable()));
935         } else if (isEnabled()) {
936             try {
937                 service.setForceScoAudio(forced, mAttributionSource);
938             } catch (RemoteException e) {
939                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
940             }
941         }
942     }
943 
944     /** @hide */
945     @Retention(RetentionPolicy.SOURCE)
946     @IntDef(
947             value = {
948                 BluetoothStatusCodes.SUCCESS,
949                 BluetoothStatusCodes.ERROR_UNKNOWN,
950                 BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
951                 BluetoothStatusCodes.ERROR_TIMEOUT,
952                 BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED,
953                 BluetoothStatusCodes.ERROR_NO_ACTIVE_DEVICES,
954                 BluetoothStatusCodes.ERROR_NOT_ACTIVE_DEVICE,
955                 BluetoothStatusCodes.ERROR_AUDIO_ROUTE_BLOCKED,
956                 BluetoothStatusCodes.ERROR_CALL_ACTIVE,
957                 BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED
958             })
959     public @interface ConnectAudioReturnValues {}
960 
961     /**
962      * Initiates a connection of SCO audio to the current active HFP device. The active HFP device
963      * can be identified with {@link BluetoothAdapter#getActiveDevices(int)}.
964      *
965      * <p>If this function returns {@link BluetoothStatusCodes#SUCCESS}, the intent {@link
966      * #ACTION_AUDIO_STATE_CHANGED} will be broadcasted twice. First with {@link #EXTRA_STATE} set
967      * to {@link #STATE_AUDIO_CONNECTING}. This will be followed by a broadcast with {@link
968      * #EXTRA_STATE} set to either {@link #STATE_AUDIO_CONNECTED} if the audio connection is
969      * established or {@link #STATE_AUDIO_DISCONNECTED} if there was a failure in establishing the
970      * audio connection.
971      *
972      * @return whether the connection was successfully initiated or an error code on failure
973      * @hide
974      */
975     @SystemApi
976     @RequiresBluetoothConnectPermission
977     @RequiresPermission(
978             allOf = {
979                 android.Manifest.permission.BLUETOOTH_CONNECT,
980                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
981             })
connectAudio()982     public @ConnectAudioReturnValues int connectAudio() {
983         if (VDBG) log("connectAudio()");
984         final IBluetoothHeadset service = getService();
985         if (service == null) {
986             Log.w(TAG, "Proxy not attached to service");
987             if (DBG) log(Log.getStackTraceString(new Throwable()));
988             return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
989         } else if (isEnabled()) {
990             try {
991                 return service.connectAudio(mAttributionSource);
992             } catch (RemoteException e) {
993                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
994                 throw e.rethrowAsRuntimeException();
995             }
996         }
997 
998         Log.e(TAG, "connectAudio: Bluetooth disabled, but profile service still bound");
999         return BluetoothStatusCodes.ERROR_UNKNOWN;
1000     }
1001 
1002     /** @hide */
1003     @Retention(RetentionPolicy.SOURCE)
1004     @IntDef(
1005             value = {
1006                 BluetoothStatusCodes.SUCCESS,
1007                 BluetoothStatusCodes.ERROR_UNKNOWN,
1008                 BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
1009                 BluetoothStatusCodes.ERROR_TIMEOUT,
1010                 BluetoothStatusCodes.ERROR_PROFILE_NOT_CONNECTED,
1011                 BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_DISCONNECTED
1012             })
1013     public @interface DisconnectAudioReturnValues {}
1014 
1015     /**
1016      * Initiates a disconnection of HFP SCO audio from actively connected devices. It also tears
1017      * down voice recognition or virtual voice call, if any exists.
1018      *
1019      * <p>If this function returns {@link BluetoothStatusCodes#SUCCESS}, the intent {@link
1020      * #ACTION_AUDIO_STATE_CHANGED} will be broadcasted with {@link #EXTRA_STATE} set to {@link
1021      * #STATE_AUDIO_DISCONNECTED}.
1022      *
1023      * @return whether the disconnection was initiated successfully or an error code on failure
1024      * @hide
1025      */
1026     @SystemApi
1027     @RequiresBluetoothConnectPermission
1028     @RequiresPermission(
1029             allOf = {
1030                 android.Manifest.permission.BLUETOOTH_CONNECT,
1031                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
1032             })
disconnectAudio()1033     public @DisconnectAudioReturnValues int disconnectAudio() {
1034         if (VDBG) log("disconnectAudio()");
1035         final IBluetoothHeadset service = getService();
1036         if (service == null) {
1037             Log.w(TAG, "Proxy not attached to service");
1038             if (DBG) log(Log.getStackTraceString(new Throwable()));
1039             return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
1040         } else if (isEnabled()) {
1041             try {
1042                 return service.disconnectAudio(mAttributionSource);
1043             } catch (RemoteException e) {
1044                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1045                 throw e.rethrowAsRuntimeException();
1046             }
1047         }
1048 
1049         Log.e(TAG, "disconnectAudio: Bluetooth disabled, but profile service still bound");
1050         return BluetoothStatusCodes.ERROR_UNKNOWN;
1051     }
1052 
1053     /**
1054      * Initiates a SCO channel connection as a virtual voice call to the current active device
1055      * Active handsfree device will be notified of incoming call and connected call.
1056      *
1057      * <p>Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. If this function returns true,
1058      * this intent will be broadcasted with {@link #EXTRA_STATE} set to {@link
1059      * #STATE_AUDIO_CONNECTING}.
1060      *
1061      * <p>{@link #EXTRA_STATE} will transition from {@link #STATE_AUDIO_CONNECTING} to {@link
1062      * #STATE_AUDIO_CONNECTED} when audio connection is established and to {@link
1063      * #STATE_AUDIO_DISCONNECTED} in case of failure to establish the audio connection.
1064      *
1065      * @return true if successful, false if one of the following case applies - SCO audio is not
1066      *     idle (connecting or connected) - virtual call has already started - there is no active
1067      *     device - a Telecom managed call is going on - binder is dead or Bluetooth is disabled or
1068      *     other error
1069      * @hide
1070      */
1071     @SystemApi
1072     @RequiresLegacyBluetoothAdminPermission
1073     @RequiresBluetoothConnectPermission
1074     @RequiresPermission(
1075             allOf = {
1076                 android.Manifest.permission.BLUETOOTH_CONNECT,
1077                 android.Manifest.permission.MODIFY_PHONE_STATE,
1078                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
1079             })
startScoUsingVirtualVoiceCall()1080     public boolean startScoUsingVirtualVoiceCall() {
1081         if (DBG) log("startScoUsingVirtualVoiceCall()");
1082         final IBluetoothHeadset service = getService();
1083         if (service == null) {
1084             Log.w(TAG, "Proxy not attached to service");
1085             if (DBG) log(Log.getStackTraceString(new Throwable()));
1086         } else if (isEnabled()) {
1087             try {
1088                 return service.startScoUsingVirtualVoiceCall(mAttributionSource);
1089             } catch (RemoteException e) {
1090                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1091             }
1092         }
1093         return false;
1094     }
1095 
1096     /**
1097      * Terminates an ongoing SCO connection and the associated virtual call.
1098      *
1099      * <p>Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. If this function returns true,
1100      * this intent will be broadcasted with {@link #EXTRA_STATE} set to {@link
1101      * #STATE_AUDIO_DISCONNECTED}.
1102      *
1103      * @return true if successful, false if one of the following case applies - virtual voice call
1104      *     is not started or has ended - binder is dead or Bluetooth is disabled or other error
1105      * @hide
1106      */
1107     @SystemApi
1108     @RequiresLegacyBluetoothAdminPermission
1109     @RequiresBluetoothConnectPermission
1110     @RequiresPermission(
1111             allOf = {
1112                 android.Manifest.permission.BLUETOOTH_CONNECT,
1113                 android.Manifest.permission.MODIFY_PHONE_STATE,
1114                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
1115             })
stopScoUsingVirtualVoiceCall()1116     public boolean stopScoUsingVirtualVoiceCall() {
1117         if (DBG) log("stopScoUsingVirtualVoiceCall()");
1118         final IBluetoothHeadset service = getService();
1119         if (service == null) {
1120             Log.w(TAG, "Proxy not attached to service");
1121             if (DBG) log(Log.getStackTraceString(new Throwable()));
1122         } else if (isEnabled()) {
1123             try {
1124                 return service.stopScoUsingVirtualVoiceCall(mAttributionSource);
1125             } catch (RemoteException e) {
1126                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1127             }
1128         }
1129         return false;
1130     }
1131 
1132     /**
1133      * Notify Headset of phone state change. This is a backdoor for phone app to call
1134      * BluetoothHeadset since there is currently not a good way to get precise call state change
1135      * outside of phone app.
1136      *
1137      * @hide
1138      */
1139     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
1140     @RequiresBluetoothConnectPermission
1141     @RequiresPermission(
1142             allOf = {
1143                 android.Manifest.permission.BLUETOOTH_CONNECT,
1144                 android.Manifest.permission.MODIFY_PHONE_STATE,
1145             })
phoneStateChanged( int numActive, int numHeld, int callState, String number, int type, String name)1146     public void phoneStateChanged(
1147             int numActive, int numHeld, int callState, String number, int type, String name) {
1148         final IBluetoothHeadset service = getService();
1149         if (service == null) {
1150             Log.w(TAG, "Proxy not attached to service");
1151             if (DBG) log(Log.getStackTraceString(new Throwable()));
1152         } else if (isEnabled()) {
1153             try {
1154                 service.phoneStateChanged(
1155                         numActive, numHeld, callState, number, type, name, mAttributionSource);
1156             } catch (RemoteException e) {
1157                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1158             }
1159         }
1160     }
1161 
1162     /**
1163      * Send Headset of CLCC response
1164      *
1165      * @hide
1166      */
1167     @RequiresBluetoothConnectPermission
1168     @RequiresPermission(
1169             allOf = {
1170                 android.Manifest.permission.BLUETOOTH_CONNECT,
1171                 android.Manifest.permission.MODIFY_PHONE_STATE,
1172             })
clccResponse( int index, int direction, int status, int mode, boolean mpty, String number, int type)1173     public void clccResponse(
1174             int index, int direction, int status, int mode, boolean mpty, String number, int type) {
1175         final IBluetoothHeadset service = getService();
1176         if (service == null) {
1177             Log.w(TAG, "Proxy not attached to service");
1178             if (DBG) log(Log.getStackTraceString(new Throwable()));
1179         } else if (isEnabled()) {
1180             try {
1181                 service.clccResponse(
1182                         index, direction, status, mode, mpty, number, type, mAttributionSource);
1183             } catch (RemoteException e) {
1184                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1185             }
1186         }
1187     }
1188 
1189     /**
1190      * Sends a vendor-specific unsolicited result code to the headset.
1191      *
1192      * <p>The actual string to be sent is <code>command + ": " + arg</code>. For example, if {@code
1193      * command} is {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} and {@code arg} is {@code "0"}, the
1194      * string <code>"+ANDROID: 0"</code> will be sent.
1195      *
1196      * <p>Currently only {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} is allowed as {@code command}.
1197      *
1198      * @param device Bluetooth headset.
1199      * @param command A vendor-specific command.
1200      * @param arg The argument that will be attached to the command.
1201      * @return {@code false} if there is no headset connected, or if the command is not an allowed
1202      *     vendor-specific unsolicited result code, or on error. {@code true} otherwise.
1203      * @throws IllegalArgumentException if {@code command} is {@code null}.
1204      */
1205     @RequiresLegacyBluetoothPermission
1206     @RequiresBluetoothConnectPermission
1207     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
sendVendorSpecificResultCode( BluetoothDevice device, String command, String arg)1208     public boolean sendVendorSpecificResultCode(
1209             BluetoothDevice device, String command, String arg) {
1210         if (DBG) {
1211             log("sendVendorSpecificResultCode()");
1212         }
1213         if (command == null) {
1214             throw new IllegalArgumentException("command is null");
1215         }
1216         final IBluetoothHeadset service = getService();
1217         if (service == null) {
1218             Log.w(TAG, "Proxy not attached to service");
1219             if (DBG) log(Log.getStackTraceString(new Throwable()));
1220         } else if (isEnabled() && isValidDevice(device)) {
1221             try {
1222                 return service.sendVendorSpecificResultCode(
1223                         device, command, arg, mAttributionSource);
1224             } catch (RemoteException e) {
1225                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1226             }
1227         }
1228         return false;
1229     }
1230 
1231     /**
1232      * Select a connected device as active.
1233      *
1234      * <p>The active device selection is per profile. An active device's purpose is
1235      * profile-specific. For example, in HFP and HSP profiles, it is the device used for phone call
1236      * audio. If a remote device is not connected, it cannot be selected as active.
1237      *
1238      * <p>This API returns false in scenarios like the profile on the device is not connected or
1239      * Bluetooth is not turned on. When this API returns true, it is guaranteed that the {@link
1240      * #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted with the active device.
1241      *
1242      * @param device Remote Bluetooth Device, could be null if phone call audio should not be
1243      *     streamed to a headset
1244      * @return false on immediate error, true otherwise
1245      * @hide
1246      */
1247     @RequiresLegacyBluetoothAdminPermission
1248     @RequiresBluetoothConnectPermission
1249     @RequiresPermission(
1250             allOf = {
1251                 android.Manifest.permission.BLUETOOTH_CONNECT,
1252                 android.Manifest.permission.MODIFY_PHONE_STATE,
1253             })
1254     @UnsupportedAppUsage(trackingBug = 171933273)
setActiveDevice(@ullable BluetoothDevice device)1255     public boolean setActiveDevice(@Nullable BluetoothDevice device) {
1256         if (DBG) {
1257             Log.d(TAG, "setActiveDevice: " + device);
1258         }
1259         final IBluetoothHeadset service = getService();
1260         if (service == null) {
1261             Log.w(TAG, "Proxy not attached to service");
1262             if (DBG) log(Log.getStackTraceString(new Throwable()));
1263         } else if (isEnabled() && (device == null || isValidDevice(device))) {
1264             try {
1265                 return service.setActiveDevice(device, mAttributionSource);
1266             } catch (RemoteException e) {
1267                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1268             }
1269         }
1270         return false;
1271     }
1272 
1273     /**
1274      * Get the connected device that is active.
1275      *
1276      * @return the connected device that is active or null if no device is active.
1277      * @hide
1278      */
1279     @UnsupportedAppUsage(trackingBug = 171933273)
1280     @Nullable
1281     @RequiresLegacyBluetoothPermission
1282     @RequiresBluetoothConnectPermission
1283     @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
getActiveDevice()1284     public BluetoothDevice getActiveDevice() {
1285         if (VDBG) Log.d(TAG, "getActiveDevice");
1286         final IBluetoothHeadset service = getService();
1287         if (service == null) {
1288             Log.w(TAG, "Proxy not attached to service");
1289             if (DBG) log(Log.getStackTraceString(new Throwable()));
1290         } else if (isEnabled()) {
1291             try {
1292                 return Attributable.setAttributionSource(
1293                         service.getActiveDevice(mAttributionSource), mAttributionSource);
1294             } catch (RemoteException e) {
1295                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1296             }
1297         }
1298         return null;
1299     }
1300 
1301     /**
1302      * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
1303      * active connection.
1304      *
1305      * @return true if in-band ringing is enabled, false if in-band ringing is disabled
1306      * @hide
1307      */
1308     @SystemApi
1309     @RequiresLegacyBluetoothPermission
1310     @RequiresBluetoothConnectPermission
1311     @RequiresPermission(
1312             allOf = {
1313                 android.Manifest.permission.BLUETOOTH_CONNECT,
1314                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
1315             })
isInbandRingingEnabled()1316     public boolean isInbandRingingEnabled() {
1317         if (DBG) log("isInbandRingingEnabled()");
1318         final IBluetoothHeadset service = getService();
1319         if (service == null) {
1320             Log.w(TAG, "Proxy not attached to service");
1321             if (DBG) log(Log.getStackTraceString(new Throwable()));
1322         } else if (isEnabled()) {
1323             try {
1324                 return service.isInbandRingingEnabled(mAttributionSource);
1325             } catch (RemoteException e) {
1326                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
1327             }
1328         }
1329         return false;
1330     }
1331 
1332     @UnsupportedAppUsage
isEnabled()1333     private boolean isEnabled() {
1334         return mAdapter.getState() == BluetoothAdapter.STATE_ON;
1335     }
1336 
isDisabled()1337     private boolean isDisabled() {
1338         return mAdapter.getState() == BluetoothAdapter.STATE_OFF;
1339     }
1340 
isValidDevice(BluetoothDevice device)1341     private static boolean isValidDevice(BluetoothDevice device) {
1342         return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
1343     }
1344 
log(String msg)1345     private static void log(String msg) {
1346         Log.d(TAG, msg);
1347     }
1348 }
1349