diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2022-01-18 23:59:58 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2022-01-18 23:59:58 +0000 |
commit | d15d8266b900c549d2eed52c56f82d7c3c3792c8 (patch) | |
tree | 0b7c4c70453c091e24e37758ff1b1fd49bf20eb3 /framework/java/android/bluetooth/BluetoothHeadset.java | |
parent | 7d69d903a5ee30dac4abc82e056f2b8ec0278708 (diff) | |
parent | aa6d8f71ae61c0e57b35258c00d28fcba5154f09 (diff) |
Merge changes from topic "migrate-sc-bt" into sc-v2-dev-plus-aosp
* changes:
Migrating BT files into packages/modules/Bluetooth
Merge history of Bluetooth API from frameworks/base
Diffstat (limited to 'framework/java/android/bluetooth/BluetoothHeadset.java')
-rw-r--r-- | framework/java/android/bluetooth/BluetoothHeadset.java | 1505 |
1 files changed, 1505 insertions, 0 deletions
diff --git a/framework/java/android/bluetooth/BluetoothHeadset.java b/framework/java/android/bluetooth/BluetoothHeadset.java new file mode 100644 index 0000000000..1b141c9afa --- /dev/null +++ b/framework/java/android/bluetooth/BluetoothHeadset.java @@ -0,0 +1,1505 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import static android.bluetooth.BluetoothUtils.getSyncTimeout; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.bluetooth.annotations.RequiresBluetoothConnectPermission; +import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission; +import android.bluetooth.annotations.RequiresLegacyBluetoothPermission; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.AttributionSource; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.CloseGuard; +import android.util.Log; + +import com.android.modules.utils.SynchronousResultReceiver; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +/** + * Public API for controlling the Bluetooth Headset Service. This includes both + * Bluetooth Headset and Handsfree (v1.5) profiles. + * + * <p>BluetoothHeadset is a proxy object for controlling the Bluetooth Headset + * Service via IPC. + * + * <p> Use {@link BluetoothAdapter#getProfileProxy} to get + * the BluetoothHeadset proxy object. Use + * {@link BluetoothAdapter#closeProfileProxy} to close the service connection. + * + * <p> Android only supports one connected Bluetooth Headset at a time. + * Each method is protected with its appropriate permission. + */ +public final class BluetoothHeadset implements BluetoothProfile { + private static final String TAG = "BluetoothHeadset"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + /** + * Intent used to broadcast the change in connection state of the Headset + * profile. + * + * <p>This intent will have 3 extras: + * <ul> + * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> + * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> + * </ul> + * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of + * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, + * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_CONNECTION_STATE_CHANGED = + "android.bluetooth.headset.profile.action.CONNECTION_STATE_CHANGED"; + + /** + * Intent used to broadcast the change in the Audio Connection state of the + * HFP profile. + * + * <p>This intent will have 3 extras: + * <ul> + * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> + * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> + * </ul> + * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of + * {@link #STATE_AUDIO_CONNECTED}, {@link #STATE_AUDIO_DISCONNECTED}, + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_AUDIO_STATE_CHANGED = + "android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED"; + + /** + * Intent used to broadcast the selection of a connected device as active. + * + * <p>This intent will have one extra: + * <ul> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can + * be null if no device is active. </li> + * </ul> + * + * @hide + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @UnsupportedAppUsage(trackingBug = 171933273) + public static final String ACTION_ACTIVE_DEVICE_CHANGED = + "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED"; + + /** + * Intent used to broadcast that the headset has posted a + * vendor-specific event. + * + * <p>This intent will have 4 extras and 1 category. + * <ul> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Bluetooth Device + * </li> + * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD} - The vendor + * specific command </li> + * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} - The AT + * command type which can be one of {@link #AT_CMD_TYPE_READ}, + * {@link #AT_CMD_TYPE_TEST}, or {@link #AT_CMD_TYPE_SET}, + * {@link #AT_CMD_TYPE_BASIC},{@link #AT_CMD_TYPE_ACTION}. </li> + * <li> {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS} - Command + * arguments. </li> + * </ul> + * + * <p> The category is the Company ID of the vendor defining the + * vendor-specific command. {@link BluetoothAssignedNumbers} + * + * For example, for Plantronics specific events + * Category will be {@link #VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY}.55 + * + * <p> For example, an AT+XEVENT=foo,3 will get translated into + * <ul> + * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = +XEVENT </li> + * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE = AT_CMD_TYPE_SET </li> + * <li> EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = foo, 3 </li> + * </ul> + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_VENDOR_SPECIFIC_HEADSET_EVENT = + "android.bluetooth.headset.action.VENDOR_SPECIFIC_HEADSET_EVENT"; + + /** + * A String extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} + * intents that contains the name of the vendor-specific command. + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD"; + + /** + * An int extra field in {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} + * intents that contains the AT command type of the vendor-specific command. + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE"; + + /** + * AT command type READ used with + * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} + * For example, AT+VGM?. There are no arguments for this command type. + */ + public static final int AT_CMD_TYPE_READ = 0; + + /** + * AT command type TEST used with + * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} + * For example, AT+VGM=?. There are no arguments for this command type. + */ + public static final int AT_CMD_TYPE_TEST = 1; + + /** + * AT command type SET used with + * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} + * For example, AT+VGM=<args>. + */ + public static final int AT_CMD_TYPE_SET = 2; + + /** + * AT command type BASIC used with + * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} + * For example, ATD. Single character commands and everything following the + * character are arguments. + */ + public static final int AT_CMD_TYPE_BASIC = 3; + + /** + * AT command type ACTION used with + * {@link #EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE} + * For example, AT+CHUP. There are no arguments for action commands. + */ + public static final int AT_CMD_TYPE_ACTION = 4; + + /** + * A Parcelable String array extra field in + * {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} intents that contains + * the arguments to the vendor-specific command. + */ + public static final String EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS = + "android.bluetooth.headset.extra.VENDOR_SPECIFIC_HEADSET_EVENT_ARGS"; + + /** + * The intent category to be used with {@link #ACTION_VENDOR_SPECIFIC_HEADSET_EVENT} + * for the companyId + */ + public static final String VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY = + "android.bluetooth.headset.intent.category.companyid"; + + /** + * A vendor-specific command for unsolicited result code. + */ + public static final String VENDOR_RESULT_CODE_COMMAND_ANDROID = "+ANDROID"; + + /** + * A vendor-specific AT command + * + * @hide + */ + public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XAPL = "+XAPL"; + + /** + * A vendor-specific AT command + * + * @hide + */ + public static final String VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV = "+IPHONEACCEV"; + + /** + * Battery level indicator associated with + * {@link #VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV} + * + * @hide + */ + public static final int VENDOR_SPECIFIC_HEADSET_EVENT_IPHONEACCEV_BATTERY_LEVEL = 1; + + /** + * A vendor-specific AT command + * + * @hide + */ + public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT = "+XEVENT"; + + /** + * Battery level indicator associated with {@link #VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT} + * + * @hide + */ + public static final String VENDOR_SPECIFIC_HEADSET_EVENT_XEVENT_BATTERY_LEVEL = "BATTERY"; + + /** + * Headset state when SCO audio is not connected. + * This state can be one of + * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of + * {@link #ACTION_AUDIO_STATE_CHANGED} intent. + */ + public static final int STATE_AUDIO_DISCONNECTED = 10; + + /** + * Headset state when SCO audio is connecting. + * This state can be one of + * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of + * {@link #ACTION_AUDIO_STATE_CHANGED} intent. + */ + public static final int STATE_AUDIO_CONNECTING = 11; + + /** + * Headset state when SCO audio is connected. + * This state can be one of + * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of + * {@link #ACTION_AUDIO_STATE_CHANGED} intent. + */ + public static final int STATE_AUDIO_CONNECTED = 12; + + /** + * Intent used to broadcast the headset's indicator status + * + * <p>This intent will have 3 extras: + * <ul> + * <li> {@link #EXTRA_HF_INDICATORS_IND_ID} - The Assigned number of headset Indicator which + * is supported by the headset ( as indicated by AT+BIND command in the SLC + * sequence) or whose value is changed (indicated by AT+BIEV command) </li> + * <li> {@link #EXTRA_HF_INDICATORS_IND_VALUE} - Updated value of headset indicator. </li> + * <li> {@link BluetoothDevice#EXTRA_DEVICE} - Remote device. </li> + * </ul> + * <p>{@link #EXTRA_HF_INDICATORS_IND_ID} is defined by Bluetooth SIG and each of the indicators + * are given an assigned number. Below shows the assigned number of Indicator added so far + * - Enhanced Safety - 1, Valid Values: 0 - Disabled, 1 - Enabled + * - Battery Level - 2, Valid Values: 0~100 - Remaining level of Battery + * + * @hide + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_HF_INDICATORS_VALUE_CHANGED = + "android.bluetooth.headset.action.HF_INDICATORS_VALUE_CHANGED"; + + /** + * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED} + * intents that contains the assigned number of the headset indicator as defined by + * Bluetooth SIG that is being sent. Value range is 0-65535 as defined in HFP 1.7 + * + * @hide + */ + public static final String EXTRA_HF_INDICATORS_IND_ID = + "android.bluetooth.headset.extra.HF_INDICATORS_IND_ID"; + + /** + * A int extra field in {@link #ACTION_HF_INDICATORS_VALUE_CHANGED} + * intents that contains the value of the Headset indicator that is being sent. + * + * @hide + */ + public static final String EXTRA_HF_INDICATORS_IND_VALUE = + "android.bluetooth.headset.extra.HF_INDICATORS_IND_VALUE"; + + private static final int MESSAGE_HEADSET_SERVICE_CONNECTED = 100; + private static final int MESSAGE_HEADSET_SERVICE_DISCONNECTED = 101; + + private final CloseGuard mCloseGuard = new CloseGuard(); + + private Context mContext; + private ServiceListener mServiceListener; + private volatile IBluetoothHeadset mService; + private final BluetoothAdapter mAdapter; + private final AttributionSource mAttributionSource; + + @SuppressLint("AndroidFrameworkBluetoothPermission") + private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = + new IBluetoothStateChangeCallback.Stub() { + public void onBluetoothStateChange(boolean up) { + if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); + if (!up) { + doUnbind(); + } else { + doBind(); + } + } + }; + + /** + * Create a BluetoothHeadset proxy object. + */ + /* package */ BluetoothHeadset(Context context, ServiceListener l, BluetoothAdapter adapter) { + mContext = context; + mServiceListener = l; + mAdapter = adapter; + mAttributionSource = adapter.getAttributionSource(); + + // Preserve legacy compatibility where apps were depending on + // registerStateChangeCallback() performing a permissions check which + // has been relaxed in modern platform versions + if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.R + && context.checkSelfPermission(android.Manifest.permission.BLUETOOTH) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Need BLUETOOTH permission"); + } + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + } + + doBind(); + mCloseGuard.open("close"); + } + + private boolean doBind() { + synchronized (mConnection) { + if (mService == null) { + if (VDBG) Log.d(TAG, "Binding service..."); + try { + return mAdapter.getBluetoothManager().bindBluetoothProfileService( + BluetoothProfile.HEADSET, mConnection); + } catch (RemoteException e) { + Log.e(TAG, "Unable to bind HeadsetService", e); + } + } + } + return false; + } + + private void doUnbind() { + synchronized (mConnection) { + if (mService != null) { + if (VDBG) Log.d(TAG, "Unbinding service..."); + try { + mAdapter.getBluetoothManager().unbindBluetoothProfileService( + BluetoothProfile.HEADSET, mConnection); + } catch (RemoteException e) { + Log.e(TAG, "Unable to unbind HeadsetService", e); + } finally { + mService = null; + } + } + } + } + + /** + * Close the connection to the backing service. + * Other public functions of BluetoothHeadset will return default error + * results once close() has been called. Multiple invocations of close() + * are ok. + */ + @UnsupportedAppUsage + /*package*/ void close() { + if (VDBG) log("close()"); + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException re) { + Log.e(TAG, "", re); + } + } + mServiceListener = null; + doUnbind(); + mCloseGuard.close(); + } + + /** {@hide} */ + @Override + protected void finalize() throws Throwable { + mCloseGuard.warnIfOpen(); + close(); + } + + /** + * Initiate connection to a profile of the remote bluetooth device. + * + * <p> Currently, the system supports only 1 connection to the + * headset/handsfree profile. The API will automatically disconnect connected + * devices before connecting. + * + * <p> This API returns false in scenarios like the profile on the + * device is already connected or Bluetooth is not turned on. + * When this API returns true, it is guaranteed that + * connection state intent for the profile will be broadcasted with + * the state. Users can get the connection state of the profile + * from this intent. + * + * @param device Remote Bluetooth Device + * @return false on immediate error, true otherwise + * @hide + */ + @SystemApi + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean connect(BluetoothDevice device) { + if (DBG) log("connect(" + device + ")"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.connectWithAttribution(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Initiate disconnection from a profile + * + * <p> This API will return false in scenarios like the profile on the + * Bluetooth device is not in connected state etc. When this API returns, + * true, it is guaranteed that the connection state change + * intent will be broadcasted with the state. Users can get the + * disconnection state of the profile from this intent. + * + * <p> If the disconnection is initiated by a remote device, the state + * will transition from {@link #STATE_CONNECTED} to + * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the + * host (local) device the state will transition from + * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to + * state {@link #STATE_DISCONNECTED}. The transition to + * {@link #STATE_DISCONNECTING} can be used to distinguish between the + * two scenarios. + * + * @param device Remote Bluetooth Device + * @return false on immediate error, true otherwise + * @hide + */ + @SystemApi + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean disconnect(BluetoothDevice device) { + if (DBG) log("disconnect(" + device + ")"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.disconnectWithAttribution(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * {@inheritDoc} + */ + @Override + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public List<BluetoothDevice> getConnectedDevices() { + if (VDBG) log("getConnectedDevices()"); + final IBluetoothHeadset service = mService; + final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<List<BluetoothDevice>> recv = + new SynchronousResultReceiver(); + service.getConnectedDevicesWithAttribution(mAttributionSource, recv); + return Attributable.setAttributionSource( + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue), + mAttributionSource); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * {@inheritDoc} + */ + @Override + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { + if (VDBG) log("getDevicesMatchingStates()"); + final IBluetoothHeadset service = mService; + final List<BluetoothDevice> defaultValue = new ArrayList<BluetoothDevice>(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<List<BluetoothDevice>> recv = + new SynchronousResultReceiver(); + service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv); + return Attributable.setAttributionSource( + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue), + mAttributionSource); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * {@inheritDoc} + */ + @Override + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public int getConnectionState(BluetoothDevice device) { + if (VDBG) log("getConnectionState(" + device + ")"); + final IBluetoothHeadset service = mService; + final int defaultValue = BluetoothProfile.STATE_DISCONNECTED; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); + service.getConnectionStateWithAttribution(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Set priority of the profile + * + * <p> The device should already be paired. + * Priority can be one of {@link BluetoothProfile#PRIORITY_ON} or + * {@link BluetoothProfile#PRIORITY_OFF} + * + * @param device Paired bluetooth device + * @param priority + * @return true if priority is set, false on error + * @hide + * @deprecated Replaced with {@link #setConnectionPolicy(BluetoothDevice, int)} + * @removed + */ + @Deprecated + @SystemApi + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean setPriority(BluetoothDevice device, int priority) { + if (DBG) log("setPriority(" + device + ", " + priority + ")"); + return setConnectionPolicy(device, BluetoothAdapter.priorityToConnectionPolicy(priority)); + } + + /** + * Set connection policy of the profile + * + * <p> The device should already be paired. + * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED}, + * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN} + * + * @param device Paired bluetooth device + * @param connectionPolicy is the connection policy to set to for this profile + * @return true if connectionPolicy is set, false on error + * @hide + */ + @SystemApi + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_PRIVILEGED, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean setConnectionPolicy(@NonNull BluetoothDevice device, + @ConnectionPolicy int connectionPolicy) { + if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device) + && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN + || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Get the priority of the profile. + * + * <p> The priority can be any of: + * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, + * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} + * + * @param device Bluetooth device + * @return priority of the device + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public int getPriority(BluetoothDevice device) { + if (VDBG) log("getPriority(" + device + ")"); + return BluetoothAdapter.connectionPolicyToPriority(getConnectionPolicy(device)); + } + + /** + * Get the connection policy of the profile. + * + * <p> The connection policy can be any of: + * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN}, + * {@link #CONNECTION_POLICY_UNKNOWN} + * + * @param device Bluetooth device + * @return connection policy of the device + * @hide + */ + @SystemApi + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.BLUETOOTH_PRIVILEGED, + }) + public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) { + if (VDBG) log("getConnectionPolicy(" + device + ")"); + final IBluetoothHeadset service = mService; + final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); + service.getConnectionPolicy(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Checks whether the headset supports some form of noise reduction + * + * @param device Bluetooth device + * @return true if echo cancellation and/or noise reduction is supported, false otherwise + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean isNoiseReductionSupported(@NonNull BluetoothDevice device) { + if (DBG) log("isNoiseReductionSupported()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isNoiseReductionSupported(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Checks whether the headset supports voice recognition + * + * @param device Bluetooth device + * @return true if voice recognition is supported, false otherwise + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean isVoiceRecognitionSupported(@NonNull BluetoothDevice device) { + if (DBG) log("isVoiceRecognitionSupported()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isVoiceRecognitionSupported(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Start Bluetooth voice recognition. This methods sends the voice + * recognition AT command to the headset and establishes the + * audio connection. + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}. + * + * <p> {@link #EXTRA_STATE} will transition from + * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when + * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED} + * in case of failure to establish the audio connection. + * + * @param device Bluetooth headset + * @return false if there is no headset connected, or the connected headset doesn't support + * voice recognition, or voice recognition is already started, or audio channel is occupied, + * or on error, true otherwise + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean startVoiceRecognition(BluetoothDevice device) { + if (DBG) log("startVoiceRecognition()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.startVoiceRecognition(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Stop Bluetooth Voice Recognition mode, and shut down the + * Bluetooth audio path. + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}. + * + * @param device Bluetooth headset + * @return false if there is no headset connected, or voice recognition has not started, + * or voice recognition has ended on this headset, or on error, true otherwise + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean stopVoiceRecognition(BluetoothDevice device) { + if (DBG) log("stopVoiceRecognition()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.stopVoiceRecognition(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Check if Bluetooth SCO audio is connected. + * + * @param device Bluetooth headset + * @return true if SCO is connected, false otherwise or on error + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean isAudioConnected(BluetoothDevice device) { + if (VDBG) log("isAudioConnected()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isAudioConnected(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Indicates if current platform supports voice dialing over bluetooth SCO. + * + * @return true if voice dialing over bluetooth is supported, false otherwise. + * @hide + */ + public static boolean isBluetoothVoiceDialingEnabled(Context context) { + return context.getResources().getBoolean( + com.android.internal.R.bool.config_bluetooth_sco_off_call); + } + + /** + * Get the current audio state of the Headset. + * Note: This is an internal function and shouldn't be exposed + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public int getAudioState(BluetoothDevice device) { + if (VDBG) log("getAudioState"); + final IBluetoothHeadset service = mService; + final int defaultValue = BluetoothHeadset.STATE_AUDIO_DISCONNECTED; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (!isDisabled()) { + try { + final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); + service.getAudioState(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Sets whether audio routing is allowed. When set to {@code false}, the AG will not route any + * audio to the HF unless explicitly told to. + * This method should be used in cases where the SCO channel is shared between multiple profiles + * and must be delegated by a source knowledgeable + * Note: This is an internal function and shouldn't be exposed + * + * @param allowed {@code true} if the profile can reroute audio, {@code false} otherwise. + * @hide + */ + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public void setAudioRouteAllowed(boolean allowed) { + if (VDBG) log("setAudioRouteAllowed"); + final IBluetoothHeadset service = mService; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver recv = new SynchronousResultReceiver(); + service.setAudioRouteAllowed(allowed, mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + } + + /** + * Returns whether audio routing is allowed. see {@link #setAudioRouteAllowed(boolean)}. + * Note: This is an internal function and shouldn't be exposed + * + * @hide + */ + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean getAudioRouteAllowed() { + if (VDBG) log("getAudioRouteAllowed"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.getAudioRouteAllowed(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Force SCO audio to be opened regardless any other restrictions + * + * @param forced Whether or not SCO audio connection should be forced: True to force SCO audio + * False to use SCO audio in normal manner + * @hide + */ + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public void setForceScoAudio(boolean forced) { + if (VDBG) log("setForceScoAudio " + String.valueOf(forced)); + final IBluetoothHeadset service = mService; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver recv = new SynchronousResultReceiver(); + service.setForceScoAudio(forced, mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + } + + /** + * Check if at least one headset's SCO audio is connected or connecting + * + * @return true if at least one device's SCO audio is connected or connecting, false otherwise + * or on error + * @hide + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean isAudioOn() { + if (VDBG) log("isAudioOn()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isAudioOn(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Initiates a connection of headset audio to the current active device + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}. + * + * <p> {@link #EXTRA_STATE} will transition from + * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when + * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED} + * in case of failure to establish the audio connection. + * + * Note that this intent will not be sent if {@link BluetoothHeadset#isAudioOn()} is true + * before calling this method + * + * @return false if there was some error such as there is no active headset + * @hide + */ + @UnsupportedAppUsage + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean connectAudio() { + if (VDBG) log("connectAudio()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.connectAudio(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Initiates a disconnection of HFP SCO audio. + * Tear down voice recognition or virtual voice call if any. + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}. + * + * @return false if audio is not connected, or on error, true otherwise + * @hide + */ + @UnsupportedAppUsage + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean disconnectAudio() { + if (VDBG) log("disconnectAudio()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.disconnectAudio(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Initiates a SCO channel connection as a virtual voice call to the current active device + * Active handsfree device will be notified of incoming call and connected call. + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_CONNECTING}. + * + * <p> {@link #EXTRA_STATE} will transition from + * {@link #STATE_AUDIO_CONNECTING} to {@link #STATE_AUDIO_CONNECTED} when + * audio connection is established and to {@link #STATE_AUDIO_DISCONNECTED} + * in case of failure to establish the audio connection. + * + * @return true if successful, false if one of the following case applies + * - SCO audio is not idle (connecting or connected) + * - virtual call has already started + * - there is no active device + * - a Telecom managed call is going on + * - binder is dead or Bluetooth is disabled or other error + * @hide + */ + @SystemApi + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean startScoUsingVirtualVoiceCall() { + if (DBG) log("startScoUsingVirtualVoiceCall()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.startScoUsingVirtualVoiceCall(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Terminates an ongoing SCO connection and the associated virtual call. + * + * <p> Users can listen to {@link #ACTION_AUDIO_STATE_CHANGED}. + * If this function returns true, this intent will be broadcasted with + * {@link #EXTRA_STATE} set to {@link #STATE_AUDIO_DISCONNECTED}. + * + * @return true if successful, false if one of the following case applies + * - virtual voice call is not started or has ended + * - binder is dead or Bluetooth is disabled or other error + * @hide + */ + @SystemApi + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public boolean stopScoUsingVirtualVoiceCall() { + if (DBG) log("stopScoUsingVirtualVoiceCall()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.stopScoUsingVirtualVoiceCall(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Notify Headset of phone state change. + * This is a backdoor for phone app to call BluetoothHeadset since + * there is currently not a good way to get precise call state change outside + * of phone app. + * + * @hide + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public void phoneStateChanged(int numActive, int numHeld, int callState, String number, + int type, String name) { + final IBluetoothHeadset service = mService; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + service.phoneStateChanged(numActive, numHeld, callState, number, type, name, + mAttributionSource); + } catch (RemoteException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + } + + /** + * Send Headset of CLCC response + * + * @hide + */ + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + public void clccResponse(int index, int direction, int status, int mode, boolean mpty, + String number, int type) { + final IBluetoothHeadset service = mService; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver recv = new SynchronousResultReceiver(); + service.clccResponse(index, direction, status, mode, mpty, number, type, + mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + } + + /** + * Sends a vendor-specific unsolicited result code to the headset. + * + * <p>The actual string to be sent is <code>command + ": " + arg</code>. For example, if {@code + * command} is {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} and {@code arg} is {@code "0"}, the + * string <code>"+ANDROID: 0"</code> will be sent. + * + * <p>Currently only {@link #VENDOR_RESULT_CODE_COMMAND_ANDROID} is allowed as {@code command}. + * + * @param device Bluetooth headset. + * @param command A vendor-specific command. + * @param arg The argument that will be attached to the command. + * @return {@code false} if there is no headset connected, or if the command is not an allowed + * vendor-specific unsolicited result code, or on error. {@code true} otherwise. + * @throws IllegalArgumentException if {@code command} is {@code null}. + */ + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public boolean sendVendorSpecificResultCode(BluetoothDevice device, String command, + String arg) { + if (DBG) { + log("sendVendorSpecificResultCode()"); + } + if (command == null) { + throw new IllegalArgumentException("command is null"); + } + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && isValidDevice(device)) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.sendVendorSpecificResultCode(device, command, arg, + mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Select a connected device as active. + * + * The active device selection is per profile. An active device's + * purpose is profile-specific. For example, in HFP and HSP profiles, + * it is the device used for phone call audio. If a remote device is not + * connected, it cannot be selected as active. + * + * <p> This API returns false in scenarios like the profile on the + * device is not connected or Bluetooth is not turned on. + * When this API returns true, it is guaranteed that the + * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted + * with the active device. + * + * @param device Remote Bluetooth Device, could be null if phone call audio should not be + * streamed to a headset + * @return false on immediate error, true otherwise + * @hide + */ + @RequiresLegacyBluetoothAdminPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(allOf = { + android.Manifest.permission.BLUETOOTH_CONNECT, + android.Manifest.permission.MODIFY_PHONE_STATE, + }) + @UnsupportedAppUsage(trackingBug = 171933273) + public boolean setActiveDevice(@Nullable BluetoothDevice device) { + if (DBG) { + Log.d(TAG, "setActiveDevice: " + device); + } + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled() && (device == null || isValidDevice(device))) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.setActiveDevice(device, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Get the connected device that is active. + * + * @return the connected device that is active or null if no device + * is active. + * @hide + */ + @UnsupportedAppUsage(trackingBug = 171933273) + @Nullable + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) + public BluetoothDevice getActiveDevice() { + if (VDBG) Log.d(TAG, "getActiveDevice"); + final IBluetoothHeadset service = mService; + final BluetoothDevice defaultValue = null; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<BluetoothDevice> recv = + new SynchronousResultReceiver(); + service.getActiveDevice(mAttributionSource, recv); + return Attributable.setAttributionSource( + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue), + mAttributionSource); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an + * active connection. + * + * @return true if in-band ringing is enabled, false if in-band ringing is disabled + * @hide + */ + @SystemApi + @RequiresLegacyBluetoothPermission + @RequiresBluetoothConnectPermission + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public boolean isInbandRingingEnabled() { + if (DBG) log("isInbandRingingEnabled()"); + final IBluetoothHeadset service = mService; + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isInbandRingingEnabled(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (RemoteException | TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } + } + return defaultValue; + } + + /** + * Check if in-band ringing is supported for this platform. + * + * @return true if in-band ringing is supported, false if in-band ringing is not supported + * @hide + */ + public static boolean isInbandRingingSupported(Context context) { + return context.getResources().getBoolean( + com.android.internal.R.bool.config_bluetooth_hfp_inband_ringing_support); + } + + @SuppressLint("AndroidFrameworkBluetoothPermission") + private final IBluetoothProfileServiceConnection mConnection = + new IBluetoothProfileServiceConnection.Stub() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + if (DBG) Log.d(TAG, "Proxy object connected"); + mService = IBluetoothHeadset.Stub.asInterface(service); + mHandler.sendMessage(mHandler.obtainMessage( + MESSAGE_HEADSET_SERVICE_CONNECTED)); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + if (DBG) Log.d(TAG, "Proxy object disconnected"); + doUnbind(); + mHandler.sendMessage(mHandler.obtainMessage( + MESSAGE_HEADSET_SERVICE_DISCONNECTED)); + } + }; + + @UnsupportedAppUsage + private boolean isEnabled() { + return mAdapter.getState() == BluetoothAdapter.STATE_ON; + } + + private boolean isDisabled() { + return mAdapter.getState() == BluetoothAdapter.STATE_OFF; + } + + private static boolean isValidDevice(BluetoothDevice device) { + return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); + } + + private static void log(String msg) { + Log.d(TAG, msg); + } + + @SuppressLint("AndroidFrameworkBluetoothPermission") + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_HEADSET_SERVICE_CONNECTED: { + if (mServiceListener != null) { + mServiceListener.onServiceConnected(BluetoothProfile.HEADSET, + BluetoothHeadset.this); + } + break; + } + case MESSAGE_HEADSET_SERVICE_DISCONNECTED: { + if (mServiceListener != null) { + mServiceListener.onServiceDisconnected(BluetoothProfile.HEADSET); + } + break; + } + } + } + }; +} |