summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--framework/Android.bp13
-rw-r--r--framework/java/android/bluetooth/Attributable.java55
-rw-r--r--framework/java/android/bluetooth/BluetoothA2dp.java1149
-rwxr-xr-xframework/java/android/bluetooth/BluetoothA2dpSink.java516
-rw-r--r--framework/java/android/bluetooth/BluetoothActivityEnergyInfo.java199
-rw-r--r--framework/java/android/bluetooth/BluetoothAdapter.java4427
-rw-r--r--framework/java/android/bluetooth/BluetoothAssignedNumbers.java1171
-rw-r--r--framework/java/android/bluetooth/BluetoothAudioConfig.java117
-rw-r--r--framework/java/android/bluetooth/BluetoothAvrcp.java93
-rw-r--r--framework/java/android/bluetooth/BluetoothAvrcpController.java298
-rw-r--r--framework/java/android/bluetooth/BluetoothAvrcpPlayerSettings.java193
-rwxr-xr-xframework/java/android/bluetooth/BluetoothClass.java441
-rw-r--r--framework/java/android/bluetooth/BluetoothCodecConfig.java807
-rw-r--r--framework/java/android/bluetooth/BluetoothCodecStatus.java208
-rw-r--r--framework/java/android/bluetooth/BluetoothCsipSetCoordinator.java555
-rw-r--r--framework/java/android/bluetooth/BluetoothDevice.java2831
-rw-r--r--framework/java/android/bluetooth/BluetoothDevicePicker.java80
-rw-r--r--framework/java/android/bluetooth/BluetoothGatt.java1848
-rw-r--r--framework/java/android/bluetooth/BluetoothGattCallback.java267
-rw-r--r--framework/java/android/bluetooth/BluetoothGattCharacteristic.java806
-rw-r--r--framework/java/android/bluetooth/BluetoothGattDescriptor.java291
-rw-r--r--framework/java/android/bluetooth/BluetoothGattIncludedService.java112
-rw-r--r--framework/java/android/bluetooth/BluetoothGattServer.java954
-rw-r--r--framework/java/android/bluetooth/BluetoothGattServerCallback.java202
-rw-r--r--framework/java/android/bluetooth/BluetoothGattService.java395
-rw-r--r--framework/java/android/bluetooth/BluetoothHeadset.java1505
-rw-r--r--framework/java/android/bluetooth/BluetoothHeadsetClient.java1356
-rw-r--r--framework/java/android/bluetooth/BluetoothHeadsetClientCall.java323
-rw-r--r--framework/java/android/bluetooth/BluetoothHealth.java386
-rw-r--r--framework/java/android/bluetooth/BluetoothHealthAppConfiguration.java115
-rw-r--r--framework/java/android/bluetooth/BluetoothHealthCallback.java88
-rw-r--r--framework/java/android/bluetooth/BluetoothHearingAid.java691
-rw-r--r--framework/java/android/bluetooth/BluetoothHidDevice.java848
-rw-r--r--framework/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java131
-rw-r--r--framework/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java123
-rw-r--r--framework/java/android/bluetooth/BluetoothHidHost.java831
-rw-r--r--framework/java/android/bluetooth/BluetoothInputStream.java93
-rw-r--r--framework/java/android/bluetooth/BluetoothLeAudio.java829
-rw-r--r--framework/java/android/bluetooth/BluetoothLeAudioCodecConfig.java129
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcast.java287
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java140
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcastSourceInfo.java788
-rw-r--r--framework/java/android/bluetooth/BluetoothLeCall.java285
-rw-r--r--framework/java/android/bluetooth/BluetoothLeCallControl.java899
-rw-r--r--framework/java/android/bluetooth/BluetoothManager.java283
-rw-r--r--framework/java/android/bluetooth/BluetoothMap.java513
-rw-r--r--framework/java/android/bluetooth/BluetoothMapClient.java686
-rw-r--r--framework/java/android/bluetooth/BluetoothMasInstance.java107
-rw-r--r--framework/java/android/bluetooth/BluetoothOutputStream.java81
-rw-r--r--framework/java/android/bluetooth/BluetoothPan.java525
-rw-r--r--framework/java/android/bluetooth/BluetoothPbap.java317
-rw-r--r--framework/java/android/bluetooth/BluetoothPbapClient.java405
-rw-r--r--framework/java/android/bluetooth/BluetoothProfile.java459
-rw-r--r--framework/java/android/bluetooth/BluetoothProfileConnector.java220
-rw-r--r--framework/java/android/bluetooth/BluetoothSap.java491
-rw-r--r--framework/java/android/bluetooth/BluetoothServerSocket.java266
-rw-r--r--framework/java/android/bluetooth/BluetoothSocket.java809
-rw-r--r--framework/java/android/bluetooth/BluetoothStatusCodes.java292
-rw-r--r--framework/java/android/bluetooth/BluetoothUtils.java41
-rw-r--r--framework/java/android/bluetooth/BluetoothUuid.java394
-rw-r--r--framework/java/android/bluetooth/BluetoothVolumeControl.java337
-rw-r--r--framework/java/android/bluetooth/BufferConstraint.java105
-rw-r--r--framework/java/android/bluetooth/BufferConstraints.java96
-rw-r--r--framework/java/android/bluetooth/OWNERS7
-rw-r--r--framework/java/android/bluetooth/OobData.java958
-rw-r--r--framework/java/android/bluetooth/SdpDipRecord.java104
-rw-r--r--framework/java/android/bluetooth/SdpMasRecord.java150
-rw-r--r--framework/java/android/bluetooth/SdpMnsRecord.java114
-rw-r--r--framework/java/android/bluetooth/SdpOppOpsRecord.java121
-rw-r--r--framework/java/android/bluetooth/SdpPseRecord.java129
-rw-r--r--framework/java/android/bluetooth/SdpRecord.java77
-rw-r--r--framework/java/android/bluetooth/SdpSapsRecord.java90
-rw-r--r--framework/java/android/bluetooth/UidTraffic.java126
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresBluetoothAdvertisePermission.java39
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresBluetoothConnectPermission.java39
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresBluetoothLocationPermission.java41
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresBluetoothScanPermission.java39
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothAdminPermission.java39
-rw-r--r--framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothPermission.java39
-rw-r--r--framework/java/android/bluetooth/le/AdvertiseCallback.java74
-rw-r--r--framework/java/android/bluetooth/le/AdvertiseData.java374
-rw-r--r--framework/java/android/bluetooth/le/AdvertiseSettings.java277
-rw-r--r--framework/java/android/bluetooth/le/AdvertisingSet.java230
-rw-r--r--framework/java/android/bluetooth/le/AdvertisingSetCallback.java164
-rw-r--r--framework/java/android/bluetooth/le/AdvertisingSetParameters.java513
-rw-r--r--framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java756
-rw-r--r--framework/java/android/bluetooth/le/BluetoothLeScanner.java658
-rw-r--r--framework/java/android/bluetooth/le/BluetoothLeUtils.java158
-rw-r--r--framework/java/android/bluetooth/le/OWNERS4
-rw-r--r--framework/java/android/bluetooth/le/PeriodicAdvertisingCallback.java81
-rw-r--r--framework/java/android/bluetooth/le/PeriodicAdvertisingManager.java264
-rw-r--r--framework/java/android/bluetooth/le/PeriodicAdvertisingParameters.java121
-rw-r--r--framework/java/android/bluetooth/le/PeriodicAdvertisingReport.java186
-rw-r--r--framework/java/android/bluetooth/le/ResultStorageDescriptor.java96
-rw-r--r--framework/java/android/bluetooth/le/ScanCallback.java88
-rw-r--r--framework/java/android/bluetooth/le/ScanFilter.java910
-rw-r--r--framework/java/android/bluetooth/le/ScanRecord.java378
-rw-r--r--framework/java/android/bluetooth/le/ScanResult.java361
-rw-r--r--framework/java/android/bluetooth/le/ScanSettings.java438
-rw-r--r--framework/java/android/bluetooth/le/TransportBlock.java177
-rw-r--r--framework/java/android/bluetooth/le/TransportDiscoveryData.java184
-rw-r--r--framework/java/android/bluetooth/le/TruncatedFilter.java64
-rw-r--r--framework/java/android/bluetooth/package.html38
-rw-r--r--framework/tests/Android.bp19
-rw-r--r--framework/tests/AndroidManifest.xml47
-rw-r--r--framework/tests/AndroidTest.xml32
-rw-r--r--framework/tests/OWNERS1
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothCodecConfigTest.java329
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothCodecStatusTest.java493
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothInstrumentation.java123
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java58
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothRebootStressTest.java94
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothStressTest.java393
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothTestRunner.java225
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothTestUtils.java1649
-rw-r--r--framework/tests/src/android/bluetooth/BluetoothUuidTest.java66
-rw-r--r--framework/tests/src/android/bluetooth/le/AdvertiseDataTest.java144
-rw-r--r--framework/tests/src/android/bluetooth/le/ScanFilterTest.java215
-rw-r--r--framework/tests/src/android/bluetooth/le/ScanRecordTest.java148
-rw-r--r--framework/tests/src/android/bluetooth/le/ScanResultTest.java56
-rw-r--r--framework/tests/src/android/bluetooth/le/ScanSettingsTest.java64
-rw-r--r--service/Android.bp28
-rw-r--r--service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java141
-rw-r--r--service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java76
-rw-r--r--service/java/com/android/server/bluetooth/BluetoothManagerService.java2956
-rw-r--r--service/java/com/android/server/bluetooth/BluetoothModeChangeHelper.java162
-rw-r--r--service/java/com/android/server/bluetooth/BluetoothService.java71
-rw-r--r--service/tests/Android.bp30
-rw-r--r--service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java123
129 files changed, 50751 insertions, 0 deletions
diff --git a/framework/Android.bp b/framework/Android.bp
new file mode 100644
index 0000000000..dd98aebb6e
--- /dev/null
+++ b/framework/Android.bp
@@ -0,0 +1,13 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "framework-bluetooth-sources",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ path: "java",
+ visibility: ["//frameworks/base"],
+}
diff --git a/framework/java/android/bluetooth/Attributable.java b/framework/java/android/bluetooth/Attributable.java
new file mode 100644
index 0000000000..d9acbe3eef
--- /dev/null
+++ b/framework/java/android/bluetooth/Attributable.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.AttributionSource;
+
+import java.util.List;
+
+/**
+ * Marker interface for a class which can have an {@link AttributionSource}
+ * assigned to it; these are typically {@link android.os.Parcelable} classes
+ * which need to be updated after crossing Binder transaction boundaries.
+ *
+ * @hide
+ */
+public interface Attributable {
+ void setAttributionSource(@NonNull AttributionSource attributionSource);
+
+ static @Nullable <T extends Attributable> T setAttributionSource(
+ @Nullable T attributable,
+ @NonNull AttributionSource attributionSource) {
+ if (attributable != null) {
+ attributable.setAttributionSource(attributionSource);
+ }
+ return attributable;
+ }
+
+ static @Nullable <T extends Attributable> List<T> setAttributionSource(
+ @Nullable List<T> attributableList,
+ @NonNull AttributionSource attributionSource) {
+ if (attributableList != null) {
+ final int size = attributableList.size();
+ for (int i = 0; i < size; i++) {
+ setAttributionSource(attributableList.get(i), attributionSource);
+ }
+ }
+ return attributableList;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothA2dp.java b/framework/java/android/bluetooth/BluetoothA2dp.java
new file mode 100644
index 0000000000..8b9cec17a1
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothA2dp.java
@@ -0,0 +1,1149 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+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.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * This class provides the public APIs to control the Bluetooth A2DP
+ * profile.
+ *
+ * <p>BluetoothA2dp is a proxy object for controlling the Bluetooth A2DP
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothA2dp proxy object.
+ *
+ * <p> Android only supports one connected Bluetooth A2dp device at a time.
+ * Each method is protected with its appropriate permission.
+ */
+public final class BluetoothA2dp implements BluetoothProfile {
+ private static final String TAG = "BluetoothA2dp";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the A2DP
+ * 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.a2dp.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Playing state of the A2DP
+ * 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_PLAYING}, {@link #STATE_NOT_PLAYING},
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PLAYING_STATE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.PLAYING_STATE_CHANGED";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AVRCP_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.AVRCP_CONNECTION_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.a2dp.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Audio Codec state of the
+ * A2DP Source profile.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothCodecStatus#EXTRA_CODEC_STATUS} - The codec status. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device if the device is currently
+ * connected, otherwise it is not included.</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @UnsupportedAppUsage(trackingBug = 181103983)
+ public static final String ACTION_CODEC_CONFIG_CHANGED =
+ "android.bluetooth.a2dp.profile.action.CODEC_CONFIG_CHANGED";
+
+ /**
+ * A2DP sink device is streaming music. This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+ */
+ public static final int STATE_PLAYING = 10;
+
+ /**
+ * A2DP sink device is NOT streaming music. This state can be one of
+ * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of
+ * {@link #ACTION_PLAYING_STATE_CHANGED} intent.
+ */
+ public static final int STATE_NOT_PLAYING = 11;
+
+ /** @hide */
+ @IntDef(prefix = "OPTIONAL_CODECS_", value = {
+ OPTIONAL_CODECS_SUPPORT_UNKNOWN,
+ OPTIONAL_CODECS_NOT_SUPPORTED,
+ OPTIONAL_CODECS_SUPPORTED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OptionalCodecsSupportStatus {}
+
+ /**
+ * We don't have a stored preference for whether or not the given A2DP sink device supports
+ * optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_SUPPORT_UNKNOWN = -1;
+
+ /**
+ * The given A2DP sink device does not support optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_NOT_SUPPORTED = 0;
+
+ /**
+ * The given A2DP sink device does support optional codecs.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_SUPPORTED = 1;
+
+ /** @hide */
+ @IntDef(prefix = "OPTIONAL_CODECS_PREF_", value = {
+ OPTIONAL_CODECS_PREF_UNKNOWN,
+ OPTIONAL_CODECS_PREF_DISABLED,
+ OPTIONAL_CODECS_PREF_ENABLED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OptionalCodecsPreferenceStatus {}
+
+ /**
+ * We don't have a stored preference for whether optional codecs should be enabled or
+ * disabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_UNKNOWN = -1;
+
+ /**
+ * Optional codecs should be disabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_DISABLED = 0;
+
+ /**
+ * Optional codecs should be enabled for the given A2DP device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int OPTIONAL_CODECS_PREF_ENABLED = 1;
+
+ /** @hide */
+ @IntDef(prefix = "DYNAMIC_BUFFER_SUPPORT_", value = {
+ DYNAMIC_BUFFER_SUPPORT_NONE,
+ DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD,
+ DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is not supported.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_NONE = 0;
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is A2DP offload.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD = 1;
+
+ /**
+ * Indicates the supported type of Dynamic Audio Buffer is A2DP software encoding.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothA2dp> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.A2DP, "BluetoothA2dp",
+ IBluetoothA2dp.class.getName()) {
+ @Override
+ public IBluetoothA2dp getServiceInterface(IBinder service) {
+ return IBluetoothA2dp.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothA2dp proxy object for interacting with the local
+ * Bluetooth A2DP service.
+ */
+ /* package */ BluetoothA2dp(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ @UnsupportedAppUsage
+ /*package*/ void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothA2dp getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ // The empty finalize needs to be kept or the
+ // cts signature tests would fail.
+ }
+
+ /**
+ * Initiate connection to a profile of the remote Bluetooth device.
+ *
+ * <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
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ 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
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ 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 IBluetoothA2dp service = getService();
+ 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 IBluetoothA2dp service = getService();
+ 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.getDevicesMatchingConnectionStatesWithAttribution(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 @BtProfileState int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ 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;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, A2DP audio streaming
+ * is to the active A2DP Sink device. 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 the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(trackingBug = 171933273)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ 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("getActiveDevice()");
+ final IBluetoothA2dp service = getService();
+ 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;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothA2dp service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ 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 IBluetoothA2dp service = getService();
+ 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 if Avrcp device supports the absolute volume feature.
+ *
+ * @return true if device supports absolute volume
+ * @hide
+ */
+ @RequiresNoPermission
+ public boolean isAvrcpAbsoluteVolumeSupported() {
+ if (DBG) Log.d(TAG, "isAvrcpAbsoluteVolumeSupported");
+ final IBluetoothA2dp service = getService();
+ 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.isAvrcpAbsoluteVolumeSupported(recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Tells remote device to set an absolute volume. Only if absolute volume is supported
+ *
+ * @param volume Absolute volume to be set on AVRCP side
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setAvrcpAbsoluteVolume(int volume) {
+ if (DBG) Log.d(TAG, "setAvrcpAbsoluteVolume");
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.setAvrcpAbsoluteVolume(volume, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Check if A2DP profile is streaming music.
+ *
+ * @param device BluetoothDevice device
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isA2dpPlaying(BluetoothDevice device) {
+ if (DBG) log("isA2dpPlaying(" + device + ")");
+ final IBluetoothA2dp service = getService();
+ 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.isA2dpPlaying(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;
+ }
+
+ /**
+ * This function checks if the remote device is an AVCRP
+ * target and thus whether we should send volume keys
+ * changes or not.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean shouldSendVolumeKeys(BluetoothDevice device) {
+ if (isEnabled() && isValidDevice(device)) {
+ ParcelUuid[] uuids = device.getUuids();
+ if (uuids == null) return false;
+
+ for (ParcelUuid uuid : uuids) {
+ if (uuid.equals(BluetoothUuid.AVRCP_TARGET)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the current codec status (configuration and capability).
+ *
+ * @param device the remote Bluetooth device.
+ * @return the current codec status
+ * @hide
+ */
+ @UnsupportedAppUsage(trackingBug = 181103983)
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getCodecStatus(" + device + ")");
+ verifyDeviceNotNull(device, "getCodecStatus");
+ final IBluetoothA2dp service = getService();
+ final BluetoothCodecStatus defaultValue = null;
+ 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<BluetoothCodecStatus> recv =
+ new SynchronousResultReceiver();
+ service.getCodecStatus(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 the codec configuration preference.
+ *
+ * @param device the remote Bluetooth device.
+ * @param codecConfig the codec configuration preference
+ * @hide
+ */
+ @UnsupportedAppUsage(trackingBug = 181103983)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setCodecConfigPreference(@NonNull BluetoothDevice device,
+ @NonNull BluetoothCodecConfig codecConfig) {
+ if (DBG) Log.d(TAG, "setCodecConfigPreference(" + device + ")");
+ verifyDeviceNotNull(device, "setCodecConfigPreference");
+ if (codecConfig == null) {
+ Log.e(TAG, "setCodecConfigPreference: Codec config can't be null");
+ throw new IllegalArgumentException("codecConfig cannot be null");
+ }
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ service.setCodecConfigPreference(device, codecConfig, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Enables the optional codecs.
+ *
+ * @param device the remote Bluetooth device.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void enableOptionalCodecs(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "enableOptionalCodecs(" + device + ")");
+ verifyDeviceNotNull(device, "enableOptionalCodecs");
+ enableDisableOptionalCodecs(device, true);
+ }
+
+ /**
+ * Disables the optional codecs.
+ *
+ * @param device the remote Bluetooth device.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void disableOptionalCodecs(@NonNull BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "disableOptionalCodecs(" + device + ")");
+ verifyDeviceNotNull(device, "disableOptionalCodecs");
+ enableDisableOptionalCodecs(device, false);
+ }
+
+ /**
+ * Enables or disables the optional codecs.
+ *
+ * @param device the remote Bluetooth device.
+ * @param enable if true, enable the optional codecs, other disable them
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void enableDisableOptionalCodecs(BluetoothDevice device, boolean enable) {
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ if (enable) {
+ service.enableOptionalCodecs(device, mAttributionSource);
+ } else {
+ service.disableOptionalCodecs(device, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Returns whether this device supports optional codecs.
+ *
+ * @param device The device to check
+ * @return one of OPTIONAL_CODECS_SUPPORT_UNKNOWN, OPTIONAL_CODECS_NOT_SUPPORTED, or
+ * OPTIONAL_CODECS_SUPPORTED.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @OptionalCodecsSupportStatus
+ public int isOptionalCodecsSupported(@NonNull BluetoothDevice device) {
+ if (DBG) log("isOptionalCodecsSupported(" + device + ")");
+ verifyDeviceNotNull(device, "isOptionalCodecsSupported");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = OPTIONAL_CODECS_SUPPORT_UNKNOWN;
+ 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.supportsOptionalCodecs(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;
+ }
+
+ /**
+ * Returns whether this device should have optional codecs enabled.
+ *
+ * @param device The device in question.
+ * @return one of OPTIONAL_CODECS_PREF_UNKNOWN, OPTIONAL_CODECS_PREF_ENABLED, or
+ * OPTIONAL_CODECS_PREF_DISABLED.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @OptionalCodecsPreferenceStatus
+ public int isOptionalCodecsEnabled(@NonNull BluetoothDevice device) {
+ if (DBG) log("isOptionalCodecsEnabled(" + device + ")");
+ verifyDeviceNotNull(device, "isOptionalCodecsEnabled");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = OPTIONAL_CODECS_PREF_UNKNOWN;
+ 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.getOptionalCodecsEnabled(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 a persistent preference for whether a given device should have optional codecs enabled.
+ *
+ * @param device The device to set this preference for.
+ * @param value Whether the optional codecs should be enabled for this device. This should be
+ * one of OPTIONAL_CODECS_PREF_UNKNOWN, OPTIONAL_CODECS_PREF_ENABLED, or
+ * OPTIONAL_CODECS_PREF_DISABLED.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setOptionalCodecsEnabled(@NonNull BluetoothDevice device,
+ @OptionalCodecsPreferenceStatus int value) {
+ if (DBG) log("setOptionalCodecsEnabled(" + device + ")");
+ verifyDeviceNotNull(device, "setOptionalCodecsEnabled");
+ if (value != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN
+ && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED
+ && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) {
+ Log.e(TAG, "Invalid value passed to setOptionalCodecsEnabled: " + value);
+ return;
+ }
+ final IBluetoothA2dp service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ service.setOptionalCodecsEnabled(device, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Get the supported type of the Dynamic Audio Buffer.
+ * <p>Possible return values are
+ * {@link #DYNAMIC_BUFFER_SUPPORT_NONE},
+ * {@link #DYNAMIC_BUFFER_SUPPORT_A2DP_OFFLOAD},
+ * {@link #DYNAMIC_BUFFER_SUPPORT_A2DP_SOFTWARE_ENCODING}.
+ *
+ * @return supported type of Dynamic Audio Buffer feature
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Type int getDynamicBufferSupport() {
+ if (VDBG) log("getDynamicBufferSupport()");
+ final IBluetoothA2dp service = getService();
+ final int defaultValue = DYNAMIC_BUFFER_SUPPORT_NONE;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getDynamicBufferSupport(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Return the record of {@link BufferConstraints} object that
+ * has the default/maximum/minimum audio buffer. This can be used to inform what the controller
+ * has support for the audio buffer.
+ *
+ * @return a record with {@link BufferConstraints} or null if report is unavailable
+ * or unsupported
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @Nullable BufferConstraints getBufferConstraints() {
+ if (VDBG) log("getBufferConstraints()");
+ final IBluetoothA2dp service = getService();
+ final BufferConstraints 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<BufferConstraints> recv =
+ new SynchronousResultReceiver();
+ service.getBufferConstraints(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 Dynamic Audio Buffer Size.
+ *
+ * @param codec audio codec
+ * @param value buffer millis
+ * @return true to indicate success, or false on immediate error
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setBufferLengthMillis(@BluetoothCodecConfig.SourceCodecType int codec,
+ int value) {
+ if (VDBG) log("setBufferLengthMillis(" + codec + ", " + value + ")");
+ if (value < 0) {
+ Log.e(TAG, "Trying to set audio buffer length to a negative value: " + value);
+ return false;
+ }
+ final IBluetoothA2dp service = getService();
+ 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.setBufferLengthMillis(codec, value, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ case STATE_PLAYING:
+ return "playing";
+ case STATE_NOT_PLAYING:
+ return "not playing";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private void verifyDeviceNotNull(BluetoothDevice device, String methodName) {
+ if (device == null) {
+ Log.e(TAG, methodName + ": device param is null");
+ throw new IllegalArgumentException("Device cannot be null");
+ }
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothA2dpSink.java b/framework/java/android/bluetooth/BluetoothA2dpSink.java
new file mode 100755
index 0000000000..59416818ce
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothA2dpSink.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2014 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.Manifest;
+import android.annotation.NonNull;
+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.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth A2DP Sink
+ * profile.
+ *
+ * <p>BluetoothA2dpSink is a proxy object for controlling the Bluetooth A2DP Sink
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothA2dpSink proxy object.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothA2dpSink implements BluetoothProfile {
+ private static final String TAG = "BluetoothA2dpSink";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the A2DP Sink
+ * 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}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothA2dpSink> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.A2DP_SINK,
+ "BluetoothA2dpSink", IBluetoothA2dpSink.class.getName()) {
+ @Override
+ public IBluetoothA2dpSink getServiceInterface(IBinder service) {
+ return IBluetoothA2dpSink.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothA2dp proxy object for interacting with the local
+ * Bluetooth A2DP service.
+ */
+ /* package */ BluetoothA2dpSink(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /*package*/ void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothA2dpSink getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> Currently, the system supports only 1 connection to the
+ * A2DP 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ 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.connect(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
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ 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.disconnect(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}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothA2dpSink service = getService();
+ 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.getConnectedDevices(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}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothA2dpSink service = getService();
+ 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}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getConnectionState(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ 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.getConnectionState(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 current audio configuration for the A2DP source device,
+ * or null if the device has no audio configuration
+ *
+ * @param device Remote bluetooth device.
+ * @return audio configuration for the device, or null
+ *
+ * {@see BluetoothAudioConfig}
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+ if (VDBG) log("getAudioConfig(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ final BluetoothAudioConfig defaultValue = null;
+ 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<BluetoothAudioConfig> recv =
+ new SynchronousResultReceiver();
+ service.getAudioConfig(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 #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothA2dpSink service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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 IBluetoothA2dpSink service = getService();
+ 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;
+ }
+
+ /**
+ * Check if audio is playing on the bluetooth device (A2DP profile is streaming music).
+ *
+ * @param device BluetoothDevice device
+ * @return true if audio is playing (A2dp is streaming music), false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean isAudioPlaying(@NonNull BluetoothDevice device) {
+ if (VDBG) log("isAudioPlaying(" + device + ")");
+ final IBluetoothA2dpSink service = getService();
+ 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.isA2dpPlaying(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;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ case BluetoothA2dp.STATE_PLAYING:
+ return "playing";
+ case BluetoothA2dp.STATE_NOT_PLAYING:
+ return "not playing";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothActivityEnergyInfo.java b/framework/java/android/bluetooth/BluetoothActivityEnergyInfo.java
new file mode 100644
index 0000000000..c17a7b4b3d
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothActivityEnergyInfo.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2014 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 android.annotation.ElapsedRealtimeLong;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Record of energy and activity information from controller and
+ * underlying bt stack state.Timestamp the record with system
+ * time.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+public final class BluetoothActivityEnergyInfo implements Parcelable {
+ private final long mTimestamp;
+ private int mBluetoothStackState;
+ private long mControllerTxTimeMs;
+ private long mControllerRxTimeMs;
+ private long mControllerIdleTimeMs;
+ private long mControllerEnergyUsed;
+ private List<UidTraffic> mUidTraffic;
+
+ /** @hide */
+ @IntDef(prefix = { "BT_STACK_STATE_" }, value = {
+ BT_STACK_STATE_INVALID,
+ BT_STACK_STATE_STATE_ACTIVE,
+ BT_STACK_STATE_STATE_SCANNING,
+ BT_STACK_STATE_STATE_IDLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BluetoothStackState {}
+
+ public static final int BT_STACK_STATE_INVALID = 0;
+ public static final int BT_STACK_STATE_STATE_ACTIVE = 1;
+ public static final int BT_STACK_STATE_STATE_SCANNING = 2;
+ public static final int BT_STACK_STATE_STATE_IDLE = 3;
+
+ /** @hide */
+ public BluetoothActivityEnergyInfo(long timestamp, int stackState,
+ long txTime, long rxTime, long idleTime, long energyUsed) {
+ mTimestamp = timestamp;
+ mBluetoothStackState = stackState;
+ mControllerTxTimeMs = txTime;
+ mControllerRxTimeMs = rxTime;
+ mControllerIdleTimeMs = idleTime;
+ mControllerEnergyUsed = energyUsed;
+ }
+
+ /** @hide */
+ private BluetoothActivityEnergyInfo(Parcel in) {
+ mTimestamp = in.readLong();
+ mBluetoothStackState = in.readInt();
+ mControllerTxTimeMs = in.readLong();
+ mControllerRxTimeMs = in.readLong();
+ mControllerIdleTimeMs = in.readLong();
+ mControllerEnergyUsed = in.readLong();
+ mUidTraffic = in.createTypedArrayList(UidTraffic.CREATOR);
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return "BluetoothActivityEnergyInfo{"
+ + " mTimestamp=" + mTimestamp
+ + " mBluetoothStackState=" + mBluetoothStackState
+ + " mControllerTxTimeMs=" + mControllerTxTimeMs
+ + " mControllerRxTimeMs=" + mControllerRxTimeMs
+ + " mControllerIdleTimeMs=" + mControllerIdleTimeMs
+ + " mControllerEnergyUsed=" + mControllerEnergyUsed
+ + " mUidTraffic=" + mUidTraffic
+ + " }";
+ }
+
+ public static final @NonNull Parcelable.Creator<BluetoothActivityEnergyInfo> CREATOR =
+ new Parcelable.Creator<BluetoothActivityEnergyInfo>() {
+ public BluetoothActivityEnergyInfo createFromParcel(Parcel in) {
+ return new BluetoothActivityEnergyInfo(in);
+ }
+
+ public BluetoothActivityEnergyInfo[] newArray(int size) {
+ return new BluetoothActivityEnergyInfo[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mTimestamp);
+ out.writeInt(mBluetoothStackState);
+ out.writeLong(mControllerTxTimeMs);
+ out.writeLong(mControllerRxTimeMs);
+ out.writeLong(mControllerIdleTimeMs);
+ out.writeLong(mControllerEnergyUsed);
+ out.writeTypedList(mUidTraffic);
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Get the Bluetooth stack state associated with the energy info.
+ *
+ * @return one of {@link #BluetoothStackState} states
+ */
+ @BluetoothStackState
+ public int getBluetoothStackState() {
+ return mBluetoothStackState;
+ }
+
+ /**
+ * @return tx time in ms
+ */
+ public long getControllerTxTimeMillis() {
+ return mControllerTxTimeMs;
+ }
+
+ /**
+ * @return rx time in ms
+ */
+ public long getControllerRxTimeMillis() {
+ return mControllerRxTimeMs;
+ }
+
+ /**
+ * @return idle time in ms
+ */
+ public long getControllerIdleTimeMillis() {
+ return mControllerIdleTimeMs;
+ }
+
+ /**
+ * Get the product of current (mA), voltage (V), and time (ms).
+ *
+ * @return energy used
+ */
+ public long getControllerEnergyUsed() {
+ return mControllerEnergyUsed;
+ }
+
+ /**
+ * @return timestamp (real time elapsed in milliseconds since boot) of record creation
+ */
+ public @ElapsedRealtimeLong long getTimestampMillis() {
+ return mTimestamp;
+ }
+
+ /**
+ * Get the {@link List} of each application {@link android.bluetooth.UidTraffic}.
+ *
+ * @return current {@link List} of {@link android.bluetooth.UidTraffic}
+ */
+ public @NonNull List<UidTraffic> getUidTraffic() {
+ if (mUidTraffic == null) {
+ return Collections.emptyList();
+ }
+ return mUidTraffic;
+ }
+
+ /** @hide */
+ public void setUidTraffic(List<UidTraffic> traffic) {
+ mUidTraffic = traffic;
+ }
+
+ /**
+ * @return true if the record Tx time, Rx time, and Idle time are more than 0.
+ */
+ public boolean isValid() {
+ return ((mControllerTxTimeMs >= 0) && (mControllerRxTimeMs >= 0)
+ && (mControllerIdleTimeMs >= 0));
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java
new file mode 100644
index 0000000000..ecbad5d60e
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAdapter.java
@@ -0,0 +1,4427 @@
+/*
+ * Copyright 2009-2016 The Android Open Source Project
+ * Copyright 2015 Samsung LSI
+ *
+ * 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 java.util.Objects.requireNonNull;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi; //import android.app.PropertyInvalidatedCache;
+import android.bluetooth.BluetoothDevice.Transport;
+import android.bluetooth.BluetoothProfile.ConnectionPolicy;
+import android.bluetooth.annotations.RequiresBluetoothAdvertisePermission;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.bluetooth.le.BluetoothLeAdvertiser;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.PeriodicAdvertisingManager;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.sysprop.BluetoothProperties;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Represents the local device Bluetooth adapter. The {@link BluetoothAdapter}
+ * lets you perform fundamental Bluetooth tasks, such as initiate
+ * device discovery, query a list of bonded (paired) devices,
+ * instantiate a {@link BluetoothDevice} using a known MAC address, and create
+ * a {@link BluetoothServerSocket} to listen for connection requests from other
+ * devices, and start a scan for Bluetooth LE devices.
+ *
+ * <p>To get a {@link BluetoothAdapter} representing the local Bluetooth
+ * adapter, call the {@link BluetoothManager#getAdapter} function on {@link BluetoothManager}.
+ * On JELLY_BEAN_MR1 and below you will need to use the static {@link #getDefaultAdapter}
+ * method instead.
+ * </p><p>
+ * Fundamentally, this is your starting point for all
+ * Bluetooth actions. Once you have the local adapter, you can get a set of
+ * {@link BluetoothDevice} objects representing all paired devices with
+ * {@link #getBondedDevices()}; start device discovery with
+ * {@link #startDiscovery()}; or create a {@link BluetoothServerSocket} to
+ * listen for incoming RFComm connection requests with {@link
+ * #listenUsingRfcommWithServiceRecord(String, UUID)}; listen for incoming L2CAP Connection-oriented
+ * Channels (CoC) connection requests with {@link #listenUsingL2capChannel()}; or start a scan for
+ * Bluetooth LE devices with {@link #startLeScan(LeScanCallback callback)}.
+ * </p>
+ * <p>This class is thread safe.</p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about using Bluetooth, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer
+ * guide.
+ * </p>
+ * </div>
+ *
+ * {@see BluetoothDevice}
+ * {@see BluetoothServerSocket}
+ */
+public final class BluetoothAdapter {
+ private static final String TAG = "BluetoothAdapter";
+ private static final String DESCRIPTOR = "android.bluetooth.BluetoothAdapter";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Default MAC address reported to a client that does not have the
+ * android.permission.LOCAL_MAC_ADDRESS permission.
+ *
+ * @hide
+ */
+ public static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00";
+
+ /**
+ * Sentinel error value for this class. Guaranteed to not equal any other
+ * integer constant in this class. Provided as a convenience for functions
+ * that require a sentinel error value, for example:
+ * <p><code>Intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ * BluetoothAdapter.ERROR)</code>
+ */
+ public static final int ERROR = Integer.MIN_VALUE;
+
+ /**
+ * Broadcast Action: The state of the local Bluetooth adapter has been
+ * changed.
+ * <p>For example, Bluetooth has been turned on or off.
+ * <p>Always contains the extra fields {@link #EXTRA_STATE} and {@link
+ * #EXTRA_PREVIOUS_STATE} containing the new and old states
+ * respectively.
+ */
+ @RequiresLegacyBluetoothPermission
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_STATE_CHANGED = "android.bluetooth.adapter.action.STATE_CHANGED";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_STATE_CHANGED}
+ * intents to request the current power state. Possible values are:
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF},
+ */
+ public static final String EXTRA_STATE = "android.bluetooth.adapter.extra.STATE";
+ /**
+ * Used as an int extra field in {@link #ACTION_STATE_CHANGED}
+ * intents to request the previous power state. Possible values are:
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF}
+ */
+ public static final String EXTRA_PREVIOUS_STATE =
+ "android.bluetooth.adapter.extra.PREVIOUS_STATE";
+
+ /** @hide */
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_OFF,
+ STATE_TURNING_ON,
+ STATE_ON,
+ STATE_TURNING_OFF,
+ STATE_BLE_TURNING_ON,
+ STATE_BLE_ON,
+ STATE_BLE_TURNING_OFF
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AdapterState {}
+
+ /**
+ * Indicates the local Bluetooth adapter is off.
+ */
+ public static final int STATE_OFF = 10;
+ /**
+ * Indicates the local Bluetooth adapter is turning on. However local
+ * clients should wait for {@link #STATE_ON} before attempting to
+ * use the adapter.
+ */
+ public static final int STATE_TURNING_ON = 11;
+ /**
+ * Indicates the local Bluetooth adapter is on, and ready for use.
+ */
+ public static final int STATE_ON = 12;
+ /**
+ * Indicates the local Bluetooth adapter is turning off. Local clients
+ * should immediately attempt graceful disconnection of any remote links.
+ */
+ public static final int STATE_TURNING_OFF = 13;
+
+ /**
+ * Indicates the local Bluetooth adapter is turning Bluetooth LE mode on.
+ *
+ * @hide
+ */
+ public static final int STATE_BLE_TURNING_ON = 14;
+
+ /**
+ * Indicates the local Bluetooth adapter is in LE only mode.
+ *
+ * @hide
+ */
+ public static final int STATE_BLE_ON = 15;
+
+ /**
+ * Indicates the local Bluetooth adapter is turning off LE only mode.
+ *
+ * @hide
+ */
+ public static final int STATE_BLE_TURNING_OFF = 16;
+
+ /**
+ * UUID of the GATT Read Characteristics for LE_PSM value.
+ *
+ * @hide
+ */
+ public static final UUID LE_PSM_CHARACTERISTIC_UUID =
+ UUID.fromString("2d410339-82b6-42aa-b34e-e2e01df8cc1a");
+
+ /**
+ * Human-readable string helper for AdapterState
+ *
+ * @hide
+ */
+ public static String nameForState(@AdapterState int state) {
+ switch (state) {
+ case STATE_OFF:
+ return "OFF";
+ case STATE_TURNING_ON:
+ return "TURNING_ON";
+ case STATE_ON:
+ return "ON";
+ case STATE_TURNING_OFF:
+ return "TURNING_OFF";
+ case STATE_BLE_TURNING_ON:
+ return "BLE_TURNING_ON";
+ case STATE_BLE_ON:
+ return "BLE_ON";
+ case STATE_BLE_TURNING_OFF:
+ return "BLE_TURNING_OFF";
+ default:
+ return "?!?!? (" + state + ")";
+ }
+ }
+
+ /**
+ * Activity Action: Show a system activity that requests discoverable mode.
+ * This activity will also request the user to turn on Bluetooth if it
+ * is not currently enabled.
+ * <p>Discoverable mode is equivalent to {@link
+ * #SCAN_MODE_CONNECTABLE_DISCOVERABLE}. It allows remote devices to see
+ * this Bluetooth adapter when they perform a discovery.
+ * <p>For privacy, Android is not discoverable by default.
+ * <p>The sender of this Intent can optionally use extra field {@link
+ * #EXTRA_DISCOVERABLE_DURATION} to request the duration of
+ * discoverability. Currently the default duration is 120 seconds, and
+ * maximum duration is capped at 300 seconds for each request.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be the duration (in seconds) of discoverability or
+ * {@link android.app.Activity#RESULT_CANCELED} if the user rejected
+ * discoverability or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_SCAN_MODE_CHANGED}
+ * for global notification whenever the scan mode changes. For example, an
+ * application can be notified when the device has ended discoverability.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";
+
+ /**
+ * Used as an optional int extra field in {@link
+ * #ACTION_REQUEST_DISCOVERABLE} intents to request a specific duration
+ * for discoverability in seconds. The current default is 120 seconds, and
+ * requests over 300 seconds will be capped. These values could change.
+ */
+ public static final String EXTRA_DISCOVERABLE_DURATION =
+ "android.bluetooth.adapter.extra.DISCOVERABLE_DURATION";
+
+ /**
+ * Activity Action: Show a system activity that allows the user to turn on
+ * Bluetooth.
+ * <p>This system activity will return once Bluetooth has completed turning
+ * on, or the user has decided not to turn Bluetooth on.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be {@link android.app.Activity#RESULT_OK} if Bluetooth has been
+ * turned on or {@link android.app.Activity#RESULT_CANCELED} if the user
+ * has rejected the request or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_STATE_CHANGED}
+ * for global notification whenever Bluetooth is turned on or off.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_ENABLE = "android.bluetooth.adapter.action.REQUEST_ENABLE";
+
+ /**
+ * Activity Action: Show a system activity that allows the user to turn off
+ * Bluetooth. This is used only if permission review is enabled which is for
+ * apps targeting API less than 23 require a permission review before any of
+ * the app's components can run.
+ * <p>This system activity will return once Bluetooth has completed turning
+ * off, or the user has decided not to turn Bluetooth off.
+ * <p>Notification of the result of this activity is posted using the
+ * {@link android.app.Activity#onActivityResult} callback. The
+ * <code>resultCode</code>
+ * will be {@link android.app.Activity#RESULT_OK} if Bluetooth has been
+ * turned off or {@link android.app.Activity#RESULT_CANCELED} if the user
+ * has rejected the request or an error has occurred.
+ * <p>Applications can also listen for {@link #ACTION_STATE_CHANGED}
+ * for global notification whenever Bluetooth is turned on or off.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String
+ ACTION_REQUEST_DISABLE = "android.bluetooth.adapter.action.REQUEST_DISABLE";
+
+ /**
+ * Activity Action: Show a system activity that allows user to enable BLE scans even when
+ * Bluetooth is turned off.<p>
+ *
+ * Notification of result of this activity is posted using
+ * {@link android.app.Activity#onActivityResult}. The <code>resultCode</code> will be
+ * {@link android.app.Activity#RESULT_OK} if BLE scan always available setting is turned on or
+ * {@link android.app.Activity#RESULT_CANCELED} if the user has rejected the request or an
+ * error occurred.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE =
+ "android.bluetooth.adapter.action.REQUEST_BLE_SCAN_ALWAYS_AVAILABLE";
+
+ /**
+ * Broadcast Action: Indicates the Bluetooth scan mode of the local Adapter
+ * has changed.
+ * <p>Always contains the extra fields {@link #EXTRA_SCAN_MODE} and {@link
+ * #EXTRA_PREVIOUS_SCAN_MODE} containing the new and old scan modes
+ * respectively.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_SCAN_MODE_CHANGED = "android.bluetooth.adapter.action.SCAN_MODE_CHANGED";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED}
+ * intents to request the current scan mode. Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE},
+ */
+ public static final String EXTRA_SCAN_MODE = "android.bluetooth.adapter.extra.SCAN_MODE";
+ /**
+ * Used as an int extra field in {@link #ACTION_SCAN_MODE_CHANGED}
+ * intents to request the previous scan mode. Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE},
+ */
+ public static final String EXTRA_PREVIOUS_SCAN_MODE =
+ "android.bluetooth.adapter.extra.PREVIOUS_SCAN_MODE";
+
+ /** @hide */
+ @IntDef(prefix = { "SCAN_" }, value = {
+ SCAN_MODE_NONE,
+ SCAN_MODE_CONNECTABLE,
+ SCAN_MODE_CONNECTABLE_DISCOVERABLE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScanMode {}
+
+ /**
+ * Indicates that both inquiry scan and page scan are disabled on the local
+ * Bluetooth adapter. Therefore this device is neither discoverable
+ * nor connectable from remote Bluetooth devices.
+ */
+ public static final int SCAN_MODE_NONE = 20;
+ /**
+ * Indicates that inquiry scan is disabled, but page scan is enabled on the
+ * local Bluetooth adapter. Therefore this device is not discoverable from
+ * remote Bluetooth devices, but is connectable from remote devices that
+ * have previously discovered this device.
+ */
+ public static final int SCAN_MODE_CONNECTABLE = 21;
+ /**
+ * Indicates that both inquiry scan and page scan are enabled on the local
+ * Bluetooth adapter. Therefore this device is both discoverable and
+ * connectable from remote Bluetooth devices.
+ */
+ public static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE = 23;
+
+ /**
+ * Device only has a display.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_OUT = 0;
+
+ /**
+ * Device has a display and the ability to input Yes/No.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_IO = 1;
+
+ /**
+ * Device only has a keyboard for entry but no display.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_IN = 2;
+
+ /**
+ * Device has no Input or Output capability.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_NONE = 3;
+
+ /**
+ * Device has a display and a full keyboard.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_KBDISP = 4;
+
+ /**
+ * Maximum range value for Input/Output capabilities.
+ *
+ * <p>This should be updated when adding a new Input/Output capability. Other code
+ * like validation depends on this being accurate.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_MAX = 5;
+
+ /**
+ * The Input/Output capability of the device is unknown.
+ *
+ * @hide
+ */
+ public static final int IO_CAPABILITY_UNKNOWN = 255;
+
+ /** @hide */
+ @IntDef({IO_CAPABILITY_OUT, IO_CAPABILITY_IO, IO_CAPABILITY_IN, IO_CAPABILITY_NONE,
+ IO_CAPABILITY_KBDISP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface IoCapability {}
+
+ /** @hide */
+ @IntDef(prefix = "ACTIVE_DEVICE_", value = {ACTIVE_DEVICE_AUDIO,
+ ACTIVE_DEVICE_PHONE_CALL, ACTIVE_DEVICE_ALL})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActiveDeviceUse {}
+
+ /**
+ * Use the specified device for audio (a2dp and hearing aid profile)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_AUDIO = 0;
+
+ /**
+ * Use the specified device for phone calls (headset profile and hearing
+ * aid profile)
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_PHONE_CALL = 1;
+
+ /**
+ * Use the specified device for a2dp, hearing aid profile, and headset profile
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACTIVE_DEVICE_ALL = 2;
+
+ /** @hide */
+ @IntDef({BluetoothProfile.HEADSET, BluetoothProfile.A2DP,
+ BluetoothProfile.HEARING_AID})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ActiveDeviceProfile {}
+
+ /**
+ * Broadcast Action: The local Bluetooth adapter has started the remote
+ * device discovery process.
+ * <p>This usually involves an inquiry scan of about 12 seconds, followed
+ * by a page scan of each new device to retrieve its Bluetooth name.
+ * <p>Register for {@link BluetoothDevice#ACTION_FOUND} to be notified as
+ * remote Bluetooth devices are found.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_STARTED = "android.bluetooth.adapter.action.DISCOVERY_STARTED";
+ /**
+ * Broadcast Action: The local Bluetooth adapter has finished the device
+ * discovery process.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_DISCOVERY_FINISHED = "android.bluetooth.adapter.action.DISCOVERY_FINISHED";
+
+ /**
+ * Broadcast Action: The local Bluetooth adapter has changed its friendly
+ * Bluetooth name.
+ * <p>This name is visible to remote Bluetooth devices.
+ * <p>Always contains the extra field {@link #EXTRA_LOCAL_NAME} containing
+ * the name.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String
+ ACTION_LOCAL_NAME_CHANGED = "android.bluetooth.adapter.action.LOCAL_NAME_CHANGED";
+ /**
+ * Used as a String extra field in {@link #ACTION_LOCAL_NAME_CHANGED}
+ * intents to request the local Bluetooth name.
+ */
+ public static final String EXTRA_LOCAL_NAME = "android.bluetooth.adapter.extra.LOCAL_NAME";
+
+ /**
+ * Intent used to broadcast the change in connection state of the local
+ * Bluetooth adapter to a profile of the remote device. When the adapter is
+ * not connected to any profiles of any remote devices and it attempts a
+ * connection to a profile this intent will be sent. Once connected, this intent
+ * will not be sent for any more connection attempts to any profiles of any
+ * remote device. When the adapter disconnects from the last profile its
+ * connected to of any remote device, this intent will be sent.
+ *
+ * <p> This intent is useful for applications that are only concerned about
+ * whether the local adapter is connected to any profile of any device and
+ * are not really concerned about which profile. For example, an application
+ * which displays an icon to display whether Bluetooth is connected or not
+ * can use this intent.
+ *
+ * <p>This intent will have 3 extras:
+ * {@link #EXTRA_CONNECTION_STATE} - The current connection state.
+ * {@link #EXTRA_PREVIOUS_CONNECTION_STATE}- The previous connection state.
+ * {@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+ *
+ * {@link #EXTRA_CONNECTION_STATE} or {@link #EXTRA_PREVIOUS_CONNECTION_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.adapter.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Extra used by {@link #ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * This extra represents the current connection state.
+ */
+ public static final String EXTRA_CONNECTION_STATE =
+ "android.bluetooth.adapter.extra.CONNECTION_STATE";
+
+ /**
+ * Extra used by {@link #ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * This extra represents the previous connection state.
+ */
+ public static final String EXTRA_PREVIOUS_CONNECTION_STATE =
+ "android.bluetooth.adapter.extra.PREVIOUS_CONNECTION_STATE";
+
+ /**
+ * Broadcast Action: The Bluetooth adapter state has changed in LE only mode.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi public static final String ACTION_BLE_STATE_CHANGED =
+ "android.bluetooth.adapter.action.BLE_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in the Bluetooth address
+ * of the local Bluetooth adapter.
+ * <p>Always contains the extra field {@link
+ * #EXTRA_BLUETOOTH_ADDRESS} containing the Bluetooth address.
+ *
+ * Note: only system level processes are allowed to send this
+ * defined broadcast.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLUETOOTH_ADDRESS_CHANGED =
+ "android.bluetooth.adapter.action.BLUETOOTH_ADDRESS_CHANGED";
+
+ /**
+ * Used as a String extra field in {@link
+ * #ACTION_BLUETOOTH_ADDRESS_CHANGED} intent to store the local
+ * Bluetooth address.
+ *
+ * @hide
+ */
+ public static final String EXTRA_BLUETOOTH_ADDRESS =
+ "android.bluetooth.adapter.extra.BLUETOOTH_ADDRESS";
+
+ /**
+ * Broadcast Action: The notifys Bluetooth ACL connected event. This will be
+ * by BLE Always on enabled application to know the ACL_CONNECTED event
+ * when Bluetooth state in STATE_BLE_ON. This denotes GATT connection
+ * as Bluetooth LE is the only feature available in STATE_BLE_ON
+ *
+ * This is counterpart of {@link BluetoothDevice#ACTION_ACL_CONNECTED} which
+ * works in Bluetooth state STATE_ON
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLE_ACL_CONNECTED =
+ "android.bluetooth.adapter.action.BLE_ACL_CONNECTED";
+
+ /**
+ * Broadcast Action: The notifys Bluetooth ACL connected event. This will be
+ * by BLE Always on enabled application to know the ACL_DISCONNECTED event
+ * when Bluetooth state in STATE_BLE_ON. This denotes GATT disconnection as Bluetooth
+ * LE is the only feature available in STATE_BLE_ON
+ *
+ * This is counterpart of {@link BluetoothDevice#ACTION_ACL_DISCONNECTED} which
+ * works in Bluetooth state STATE_ON
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BLE_ACL_DISCONNECTED =
+ "android.bluetooth.adapter.action.BLE_ACL_DISCONNECTED";
+
+ /** The profile is in disconnected state */
+ public static final int STATE_DISCONNECTED =
+ 0; //BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTED;
+ /** The profile is in connecting state */
+ public static final int STATE_CONNECTING = 1; //BluetoothProtoEnums.CONNECTION_STATE_CONNECTING;
+ /** The profile is in connected state */
+ public static final int STATE_CONNECTED = 2; //BluetoothProtoEnums.CONNECTION_STATE_CONNECTED;
+ /** The profile is in disconnecting state */
+ public static final int STATE_DISCONNECTING =
+ 3; //BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTING;
+
+ /** @hide */
+ public static final String BLUETOOTH_MANAGER_SERVICE = "bluetooth_manager";
+ private final IBinder mToken;
+
+
+ /**
+ * When creating a ServerSocket using listenUsingRfcommOn() or
+ * listenUsingL2capOn() use SOCKET_CHANNEL_AUTO_STATIC to create
+ * a ServerSocket that auto assigns a channel number to the first
+ * bluetooth socket.
+ * The channel number assigned to this first Bluetooth Socket will
+ * be stored in the ServerSocket, and reused for subsequent Bluetooth
+ * sockets.
+ *
+ * @hide
+ */
+ public static final int SOCKET_CHANNEL_AUTO_STATIC_NO_SDP = -2;
+
+
+ private static final int ADDRESS_LENGTH = 17;
+
+ /**
+ * Lazily initialized singleton. Guaranteed final after first object
+ * constructed.
+ */
+ private static BluetoothAdapter sAdapter;
+
+ private BluetoothLeScanner mBluetoothLeScanner;
+ private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+ private PeriodicAdvertisingManager mPeriodicAdvertisingManager;
+
+ private final IBluetoothManager mManagerService;
+ private final AttributionSource mAttributionSource;
+
+ // Yeah, keeping both mService and sService isn't pretty, but it's too late
+ // in the current release for a major refactoring, so we leave them both
+ // intact until this can be cleaned up in a future release
+
+ @UnsupportedAppUsage
+ @GuardedBy("mServiceLock")
+ private IBluetooth mService;
+ private final ReentrantReadWriteLock mServiceLock = new ReentrantReadWriteLock();
+
+ @GuardedBy("sServiceLock")
+ private static boolean sServiceRegistered;
+ @GuardedBy("sServiceLock")
+ private static IBluetooth sService;
+ private static final Object sServiceLock = new Object();
+
+ private final Object mLock = new Object();
+ private final Map<LeScanCallback, ScanCallback> mLeScanClients;
+ private final Map<BluetoothDevice, List<Pair<OnMetadataChangedListener, Executor>>>
+ mMetadataListeners = new HashMap<>();
+ private final Map<BluetoothConnectionCallback, Executor>
+ mBluetoothConnectionCallbackExecutorMap = new HashMap<>();
+
+ /**
+ * Bluetooth metadata listener. Overrides the default BluetoothMetadataListener
+ * implementation.
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothMetadataListener mBluetoothMetadataListener =
+ new IBluetoothMetadataListener.Stub() {
+ @Override
+ public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ synchronized (mMetadataListeners) {
+ if (mMetadataListeners.containsKey(device)) {
+ List<Pair<OnMetadataChangedListener, Executor>> list =
+ mMetadataListeners.get(device);
+ for (Pair<OnMetadataChangedListener, Executor> pair : list) {
+ OnMetadataChangedListener listener = pair.first;
+ Executor executor = pair.second;
+ executor.execute(() -> {
+ listener.onMetadataChanged(device, key, value);
+ });
+ }
+ }
+ }
+ return;
+ }
+ };
+
+ /**
+ * Get a handle to the default local Bluetooth adapter.
+ * <p>
+ * Currently Android only supports one Bluetooth adapter, but the API could
+ * be extended to support more. This will always return the default adapter.
+ * </p>
+ *
+ * @return the default local adapter, or null if Bluetooth is not supported
+ * on this hardware platform
+ * @deprecated this method will continue to work, but developers are
+ * strongly encouraged to migrate to using
+ * {@link BluetoothManager#getAdapter()}, since that approach
+ * enables support for {@link Context#createAttributionContext}.
+ */
+ @Deprecated
+ @RequiresNoPermission
+ public static synchronized BluetoothAdapter getDefaultAdapter() {
+ if (sAdapter == null) {
+ sAdapter = createAdapter(AttributionSource.myAttributionSource());
+ }
+ return sAdapter;
+ }
+
+ /** {@hide} */
+ public static BluetoothAdapter createAdapter(AttributionSource attributionSource) {
+ IBinder binder = ServiceManager.getService(BLUETOOTH_MANAGER_SERVICE);
+ if (binder != null) {
+ return new BluetoothAdapter(IBluetoothManager.Stub.asInterface(binder),
+ attributionSource);
+ } else {
+ Log.e(TAG, "Bluetooth binder is null");
+ return null;
+ }
+ }
+
+ /**
+ * Use {@link #getDefaultAdapter} to get the BluetoothAdapter instance.
+ */
+ BluetoothAdapter(IBluetoothManager managerService, AttributionSource attributionSource) {
+ mManagerService = Objects.requireNonNull(managerService);
+ mAttributionSource = Objects.requireNonNull(attributionSource);
+ synchronized (mServiceLock.writeLock()) {
+ mService = getBluetoothService(mManagerCallback);
+ }
+ mLeScanClients = new HashMap<LeScanCallback, ScanCallback>();
+ mToken = new Binder(DESCRIPTOR);
+ }
+
+ /**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address.
+ * <p>Valid Bluetooth hardware addresses must be upper case, in a format
+ * such as "00:11:22:33:AA:BB". The helper {@link #checkBluetoothAddress} is
+ * available to validate a Bluetooth address.
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address, even if this adapter has never seen that device.
+ *
+ * @param address valid Bluetooth MAC address
+ * @throws IllegalArgumentException if address is invalid
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getRemoteDevice(String address) {
+ final BluetoothDevice res = new BluetoothDevice(address);
+ res.setAttributionSource(mAttributionSource);
+ return res;
+ }
+
+ /**
+ * Get a {@link BluetoothDevice} object for the given Bluetooth hardware
+ * address.
+ * <p>Valid Bluetooth hardware addresses must be 6 bytes. This method
+ * expects the address in network byte order (MSB first).
+ * <p>A {@link BluetoothDevice} will always be returned for a valid
+ * hardware address, even if this adapter has never seen that device.
+ *
+ * @param address Bluetooth MAC address (6 bytes)
+ * @throws IllegalArgumentException if address is invalid
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getRemoteDevice(byte[] address) {
+ if (address == null || address.length != 6) {
+ throw new IllegalArgumentException("Bluetooth address must have 6 bytes");
+ }
+ final BluetoothDevice res = new BluetoothDevice(
+ String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X", address[0], address[1],
+ address[2], address[3], address[4], address[5]));
+ res.setAttributionSource(mAttributionSource);
+ return res;
+ }
+
+ /**
+ * Returns a {@link BluetoothLeAdvertiser} object for Bluetooth LE Advertising operations.
+ * Will return null if Bluetooth is turned off or if Bluetooth LE Advertising is not
+ * supported on this device.
+ * <p>
+ * Use {@link #isMultipleAdvertisementSupported()} to check whether LE Advertising is supported
+ * on this device before calling this method.
+ */
+ @RequiresNoPermission
+ public BluetoothLeAdvertiser getBluetoothLeAdvertiser() {
+ if (!getLeAccess()) {
+ return null;
+ }
+ synchronized (mLock) {
+ if (mBluetoothLeAdvertiser == null) {
+ mBluetoothLeAdvertiser = new BluetoothLeAdvertiser(this);
+ }
+ return mBluetoothLeAdvertiser;
+ }
+ }
+
+ /**
+ * Returns a {@link PeriodicAdvertisingManager} object for Bluetooth LE Periodic Advertising
+ * operations. Will return null if Bluetooth is turned off or if Bluetooth LE Periodic
+ * Advertising is not supported on this device.
+ * <p>
+ * Use {@link #isLePeriodicAdvertisingSupported()} to check whether LE Periodic Advertising is
+ * supported on this device before calling this method.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public PeriodicAdvertisingManager getPeriodicAdvertisingManager() {
+ if (!getLeAccess()) {
+ return null;
+ }
+
+ if (!isLePeriodicAdvertisingSupported()) {
+ return null;
+ }
+
+ synchronized (mLock) {
+ if (mPeriodicAdvertisingManager == null) {
+ mPeriodicAdvertisingManager = new PeriodicAdvertisingManager(this);
+ }
+ return mPeriodicAdvertisingManager;
+ }
+ }
+
+ /**
+ * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations.
+ */
+ @RequiresNoPermission
+ public BluetoothLeScanner getBluetoothLeScanner() {
+ if (!getLeAccess()) {
+ return null;
+ }
+ synchronized (mLock) {
+ if (mBluetoothLeScanner == null) {
+ mBluetoothLeScanner = new BluetoothLeScanner(this);
+ }
+ return mBluetoothLeScanner;
+ }
+ }
+
+ /**
+ * Return true if Bluetooth is currently enabled and ready for use.
+ * <p>Equivalent to:
+ * <code>getBluetoothState() == STATE_ON</code>
+ *
+ * @return true if the local adapter is turned on
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isEnabled() {
+ return getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ /**
+ * Return true if Bluetooth LE(Always BLE On feature) is currently
+ * enabled and ready for use
+ * <p>This returns true if current state is either STATE_ON or STATE_BLE_ON
+ *
+ * @return true if the local Bluetooth LE adapter is turned on
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ public boolean isLeEnabled() {
+ final int state = getLeState();
+ if (DBG) {
+ Log.d(TAG, "isLeEnabled(): " + BluetoothAdapter.nameForState(state));
+ }
+ return (state == BluetoothAdapter.STATE_ON
+ || state == BluetoothAdapter.STATE_BLE_ON
+ || state == BluetoothAdapter.STATE_TURNING_ON
+ || state == BluetoothAdapter.STATE_TURNING_OFF);
+ }
+
+ /**
+ * Turns off Bluetooth LE which was earlier turned on by calling enableBLE().
+ *
+ * <p> If the internal Adapter state is STATE_BLE_ON, this would trigger the transition
+ * to STATE_OFF and completely shut-down Bluetooth
+ *
+ * <p> If the Adapter state is STATE_ON, This would unregister the existance of
+ * special Bluetooth LE application and hence the further turning off of Bluetooth
+ * from UI would ensure the complete turn-off of Bluetooth rather than staying back
+ * BLE only state
+ *
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_BLE_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
+ * later transition to either {@link #STATE_BLE_ON} or {@link
+ * #STATE_OFF} based on the existance of the further Always BLE ON enabled applications
+ * If this call returns false then there was an
+ * immediate problem that will prevent the QAdapter from being turned off -
+ * such as the QAadapter already being turned off.
+ *
+ * @return true to indicate success, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disableBLE() {
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
+ try {
+ return mManagerService.disableBle(mAttributionSource, mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Applications who want to only use Bluetooth Low Energy (BLE) can call enableBLE.
+ *
+ * enableBLE registers the existence of an app using only LE functions.
+ *
+ * enableBLE may enable Bluetooth to an LE only mode so that an app can use
+ * LE related features (BluetoothGatt or BluetoothGattServer classes)
+ *
+ * If the user disables Bluetooth while an app is registered to use LE only features,
+ * Bluetooth will remain on in LE only mode for the app.
+ *
+ * When Bluetooth is in LE only mode, it is not shown as ON to the UI.
+ *
+ * <p>This is an asynchronous call: it returns immediately, and
+ * clients should listen for {@link #ACTION_BLE_STATE_CHANGED}
+ * to be notified of adapter state changes.
+ *
+ * If this call returns * true, then the adapter state is either in a mode where
+ * LE is available, or will transition from {@link #STATE_OFF} to {@link #STATE_BLE_TURNING_ON},
+ * and some time later transition to either {@link #STATE_OFF} or {@link #STATE_BLE_ON}.
+ *
+ * If this call returns false then there was an immediate problem that prevents the
+ * adapter from being turned on - such as Airplane mode.
+ *
+ * {@link #ACTION_BLE_STATE_CHANGED} returns the Bluetooth Adapter's various
+ * states, It includes all the classic Bluetooth Adapter states along with
+ * internal BLE only states
+ *
+ * @return true to indicate Bluetooth LE will be available, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enableBLE() {
+ if (!isBleScanAlwaysAvailable()) {
+ return false;
+ }
+ try {
+ return mManagerService.enableBle(mAttributionSource, mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+
+ return false;
+ }
+
+ /*
+ private static final String BLUETOOTH_GET_STATE_CACHE_PROPERTY = "cache_key.bluetooth.get_state";
+
+ private final PropertyInvalidatedCache<Void, Integer> mBluetoothGetStateCache =
+ new PropertyInvalidatedCache<Void, Integer>(
+ 8, BLUETOOTH_GET_STATE_CACHE_PROPERTY) {
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ protected Integer recompute(Void query) {
+ try {
+ return mService.getState();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ };
+ */
+
+ /** @hide */
+ /*
+ @RequiresNoPermission
+ public void disableBluetoothGetStateCache() {
+ mBluetoothGetStateCache.disableLocal();
+ }
+ */
+
+ /** @hide */
+ /*
+ public static void invalidateBluetoothGetStateCache() {
+ PropertyInvalidatedCache.invalidateCache(BLUETOOTH_GET_STATE_CACHE_PROPERTY);
+ }
+ */
+
+ /**
+ * Fetch the current bluetooth state. If the service is down, return
+ * OFF.
+ */
+ @AdapterState
+ private int getStateInternal() {
+ int state = BluetoothAdapter.STATE_OFF;
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ //state = mBluetoothGetStateCache.query(null);
+ state = mService.getState();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return state;
+ }
+
+ /**
+ * Get the current state of the local Bluetooth adapter.
+ * <p>Possible return values are
+ * {@link #STATE_OFF},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF}.
+ *
+ * @return current state of Bluetooth adapter
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @AdapterState
+ public int getState() {
+ int state = getStateInternal();
+
+ // Consider all internal states as OFF
+ if (state == BluetoothAdapter.STATE_BLE_ON || state == BluetoothAdapter.STATE_BLE_TURNING_ON
+ || state == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
+ if (VDBG) {
+ Log.d(TAG, "Consider " + BluetoothAdapter.nameForState(state) + " state as OFF");
+ }
+ state = BluetoothAdapter.STATE_OFF;
+ }
+ if (VDBG) {
+ Log.d(TAG, "" + hashCode() + ": getState(). Returning " + BluetoothAdapter.nameForState(
+ state));
+ }
+ return state;
+ }
+
+ /**
+ * Get the current state of the local Bluetooth adapter
+ * <p>This returns current internal state of Adapter including LE ON/OFF
+ *
+ * <p>Possible return values are
+ * {@link #STATE_OFF},
+ * {@link #STATE_BLE_TURNING_ON},
+ * {@link #STATE_BLE_ON},
+ * {@link #STATE_TURNING_ON},
+ * {@link #STATE_ON},
+ * {@link #STATE_TURNING_OFF},
+ * {@link #STATE_BLE_TURNING_OFF}.
+ *
+ * @return current state of Bluetooth adapter
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ @AdapterState
+ @UnsupportedAppUsage(publicAlternatives = "Use {@link #getState()} instead to determine "
+ + "whether you can use BLE & BT classic.")
+ public int getLeState() {
+ int state = getStateInternal();
+
+ if (VDBG) {
+ Log.d(TAG, "getLeState() returning " + BluetoothAdapter.nameForState(state));
+ }
+ return state;
+ }
+
+ boolean getLeAccess() {
+ if (getLeState() == STATE_ON) {
+ return true;
+ } else if (getLeState() == STATE_BLE_ON) {
+ return true; // TODO: FILTER SYSTEM APPS HERE <--
+ }
+
+ return false;
+ }
+
+ /**
+ * Turn on the local Bluetooth adapter&mdash;do not use without explicit
+ * user action to turn on Bluetooth.
+ * <p>This powers on the underlying Bluetooth hardware, and starts all
+ * Bluetooth system services.
+ * <p class="caution"><strong>Bluetooth should never be enabled without
+ * direct user consent</strong>. If you want to turn on Bluetooth in order
+ * to create a wireless connection, you should use the {@link
+ * #ACTION_REQUEST_ENABLE} Intent, which will raise a dialog that requests
+ * user permission to turn on Bluetooth. The {@link #enable()} method is
+ * provided only for applications that include a user interface for changing
+ * system settings, such as a "power manager" app.</p>
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes. If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_OFF} to {@link #STATE_TURNING_ON}, and some time
+ * later transition to either {@link #STATE_OFF} or {@link
+ * #STATE_ON}. If this call returns false then there was an
+ * immediate problem that will prevent the adapter from being turned on -
+ * such as Airplane mode, or the adapter is already turned on.
+ *
+ * @return true to indicate adapter startup has begun, or false on immediate error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enable() {
+ if (isEnabled()) {
+ if (DBG) {
+ Log.d(TAG, "enable(): BT already enabled!");
+ }
+ return true;
+ }
+ try {
+ return mManagerService.enable(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Turn off the local Bluetooth adapter&mdash;do not use without explicit
+ * user action to turn off Bluetooth.
+ * <p>This gracefully shuts down all Bluetooth connections, stops Bluetooth
+ * system services, and powers down the underlying Bluetooth hardware.
+ * <p class="caution"><strong>Bluetooth should never be disabled without
+ * direct user consent</strong>. The {@link #disable()} method is
+ * provided only for applications that include a user interface for changing
+ * system settings, such as a "power manager" app.</p>
+ * <p>This is an asynchronous call: it will return immediately, and
+ * clients should listen for {@link #ACTION_STATE_CHANGED}
+ * to be notified of subsequent adapter state changes. If this call returns
+ * true, then the adapter state will immediately transition from {@link
+ * #STATE_ON} to {@link #STATE_TURNING_OFF}, and some time
+ * later transition to either {@link #STATE_OFF} or {@link
+ * #STATE_ON}. If this call returns false then there was an
+ * immediate problem that will prevent the adapter from being turned off -
+ * such as the adapter already being turned off.
+ *
+ * @return true to indicate adapter shutdown has begun, or false on immediate error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disable() {
+ try {
+ return mManagerService.disable(mAttributionSource, true);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Turn off the local Bluetooth adapter and don't persist the setting.
+ *
+ * @param persist Indicate whether the off state should be persisted following the next reboot
+ * @return true to indicate adapter shutdown has begun, or false on immediate error
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disable(boolean persist) {
+
+ try {
+ return mManagerService.disable(mAttributionSource, persist);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the hardware address of the local Bluetooth adapter.
+ * <p>For example, "00:11:22:AA:BB:CC".
+ *
+ * @return Bluetooth hardware address as string
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.LOCAL_MAC_ADDRESS,
+ })
+ public String getAddress() {
+ try {
+ return mManagerService.getAddress(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get the friendly Bluetooth name of the local Bluetooth adapter.
+ * <p>This name is visible to remote Bluetooth devices.
+ *
+ * @return the Bluetooth name, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getName() {
+ try {
+ return mManagerService.getName(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /** {@hide} */
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public int getNameLengthForAdvertise() {
+ try {
+ return mService.getNameLengthForAdvertise(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return -1;
+ }
+
+ /**
+ * Factory reset bluetooth settings.
+ *
+ * @return true to indicate that the config file was successfully cleared
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean factoryReset() {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && mService.factoryReset(mAttributionSource)
+ && mManagerService != null
+ && mManagerService.onFactoryReset(mAttributionSource)) {
+ return true;
+ }
+ Log.e(TAG, "factoryReset(): Setting persist.bluetooth.factoryreset to retry later");
+ BluetoothProperties.factory_reset(true);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Get the UUIDs supported by the local Bluetooth adapter.
+ *
+ * @return the UUIDs supported by the local Bluetooth Adapter.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @Nullable ParcelUuid[] getUuids() {
+ if (getState() != STATE_ON) {
+ return null;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getUuids(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Set the friendly Bluetooth name of the local Bluetooth adapter.
+ * <p>This name is visible to remote Bluetooth devices.
+ * <p>Valid Bluetooth names are a maximum of 248 bytes using UTF-8
+ * encoding, although many remote devices can only display the first
+ * 40 characters, and some may be limited to just 20.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @param name a valid Bluetooth name
+ * @return true if the name was set, false otherwise
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setName(String name) {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.setName(name, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the {@link BluetoothClass} Bluetooth Class of Device (CoD) of the local Bluetooth
+ * adapter.
+ *
+ * @return {@link BluetoothClass} Bluetooth CoD of local Bluetooth device.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothClass getBluetoothClass() {
+ if (getState() != STATE_ON) {
+ return null;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getBluetoothClass(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the {@link BluetoothClass} Bluetooth Class of Device (CoD) of the local Bluetooth
+ * adapter.
+ *
+ * <p>Note: This value persists across system reboot.
+ *
+ * @param bluetoothClass {@link BluetoothClass} to set the local Bluetooth adapter to.
+ * @return true if successful, false if unsuccessful.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setBluetoothClass(BluetoothClass bluetoothClass) {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.setBluetoothClass(bluetoothClass, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the Input/Output capability of the device for classic Bluetooth.
+ *
+ * @return Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN}, {@link #IO_CAPABILITY_NONE},
+ * {@link #IO_CAPABILITY_KBDISP} or {@link #IO_CAPABILITY_UNKNOWN}.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @IoCapability
+ public int getIoCapability() {
+ if (getState() != STATE_ON) return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) return mService.getIoCapability(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ }
+
+ /**
+ * Sets the Input/Output capability of the device for classic Bluetooth.
+ *
+ * <p>Changing the Input/Output capability of a device only takes effect on restarting the
+ * Bluetooth stack. You would need to restart the stack using {@link BluetoothAdapter#disable()}
+ * and {@link BluetoothAdapter#enable()} to see the changes.
+ *
+ * @param capability Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN},
+ * {@link #IO_CAPABILITY_NONE} or {@link #IO_CAPABILITY_KBDISP}.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setIoCapability(@IoCapability int capability) {
+ if (getState() != STATE_ON) return false;
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) return mService.setIoCapability(capability, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the Input/Output capability of the device for BLE operations.
+ *
+ * @return Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN}, {@link #IO_CAPABILITY_NONE},
+ * {@link #IO_CAPABILITY_KBDISP} or {@link #IO_CAPABILITY_UNKNOWN}.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @IoCapability
+ public int getLeIoCapability() {
+ if (getState() != STATE_ON) return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) return mService.getLeIoCapability(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothAdapter.IO_CAPABILITY_UNKNOWN;
+ }
+
+ /**
+ * Sets the Input/Output capability of the device for BLE operations.
+ *
+ * <p>Changing the Input/Output capability of a device only takes effect on restarting the
+ * Bluetooth stack. You would need to restart the stack using {@link BluetoothAdapter#disable()}
+ * and {@link BluetoothAdapter#enable()} to see the changes.
+ *
+ * @param capability Input/Output capability of the device. One of {@link #IO_CAPABILITY_OUT},
+ * {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_IN},
+ * {@link #IO_CAPABILITY_NONE} or {@link #IO_CAPABILITY_KBDISP}.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setLeIoCapability(@IoCapability int capability) {
+ if (getState() != STATE_ON) return false;
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) return mService.setLeIoCapability(capability, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.getMessage(), e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Get the current Bluetooth scan mode of the local Bluetooth adapter.
+ * <p>The Bluetooth scan mode determines if the local adapter is
+ * connectable and/or discoverable from remote Bluetooth devices.
+ * <p>Possible values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return {@link #SCAN_MODE_NONE}. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return scan mode
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @ScanMode
+ public int getScanMode() {
+ if (getState() != STATE_ON) {
+ return SCAN_MODE_NONE;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getScanMode(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return SCAN_MODE_NONE;
+ }
+
+ /**
+ * Set the Bluetooth scan mode of the local Bluetooth adapter.
+ * <p>The Bluetooth scan mode determines if the local adapter is
+ * connectable and/or discoverable from remote Bluetooth devices.
+ * <p>For privacy reasons, discoverable mode is automatically turned off
+ * after <code>durationMillis</code> milliseconds. For example, 120000 milliseconds should be
+ * enough for a remote device to initiate and complete its discovery process.
+ * <p>Valid scan mode values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ * <p>Applications cannot set the scan mode. They should use
+ * <code>startActivityForResult(
+ * BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE})
+ * </code>instead.
+ *
+ * @param mode valid scan mode
+ * @param durationMillis time in milliseconds to apply scan mode, only used for {@link
+ * #SCAN_MODE_CONNECTABLE_DISCOVERABLE}
+ * @return true if the scan mode was set, false otherwise
+ * @hide
+ */
+ @UnsupportedAppUsage(publicAlternatives = "Use {@link #ACTION_REQUEST_DISCOVERABLE}, which "
+ + "shows UI that confirms the user wants to go into discoverable mode.")
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean setScanMode(@ScanMode int mode, long durationMillis) {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ int durationSeconds = Math.toIntExact(durationMillis / 1000);
+ return mService.setScanMode(mode, durationSeconds, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } catch (ArithmeticException ex) {
+ Log.e(TAG, "setScanMode: Duration in seconds outside of the bounds of an int");
+ throw new IllegalArgumentException("Duration not in bounds. In seconds, the "
+ + "durationMillis must be in the range of an int");
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Set the Bluetooth scan mode of the local Bluetooth adapter.
+ * <p>The Bluetooth scan mode determines if the local adapter is
+ * connectable and/or discoverable from remote Bluetooth devices.
+ * <p>For privacy reasons, discoverable mode is automatically turned off
+ * after <code>duration</code> seconds. For example, 120 seconds should be
+ * enough for a remote device to initiate and complete its discovery
+ * process.
+ * <p>Valid scan mode values are:
+ * {@link #SCAN_MODE_NONE},
+ * {@link #SCAN_MODE_CONNECTABLE},
+ * {@link #SCAN_MODE_CONNECTABLE_DISCOVERABLE}.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ * <p>Applications cannot set the scan mode. They should use
+ * <code>startActivityForResult(
+ * BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE})
+ * </code>instead.
+ *
+ * @param mode valid scan mode
+ * @return true if the scan mode was set, false otherwise
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean setScanMode(@ScanMode int mode) {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.setScanMode(mode, getDiscoverableTimeout(), mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public int getDiscoverableTimeout() {
+ if (getState() != STATE_ON) {
+ return -1;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getDiscoverableTimeout(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return -1;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void setDiscoverableTimeout(int timeout) {
+ if (getState() != STATE_ON) {
+ return;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ mService.setDiscoverableTimeout(timeout, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get the end time of the latest remote device discovery process.
+ *
+ * @return the latest time that the bluetooth adapter was/will be in discovery mode, in
+ * milliseconds since the epoch. This time can be in the future if {@link #startDiscovery()} has
+ * been called recently.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public long getDiscoveryEndMillis() {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getDiscoveryEndMillis(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return -1;
+ }
+
+ /**
+ * Start the remote device discovery process.
+ * <p>The discovery process usually involves an inquiry scan of about 12
+ * seconds, followed by a page scan of each new device to retrieve its
+ * Bluetooth name.
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_DISCOVERY_STARTED} and {@link
+ * #ACTION_DISCOVERY_FINISHED} intents to determine exactly when the
+ * discovery starts and completes. Register for {@link
+ * BluetoothDevice#ACTION_FOUND} to be notified as remote Bluetooth devices
+ * are found.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery. Discovery is not managed by the Activity,
+ * but is run as a system service, so an application should always call
+ * {@link BluetoothAdapter#cancelDiscovery()} even if it
+ * did not directly request a discovery, just to be sure.
+ * <p>Device discovery will only find remote devices that are currently
+ * <i>discoverable</i> (inquiry scan enabled). Many Bluetooth devices are
+ * not discoverable by default, and need to be entered into a special mode.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth, wait for {@link #ACTION_STATE_CHANGED}
+ * with {@link #STATE_ON} to get the updated value.
+ * <p>If a device is currently bonding, this request will be queued and executed once that
+ * device has finished bonding. If a request is already queued, this request will be ignored.
+ *
+ * @return true on success, false on error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startDiscovery() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.startDiscovery(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Cancel the current device discovery process.
+ * <p>Because discovery is a heavyweight procedure for the Bluetooth
+ * adapter, this method should always be called before attempting to connect
+ * to a remote device with {@link
+ * android.bluetooth.BluetoothSocket#connect()}. Discovery is not managed by
+ * the Activity, but is run as a system service, so an application should
+ * always call cancel discovery even if it did not directly request a
+ * discovery, just to be sure.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return true on success, false on error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean cancelDiscovery() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.cancelDiscovery(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if the local Bluetooth adapter is currently in the device
+ * discovery process.
+ * <p>Device discovery is a heavyweight procedure. New connections to
+ * remote Bluetooth devices should not be attempted while discovery is in
+ * progress, and existing connections will experience limited bandwidth
+ * and high latency. Use {@link #cancelDiscovery()} to cancel an ongoing
+ * discovery.
+ * <p>Applications can also register for {@link #ACTION_DISCOVERY_STARTED}
+ * or {@link #ACTION_DISCOVERY_FINISHED} to be notified when discovery
+ * starts or completes.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return false. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return true if discovering
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean isDiscovering() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isDiscovering(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Removes the active device for the grouping of @ActiveDeviceUse specified
+ *
+ * @param profiles represents the purpose for which we are setting this as the active device.
+ * Possible values are:
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_AUDIO},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_PHONE_CALL},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_ALL}
+ * @return false on immediate error, true otherwise
+ * @throws IllegalArgumentException if device is null or profiles is not one of
+ * {@link ActiveDeviceUse}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean removeActiveDevice(@ActiveDeviceUse int profiles) {
+ if (profiles != ACTIVE_DEVICE_AUDIO && profiles != ACTIVE_DEVICE_PHONE_CALL
+ && profiles != ACTIVE_DEVICE_ALL) {
+ Log.e(TAG, "Invalid profiles param value in removeActiveDevice");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothAdapter.ACTIVE_DEVICE_AUDIO, "
+ + "BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL, or "
+ + "BluetoothAdapter.ACTIVE_DEVICE_ALL");
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ if (DBG) Log.d(TAG, "removeActiveDevice, profiles: " + profiles);
+ return mService.removeActiveDevice(profiles, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * Sets device as the active devices for the profiles passed into the function
+ *
+ * @param device is the remote bluetooth device
+ * @param profiles represents the purpose for which we are setting this as the active device.
+ * Possible values are:
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_AUDIO},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_PHONE_CALL},
+ * {@link BluetoothAdapter#ACTIVE_DEVICE_ALL}
+ * @return false on immediate error, true otherwise
+ * @throws IllegalArgumentException if device is null or profiles is not one of
+ * {@link ActiveDeviceUse}
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public boolean setActiveDevice(@NonNull BluetoothDevice device,
+ @ActiveDeviceUse int profiles) {
+ if (device == null) {
+ Log.e(TAG, "setActiveDevice: Null device passed as parameter");
+ throw new IllegalArgumentException("device cannot be null");
+ }
+ if (profiles != ACTIVE_DEVICE_AUDIO && profiles != ACTIVE_DEVICE_PHONE_CALL
+ && profiles != ACTIVE_DEVICE_ALL) {
+ Log.e(TAG, "Invalid profiles param value in setActiveDevice");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothAdapter.ACTIVE_DEVICE_AUDIO, "
+ + "BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL, or "
+ + "BluetoothAdapter.ACTIVE_DEVICE_ALL");
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ if (DBG) {
+ Log.d(TAG, "setActiveDevice, device: " + device + ", profiles: " + profiles);
+ }
+ return mService.setActiveDevice(device, profiles, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the active devices for the BluetoothProfile specified
+ *
+ * @param profile is the profile from which we want the active devices.
+ * Possible values are:
+ * {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEARING_AID}
+ * {@link BluetoothProfile#LE_AUDIO}
+ * @return A list of active bluetooth devices
+ * @throws IllegalArgumentException If profile is not one of {@link ActiveDeviceProfile}
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getActiveDevices(@ActiveDeviceProfile int profile) {
+ if (profile != BluetoothProfile.HEADSET
+ && profile != BluetoothProfile.A2DP
+ && profile != BluetoothProfile.HEARING_AID
+ && profile != BluetoothProfile.LE_AUDIO) {
+ Log.e(TAG, "Invalid profile param value in getActiveDevices");
+ throw new IllegalArgumentException("Profiles must be one of "
+ + "BluetoothProfile.A2DP, "
+ + "BluetoothProfile.HEARING_AID, or"
+ + "BluetoothProfile.HEARING_AID"
+ + "BluetoothProfile.LE_AUDIO");
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ if (DBG) {
+ Log.d(TAG, "getActiveDevices(profile= "
+ + BluetoothProfile.getProfileName(profile) + ")");
+ }
+ return mService.getActiveDevices(profile, mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return new ArrayList<>();
+ }
+
+ /**
+ * Return true if the multi advertisement is supported by the chipset
+ *
+ * @return true if Multiple Advertisement feature is supported
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isMultipleAdvertisementSupported() {
+ if (getState() != STATE_ON) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isMultiAdvertisementSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isMultipleAdvertisementSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Returns {@code true} if BLE scan is always available, {@code false} otherwise. <p>
+ *
+ * If this returns {@code true}, application can issue {@link BluetoothLeScanner#startScan} and
+ * fetch scan results even when Bluetooth is turned off.<p>
+ *
+ * To change this setting, use {@link #ACTION_REQUEST_BLE_SCAN_ALWAYS_AVAILABLE}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresNoPermission
+ public boolean isBleScanAlwaysAvailable() {
+ try {
+ return mManagerService.isBleScanAlwaysAvailable();
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception when calling isBleScanAlwaysAvailable", e);
+ return false;
+ }
+ }
+
+ /*
+ private static final String BLUETOOTH_FILTERING_CACHE_PROPERTY =
+ "cache_key.bluetooth.is_offloaded_filtering_supported";
+ private final PropertyInvalidatedCache<Void, Boolean> mBluetoothFilteringCache =
+ new PropertyInvalidatedCache<Void, Boolean>(
+ 8, BLUETOOTH_FILTERING_CACHE_PROPERTY) {
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ protected Boolean recompute(Void query) {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isOffloadedFilteringSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isOffloadedFilteringSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+
+ }
+ };
+ */
+
+ /** @hide */
+ /*
+ @RequiresNoPermission
+ public void disableIsOffloadedFilteringSupportedCache() {
+ mBluetoothFilteringCache.disableLocal();
+ }
+ */
+
+ /** @hide */
+ /*
+ public static void invalidateIsOffloadedFilteringSupportedCache() {
+ PropertyInvalidatedCache.invalidateCache(BLUETOOTH_FILTERING_CACHE_PROPERTY);
+ }
+ */
+
+ /**
+ * Return true if offloaded filters are supported
+ *
+ * @return true if chipset supports on-chip filtering
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isOffloadedFilteringSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ //return mBluetoothFilteringCache.query(null);
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isOffloadedFilteringSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isOffloadedFilteringSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if offloaded scan batching is supported
+ *
+ * @return true if chipset supports on-chip scan batching
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isOffloadedScanBatchingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isOffloadedScanBatchingSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isOffloadedScanBatchingSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE 2M PHY feature is supported.
+ *
+ * @return true if chipset supports LE 2M PHY feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLe2MPhySupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLe2MPhySupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isExtendedAdvertisingSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Coded PHY feature is supported.
+ *
+ * @return true if chipset supports LE Coded PHY feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLeCodedPhySupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLeCodedPhySupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isLeCodedPhySupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Extended Advertising feature is supported.
+ *
+ * @return true if chipset supports LE Extended Advertising feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLeExtendedAdvertisingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLeExtendedAdvertisingSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isLeExtendedAdvertisingSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /**
+ * Return true if LE Periodic Advertising feature is supported.
+ *
+ * @return true if chipset supports LE Periodic Advertising feature
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public boolean isLePeriodicAdvertisingSupported() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLePeriodicAdvertisingSupported();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get isLePeriodicAdvertisingSupported, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_FEATURE_NOT_SUPPORTED,
+ })
+ public @interface LeFeatureReturnValues {}
+
+ /**
+ * Returns {@link BluetoothStatusCodes#SUCCESS} if the LE audio feature is
+ * supported, returns {@link BluetoothStatusCodes#ERROR_FEATURE_NOT_SUPPORTED} if
+ * the feature is not supported or an error code.
+ *
+ * @return whether the LE audio is supported
+ */
+ @RequiresNoPermission
+ public @LeFeatureReturnValues int isLeAudioSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLeAudioSupported();
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Returns {@link BluetoothStatusCodes#SUCCESS} if LE Periodic Advertising Sync Transfer Sender
+ * feature is supported, returns {@link BluetoothStatusCodes#ERROR_FEATURE_NOT_SUPPORTED} if the
+ * feature is not supported or an error code
+ *
+ * @return whether the chipset supports the LE Periodic Advertising Sync Transfer Sender feature
+ */
+ @RequiresNoPermission
+ public @LeFeatureReturnValues int isLePeriodicAdvertisingSyncTransferSenderSupported() {
+ if (!getLeAccess()) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.isLePeriodicAdvertisingSyncTransferSenderSupported();
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Return the maximum LE advertising data length in bytes,
+ * if LE Extended Advertising feature is supported, 0 otherwise.
+ *
+ * @return the maximum LE advertising data length.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public int getLeMaximumAdvertisingDataLength() {
+ if (!getLeAccess()) {
+ return 0;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getLeMaximumAdvertisingDataLength();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get getLeMaximumAdvertisingDataLength, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return 0;
+ }
+
+ /**
+ * Return true if Hearing Aid Profile is supported.
+ *
+ * @return true if phone supports Hearing Aid Profile
+ */
+ @RequiresNoPermission
+ private boolean isHearingAidProfileSupported() {
+ try {
+ return mManagerService.isHearingAidProfileSupported();
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception when calling isHearingAidProfileSupported", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get the maximum number of connected audio devices.
+ *
+ * @return the maximum number of connected audio devices
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getMaxConnectedAudioDevices() {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getMaxConnectedAudioDevices(mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get getMaxConnectedAudioDevices, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return 1;
+ }
+
+ /**
+ * Return true if hardware has entries available for matching beacons
+ *
+ * @return true if there are hw entries available for matching beacons
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isHardwareTrackingFiltersAvailable() {
+ if (!getLeAccess()) {
+ return false;
+ }
+ try {
+ IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return false;
+ }
+ return (iGatt.numHwTrackFiltersAvailable(mAttributionSource) != 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Request the record of {@link BluetoothActivityEnergyInfo} object that
+ * has the activity and energy info. This can be used to ascertain what
+ * the controller has been up to, since the last sample.
+ *
+ * A null value for the activity info object may be sent if the bluetooth service is
+ * unreachable or the device does not support reporting such information.
+ *
+ * @param result The callback to which to send the activity info.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void requestControllerActivityEnergyInfo(ResultReceiver result) {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ mService.requestActivityInfo(result, mAttributionSource);
+ result = null;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "getControllerActivityEnergyInfoCallback: " + e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ if (result != null) {
+ // Only send an immediate result if we failed.
+ result.send(0, null);
+ }
+ }
+ }
+
+ /**
+ * Fetches a list of the most recently connected bluetooth devices ordered by how recently they
+ * were connected with most recently first and least recently last
+ *
+ * @return {@link List} of bonded {@link BluetoothDevice} ordered by how recently they were
+ * connected
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getMostRecentlyConnectedDevices() {
+ if (getState() != STATE_ON) {
+ return new ArrayList<>();
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return Attributable.setAttributionSource(
+ mService.getMostRecentlyConnectedDevices(mAttributionSource),
+ mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Return the set of {@link BluetoothDevice} objects that are bonded
+ * (paired) to the local adapter.
+ * <p>If Bluetooth state is not {@link #STATE_ON}, this API
+ * will return an empty set. After turning on Bluetooth,
+ * wait for {@link #ACTION_STATE_CHANGED} with {@link #STATE_ON}
+ * to get the updated value.
+ *
+ * @return unmodifiable set of {@link BluetoothDevice}, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Set<BluetoothDevice> getBondedDevices() {
+ if (getState() != STATE_ON) {
+ return toDeviceSet(Arrays.asList());
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return toDeviceSet(Attributable.setAttributionSource(
+ Arrays.asList(mService.getBondedDevices(mAttributionSource)),
+ mAttributionSource));
+ }
+ return toDeviceSet(Arrays.asList());
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return null;
+ }
+
+ /**
+ * Gets the currently supported profiles by the adapter.
+ *
+ * <p> This can be used to check whether a profile is supported before attempting
+ * to connect to its respective proxy.
+ *
+ * @return a list of integers indicating the ids of supported profiles as defined in {@link
+ * BluetoothProfile}.
+ * @hide
+ */
+ @RequiresNoPermission
+ public @NonNull List<Integer> getSupportedProfiles() {
+ final ArrayList<Integer> supportedProfiles = new ArrayList<Integer>();
+
+ try {
+ synchronized (mManagerCallback) {
+ if (mService != null) {
+ final long supportedProfilesBitMask = mService.getSupportedProfiles();
+
+ for (int i = 0; i <= BluetoothProfile.MAX_PROFILE_ID; i++) {
+ if ((supportedProfilesBitMask & (1 << i)) != 0) {
+ supportedProfiles.add(i);
+ }
+ }
+ } else {
+ // Bluetooth is disabled. Just fill in known supported Profiles
+ if (isHearingAidProfileSupported()) {
+ supportedProfiles.add(BluetoothProfile.HEARING_AID);
+ }
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "getSupportedProfiles:", e);
+ }
+ return supportedProfiles;
+ }
+
+ /*
+ private static final String BLUETOOTH_GET_ADAPTER_CONNECTION_STATE_CACHE_PROPERTY =
+ "cache_key.bluetooth.get_adapter_connection_state";
+ private final PropertyInvalidatedCache<Void, Integer>
+ mBluetoothGetAdapterConnectionStateCache =
+ new PropertyInvalidatedCache<Void, Integer> (
+ 8, BLUETOOTH_GET_ADAPTER_CONNECTION_STATE_CACHE_PROPERTY) {
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ protected Integer recompute(Void query) {
+ try {
+ return mService.getAdapterConnectionState();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ };
+ */
+
+ /** @hide */
+ /*
+ @RequiresNoPermission
+ public void disableGetAdapterConnectionStateCache() {
+ mBluetoothGetAdapterConnectionStateCache.disableLocal();
+ }
+ */
+
+ /** @hide */
+ /*
+ public static void invalidateGetAdapterConnectionStateCache() {
+ PropertyInvalidatedCache.invalidateCache(
+ BLUETOOTH_GET_ADAPTER_CONNECTION_STATE_CACHE_PROPERTY);
+ }
+ */
+
+ /**
+ * Get the current connection state of the local Bluetooth adapter.
+ * This can be used to check whether the local Bluetooth adapter is connected
+ * to any profile of any other remote Bluetooth Device.
+ *
+ * <p> Use this function along with {@link #ACTION_CONNECTION_STATE_CHANGED}
+ * intent to get the connection state of the adapter.
+ *
+ * @return One of {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTED}, {@link
+ * #STATE_CONNECTING} or {@link #STATE_DISCONNECTED}
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public int getConnectionState() {
+ if (getState() != STATE_ON) {
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getAdapterConnectionState();
+ }
+ //return mBluetoothGetAdapterConnectionStateCache.query(null);
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to getConnectionState, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothAdapter.STATE_DISCONNECTED;
+ }
+
+ /*
+ private static final String BLUETOOTH_PROFILE_CACHE_PROPERTY =
+ "cache_key.bluetooth.get_profile_connection_state";
+ private final PropertyInvalidatedCache<Integer, Integer>
+ mGetProfileConnectionStateCache =
+ new PropertyInvalidatedCache<Integer, Integer>(
+ 8, BLUETOOTH_PROFILE_CACHE_PROPERTY) {
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ protected Integer recompute(Integer query) {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getProfileConnectionState(query);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "getProfileConnectionState:", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ @Override
+ public String queryToString(Integer query) {
+ return String.format("getProfileConnectionState(profile=\"%d\")",
+ query);
+ }
+ };
+ */
+
+ /** @hide */
+ /*
+ @RequiresNoPermission
+ public void disableGetProfileConnectionStateCache() {
+ mGetProfileConnectionStateCache.disableLocal();
+ }
+ */
+
+ /** @hide */
+ /*
+ public static void invalidateGetProfileConnectionStateCache() {
+ PropertyInvalidatedCache.invalidateCache(BLUETOOTH_PROFILE_CACHE_PROPERTY);
+ }
+ */
+
+ /**
+ * Get the current connection state of a profile.
+ * This function can be used to check whether the local Bluetooth adapter
+ * is connected to any remote device for a specific profile.
+ * Profile can be one of {@link BluetoothProfile#HEADSET}, {@link BluetoothProfile#A2DP}.
+ *
+ * <p> Return value can be one of
+ * {@link BluetoothProfile#STATE_DISCONNECTED},
+ * {@link BluetoothProfile#STATE_CONNECTING},
+ * {@link BluetoothProfile#STATE_CONNECTED},
+ * {@link BluetoothProfile#STATE_DISCONNECTING}
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public int getProfileConnectionState(int profile) {
+ if (getState() != STATE_ON) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ mService.getProfileConnectionState(profile);
+ }
+ //return mGetProfileConnectionStateCache.query(new Integer(profile));
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to getProfileConnectionState, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ *
+ * @param channel RFCOMM channel to listen on
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommOn(int channel) throws IOException {
+ return listenUsingRfcommOn(channel, false, false);
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ * <p>To auto assign a channel without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as channel number.
+ *
+ * @param channel RFCOMM channel to listen on
+ * @param mitm enforce person-in-the-middle protection for authentication.
+ * @param min16DigitPin enforce a pin key length og minimum 16 digit for sec mode 2
+ * connections.
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommOn(int channel, boolean mitm,
+ boolean min16DigitPin) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, true, true, channel, mitm,
+ min16DigitPin);
+ int errno = socket.mSocket.bindListen();
+ if (channel == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ socket.setChannel(socket.mSocket.getPort());
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Create a listening, secure RFCOMM Bluetooth socket with Service Record.
+ * <p>A remote device connecting to this socket will be authenticated and
+ * communication on this socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, true, true);
+ }
+
+ /**
+ * Create a listening, insecure RFCOMM Bluetooth socket with Service Record.
+ * <p>The link key is not required to be authenticated, i.e the communication may be
+ * vulnerable to Person In the Middle attacks. For Bluetooth 2.1 devices,
+ * the link will be encrypted, as encryption is mandatory.
+ * For legacy devices (pre Bluetooth 2.1 devices) the link will not
+ * be encrypted. Use {@link #listenUsingRfcommWithServiceRecord}, if an
+ * encrypted and authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, false, false);
+ }
+
+ /**
+ * Create a listening, encrypted,
+ * RFCOMM Bluetooth socket with Service Record.
+ * <p>The link will be encrypted, but the link key is not required to be authenticated
+ * i.e the communication is vulnerable to Person In the Middle attacks. Use
+ * {@link #listenUsingRfcommWithServiceRecord}, to ensure an authenticated link key.
+ * <p> Use this socket if authentication of link key is not possible.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not have
+ * an input and output capability or just has the ability to display a numeric key,
+ * a secure socket connection is not possible and this socket can be used.
+ * Use {@link #listenUsingInsecureRfcommWithServiceRecord}, if encryption is not required.
+ * For Bluetooth 2.1 devices, the link will be encrypted, as encryption is mandatory.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming
+ * connections from a listening {@link BluetoothServerSocket}.
+ * <p>The system will assign an unused RFCOMM channel to listen on.
+ * <p>The system will also register a Service Discovery
+ * Protocol (SDP) record with the local SDP server containing the specified
+ * UUID, service name, and auto-assigned channel. Remote Bluetooth devices
+ * can use the same UUID to query our SDP server and discover which channel
+ * to connect to. This SDP record will be removed when this socket is
+ * closed, or if this application closes unexpectedly.
+ * <p>Use {@link BluetoothDevice#createRfcommSocketToServiceRecord} to
+ * connect to this socket from another device using the same {@link UUID}.
+ *
+ * @param name service name for SDP record
+ * @param uuid uuid for SDP record
+ * @return a listening RFCOMM BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or channel in use.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingEncryptedRfcommWithServiceRecord(String name, UUID uuid)
+ throws IOException {
+ return createNewRfcommSocketAndRecord(name, uuid, false, true);
+ }
+
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private BluetoothServerSocket createNewRfcommSocketAndRecord(String name, UUID uuid,
+ boolean auth, boolean encrypt) throws IOException {
+ BluetoothServerSocket socket;
+ socket = new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, auth, encrypt,
+ new ParcelUuid(uuid));
+ socket.setServiceName(name);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an unencrypted, unauthenticated, RFCOMM server socket.
+ * Call #accept to retrieve connections to this socket.
+ *
+ * @return An RFCOMM BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_RFCOMM, false, false, port);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ socket.setChannel(socket.mSocket.getPort());
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an encrypted, authenticated, L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @param mitm enforce person-in-the-middle protection for authentication.
+ * @param min16DigitPin enforce a pin key length og minimum 16 digit for sec mode 2
+ * connections.
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingL2capOn(int port, boolean mitm, boolean min16DigitPin)
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, true, true, port, mitm,
+ min16DigitPin);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) Log.d(TAG, "listenUsingL2capOn: set assigned channel to " + assignedChannel);
+ socket.setChannel(assignedChannel);
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+ }
+
+ /**
+ * Construct an encrypted, authenticated, L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingL2capOn(int port) throws IOException {
+ return listenUsingL2capOn(port, false, false);
+ }
+
+ /**
+ * Construct an insecure L2CAP server socket.
+ * Call #accept to retrieve connections to this socket.
+ * <p>To auto assign a port without creating a SDP record use
+ * {@link #SOCKET_CHANNEL_AUTO_STATIC_NO_SDP} as port number.
+ *
+ * @param port the PSM to listen on
+ * @return An L2CAP BluetoothServerSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException {
+ Log.d(TAG, "listenUsingInsecureL2capOn: port=" + port);
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false,
+ false);
+ int errno = socket.mSocket.bindListen();
+ if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned channel to "
+ + assignedChannel);
+ }
+ socket.setChannel(assignedChannel);
+ }
+ if (errno != 0) {
+ //TODO(BT): Throw the same exception error code
+ // that the previous code was using.
+ //socket.mSocket.throwErrnoNative(errno);
+ throw new IOException("Error: " + errno);
+ }
+ return socket;
+
+ }
+
+ /**
+ * Read the local Out of Band Pairing Data
+ *
+ * @return Pair<byte[], byte[]> of Hash and Randomizer
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public Pair<byte[], byte[]> readOutOfBandData() {
+ return null;
+ }
+
+ /**
+ * Get the profile proxy object associated with the profile.
+ *
+ * <p>Profile can be one of {@link BluetoothProfile#HEADSET}, {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#GATT}, {@link BluetoothProfile#HEARING_AID}, or {@link
+ * BluetoothProfile#GATT_SERVER}. Clients must implement {@link
+ * BluetoothProfile.ServiceListener} to get notified of the connection status and to get the
+ * proxy object.
+ *
+ * @param context Context of the application
+ * @param listener The service Listener for connection callbacks.
+ * @param profile The Bluetooth profile; either {@link BluetoothProfile#HEADSET},
+ * {@link BluetoothProfile#A2DP}, {@link BluetoothProfile#GATT}, {@link
+ * BluetoothProfile#HEARING_AID} or {@link BluetoothProfile#GATT_SERVER}.
+ * @return true on success, false on error
+ */
+ @SuppressLint({
+ "AndroidFrameworkRequiresPermission",
+ "AndroidFrameworkBluetoothPermission"
+ })
+ public boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener,
+ int profile) {
+ if (context == null || listener == null) {
+ return false;
+ }
+
+ if (profile == BluetoothProfile.HEADSET) {
+ BluetoothHeadset headset = new BluetoothHeadset(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.A2DP) {
+ BluetoothA2dp a2dp = new BluetoothA2dp(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.A2DP_SINK) {
+ BluetoothA2dpSink a2dpSink = new BluetoothA2dpSink(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
+ BluetoothAvrcpController avrcp = new BluetoothAvrcpController(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HID_HOST) {
+ BluetoothHidHost iDev = new BluetoothHidHost(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PAN) {
+ BluetoothPan pan = new BluetoothPan(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PBAP) {
+ BluetoothPbap pbap = new BluetoothPbap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HEALTH) {
+ Log.e(TAG, "getProfileProxy(): BluetoothHealth is deprecated");
+ return false;
+ } else if (profile == BluetoothProfile.MAP) {
+ BluetoothMap map = new BluetoothMap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HEADSET_CLIENT) {
+ BluetoothHeadsetClient headsetClient =
+ new BluetoothHeadsetClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.SAP) {
+ BluetoothSap sap = new BluetoothSap(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.PBAP_CLIENT) {
+ BluetoothPbapClient pbapClient = new BluetoothPbapClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.MAP_CLIENT) {
+ BluetoothMapClient mapClient = new BluetoothMapClient(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HID_DEVICE) {
+ BluetoothHidDevice hidDevice = new BluetoothHidDevice(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.HEARING_AID) {
+ if (isHearingAidProfileSupported()) {
+ BluetoothHearingAid hearingAid = new BluetoothHearingAid(context, listener, this);
+ return true;
+ }
+ return false;
+ } else if (profile == BluetoothProfile.VOLUME_CONTROL) {
+ BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.CSIP_SET_COORDINATOR) {
+ BluetoothCsipSetCoordinator csipSetCoordinator =
+ new BluetoothCsipSetCoordinator(context, listener, this);
+ return true;
+ } else if (profile == BluetoothProfile.LE_CALL_CONTROL) {
+ BluetoothLeCallControl tbs = new BluetoothLeCallControl(context, listener);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Close the connection of the profile proxy to the Service.
+ *
+ * <p> Clients should call this when they are no longer using
+ * the proxy obtained from {@link #getProfileProxy}.
+ * Profile can be one of {@link BluetoothProfile#HEADSET} or {@link BluetoothProfile#A2DP}
+ *
+ * @param profile
+ * @param proxy Profile proxy object
+ */
+ @SuppressLint({
+ "AndroidFrameworkRequiresPermission",
+ "AndroidFrameworkBluetoothPermission"
+ })
+ public void closeProfileProxy(int profile, BluetoothProfile proxy) {
+ if (proxy == null) {
+ return;
+ }
+
+ switch (profile) {
+ case BluetoothProfile.HEADSET:
+ BluetoothHeadset headset = (BluetoothHeadset) proxy;
+ headset.close();
+ break;
+ case BluetoothProfile.A2DP:
+ BluetoothA2dp a2dp = (BluetoothA2dp) proxy;
+ a2dp.close();
+ break;
+ case BluetoothProfile.A2DP_SINK:
+ BluetoothA2dpSink a2dpSink = (BluetoothA2dpSink) proxy;
+ a2dpSink.close();
+ break;
+ case BluetoothProfile.AVRCP_CONTROLLER:
+ BluetoothAvrcpController avrcp = (BluetoothAvrcpController) proxy;
+ avrcp.close();
+ break;
+ case BluetoothProfile.HID_HOST:
+ BluetoothHidHost iDev = (BluetoothHidHost) proxy;
+ iDev.close();
+ break;
+ case BluetoothProfile.PAN:
+ BluetoothPan pan = (BluetoothPan) proxy;
+ pan.close();
+ break;
+ case BluetoothProfile.PBAP:
+ BluetoothPbap pbap = (BluetoothPbap) proxy;
+ pbap.close();
+ break;
+ case BluetoothProfile.GATT:
+ BluetoothGatt gatt = (BluetoothGatt) proxy;
+ gatt.close();
+ break;
+ case BluetoothProfile.GATT_SERVER:
+ BluetoothGattServer gattServer = (BluetoothGattServer) proxy;
+ gattServer.close();
+ break;
+ case BluetoothProfile.MAP:
+ BluetoothMap map = (BluetoothMap) proxy;
+ map.close();
+ break;
+ case BluetoothProfile.HEADSET_CLIENT:
+ BluetoothHeadsetClient headsetClient = (BluetoothHeadsetClient) proxy;
+ headsetClient.close();
+ break;
+ case BluetoothProfile.SAP:
+ BluetoothSap sap = (BluetoothSap) proxy;
+ sap.close();
+ break;
+ case BluetoothProfile.PBAP_CLIENT:
+ BluetoothPbapClient pbapClient = (BluetoothPbapClient) proxy;
+ pbapClient.close();
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ BluetoothMapClient mapClient = (BluetoothMapClient) proxy;
+ mapClient.close();
+ break;
+ case BluetoothProfile.HID_DEVICE:
+ BluetoothHidDevice hidDevice = (BluetoothHidDevice) proxy;
+ hidDevice.close();
+ break;
+ case BluetoothProfile.HEARING_AID:
+ BluetoothHearingAid hearingAid = (BluetoothHearingAid) proxy;
+ hearingAid.close();
+ break;
+ case BluetoothProfile.VOLUME_CONTROL:
+ BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy;
+ vcs.close();
+ break;
+ case BluetoothProfile.CSIP_SET_COORDINATOR:
+ BluetoothCsipSetCoordinator csipSetCoordinator =
+ (BluetoothCsipSetCoordinator) proxy;
+ csipSetCoordinator.close();
+ break;
+ case BluetoothProfile.LE_CALL_CONTROL:
+ BluetoothLeCallControl tbs = (BluetoothLeCallControl) proxy;
+ tbs.close();
+ break;
+ }
+ }
+
+ private static final IBluetoothManagerCallback sManagerCallback =
+ new IBluetoothManagerCallback.Stub() {
+ public void onBluetoothServiceUp(IBluetooth bluetoothService) {
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceUp: " + bluetoothService);
+ }
+
+ synchronized (sServiceLock) {
+ sService = bluetoothService;
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBluetoothServiceUp(bluetoothService);
+ } else {
+ Log.d(TAG, "onBluetoothServiceUp: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+
+ public void onBluetoothServiceDown() {
+ if (DBG) {
+ Log.d(TAG, "onBluetoothServiceDown");
+ }
+
+ synchronized (sServiceLock) {
+ sService = null;
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBluetoothServiceDown();
+ } else {
+ Log.d(TAG, "onBluetoothServiceDown: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+
+ public void onBrEdrDown() {
+ if (VDBG) {
+ Log.i(TAG, "onBrEdrDown");
+ }
+
+ synchronized (sServiceLock) {
+ for (IBluetoothManagerCallback cb : sProxyServiceStateCallbacks.keySet()) {
+ try {
+ if (cb != null) {
+ cb.onBrEdrDown();
+ } else {
+ Log.d(TAG, "onBrEdrDown: cb is null!");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+ }
+ };
+
+ private final IBluetoothManagerCallback mManagerCallback =
+ new IBluetoothManagerCallback.Stub() {
+ public void onBluetoothServiceUp(IBluetooth bluetoothService) {
+ synchronized (mServiceLock.writeLock()) {
+ mService = bluetoothService;
+ }
+ synchronized (mMetadataListeners) {
+ mMetadataListeners.forEach((device, pair) -> {
+ try {
+ mService.registerMetadataListener(mBluetoothMetadataListener,
+ device, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register metadata listener", e);
+ }
+ });
+ }
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ try {
+ mService.registerBluetoothConnectionCallback(mConnectionCallback,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onBluetoothServiceUp: Failed to register bluetooth"
+ + "connection callback", e);
+ }
+ }
+ }
+ }
+
+ public void onBluetoothServiceDown() {
+ synchronized (mServiceLock.writeLock()) {
+ mService = null;
+ if (mLeScanClients != null) {
+ mLeScanClients.clear();
+ }
+ if (mBluetoothLeAdvertiser != null) {
+ mBluetoothLeAdvertiser.cleanup();
+ }
+ if (mBluetoothLeScanner != null) {
+ mBluetoothLeScanner.cleanup();
+ }
+ }
+ }
+
+ public void onBrEdrDown() {
+ }
+ };
+
+ /**
+ * Enable the Bluetooth Adapter, but don't auto-connect devices
+ * and don't persist state. Only for use by system applications.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enableNoAutoConnect() {
+ if (isEnabled()) {
+ if (DBG) {
+ Log.d(TAG, "enableNoAutoConnect(): BT already enabled!");
+ }
+ return true;
+ }
+ try {
+ return mManagerService.enableNoAutoConnect(mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_ANOTHER_ACTIVE_OOB_REQUEST,
+ })
+ public @interface OobError {}
+
+ /**
+ * Provides callback methods for receiving {@link OobData} from the host stack, as well as an
+ * error interface in order to allow the caller to determine next steps based on the {@code
+ * ErrorCode}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public interface OobDataCallback {
+ /**
+ * Handles the {@link OobData} received from the host stack.
+ *
+ * @param transport - whether the {@link OobData} is generated for LE or Classic.
+ * @param oobData - data generated in the host stack(LE) or controller (Classic)
+ */
+ void onOobData(@Transport int transport, @NonNull OobData oobData);
+
+ /**
+ * Provides feedback when things don't go as expected.
+ *
+ * @param errorCode - the code describing the type of error that occurred.
+ */
+ void onError(@OobError int errorCode);
+ }
+
+ /**
+ * Wraps an AIDL interface around an {@link OobDataCallback} interface.
+ *
+ * @see {@link IBluetoothOobDataCallback} for interface definition.
+ *
+ * @hide
+ */
+ public class WrappedOobDataCallback extends IBluetoothOobDataCallback.Stub {
+ private final OobDataCallback mCallback;
+ private final Executor mExecutor;
+
+ /**
+ * @param callback - object to receive {@link OobData} must be a non null argument
+ *
+ * @throws NullPointerException if the callback is null.
+ */
+ WrappedOobDataCallback(@NonNull OobDataCallback callback,
+ @NonNull @CallbackExecutor Executor executor) {
+ requireNonNull(callback);
+ requireNonNull(executor);
+ mCallback = callback;
+ mExecutor = executor;
+ }
+ /**
+ * Wrapper function to relay to the {@link OobDataCallback#onOobData}
+ *
+ * @param transport - whether the {@link OobData} is generated for LE or Classic.
+ * @param oobData - data generated in the host stack(LE) or controller (Classic)
+ *
+ * @hide
+ */
+ public void onOobData(@Transport int transport, @NonNull OobData oobData) {
+ mExecutor.execute(new Runnable() {
+ public void run() {
+ mCallback.onOobData(transport, oobData);
+ }
+ });
+ }
+ /**
+ * Wrapper function to relay to the {@link OobDataCallback#onError}
+ *
+ * @param errorCode - the code descibing the type of error that occurred.
+ *
+ * @hide
+ */
+ public void onError(@OobError int errorCode) {
+ mExecutor.execute(new Runnable() {
+ public void run() {
+ mCallback.onError(errorCode);
+ }
+ });
+ }
+ }
+
+ /**
+ * Fetches a secret data value that can be used for a secure and simple pairing experience.
+ *
+ * <p>This is the Local Out of Band data the comes from the
+ *
+ * <p>This secret is the local Out of Band data. This data is used to securely and quickly
+ * pair two devices with minimal user interaction.
+ *
+ * <p>For example, this secret can be transferred to a remote device out of band (meaning any
+ * other way besides using bluetooth). Once the remote device finds this device using the
+ * information given in the data, such as the PUBLIC ADDRESS, the remote device could then
+ * connect to this device using this secret when the pairing sequenece asks for the secret.
+ * This device will respond by automatically accepting the pairing due to the secret being so
+ * trustworthy.
+ *
+ * @param transport - provide type of transport (e.g. LE or Classic).
+ * @param callback - target object to receive the {@link OobData} value.
+ *
+ * @throws NullPointerException if callback is null.
+ * @throws IllegalArgumentException if the transport is not valid.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void generateLocalOobData(@Transport int transport,
+ @NonNull @CallbackExecutor Executor executor, @NonNull OobDataCallback callback) {
+ if (transport != BluetoothDevice.TRANSPORT_BREDR && transport
+ != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Invalid transport '" + transport + "'!");
+ }
+ requireNonNull(callback);
+ if (!isEnabled()) {
+ Log.w(TAG, "generateLocalOobData(): Adapter isn't enabled!");
+ callback.onError(BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED);
+ } else {
+ try {
+ mService.generateLocalOobData(transport, new WrappedOobDataCallback(callback,
+ executor), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+
+ /**
+ * Enable control of the Bluetooth Adapter for a single application.
+ *
+ * <p>Some applications need to use Bluetooth for short periods of time to
+ * transfer data but don't want all the associated implications like
+ * automatic connection to headsets etc.
+ *
+ * <p> Multiple applications can call this. This is reference counted and
+ * Bluetooth disabled only when no one else is using it. There will be no UI
+ * shown to the user while bluetooth is being enabled. Any user action will
+ * override this call. For example, if user wants Bluetooth on and the last
+ * user of this API wanted to disable Bluetooth, Bluetooth will not be
+ * turned off.
+ *
+ * <p> This API is only meant to be used by internal applications. Third
+ * party applications but use {@link #enable} and {@link #disable} APIs.
+ *
+ * <p> If this API returns true, it means the callback will be called.
+ * The callback will be called with the current state of Bluetooth.
+ * If the state is not what was requested, an internal error would be the
+ * reason. If Bluetooth is already on and if this function is called to turn
+ * it on, the api will return true and a callback will be called.
+ *
+ * @param on True for on, false for off.
+ * @param callback The callback to notify changes to the state.
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean changeApplicationBluetoothState(boolean on,
+ BluetoothStateChangeCallback callback) {
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public interface BluetoothStateChangeCallback {
+ /**
+ * @hide
+ */
+ void onBluetoothStateChange(boolean on);
+ }
+
+ /**
+ * @hide
+ */
+ public class StateChangeCallbackWrapper extends IBluetoothStateChangeCallback.Stub {
+ private BluetoothStateChangeCallback mCallback;
+
+ StateChangeCallbackWrapper(BluetoothStateChangeCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onBluetoothStateChange(boolean on) {
+ mCallback.onBluetoothStateChange(on);
+ }
+ }
+
+ private Set<BluetoothDevice> toDeviceSet(List<BluetoothDevice> devices) {
+ Set<BluetoothDevice> deviceSet = new HashSet<BluetoothDevice>(devices);
+ return Collections.unmodifiableSet(deviceSet);
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ removeServiceStateCallback(mManagerCallback);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Validate a String Bluetooth address, such as "00:43:A8:23:10:F0"
+ * <p>Alphabetic characters must be uppercase to be valid.
+ *
+ * @param address Bluetooth address as string
+ * @return true if the address is valid, false otherwise
+ */
+ public static boolean checkBluetoothAddress(String address) {
+ if (address == null || address.length() != ADDRESS_LENGTH) {
+ return false;
+ }
+ for (int i = 0; i < ADDRESS_LENGTH; i++) {
+ char c = address.charAt(i);
+ switch (i % 3) {
+ case 0:
+ case 1:
+ if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) {
+ // hex character, OK
+ break;
+ }
+ return false;
+ case 2:
+ if (c == ':') {
+ break; // OK
+ }
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines whether a String Bluetooth address, such as "F0:43:A8:23:10:00"
+ * is a RANDOM STATIC address.
+ *
+ * RANDOM STATIC: (addr & 0xC0) == 0xC0
+ * RANDOM RESOLVABLE: (addr & 0xC0) == 0x40
+ * RANDOM non-RESOLVABLE: (addr & 0xC0) == 0x00
+ *
+ * @param address Bluetooth address as string
+ * @return true if the 2 Most Significant Bits of the address equals 0xC0.
+ *
+ * @hide
+ */
+ public static boolean isAddressRandomStatic(@NonNull String address) {
+ requireNonNull(address);
+ return checkBluetoothAddress(address)
+ && (Integer.parseInt(address.split(":")[0], 16) & 0xC0) == 0xC0;
+ }
+
+ /** {@hide} */
+ @UnsupportedAppUsage
+ @RequiresNoPermission
+ public IBluetoothManager getBluetoothManager() {
+ return mManagerService;
+ }
+
+ /** {@hide} */
+ @RequiresNoPermission
+ public AttributionSource getAttributionSource() {
+ return mAttributionSource;
+ }
+
+ @GuardedBy("sServiceLock")
+ private static final WeakHashMap<IBluetoothManagerCallback, Void> sProxyServiceStateCallbacks =
+ new WeakHashMap<>();
+
+ /*package*/ IBluetooth getBluetoothService() {
+ synchronized (sServiceLock) {
+ if (sProxyServiceStateCallbacks.isEmpty()) {
+ throw new IllegalStateException(
+ "Anonymous service access requires at least one lifecycle in process");
+ }
+ return sService;
+ }
+ }
+
+ @UnsupportedAppUsage
+ /*package*/ IBluetooth getBluetoothService(IBluetoothManagerCallback cb) {
+ Objects.requireNonNull(cb);
+ synchronized (sServiceLock) {
+ sProxyServiceStateCallbacks.put(cb, null);
+ registerOrUnregisterAdapterLocked();
+ return sService;
+ }
+ }
+
+ /*package*/ void removeServiceStateCallback(IBluetoothManagerCallback cb) {
+ Objects.requireNonNull(cb);
+ synchronized (sServiceLock) {
+ sProxyServiceStateCallbacks.remove(cb);
+ registerOrUnregisterAdapterLocked();
+ }
+ }
+
+ /**
+ * Handle registering (or unregistering) a single process-wide
+ * {@link IBluetoothManagerCallback} based on the presence of local
+ * {@link #sProxyServiceStateCallbacks} clients.
+ */
+ @GuardedBy("sServiceLock")
+ private void registerOrUnregisterAdapterLocked() {
+ final boolean isRegistered = sServiceRegistered;
+ final boolean wantRegistered = !sProxyServiceStateCallbacks.isEmpty();
+
+ if (isRegistered != wantRegistered) {
+ if (wantRegistered) {
+ try {
+ sService = mManagerService.registerAdapter(sManagerCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ } else {
+ try {
+ mManagerService.unregisterAdapter(sManagerCallback);
+ sService = null;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ sServiceRegistered = wantRegistered;
+ }
+ }
+
+ /**
+ * Callback interface used to deliver LE scan results.
+ *
+ * @see #startLeScan(LeScanCallback)
+ * @see #startLeScan(UUID[], LeScanCallback)
+ */
+ public interface LeScanCallback {
+ /**
+ * Callback reporting an LE device found during a device scan initiated
+ * by the {@link BluetoothAdapter#startLeScan} function.
+ *
+ * @param device Identifies the remote device
+ * @param rssi The RSSI value for the remote device as reported by the Bluetooth hardware. 0
+ * if no RSSI value is available.
+ * @param scanRecord The content of the advertisement record offered by the remote device.
+ */
+ void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord);
+ }
+
+ /**
+ * Register a callback to receive events whenever the bluetooth stack goes down and back up,
+ * e.g. in the event the bluetooth is turned off/on via settings.
+ *
+ * If the bluetooth stack is currently up, there will not be an initial callback call.
+ * You can use the return value as an indication of this being the case.
+ *
+ * Callbacks will be delivered on a binder thread.
+ *
+ * @return whether bluetooth is already up currently
+ *
+ * @hide
+ */
+ public boolean registerServiceLifecycleCallback(ServiceLifecycleCallback callback) {
+ return getBluetoothService(callback.mRemote) != null;
+ }
+
+ /**
+ * Unregister a callback registered via {@link #registerServiceLifecycleCallback}
+ *
+ * @hide
+ */
+ public void unregisterServiceLifecycleCallback(ServiceLifecycleCallback callback) {
+ removeServiceStateCallback(callback.mRemote);
+ }
+
+ /**
+ * A callback for {@link #registerServiceLifecycleCallback}
+ *
+ * @hide
+ */
+ public abstract static class ServiceLifecycleCallback {
+
+ /** Called when the bluetooth stack is up */
+ public abstract void onBluetoothServiceUp();
+
+ /** Called when the bluetooth stack is down */
+ public abstract void onBluetoothServiceDown();
+
+ IBluetoothManagerCallback mRemote = new IBluetoothManagerCallback.Stub() {
+ @Override
+ public void onBluetoothServiceUp(IBluetooth bluetoothService) {
+ ServiceLifecycleCallback.this.onBluetoothServiceUp();
+ }
+
+ @Override
+ public void onBluetoothServiceDown() {
+ ServiceLifecycleCallback.this.onBluetoothServiceDown();
+ }
+
+ @Override
+ public void onBrEdrDown() {}
+ };
+ }
+
+ /**
+ * Starts a scan for Bluetooth LE devices.
+ *
+ * <p>Results of the scan are reported using the
+ * {@link LeScanCallback#onLeScan} callback.
+ *
+ * @param callback the callback LE scan results are delivered
+ * @return true, if the scan was started successfully
+ * @deprecated use {@link BluetoothLeScanner#startScan(List, ScanSettings, ScanCallback)}
+ * instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startLeScan(LeScanCallback callback) {
+ return startLeScan(null, callback);
+ }
+
+ /**
+ * Starts a scan for Bluetooth LE devices, looking for devices that
+ * advertise given services.
+ *
+ * <p>Devices which advertise all specified services are reported using the
+ * {@link LeScanCallback#onLeScan} callback.
+ *
+ * @param serviceUuids Array of services to look for
+ * @param callback the callback LE scan results are delivered
+ * @return true, if the scan was started successfully
+ * @deprecated use {@link BluetoothLeScanner#startScan(List, ScanSettings, ScanCallback)}
+ * instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public boolean startLeScan(final UUID[] serviceUuids, final LeScanCallback callback) {
+ if (DBG) {
+ Log.d(TAG, "startLeScan(): " + Arrays.toString(serviceUuids));
+ }
+ if (callback == null) {
+ if (DBG) {
+ Log.e(TAG, "startLeScan: null callback");
+ }
+ return false;
+ }
+ BluetoothLeScanner scanner = getBluetoothLeScanner();
+ if (scanner == null) {
+ if (DBG) {
+ Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner");
+ }
+ return false;
+ }
+
+ synchronized (mLeScanClients) {
+ if (mLeScanClients.containsKey(callback)) {
+ if (DBG) {
+ Log.e(TAG, "LE Scan has already started");
+ }
+ return false;
+ }
+
+ try {
+ IBluetoothGatt iGatt = mManagerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return false;
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ ScanCallback scanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
+ // Should not happen.
+ Log.e(TAG, "LE Scan has already started");
+ return;
+ }
+ ScanRecord scanRecord = result.getScanRecord();
+ if (scanRecord == null) {
+ return;
+ }
+ if (serviceUuids != null) {
+ List<ParcelUuid> uuids = new ArrayList<ParcelUuid>();
+ for (UUID uuid : serviceUuids) {
+ uuids.add(new ParcelUuid(uuid));
+ }
+ List<ParcelUuid> scanServiceUuids = scanRecord.getServiceUuids();
+ if (scanServiceUuids == null || !scanServiceUuids.containsAll(uuids)) {
+ if (DBG) {
+ Log.d(TAG, "uuids does not match");
+ }
+ return;
+ }
+ }
+ callback.onLeScan(result.getDevice(), result.getRssi(),
+ scanRecord.getBytes());
+ }
+ };
+ ScanSettings settings = new ScanSettings.Builder().setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build();
+
+ List<ScanFilter> filters = new ArrayList<ScanFilter>();
+ if (serviceUuids != null && serviceUuids.length > 0) {
+ // Note scan filter does not support matching an UUID array so we put one
+ // UUID to hardware and match the whole array in callback.
+ ScanFilter filter =
+ new ScanFilter.Builder().setServiceUuid(new ParcelUuid(serviceUuids[0]))
+ .build();
+ filters.add(filter);
+ }
+ scanner.startScan(filters, settings, scanCallback);
+
+ mLeScanClients.put(callback, scanCallback);
+ return true;
+
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stops an ongoing Bluetooth LE device scan.
+ *
+ * @param callback used to identify which scan to stop must be the same handle used to start the
+ * scan
+ * @deprecated Use {@link BluetoothLeScanner#stopScan(ScanCallback)} instead.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopLeScan(LeScanCallback callback) {
+ if (DBG) {
+ Log.d(TAG, "stopLeScan()");
+ }
+ BluetoothLeScanner scanner = getBluetoothLeScanner();
+ if (scanner == null) {
+ return;
+ }
+ synchronized (mLeScanClients) {
+ ScanCallback scanCallback = mLeScanClients.remove(callback);
+ if (scanCallback == null) {
+ if (DBG) {
+ Log.d(TAG, "scan not started yet");
+ }
+ return;
+ }
+ scanner.stopScan(scanCallback);
+ }
+ }
+
+ /**
+ * Create a secure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic protocol/service multiplexer (PSM) value. This socket can be used to listen
+ * for incoming connections. The supported Bluetooth transport is LE only.
+ * <p>A remote device connecting to this socket will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic PSM value. This PSM value can be read from the {@link
+ * BluetoothServerSocket#getPsm()} and this value will be released when this server socket is
+ * closed, Bluetooth is turned off, or the application exits unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createL2capChannel(int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull BluetoothServerSocket listenUsingL2capChannel()
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, true, true,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingL2capChannel: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Create an insecure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic PSM value. This socket can be used to listen for incoming connections. The
+ * supported Bluetooth transport is LE only.
+ * <p>The link key is not required to be authenticated, i.e the communication may be vulnerable
+ * to person-in-the-middle attacks. Use {@link #listenUsingL2capChannel}, if an encrypted and
+ * authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic protocol/service multiplexer (PSM) value. This PSM value
+ * can be read from the {@link BluetoothServerSocket#getPsm()} and this value will be released
+ * when this server socket is closed, Bluetooth is turned off, or the application exits
+ * unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createInsecureL2capChannel(int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull BluetoothServerSocket listenUsingInsecureL2capChannel()
+ throws IOException {
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, false, false,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capChannel: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Register a {@link #OnMetadataChangedListener} to receive update about metadata
+ * changes for this {@link BluetoothDevice}.
+ * Registration must be done when Bluetooth is ON and will last until
+ * {@link #removeOnMetadataChangedListener(BluetoothDevice)} is called, even when Bluetooth
+ * restarted in the middle.
+ * All input parameters should not be null or {@link NullPointerException} will be triggered.
+ * The same {@link BluetoothDevice} and {@link #OnMetadataChangedListener} pair can only be
+ * registered once, double registration would cause {@link IllegalArgumentException}.
+ *
+ * @param device {@link BluetoothDevice} that will be registered
+ * @param executor the executor for listener callback
+ * @param listener {@link #OnMetadataChangedListener} that will receive asynchronous callbacks
+ * @return true on success, false on error
+ * @throws NullPointerException If one of {@code listener}, {@code device} or {@code executor}
+ * is null.
+ * @throws IllegalArgumentException The same {@link #OnMetadataChangedListener} and
+ * {@link BluetoothDevice} are registered twice.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean addOnMetadataChangedListener(@NonNull BluetoothDevice device,
+ @NonNull Executor executor, @NonNull OnMetadataChangedListener listener) {
+ if (DBG) Log.d(TAG, "addOnMetadataChangedListener()");
+
+ final IBluetooth service = mService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot register metadata listener");
+ return false;
+ }
+ if (listener == null) {
+ throw new NullPointerException("listener is null");
+ }
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+ if (executor == null) {
+ throw new NullPointerException("executor is null");
+ }
+
+ synchronized (mMetadataListeners) {
+ List<Pair<OnMetadataChangedListener, Executor>> listenerList =
+ mMetadataListeners.get(device);
+ if (listenerList == null) {
+ // Create new listener/executor list for registeration
+ listenerList = new ArrayList<>();
+ mMetadataListeners.put(device, listenerList);
+ } else {
+ // Check whether this device was already registed by the lisenter
+ if (listenerList.stream().anyMatch((pair) -> (pair.first.equals(listener)))) {
+ throw new IllegalArgumentException("listener was already regestered"
+ + " for the device");
+ }
+ }
+
+ Pair<OnMetadataChangedListener, Executor> listenerPair = new Pair(listener, executor);
+ listenerList.add(listenerPair);
+
+ boolean ret = false;
+ try {
+ ret = service.registerMetadataListener(mBluetoothMetadataListener, device,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "registerMetadataListener fail", e);
+ } finally {
+ if (!ret) {
+ // Remove listener registered earlier when fail.
+ listenerList.remove(listenerPair);
+ if (listenerList.isEmpty()) {
+ // Remove the device if its listener list is empty
+ mMetadataListeners.remove(device);
+ }
+ }
+ }
+ return ret;
+ }
+ }
+
+ /**
+ * Unregister a {@link #OnMetadataChangedListener} from a registered {@link BluetoothDevice}.
+ * Unregistration can be done when Bluetooth is either ON or OFF.
+ * {@link #addOnMetadataChangedListener(OnMetadataChangedListener, BluetoothDevice, Executor)}
+ * must be called before unregisteration.
+ *
+ * @param device {@link BluetoothDevice} that will be unregistered. It
+ * should not be null or {@link NullPointerException} will be triggered.
+ * @param listener {@link OnMetadataChangedListener} that will be unregistered. It
+ * should not be null or {@link NullPointerException} will be triggered.
+ * @return true on success, false on error
+ * @throws NullPointerException If {@code listener} or {@code device} is null.
+ * @throws IllegalArgumentException If {@code device} has not been registered before.
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean removeOnMetadataChangedListener(@NonNull BluetoothDevice device,
+ @NonNull OnMetadataChangedListener listener) {
+ if (DBG) Log.d(TAG, "removeOnMetadataChangedListener()");
+ if (device == null) {
+ throw new NullPointerException("device is null");
+ }
+ if (listener == null) {
+ throw new NullPointerException("listener is null");
+ }
+
+ synchronized (mMetadataListeners) {
+ if (!mMetadataListeners.containsKey(device)) {
+ throw new IllegalArgumentException("device was not registered");
+ }
+ // Remove issued listener from the registered device
+ mMetadataListeners.get(device).removeIf((pair) -> (pair.first.equals(listener)));
+
+ if (mMetadataListeners.get(device).isEmpty()) {
+ // Unregister to Bluetooth service if all listeners are removed from
+ // the registered device
+ mMetadataListeners.remove(device);
+ final IBluetooth service = mService;
+ if (service == null) {
+ // Bluetooth is OFF, do nothing to Bluetooth service.
+ return true;
+ }
+ try {
+ return service.unregisterMetadataListener(device, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "unregisterMetadataListener fail", e);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * This interface is used to implement {@link BluetoothAdapter} metadata listener.
+ * @hide
+ */
+ @SystemApi
+ public interface OnMetadataChangedListener {
+ /**
+ * Callback triggered if the metadata of {@link BluetoothDevice} registered in
+ * {@link #addOnMetadataChangedListener}.
+ *
+ * @param device changed {@link BluetoothDevice}.
+ * @param key changed metadata key, one of BluetoothDevice.METADATA_*.
+ * @param value the new value of metadata as byte array.
+ */
+ void onMetadataChanged(@NonNull BluetoothDevice device, int key,
+ @Nullable byte[] value);
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothConnectionCallback mConnectionCallback =
+ new IBluetoothConnectionCallback.Stub() {
+ @Override
+ public void onDeviceConnected(BluetoothDevice device) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothConnectionCallback, Executor> callbackExecutorEntry:
+ mBluetoothConnectionCallbackExecutorMap.entrySet()) {
+ BluetoothConnectionCallback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onDeviceConnected(device));
+ }
+ }
+
+ @Override
+ public void onDeviceDisconnected(BluetoothDevice device, int hciReason) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ for (Map.Entry<BluetoothConnectionCallback, Executor> callbackExecutorEntry:
+ mBluetoothConnectionCallbackExecutorMap.entrySet()) {
+ BluetoothConnectionCallback callback = callbackExecutorEntry.getKey();
+ Executor executor = callbackExecutorEntry.getValue();
+ executor.execute(() -> callback.onDeviceDisconnected(device, hciReason));
+ }
+ }
+ };
+
+ /**
+ * Registers the BluetoothConnectionCallback to receive callback events when a bluetooth device
+ * (classic or low energy) is connected or disconnected.
+ *
+ * @param executor is the callback executor
+ * @param callback is the connection callback you wish to register
+ * @return true if the callback was registered successfully, false otherwise
+ * @throws IllegalArgumentException if the callback is already registered
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean registerBluetoothConnectionCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull BluetoothConnectionCallback callback) {
+ if (DBG) Log.d(TAG, "registerBluetoothConnectionCallback()");
+ if (callback == null) {
+ return false;
+ }
+
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ // If the callback map is empty, we register the service-to-app callback
+ if (mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ if (!mService.registerBluetoothConnectionCallback(mConnectionCallback,
+ mAttributionSource)) {
+ return false;
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ mBluetoothConnectionCallbackExecutorMap.remove(callback);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ // Adds the passed in callback to our map of callbacks to executors
+ if (mBluetoothConnectionCallbackExecutorMap.containsKey(callback)) {
+ throw new IllegalArgumentException("This callback has already been registered");
+ }
+ mBluetoothConnectionCallbackExecutorMap.put(callback, executor);
+ }
+
+ return true;
+ }
+
+ /**
+ * Unregisters the BluetoothConnectionCallback that was previously registered by the application
+ *
+ * @param callback is the connection callback you wish to unregister
+ * @return true if the callback was unregistered successfully, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean unregisterBluetoothConnectionCallback(
+ @NonNull BluetoothConnectionCallback callback) {
+ if (DBG) Log.d(TAG, "unregisterBluetoothConnectionCallback()");
+ if (callback == null) {
+ return false;
+ }
+
+ synchronized (mBluetoothConnectionCallbackExecutorMap) {
+ if (mBluetoothConnectionCallbackExecutorMap.remove(callback) != null) {
+ return false;
+ }
+ }
+
+ if (!mBluetoothConnectionCallbackExecutorMap.isEmpty()) {
+ return true;
+ }
+
+ // If the callback map is empty, we unregister the service-to-app callback
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.unregisterBluetoothConnectionCallback(mConnectionCallback,
+ mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+
+ return false;
+ }
+
+ /**
+ * This abstract class is used to implement callbacks for when a bluetooth classic or Bluetooth
+ * Low Energy (BLE) device is either connected or disconnected.
+ *
+ * @hide
+ */
+ public abstract static class BluetoothConnectionCallback {
+ /**
+ * Callback triggered when a bluetooth device (classic or BLE) is connected
+ * @param device is the connected bluetooth device
+ */
+ public void onDeviceConnected(BluetoothDevice device) {}
+
+ /**
+ * Callback triggered when a bluetooth device (classic or BLE) is disconnected
+ * @param device is the disconnected bluetooth device
+ * @param reason is the disconnect reason
+ */
+ public void onDeviceDisconnected(BluetoothDevice device, @DisconnectReason int reason) {}
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "REASON_" }, value = {
+ BluetoothStatusCodes.ERROR_UNKNOWN,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL_REQUEST,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE_REQUEST,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_TIMEOUT,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SECURITY,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SYSTEM_POLICY,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_RESOURCE_LIMIT_REACHED,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_CONNECTION_ALREADY_EXISTS,
+ BluetoothStatusCodes.ERROR_DISCONNECT_REASON_BAD_PARAMETERS})
+ public @interface DisconnectReason {}
+
+ /**
+ * Returns human-readable strings corresponding to {@link DisconnectReason}.
+ */
+ public static String disconnectReasonText(@DisconnectReason int reason) {
+ switch (reason) {
+ case BluetoothStatusCodes.ERROR_UNKNOWN:
+ return "Reason unknown";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL_REQUEST:
+ return "Local request";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE_REQUEST:
+ return "Remote request";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_LOCAL:
+ return "Local error";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_REMOTE:
+ return "Remote error";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_TIMEOUT:
+ return "Timeout";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SECURITY:
+ return "Security";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_SYSTEM_POLICY:
+ return "System policy";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_RESOURCE_LIMIT_REACHED:
+ return "Resource constrained";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_CONNECTION_ALREADY_EXISTS:
+ return "Connection already exists";
+ case BluetoothStatusCodes.ERROR_DISCONNECT_REASON_BAD_PARAMETERS:
+ return "Bad parameters";
+ default:
+ return "Unrecognized disconnect reason: " + reason;
+ }
+ }
+ }
+
+ /**
+ * Converts old constant of priority to the new for connection policy
+ *
+ * @param priority is the priority to convert to connection policy
+ * @return the equivalent connection policy constant to the priority
+ *
+ * @hide
+ */
+ public static @ConnectionPolicy int priorityToConnectionPolicy(int priority) {
+ switch(priority) {
+ case BluetoothProfile.PRIORITY_AUTO_CONNECT:
+ return BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+ case BluetoothProfile.PRIORITY_ON:
+ return BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+ case BluetoothProfile.PRIORITY_OFF:
+ return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ case BluetoothProfile.PRIORITY_UNDEFINED:
+ return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ default:
+ Log.e(TAG, "setPriority: Invalid priority: " + priority);
+ return BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ }
+ }
+
+ /**
+ * Converts new constant of connection policy to the old for priority
+ *
+ * @param connectionPolicy is the connection policy to convert to priority
+ * @return the equivalent priority constant to the connectionPolicy
+ *
+ * @hide
+ */
+ public static int connectionPolicyToPriority(@ConnectionPolicy int connectionPolicy) {
+ switch(connectionPolicy) {
+ case BluetoothProfile.CONNECTION_POLICY_ALLOWED:
+ return BluetoothProfile.PRIORITY_ON;
+ case BluetoothProfile.CONNECTION_POLICY_FORBIDDEN:
+ return BluetoothProfile.PRIORITY_OFF;
+ case BluetoothProfile.CONNECTION_POLICY_UNKNOWN:
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+ return BluetoothProfile.PRIORITY_UNDEFINED;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothAssignedNumbers.java b/framework/java/android/bluetooth/BluetoothAssignedNumbers.java
new file mode 100644
index 0000000000..41a34e0618
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAssignedNumbers.java
@@ -0,0 +1,1171 @@
+/*
+ * Copyright (C) 2010 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;
+
+/**
+ * Bluetooth Assigned Numbers.
+ * <p>
+ * For now we only include Company ID values.
+ *
+ * @see <a href="https://www.bluetooth.org/technical/assignednumbers/identifiers.htm"> The Official
+ * Bluetooth SIG Member Website | Company Identifiers</a>
+ */
+public class BluetoothAssignedNumbers {
+
+ // Bluetooth SIG Company ID values
+ /*
+ * Ericsson Technology Licensing.
+ */
+ public static final int ERICSSON_TECHNOLOGY = 0x0000;
+
+ /*
+ * Nokia Mobile Phones.
+ */
+ public static final int NOKIA_MOBILE_PHONES = 0x0001;
+
+ /*
+ * Intel Corp.
+ */
+ public static final int INTEL = 0x0002;
+
+ /*
+ * IBM Corp.
+ */
+ public static final int IBM = 0x0003;
+
+ /*
+ * Toshiba Corp.
+ */
+ public static final int TOSHIBA = 0x0004;
+
+ /*
+ * 3Com.
+ */
+ public static final int THREECOM = 0x0005;
+
+ /*
+ * Microsoft.
+ */
+ public static final int MICROSOFT = 0x0006;
+
+ /*
+ * Lucent.
+ */
+ public static final int LUCENT = 0x0007;
+
+ /*
+ * Motorola.
+ */
+ public static final int MOTOROLA = 0x0008;
+
+ /*
+ * Infineon Technologies AG.
+ */
+ public static final int INFINEON_TECHNOLOGIES = 0x0009;
+
+ /*
+ * Cambridge Silicon Radio.
+ */
+ public static final int CAMBRIDGE_SILICON_RADIO = 0x000A;
+
+ /*
+ * Silicon Wave.
+ */
+ public static final int SILICON_WAVE = 0x000B;
+
+ /*
+ * Digianswer A/S.
+ */
+ public static final int DIGIANSWER = 0x000C;
+
+ /*
+ * Texas Instruments Inc.
+ */
+ public static final int TEXAS_INSTRUMENTS = 0x000D;
+
+ /*
+ * Parthus Technologies Inc.
+ */
+ public static final int PARTHUS_TECHNOLOGIES = 0x000E;
+
+ /*
+ * Broadcom Corporation.
+ */
+ public static final int BROADCOM = 0x000F;
+
+ /*
+ * Mitel Semiconductor.
+ */
+ public static final int MITEL_SEMICONDUCTOR = 0x0010;
+
+ /*
+ * Widcomm, Inc.
+ */
+ public static final int WIDCOMM = 0x0011;
+
+ /*
+ * Zeevo, Inc.
+ */
+ public static final int ZEEVO = 0x0012;
+
+ /*
+ * Atmel Corporation.
+ */
+ public static final int ATMEL = 0x0013;
+
+ /*
+ * Mitsubishi Electric Corporation.
+ */
+ public static final int MITSUBISHI_ELECTRIC = 0x0014;
+
+ /*
+ * RTX Telecom A/S.
+ */
+ public static final int RTX_TELECOM = 0x0015;
+
+ /*
+ * KC Technology Inc.
+ */
+ public static final int KC_TECHNOLOGY = 0x0016;
+
+ /*
+ * Newlogic.
+ */
+ public static final int NEWLOGIC = 0x0017;
+
+ /*
+ * Transilica, Inc.
+ */
+ public static final int TRANSILICA = 0x0018;
+
+ /*
+ * Rohde & Schwarz GmbH & Co. KG.
+ */
+ public static final int ROHDE_AND_SCHWARZ = 0x0019;
+
+ /*
+ * TTPCom Limited.
+ */
+ public static final int TTPCOM = 0x001A;
+
+ /*
+ * Signia Technologies, Inc.
+ */
+ public static final int SIGNIA_TECHNOLOGIES = 0x001B;
+
+ /*
+ * Conexant Systems Inc.
+ */
+ public static final int CONEXANT_SYSTEMS = 0x001C;
+
+ /*
+ * Qualcomm.
+ */
+ public static final int QUALCOMM = 0x001D;
+
+ /*
+ * Inventel.
+ */
+ public static final int INVENTEL = 0x001E;
+
+ /*
+ * AVM Berlin.
+ */
+ public static final int AVM_BERLIN = 0x001F;
+
+ /*
+ * BandSpeed, Inc.
+ */
+ public static final int BANDSPEED = 0x0020;
+
+ /*
+ * Mansella Ltd.
+ */
+ public static final int MANSELLA = 0x0021;
+
+ /*
+ * NEC Corporation.
+ */
+ public static final int NEC = 0x0022;
+
+ /*
+ * WavePlus Technology Co., Ltd.
+ */
+ public static final int WAVEPLUS_TECHNOLOGY = 0x0023;
+
+ /*
+ * Alcatel.
+ */
+ public static final int ALCATEL = 0x0024;
+
+ /*
+ * Philips Semiconductors.
+ */
+ public static final int PHILIPS_SEMICONDUCTORS = 0x0025;
+
+ /*
+ * C Technologies.
+ */
+ public static final int C_TECHNOLOGIES = 0x0026;
+
+ /*
+ * Open Interface.
+ */
+ public static final int OPEN_INTERFACE = 0x0027;
+
+ /*
+ * R F Micro Devices.
+ */
+ public static final int RF_MICRO_DEVICES = 0x0028;
+
+ /*
+ * Hitachi Ltd.
+ */
+ public static final int HITACHI = 0x0029;
+
+ /*
+ * Symbol Technologies, Inc.
+ */
+ public static final int SYMBOL_TECHNOLOGIES = 0x002A;
+
+ /*
+ * Tenovis.
+ */
+ public static final int TENOVIS = 0x002B;
+
+ /*
+ * Macronix International Co. Ltd.
+ */
+ public static final int MACRONIX = 0x002C;
+
+ /*
+ * GCT Semiconductor.
+ */
+ public static final int GCT_SEMICONDUCTOR = 0x002D;
+
+ /*
+ * Norwood Systems.
+ */
+ public static final int NORWOOD_SYSTEMS = 0x002E;
+
+ /*
+ * MewTel Technology Inc.
+ */
+ public static final int MEWTEL_TECHNOLOGY = 0x002F;
+
+ /*
+ * ST Microelectronics.
+ */
+ public static final int ST_MICROELECTRONICS = 0x0030;
+
+ /*
+ * Synopsys.
+ */
+ public static final int SYNOPSYS = 0x0031;
+
+ /*
+ * Red-M (Communications) Ltd.
+ */
+ public static final int RED_M = 0x0032;
+
+ /*
+ * Commil Ltd.
+ */
+ public static final int COMMIL = 0x0033;
+
+ /*
+ * Computer Access Technology Corporation (CATC).
+ */
+ public static final int CATC = 0x0034;
+
+ /*
+ * Eclipse (HQ Espana) S.L.
+ */
+ public static final int ECLIPSE = 0x0035;
+
+ /*
+ * Renesas Technology Corp.
+ */
+ public static final int RENESAS_TECHNOLOGY = 0x0036;
+
+ /*
+ * Mobilian Corporation.
+ */
+ public static final int MOBILIAN_CORPORATION = 0x0037;
+
+ /*
+ * Terax.
+ */
+ public static final int TERAX = 0x0038;
+
+ /*
+ * Integrated System Solution Corp.
+ */
+ public static final int INTEGRATED_SYSTEM_SOLUTION = 0x0039;
+
+ /*
+ * Matsushita Electric Industrial Co., Ltd.
+ */
+ public static final int MATSUSHITA_ELECTRIC = 0x003A;
+
+ /*
+ * Gennum Corporation.
+ */
+ public static final int GENNUM = 0x003B;
+
+ /*
+ * Research In Motion.
+ */
+ public static final int RESEARCH_IN_MOTION = 0x003C;
+
+ /*
+ * IPextreme, Inc.
+ */
+ public static final int IPEXTREME = 0x003D;
+
+ /*
+ * Systems and Chips, Inc.
+ */
+ public static final int SYSTEMS_AND_CHIPS = 0x003E;
+
+ /*
+ * Bluetooth SIG, Inc.
+ */
+ public static final int BLUETOOTH_SIG = 0x003F;
+
+ /*
+ * Seiko Epson Corporation.
+ */
+ public static final int SEIKO_EPSON = 0x0040;
+
+ /*
+ * Integrated Silicon Solution Taiwan, Inc.
+ */
+ public static final int INTEGRATED_SILICON_SOLUTION = 0x0041;
+
+ /*
+ * CONWISE Technology Corporation Ltd.
+ */
+ public static final int CONWISE_TECHNOLOGY = 0x0042;
+
+ /*
+ * PARROT SA.
+ */
+ public static final int PARROT = 0x0043;
+
+ /*
+ * Socket Mobile.
+ */
+ public static final int SOCKET_MOBILE = 0x0044;
+
+ /*
+ * Atheros Communications, Inc.
+ */
+ public static final int ATHEROS_COMMUNICATIONS = 0x0045;
+
+ /*
+ * MediaTek, Inc.
+ */
+ public static final int MEDIATEK = 0x0046;
+
+ /*
+ * Bluegiga.
+ */
+ public static final int BLUEGIGA = 0x0047;
+
+ /*
+ * Marvell Technology Group Ltd.
+ */
+ public static final int MARVELL = 0x0048;
+
+ /*
+ * 3DSP Corporation.
+ */
+ public static final int THREE_DSP = 0x0049;
+
+ /*
+ * Accel Semiconductor Ltd.
+ */
+ public static final int ACCEL_SEMICONDUCTOR = 0x004A;
+
+ /*
+ * Continental Automotive Systems.
+ */
+ public static final int CONTINENTAL_AUTOMOTIVE = 0x004B;
+
+ /*
+ * Apple, Inc.
+ */
+ public static final int APPLE = 0x004C;
+
+ /*
+ * Staccato Communications, Inc.
+ */
+ public static final int STACCATO_COMMUNICATIONS = 0x004D;
+
+ /*
+ * Avago Technologies.
+ */
+ public static final int AVAGO = 0x004E;
+
+ /*
+ * APT Licensing Ltd.
+ */
+ public static final int APT_LICENSING = 0x004F;
+
+ /*
+ * SiRF Technology, Inc.
+ */
+ public static final int SIRF_TECHNOLOGY = 0x0050;
+
+ /*
+ * Tzero Technologies, Inc.
+ */
+ public static final int TZERO_TECHNOLOGIES = 0x0051;
+
+ /*
+ * J&M Corporation.
+ */
+ public static final int J_AND_M = 0x0052;
+
+ /*
+ * Free2move AB.
+ */
+ public static final int FREE2MOVE = 0x0053;
+
+ /*
+ * 3DiJoy Corporation.
+ */
+ public static final int THREE_DIJOY = 0x0054;
+
+ /*
+ * Plantronics, Inc.
+ */
+ public static final int PLANTRONICS = 0x0055;
+
+ /*
+ * Sony Ericsson Mobile Communications.
+ */
+ public static final int SONY_ERICSSON = 0x0056;
+
+ /*
+ * Harman International Industries, Inc.
+ */
+ public static final int HARMAN_INTERNATIONAL = 0x0057;
+
+ /*
+ * Vizio, Inc.
+ */
+ public static final int VIZIO = 0x0058;
+
+ /*
+ * Nordic Semiconductor ASA.
+ */
+ public static final int NORDIC_SEMICONDUCTOR = 0x0059;
+
+ /*
+ * EM Microelectronic-Marin SA.
+ */
+ public static final int EM_MICROELECTRONIC_MARIN = 0x005A;
+
+ /*
+ * Ralink Technology Corporation.
+ */
+ public static final int RALINK_TECHNOLOGY = 0x005B;
+
+ /*
+ * Belkin International, Inc.
+ */
+ public static final int BELKIN_INTERNATIONAL = 0x005C;
+
+ /*
+ * Realtek Semiconductor Corporation.
+ */
+ public static final int REALTEK_SEMICONDUCTOR = 0x005D;
+
+ /*
+ * Stonestreet One, LLC.
+ */
+ public static final int STONESTREET_ONE = 0x005E;
+
+ /*
+ * Wicentric, Inc.
+ */
+ public static final int WICENTRIC = 0x005F;
+
+ /*
+ * RivieraWaves S.A.S.
+ */
+ public static final int RIVIERAWAVES = 0x0060;
+
+ /*
+ * RDA Microelectronics.
+ */
+ public static final int RDA_MICROELECTRONICS = 0x0061;
+
+ /*
+ * Gibson Guitars.
+ */
+ public static final int GIBSON_GUITARS = 0x0062;
+
+ /*
+ * MiCommand Inc.
+ */
+ public static final int MICOMMAND = 0x0063;
+
+ /*
+ * Band XI International, LLC.
+ */
+ public static final int BAND_XI_INTERNATIONAL = 0x0064;
+
+ /*
+ * Hewlett-Packard Company.
+ */
+ public static final int HEWLETT_PACKARD = 0x0065;
+
+ /*
+ * 9Solutions Oy.
+ */
+ public static final int NINE_SOLUTIONS = 0x0066;
+
+ /*
+ * GN Netcom A/S.
+ */
+ public static final int GN_NETCOM = 0x0067;
+
+ /*
+ * General Motors.
+ */
+ public static final int GENERAL_MOTORS = 0x0068;
+
+ /*
+ * A&D Engineering, Inc.
+ */
+ public static final int A_AND_D_ENGINEERING = 0x0069;
+
+ /*
+ * MindTree Ltd.
+ */
+ public static final int MINDTREE = 0x006A;
+
+ /*
+ * Polar Electro OY.
+ */
+ public static final int POLAR_ELECTRO = 0x006B;
+
+ /*
+ * Beautiful Enterprise Co., Ltd.
+ */
+ public static final int BEAUTIFUL_ENTERPRISE = 0x006C;
+
+ /*
+ * BriarTek, Inc.
+ */
+ public static final int BRIARTEK = 0x006D;
+
+ /*
+ * Summit Data Communications, Inc.
+ */
+ public static final int SUMMIT_DATA_COMMUNICATIONS = 0x006E;
+
+ /*
+ * Sound ID.
+ */
+ public static final int SOUND_ID = 0x006F;
+
+ /*
+ * Monster, LLC.
+ */
+ public static final int MONSTER = 0x0070;
+
+ /*
+ * connectBlue AB.
+ */
+ public static final int CONNECTBLUE = 0x0071;
+
+ /*
+ * ShangHai Super Smart Electronics Co. Ltd.
+ */
+ public static final int SHANGHAI_SUPER_SMART_ELECTRONICS = 0x0072;
+
+ /*
+ * Group Sense Ltd.
+ */
+ public static final int GROUP_SENSE = 0x0073;
+
+ /*
+ * Zomm, LLC.
+ */
+ public static final int ZOMM = 0x0074;
+
+ /*
+ * Samsung Electronics Co. Ltd.
+ */
+ public static final int SAMSUNG_ELECTRONICS = 0x0075;
+
+ /*
+ * Creative Technology Ltd.
+ */
+ public static final int CREATIVE_TECHNOLOGY = 0x0076;
+
+ /*
+ * Laird Technologies.
+ */
+ public static final int LAIRD_TECHNOLOGIES = 0x0077;
+
+ /*
+ * Nike, Inc.
+ */
+ public static final int NIKE = 0x0078;
+
+ /*
+ * lesswire AG.
+ */
+ public static final int LESSWIRE = 0x0079;
+
+ /*
+ * MStar Semiconductor, Inc.
+ */
+ public static final int MSTAR_SEMICONDUCTOR = 0x007A;
+
+ /*
+ * Hanlynn Technologies.
+ */
+ public static final int HANLYNN_TECHNOLOGIES = 0x007B;
+
+ /*
+ * A & R Cambridge.
+ */
+ public static final int A_AND_R_CAMBRIDGE = 0x007C;
+
+ /*
+ * Seers Technology Co. Ltd.
+ */
+ public static final int SEERS_TECHNOLOGY = 0x007D;
+
+ /*
+ * Sports Tracking Technologies Ltd.
+ */
+ public static final int SPORTS_TRACKING_TECHNOLOGIES = 0x007E;
+
+ /*
+ * Autonet Mobile.
+ */
+ public static final int AUTONET_MOBILE = 0x007F;
+
+ /*
+ * DeLorme Publishing Company, Inc.
+ */
+ public static final int DELORME_PUBLISHING_COMPANY = 0x0080;
+
+ /*
+ * WuXi Vimicro.
+ */
+ public static final int WUXI_VIMICRO = 0x0081;
+
+ /*
+ * Sennheiser Communications A/S.
+ */
+ public static final int SENNHEISER_COMMUNICATIONS = 0x0082;
+
+ /*
+ * TimeKeeping Systems, Inc.
+ */
+ public static final int TIMEKEEPING_SYSTEMS = 0x0083;
+
+ /*
+ * Ludus Helsinki Ltd.
+ */
+ public static final int LUDUS_HELSINKI = 0x0084;
+
+ /*
+ * BlueRadios, Inc.
+ */
+ public static final int BLUERADIOS = 0x0085;
+
+ /*
+ * equinox AG.
+ */
+ public static final int EQUINOX_AG = 0x0086;
+
+ /*
+ * Garmin International, Inc.
+ */
+ public static final int GARMIN_INTERNATIONAL = 0x0087;
+
+ /*
+ * Ecotest.
+ */
+ public static final int ECOTEST = 0x0088;
+
+ /*
+ * GN ReSound A/S.
+ */
+ public static final int GN_RESOUND = 0x0089;
+
+ /*
+ * Jawbone.
+ */
+ public static final int JAWBONE = 0x008A;
+
+ /*
+ * Topcorn Positioning Systems, LLC.
+ */
+ public static final int TOPCORN_POSITIONING_SYSTEMS = 0x008B;
+
+ /*
+ * Qualcomm Labs, Inc.
+ */
+ public static final int QUALCOMM_LABS = 0x008C;
+
+ /*
+ * Zscan Software.
+ */
+ public static final int ZSCAN_SOFTWARE = 0x008D;
+
+ /*
+ * Quintic Corp.
+ */
+ public static final int QUINTIC = 0x008E;
+
+ /*
+ * Stollman E+V GmbH.
+ */
+ public static final int STOLLMAN_E_PLUS_V = 0x008F;
+
+ /*
+ * Funai Electric Co., Ltd.
+ */
+ public static final int FUNAI_ELECTRIC = 0x0090;
+
+ /*
+ * Advanced PANMOBIL Systems GmbH & Co. KG.
+ */
+ public static final int ADVANCED_PANMOBIL_SYSTEMS = 0x0091;
+
+ /*
+ * ThinkOptics, Inc.
+ */
+ public static final int THINKOPTICS = 0x0092;
+
+ /*
+ * Universal Electronics, Inc.
+ */
+ public static final int UNIVERSAL_ELECTRONICS = 0x0093;
+
+ /*
+ * Airoha Technology Corp.
+ */
+ public static final int AIROHA_TECHNOLOGY = 0x0094;
+
+ /*
+ * NEC Lighting, Ltd.
+ */
+ public static final int NEC_LIGHTING = 0x0095;
+
+ /*
+ * ODM Technology, Inc.
+ */
+ public static final int ODM_TECHNOLOGY = 0x0096;
+
+ /*
+ * Bluetrek Technologies Limited.
+ */
+ public static final int BLUETREK_TECHNOLOGIES = 0x0097;
+
+ /*
+ * zer01.tv GmbH.
+ */
+ public static final int ZER01_TV = 0x0098;
+
+ /*
+ * i.Tech Dynamic Global Distribution Ltd.
+ */
+ public static final int I_TECH_DYNAMIC_GLOBAL_DISTRIBUTION = 0x0099;
+
+ /*
+ * Alpwise.
+ */
+ public static final int ALPWISE = 0x009A;
+
+ /*
+ * Jiangsu Toppower Automotive Electronics Co., Ltd.
+ */
+ public static final int JIANGSU_TOPPOWER_AUTOMOTIVE_ELECTRONICS = 0x009B;
+
+ /*
+ * Colorfy, Inc.
+ */
+ public static final int COLORFY = 0x009C;
+
+ /*
+ * Geoforce Inc.
+ */
+ public static final int GEOFORCE = 0x009D;
+
+ /*
+ * Bose Corporation.
+ */
+ public static final int BOSE = 0x009E;
+
+ /*
+ * Suunto Oy.
+ */
+ public static final int SUUNTO = 0x009F;
+
+ /*
+ * Kensington Computer Products Group.
+ */
+ public static final int KENSINGTON_COMPUTER_PRODUCTS_GROUP = 0x00A0;
+
+ /*
+ * SR-Medizinelektronik.
+ */
+ public static final int SR_MEDIZINELEKTRONIK = 0x00A1;
+
+ /*
+ * Vertu Corporation Limited.
+ */
+ public static final int VERTU = 0x00A2;
+
+ /*
+ * Meta Watch Ltd.
+ */
+ public static final int META_WATCH = 0x00A3;
+
+ /*
+ * LINAK A/S.
+ */
+ public static final int LINAK = 0x00A4;
+
+ /*
+ * OTL Dynamics LLC.
+ */
+ public static final int OTL_DYNAMICS = 0x00A5;
+
+ /*
+ * Panda Ocean Inc.
+ */
+ public static final int PANDA_OCEAN = 0x00A6;
+
+ /*
+ * Visteon Corporation.
+ */
+ public static final int VISTEON = 0x00A7;
+
+ /*
+ * ARP Devices Limited.
+ */
+ public static final int ARP_DEVICES = 0x00A8;
+
+ /*
+ * Magneti Marelli S.p.A.
+ */
+ public static final int MAGNETI_MARELLI = 0x00A9;
+
+ /*
+ * CAEN RFID srl.
+ */
+ public static final int CAEN_RFID = 0x00AA;
+
+ /*
+ * Ingenieur-Systemgruppe Zahn GmbH.
+ */
+ public static final int INGENIEUR_SYSTEMGRUPPE_ZAHN = 0x00AB;
+
+ /*
+ * Green Throttle Games.
+ */
+ public static final int GREEN_THROTTLE_GAMES = 0x00AC;
+
+ /*
+ * Peter Systemtechnik GmbH.
+ */
+ public static final int PETER_SYSTEMTECHNIK = 0x00AD;
+
+ /*
+ * Omegawave Oy.
+ */
+ public static final int OMEGAWAVE = 0x00AE;
+
+ /*
+ * Cinetix.
+ */
+ public static final int CINETIX = 0x00AF;
+
+ /*
+ * Passif Semiconductor Corp.
+ */
+ public static final int PASSIF_SEMICONDUCTOR = 0x00B0;
+
+ /*
+ * Saris Cycling Group, Inc.
+ */
+ public static final int SARIS_CYCLING_GROUP = 0x00B1;
+
+ /*
+ * Bekey A/S.
+ */
+ public static final int BEKEY = 0x00B2;
+
+ /*
+ * Clarinox Technologies Pty. Ltd.
+ */
+ public static final int CLARINOX_TECHNOLOGIES = 0x00B3;
+
+ /*
+ * BDE Technology Co., Ltd.
+ */
+ public static final int BDE_TECHNOLOGY = 0x00B4;
+
+ /*
+ * Swirl Networks.
+ */
+ public static final int SWIRL_NETWORKS = 0x00B5;
+
+ /*
+ * Meso international.
+ */
+ public static final int MESO_INTERNATIONAL = 0x00B6;
+
+ /*
+ * TreLab Ltd.
+ */
+ public static final int TRELAB = 0x00B7;
+
+ /*
+ * Qualcomm Innovation Center, Inc. (QuIC).
+ */
+ public static final int QUALCOMM_INNOVATION_CENTER = 0x00B8;
+
+ /*
+ * Johnson Controls, Inc.
+ */
+ public static final int JOHNSON_CONTROLS = 0x00B9;
+
+ /*
+ * Starkey Laboratories Inc.
+ */
+ public static final int STARKEY_LABORATORIES = 0x00BA;
+
+ /*
+ * S-Power Electronics Limited.
+ */
+ public static final int S_POWER_ELECTRONICS = 0x00BB;
+
+ /*
+ * Ace Sensor Inc.
+ */
+ public static final int ACE_SENSOR = 0x00BC;
+
+ /*
+ * Aplix Corporation.
+ */
+ public static final int APLIX = 0x00BD;
+
+ /*
+ * AAMP of America.
+ */
+ public static final int AAMP_OF_AMERICA = 0x00BE;
+
+ /*
+ * Stalmart Technology Limited.
+ */
+ public static final int STALMART_TECHNOLOGY = 0x00BF;
+
+ /*
+ * AMICCOM Electronics Corporation.
+ */
+ public static final int AMICCOM_ELECTRONICS = 0x00C0;
+
+ /*
+ * Shenzhen Excelsecu Data Technology Co.,Ltd.
+ */
+ public static final int SHENZHEN_EXCELSECU_DATA_TECHNOLOGY = 0x00C1;
+
+ /*
+ * Geneq Inc.
+ */
+ public static final int GENEQ = 0x00C2;
+
+ /*
+ * adidas AG.
+ */
+ public static final int ADIDAS = 0x00C3;
+
+ /*
+ * LG Electronics.
+ */
+ public static final int LG_ELECTRONICS = 0x00C4;
+
+ /*
+ * Onset Computer Corporation.
+ */
+ public static final int ONSET_COMPUTER = 0x00C5;
+
+ /*
+ * Selfly BV.
+ */
+ public static final int SELFLY = 0x00C6;
+
+ /*
+ * Quuppa Oy.
+ */
+ public static final int QUUPPA = 0x00C7;
+
+ /*
+ * GeLo Inc.
+ */
+ public static final int GELO = 0x00C8;
+
+ /*
+ * Evluma.
+ */
+ public static final int EVLUMA = 0x00C9;
+
+ /*
+ * MC10.
+ */
+ public static final int MC10 = 0x00CA;
+
+ /*
+ * Binauric SE.
+ */
+ public static final int BINAURIC = 0x00CB;
+
+ /*
+ * Beats Electronics.
+ */
+ public static final int BEATS_ELECTRONICS = 0x00CC;
+
+ /*
+ * Microchip Technology Inc.
+ */
+ public static final int MICROCHIP_TECHNOLOGY = 0x00CD;
+
+ /*
+ * Elgato Systems GmbH.
+ */
+ public static final int ELGATO_SYSTEMS = 0x00CE;
+
+ /*
+ * ARCHOS SA.
+ */
+ public static final int ARCHOS = 0x00CF;
+
+ /*
+ * Dexcom, Inc.
+ */
+ public static final int DEXCOM = 0x00D0;
+
+ /*
+ * Polar Electro Europe B.V.
+ */
+ public static final int POLAR_ELECTRO_EUROPE = 0x00D1;
+
+ /*
+ * Dialog Semiconductor B.V.
+ */
+ public static final int DIALOG_SEMICONDUCTOR = 0x00D2;
+
+ /*
+ * Taixingbang Technology (HK) Co,. LTD.
+ */
+ public static final int TAIXINGBANG_TECHNOLOGY = 0x00D3;
+
+ /*
+ * Kawantech.
+ */
+ public static final int KAWANTECH = 0x00D4;
+
+ /*
+ * Austco Communication Systems.
+ */
+ public static final int AUSTCO_COMMUNICATION_SYSTEMS = 0x00D5;
+
+ /*
+ * Timex Group USA, Inc.
+ */
+ public static final int TIMEX_GROUP_USA = 0x00D6;
+
+ /*
+ * Qualcomm Technologies, Inc.
+ */
+ public static final int QUALCOMM_TECHNOLOGIES = 0x00D7;
+
+ /*
+ * Qualcomm Connected Experiences, Inc.
+ */
+ public static final int QUALCOMM_CONNECTED_EXPERIENCES = 0x00D8;
+
+ /*
+ * Voyetra Turtle Beach.
+ */
+ public static final int VOYETRA_TURTLE_BEACH = 0x00D9;
+
+ /*
+ * txtr GmbH.
+ */
+ public static final int TXTR = 0x00DA;
+
+ /*
+ * Biosentronics.
+ */
+ public static final int BIOSENTRONICS = 0x00DB;
+
+ /*
+ * Procter & Gamble.
+ */
+ public static final int PROCTER_AND_GAMBLE = 0x00DC;
+
+ /*
+ * Hosiden Corporation.
+ */
+ public static final int HOSIDEN = 0x00DD;
+
+ /*
+ * Muzik LLC.
+ */
+ public static final int MUZIK = 0x00DE;
+
+ /*
+ * Misfit Wearables Corp.
+ */
+ public static final int MISFIT_WEARABLES = 0x00DF;
+
+ /*
+ * Google.
+ */
+ public static final int GOOGLE = 0x00E0;
+
+ /*
+ * Danlers Ltd.
+ */
+ public static final int DANLERS = 0x00E1;
+
+ /*
+ * Semilink Inc.
+ */
+ public static final int SEMILINK = 0x00E2;
+
+ /*
+ * You can't instantiate one of these.
+ */
+ private BluetoothAssignedNumbers() {
+ }
+
+}
diff --git a/framework/java/android/bluetooth/BluetoothAudioConfig.java b/framework/java/android/bluetooth/BluetoothAudioConfig.java
new file mode 100644
index 0000000000..4c8b8c11fb
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAudioConfig.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the audio configuration for a Bluetooth A2DP source device.
+ *
+ * {@see BluetoothA2dpSink}
+ *
+ * {@hide}
+ */
+public final class BluetoothAudioConfig implements Parcelable {
+
+ private final int mSampleRate;
+ private final int mChannelConfig;
+ private final int mAudioFormat;
+
+ public BluetoothAudioConfig(int sampleRate, int channelConfig, int audioFormat) {
+ mSampleRate = sampleRate;
+ mChannelConfig = channelConfig;
+ mAudioFormat = audioFormat;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothAudioConfig) {
+ BluetoothAudioConfig bac = (BluetoothAudioConfig) o;
+ return (bac.mSampleRate == mSampleRate && bac.mChannelConfig == mChannelConfig
+ && bac.mAudioFormat == mAudioFormat);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mSampleRate | (mChannelConfig << 24) | (mAudioFormat << 28);
+ }
+
+ @Override
+ public String toString() {
+ return "{mSampleRate:" + mSampleRate + ",mChannelConfig:" + mChannelConfig
+ + ",mAudioFormat:" + mAudioFormat + "}";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothAudioConfig> CREATOR =
+ new Parcelable.Creator<BluetoothAudioConfig>() {
+ public BluetoothAudioConfig createFromParcel(Parcel in) {
+ int sampleRate = in.readInt();
+ int channelConfig = in.readInt();
+ int audioFormat = in.readInt();
+ return new BluetoothAudioConfig(sampleRate, channelConfig, audioFormat);
+ }
+
+ public BluetoothAudioConfig[] newArray(int size) {
+ return new BluetoothAudioConfig[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSampleRate);
+ out.writeInt(mChannelConfig);
+ out.writeInt(mAudioFormat);
+ }
+
+ /**
+ * Returns the sample rate in samples per second
+ *
+ * @return sample rate
+ */
+ public int getSampleRate() {
+ return mSampleRate;
+ }
+
+ /**
+ * Returns the channel configuration (either {@link android.media.AudioFormat#CHANNEL_IN_MONO}
+ * or {@link android.media.AudioFormat#CHANNEL_IN_STEREO})
+ *
+ * @return channel configuration
+ */
+ public int getChannelConfig() {
+ return mChannelConfig;
+ }
+
+ /**
+ * Returns the channel audio format (either {@link android.media.AudioFormat#ENCODING_PCM_16BIT}
+ * or {@link android.media.AudioFormat#ENCODING_PCM_8BIT}
+ *
+ * @return audio format
+ */
+ public int getAudioFormat() {
+ return mAudioFormat;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothAvrcp.java b/framework/java/android/bluetooth/BluetoothAvrcp.java
new file mode 100644
index 0000000000..1a4c759064
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAvrcp.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * This class contains constants for Bluetooth AVRCP profile.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcp {
+
+ /*
+ * State flags for Passthrough commands
+ */
+ public static final int PASSTHROUGH_STATE_PRESS = 0;
+ public static final int PASSTHROUGH_STATE_RELEASE = 1;
+
+ /*
+ * Operation IDs for Passthrough commands
+ */
+ public static final int PASSTHROUGH_ID_SELECT = 0x00; /* select */
+ public static final int PASSTHROUGH_ID_UP = 0x01; /* up */
+ public static final int PASSTHROUGH_ID_DOWN = 0x02; /* down */
+ public static final int PASSTHROUGH_ID_LEFT = 0x03; /* left */
+ public static final int PASSTHROUGH_ID_RIGHT = 0x04; /* right */
+ public static final int PASSTHROUGH_ID_RIGHT_UP = 0x05; /* right-up */
+ public static final int PASSTHROUGH_ID_RIGHT_DOWN = 0x06; /* right-down */
+ public static final int PASSTHROUGH_ID_LEFT_UP = 0x07; /* left-up */
+ public static final int PASSTHROUGH_ID_LEFT_DOWN = 0x08; /* left-down */
+ public static final int PASSTHROUGH_ID_ROOT_MENU = 0x09; /* root menu */
+ public static final int PASSTHROUGH_ID_SETUP_MENU = 0x0A; /* setup menu */
+ public static final int PASSTHROUGH_ID_CONT_MENU = 0x0B; /* contents menu */
+ public static final int PASSTHROUGH_ID_FAV_MENU = 0x0C; /* favorite menu */
+ public static final int PASSTHROUGH_ID_EXIT = 0x0D; /* exit */
+ public static final int PASSTHROUGH_ID_0 = 0x20; /* 0 */
+ public static final int PASSTHROUGH_ID_1 = 0x21; /* 1 */
+ public static final int PASSTHROUGH_ID_2 = 0x22; /* 2 */
+ public static final int PASSTHROUGH_ID_3 = 0x23; /* 3 */
+ public static final int PASSTHROUGH_ID_4 = 0x24; /* 4 */
+ public static final int PASSTHROUGH_ID_5 = 0x25; /* 5 */
+ public static final int PASSTHROUGH_ID_6 = 0x26; /* 6 */
+ public static final int PASSTHROUGH_ID_7 = 0x27; /* 7 */
+ public static final int PASSTHROUGH_ID_8 = 0x28; /* 8 */
+ public static final int PASSTHROUGH_ID_9 = 0x29; /* 9 */
+ public static final int PASSTHROUGH_ID_DOT = 0x2A; /* dot */
+ public static final int PASSTHROUGH_ID_ENTER = 0x2B; /* enter */
+ public static final int PASSTHROUGH_ID_CLEAR = 0x2C; /* clear */
+ public static final int PASSTHROUGH_ID_CHAN_UP = 0x30; /* channel up */
+ public static final int PASSTHROUGH_ID_CHAN_DOWN = 0x31; /* channel down */
+ public static final int PASSTHROUGH_ID_PREV_CHAN = 0x32; /* previous channel */
+ public static final int PASSTHROUGH_ID_SOUND_SEL = 0x33; /* sound select */
+ public static final int PASSTHROUGH_ID_INPUT_SEL = 0x34; /* input select */
+ public static final int PASSTHROUGH_ID_DISP_INFO = 0x35; /* display information */
+ public static final int PASSTHROUGH_ID_HELP = 0x36; /* help */
+ public static final int PASSTHROUGH_ID_PAGE_UP = 0x37; /* page up */
+ public static final int PASSTHROUGH_ID_PAGE_DOWN = 0x38; /* page down */
+ public static final int PASSTHROUGH_ID_POWER = 0x40; /* power */
+ public static final int PASSTHROUGH_ID_VOL_UP = 0x41; /* volume up */
+ public static final int PASSTHROUGH_ID_VOL_DOWN = 0x42; /* volume down */
+ public static final int PASSTHROUGH_ID_MUTE = 0x43; /* mute */
+ public static final int PASSTHROUGH_ID_PLAY = 0x44; /* play */
+ public static final int PASSTHROUGH_ID_STOP = 0x45; /* stop */
+ public static final int PASSTHROUGH_ID_PAUSE = 0x46; /* pause */
+ public static final int PASSTHROUGH_ID_RECORD = 0x47; /* record */
+ public static final int PASSTHROUGH_ID_REWIND = 0x48; /* rewind */
+ public static final int PASSTHROUGH_ID_FAST_FOR = 0x49; /* fast forward */
+ public static final int PASSTHROUGH_ID_EJECT = 0x4A; /* eject */
+ public static final int PASSTHROUGH_ID_FORWARD = 0x4B; /* forward */
+ public static final int PASSTHROUGH_ID_BACKWARD = 0x4C; /* backward */
+ public static final int PASSTHROUGH_ID_ANGLE = 0x50; /* angle */
+ public static final int PASSTHROUGH_ID_SUBPICT = 0x51; /* subpicture */
+ public static final int PASSTHROUGH_ID_F1 = 0x71; /* F1 */
+ public static final int PASSTHROUGH_ID_F2 = 0x72; /* F2 */
+ public static final int PASSTHROUGH_ID_F3 = 0x73; /* F3 */
+ public static final int PASSTHROUGH_ID_F4 = 0x74; /* F4 */
+ public static final int PASSTHROUGH_ID_F5 = 0x75; /* F5 */
+ public static final int PASSTHROUGH_ID_VENDOR = 0x7E; /* vendor unique */
+ public static final int PASSTHROUGH_KEYPRESSED_RELEASE = 0x80;
+}
diff --git a/framework/java/android/bluetooth/BluetoothAvrcpController.java b/framework/java/android/bluetooth/BluetoothAvrcpController.java
new file mode 100644
index 0000000000..81fc3e11e9
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAvrcpController.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2014 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.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth AVRCP Controller. It currently
+ * supports player information, playback support and track metadata.
+ *
+ * <p>BluetoothAvrcpController is a proxy object for controlling the Bluetooth AVRCP
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothAvrcpController proxy object.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcpController implements BluetoothProfile {
+ private static final String TAG = "BluetoothAvrcpController";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the AVRCP Controller
+ * 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.avrcp-controller.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the change in player application setting state on AVRCP AG.
+ *
+ * <p>This intent will have the following extras:
+ * <ul>
+ * <li> {@link #EXTRA_PLAYER_SETTING} - {@link BluetoothAvrcpPlayerSettings} containing the
+ * most recent player setting. </li>
+ * </ul>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PLAYER_SETTING =
+ "android.bluetooth.avrcp-controller.profile.action.PLAYER_SETTING";
+
+ public static final String EXTRA_PLAYER_SETTING =
+ "android.bluetooth.avrcp-controller.profile.extra.PLAYER_SETTING";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothAvrcpController> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.AVRCP_CONTROLLER,
+ "BluetoothAvrcpController", IBluetoothAvrcpController.class.getName()) {
+ @Override
+ public IBluetoothAvrcpController getServiceInterface(IBinder service) {
+ return IBluetoothAvrcpController.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothAvrcpController proxy object for interacting with the local
+ * Bluetooth AVRCP service.
+ */
+ /* package */ BluetoothAvrcpController(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /*package*/ void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothAvrcpController getService() {
+ return mProfileConnector.getService();
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothAvrcpController service = getService();
+ 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.getConnectedDevices(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 IBluetoothAvrcpController service = getService();
+ 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("getState(" + device + ")");
+ final IBluetoothAvrcpController service = getService();
+ 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.getConnectionState(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;
+ }
+
+ /**
+ * Gets the player application settings.
+ *
+ * @return the {@link BluetoothAvrcpPlayerSettings} or {@link null} if there is an error.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothAvrcpPlayerSettings getPlayerSettings(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getPlayerSettings");
+ BluetoothAvrcpPlayerSettings settings = null;
+ final IBluetoothAvrcpController service = getService();
+ final BluetoothAvrcpPlayerSettings 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<BluetoothAvrcpPlayerSettings> recv =
+ new SynchronousResultReceiver();
+ service.getPlayerSettings(device, mAttributionSource, recv);
+ settings = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the player app setting for current player.
+ * returns true in case setting is supported by remote, false otherwise
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPlayerApplicationSetting(BluetoothAvrcpPlayerSettings plAppSetting) {
+ if (DBG) Log.d(TAG, "setPlayerApplicationSetting");
+ final IBluetoothAvrcpController service = getService();
+ 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.setPlayerApplicationSetting(plAppSetting, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Group Navigation Command to Remote.
+ * possible keycode values: next_grp, previous_grp defined above
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void sendGroupNavigationCmd(BluetoothDevice device, int keyCode, int keyState) {
+ Log.d(TAG, "sendGroupNavigationCmd dev = " + device + " key " + keyCode + " State = "
+ + keyState);
+ final IBluetoothAvrcpController service = getService();
+ 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.sendGroupNavigationCmd(device, keyCode, keyState, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothAvrcpPlayerSettings.java b/framework/java/android/bluetooth/BluetoothAvrcpPlayerSettings.java
new file mode 100644
index 0000000000..30aea1abf7
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothAvrcpPlayerSettings.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2015 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 android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Class used to identify settings associated with the player on AG.
+ *
+ * {@hide}
+ */
+public final class BluetoothAvrcpPlayerSettings implements Parcelable {
+ public static final String TAG = "BluetoothAvrcpPlayerSettings";
+
+ /**
+ * Equalizer setting.
+ */
+ public static final int SETTING_EQUALIZER = 0x01;
+
+ /**
+ * Repeat setting.
+ */
+ public static final int SETTING_REPEAT = 0x02;
+
+ /**
+ * Shuffle setting.
+ */
+ public static final int SETTING_SHUFFLE = 0x04;
+
+ /**
+ * Scan mode setting.
+ */
+ public static final int SETTING_SCAN = 0x08;
+
+ /**
+ * Invalid state.
+ *
+ * Used for returning error codes.
+ */
+ public static final int STATE_INVALID = -1;
+
+ /**
+ * OFF state.
+ *
+ * Denotes a general OFF state. Applies to all settings.
+ */
+ public static final int STATE_OFF = 0x00;
+
+ /**
+ * ON state.
+ *
+ * Applies to {@link SETTING_EQUALIZER}.
+ */
+ public static final int STATE_ON = 0x01;
+
+ /**
+ * Single track repeat.
+ *
+ * Applies only to {@link SETTING_REPEAT}.
+ */
+ public static final int STATE_SINGLE_TRACK = 0x02;
+
+ /**
+ * All track repeat/shuffle.
+ *
+ * Applies to {@link #SETTING_REPEAT}, {@link #SETTING_SHUFFLE} and {@link #SETTING_SCAN}.
+ */
+ public static final int STATE_ALL_TRACK = 0x03;
+
+ /**
+ * Group repeat/shuffle.
+ *
+ * Applies to {@link #SETTING_REPEAT}, {@link #SETTING_SHUFFLE} and {@link #SETTING_SCAN}.
+ */
+ public static final int STATE_GROUP = 0x04;
+
+ /**
+ * List of supported settings ORed.
+ */
+ private int mSettings;
+
+ /**
+ * Hash map of current capability values.
+ */
+ private Map<Integer, Integer> mSettingsValue = new HashMap<Integer, Integer>();
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSettings);
+ out.writeInt(mSettingsValue.size());
+ for (int k : mSettingsValue.keySet()) {
+ out.writeInt(k);
+ out.writeInt(mSettingsValue.get(k));
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothAvrcpPlayerSettings> CREATOR =
+ new Parcelable.Creator<BluetoothAvrcpPlayerSettings>() {
+ public BluetoothAvrcpPlayerSettings createFromParcel(Parcel in) {
+ return new BluetoothAvrcpPlayerSettings(in);
+ }
+
+ public BluetoothAvrcpPlayerSettings[] newArray(int size) {
+ return new BluetoothAvrcpPlayerSettings[size];
+ }
+ };
+
+ private BluetoothAvrcpPlayerSettings(Parcel in) {
+ mSettings = in.readInt();
+ int numSettings = in.readInt();
+ for (int i = 0; i < numSettings; i++) {
+ mSettingsValue.put(in.readInt(), in.readInt());
+ }
+ }
+
+ /**
+ * Create a new player settings object.
+ *
+ * @param settings a ORed value of SETTINGS_* defined above.
+ */
+ public BluetoothAvrcpPlayerSettings(int settings) {
+ mSettings = settings;
+ }
+
+ /**
+ * Get the supported settings.
+ *
+ * @return int ORed value of supported settings.
+ */
+ public int getSettings() {
+ return mSettings;
+ }
+
+ /**
+ * Add a setting value.
+ *
+ * The setting must be part of possible settings in {@link getSettings()}.
+ *
+ * @param setting setting config.
+ * @param value value for the setting.
+ * @throws IllegalStateException if the setting is not supported.
+ */
+ public void addSettingValue(int setting, int value) {
+ if ((setting & mSettings) == 0) {
+ Log.e(TAG, "Setting not supported: " + setting + " " + mSettings);
+ throw new IllegalStateException("Setting not supported: " + setting);
+ }
+ mSettingsValue.put(setting, value);
+ }
+
+ /**
+ * Get a setting value.
+ *
+ * The setting must be part of possible settings in {@link getSettings()}.
+ *
+ * @param setting setting config.
+ * @return value value for the setting.
+ * @throws IllegalStateException if the setting is not supported.
+ */
+ public int getSettingValue(int setting) {
+ if ((setting & mSettings) == 0) {
+ Log.e(TAG, "Setting not supported: " + setting + " " + mSettings);
+ throw new IllegalStateException("Setting not supported: " + setting);
+ }
+ Integer i = mSettingsValue.get(setting);
+ if (i == null) return -1;
+ return i;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothClass.java b/framework/java/android/bluetooth/BluetoothClass.java
new file mode 100755
index 0000000000..8535b4fd28
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothClass.java
@@ -0,0 +1,441 @@
+/*
+ * 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 android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Represents a Bluetooth class, which describes general characteristics
+ * and capabilities of a device. For example, a Bluetooth class will
+ * specify the general device type such as a phone, a computer, or
+ * headset, and whether it's capable of services such as audio or telephony.
+ *
+ * <p>Every Bluetooth class is composed of zero or more service classes, and
+ * exactly one device class. The device class is further broken down into major
+ * and minor device class components.
+ *
+ * <p>{@link BluetoothClass} is useful as a hint to roughly describe a device
+ * (for example to show an icon in the UI), but does not reliably describe which
+ * Bluetooth profiles or services are actually supported by a device. Accurate
+ * service discovery is done through SDP requests, which are automatically
+ * performed when creating an RFCOMM socket with {@link
+ * BluetoothDevice#createRfcommSocketToServiceRecord} and {@link
+ * BluetoothAdapter#listenUsingRfcommWithServiceRecord}</p>
+ *
+ * <p>Use {@link BluetoothDevice#getBluetoothClass} to retrieve the class for
+ * a remote device.
+ *
+ * <!--
+ * The Bluetooth class is a 32 bit field. The format of these bits is defined at
+ * http://www.bluetooth.org/Technical/AssignedNumbers/baseband.htm
+ * (login required). This class contains that 32 bit field, and provides
+ * constants and methods to determine which Service Class(es) and Device Class
+ * are encoded in that field.
+ * -->
+ */
+public final class BluetoothClass implements Parcelable {
+ /**
+ * Legacy error value. Applications should use null instead.
+ *
+ * @hide
+ */
+ public static final int ERROR = 0xFF000000;
+
+ private final int mClass;
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public BluetoothClass(int classInt) {
+ mClass = classInt;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothClass) {
+ return mClass == ((BluetoothClass) o).mClass;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mClass;
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toHexString(mClass);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothClass> CREATOR =
+ new Parcelable.Creator<BluetoothClass>() {
+ public BluetoothClass createFromParcel(Parcel in) {
+ return new BluetoothClass(in.readInt());
+ }
+
+ public BluetoothClass[] newArray(int size) {
+ return new BluetoothClass[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mClass);
+ }
+
+ /**
+ * Defines all service class constants.
+ * <p>Each {@link BluetoothClass} encodes zero or more service classes.
+ */
+ public static final class Service {
+ private static final int BITMASK = 0xFFE000;
+
+ public static final int LIMITED_DISCOVERABILITY = 0x002000;
+ public static final int LE_AUDIO = 0x004000;
+ public static final int POSITIONING = 0x010000;
+ public static final int NETWORKING = 0x020000;
+ public static final int RENDER = 0x040000;
+ public static final int CAPTURE = 0x080000;
+ public static final int OBJECT_TRANSFER = 0x100000;
+ public static final int AUDIO = 0x200000;
+ public static final int TELEPHONY = 0x400000;
+ public static final int INFORMATION = 0x800000;
+ }
+
+ /**
+ * Return true if the specified service class is supported by this
+ * {@link BluetoothClass}.
+ * <p>Valid service classes are the public constants in
+ * {@link BluetoothClass.Service}. For example, {@link
+ * BluetoothClass.Service#AUDIO}.
+ *
+ * @param service valid service class
+ * @return true if the service class is supported
+ */
+ public boolean hasService(int service) {
+ return ((mClass & Service.BITMASK & service) != 0);
+ }
+
+ /**
+ * Defines all device class constants.
+ * <p>Each {@link BluetoothClass} encodes exactly one device class, with
+ * major and minor components.
+ * <p>The constants in {@link
+ * BluetoothClass.Device} represent a combination of major and minor
+ * device components (the complete device class). The constants in {@link
+ * BluetoothClass.Device.Major} represent only major device classes.
+ * <p>See {@link BluetoothClass.Service} for service class constants.
+ */
+ public static class Device {
+ private static final int BITMASK = 0x1FFC;
+
+ /**
+ * Defines all major device class constants.
+ * <p>See {@link BluetoothClass.Device} for minor classes.
+ */
+ public static class Major {
+ private static final int BITMASK = 0x1F00;
+
+ public static final int MISC = 0x0000;
+ public static final int COMPUTER = 0x0100;
+ public static final int PHONE = 0x0200;
+ public static final int NETWORKING = 0x0300;
+ public static final int AUDIO_VIDEO = 0x0400;
+ public static final int PERIPHERAL = 0x0500;
+ public static final int IMAGING = 0x0600;
+ public static final int WEARABLE = 0x0700;
+ public static final int TOY = 0x0800;
+ public static final int HEALTH = 0x0900;
+ public static final int UNCATEGORIZED = 0x1F00;
+ }
+
+ // Devices in the COMPUTER major class
+ public static final int COMPUTER_UNCATEGORIZED = 0x0100;
+ public static final int COMPUTER_DESKTOP = 0x0104;
+ public static final int COMPUTER_SERVER = 0x0108;
+ public static final int COMPUTER_LAPTOP = 0x010C;
+ public static final int COMPUTER_HANDHELD_PC_PDA = 0x0110;
+ public static final int COMPUTER_PALM_SIZE_PC_PDA = 0x0114;
+ public static final int COMPUTER_WEARABLE = 0x0118;
+
+ // Devices in the PHONE major class
+ public static final int PHONE_UNCATEGORIZED = 0x0200;
+ public static final int PHONE_CELLULAR = 0x0204;
+ public static final int PHONE_CORDLESS = 0x0208;
+ public static final int PHONE_SMART = 0x020C;
+ public static final int PHONE_MODEM_OR_GATEWAY = 0x0210;
+ public static final int PHONE_ISDN = 0x0214;
+
+ // Minor classes for the AUDIO_VIDEO major class
+ public static final int AUDIO_VIDEO_UNCATEGORIZED = 0x0400;
+ public static final int AUDIO_VIDEO_WEARABLE_HEADSET = 0x0404;
+ public static final int AUDIO_VIDEO_HANDSFREE = 0x0408;
+ //public static final int AUDIO_VIDEO_RESERVED = 0x040C;
+ public static final int AUDIO_VIDEO_MICROPHONE = 0x0410;
+ public static final int AUDIO_VIDEO_LOUDSPEAKER = 0x0414;
+ public static final int AUDIO_VIDEO_HEADPHONES = 0x0418;
+ public static final int AUDIO_VIDEO_PORTABLE_AUDIO = 0x041C;
+ public static final int AUDIO_VIDEO_CAR_AUDIO = 0x0420;
+ public static final int AUDIO_VIDEO_SET_TOP_BOX = 0x0424;
+ public static final int AUDIO_VIDEO_HIFI_AUDIO = 0x0428;
+ public static final int AUDIO_VIDEO_VCR = 0x042C;
+ public static final int AUDIO_VIDEO_VIDEO_CAMERA = 0x0430;
+ public static final int AUDIO_VIDEO_CAMCORDER = 0x0434;
+ public static final int AUDIO_VIDEO_VIDEO_MONITOR = 0x0438;
+ public static final int AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x043C;
+ public static final int AUDIO_VIDEO_VIDEO_CONFERENCING = 0x0440;
+ //public static final int AUDIO_VIDEO_RESERVED = 0x0444;
+ public static final int AUDIO_VIDEO_VIDEO_GAMING_TOY = 0x0448;
+
+ // Devices in the WEARABLE major class
+ public static final int WEARABLE_UNCATEGORIZED = 0x0700;
+ public static final int WEARABLE_WRIST_WATCH = 0x0704;
+ public static final int WEARABLE_PAGER = 0x0708;
+ public static final int WEARABLE_JACKET = 0x070C;
+ public static final int WEARABLE_HELMET = 0x0710;
+ public static final int WEARABLE_GLASSES = 0x0714;
+
+ // Devices in the TOY major class
+ public static final int TOY_UNCATEGORIZED = 0x0800;
+ public static final int TOY_ROBOT = 0x0804;
+ public static final int TOY_VEHICLE = 0x0808;
+ public static final int TOY_DOLL_ACTION_FIGURE = 0x080C;
+ public static final int TOY_CONTROLLER = 0x0810;
+ public static final int TOY_GAME = 0x0814;
+
+ // Devices in the HEALTH major class
+ public static final int HEALTH_UNCATEGORIZED = 0x0900;
+ public static final int HEALTH_BLOOD_PRESSURE = 0x0904;
+ public static final int HEALTH_THERMOMETER = 0x0908;
+ public static final int HEALTH_WEIGHING = 0x090C;
+ public static final int HEALTH_GLUCOSE = 0x0910;
+ public static final int HEALTH_PULSE_OXIMETER = 0x0914;
+ public static final int HEALTH_PULSE_RATE = 0x0918;
+ public static final int HEALTH_DATA_DISPLAY = 0x091C;
+
+ // Devices in PERIPHERAL major class
+ /**
+ * @hide
+ */
+ public static final int PERIPHERAL_NON_KEYBOARD_NON_POINTING = 0x0500;
+ /**
+ * @hide
+ */
+ public static final int PERIPHERAL_KEYBOARD = 0x0540;
+ /**
+ * @hide
+ */
+ public static final int PERIPHERAL_POINTING = 0x0580;
+ /**
+ * @hide
+ */
+ public static final int PERIPHERAL_KEYBOARD_POINTING = 0x05C0;
+ }
+
+ /**
+ * Return the major device class component of this {@link BluetoothClass}.
+ * <p>Values returned from this function can be compared with the
+ * public constants in {@link BluetoothClass.Device.Major} to determine
+ * which major class is encoded in this Bluetooth class.
+ *
+ * @return major device class component
+ */
+ public int getMajorDeviceClass() {
+ return (mClass & Device.Major.BITMASK);
+ }
+
+ /**
+ * Return the (major and minor) device class component of this
+ * {@link BluetoothClass}.
+ * <p>Values returned from this function can be compared with the
+ * public constants in {@link BluetoothClass.Device} to determine which
+ * device class is encoded in this Bluetooth class.
+ *
+ * @return device class component
+ */
+ public int getDeviceClass() {
+ return (mClass & Device.BITMASK);
+ }
+
+ /**
+ * Return the Bluetooth Class of Device (CoD) value including the
+ * {@link BluetoothClass.Service}, {@link BluetoothClass.Device.Major} and
+ * minor device fields.
+ *
+ * <p>This value is an integer representation of Bluetooth CoD as in
+ * Bluetooth specification.
+ *
+ * @see <a href="Bluetooth CoD">https://www.bluetooth.com/specifications/assigned-numbers/baseband</a>
+ *
+ * @hide
+ */
+ @TestApi
+ public int getClassOfDevice() {
+ return mClass;
+ }
+
+ /**
+ * Return the Bluetooth Class of Device (CoD) value including the
+ * {@link BluetoothClass.Service}, {@link BluetoothClass.Device.Major} and
+ * minor device fields.
+ *
+ * <p>This value is a byte array representation of Bluetooth CoD as in
+ * Bluetooth specification.
+ *
+ * <p>Bluetooth COD information is 3 bytes, but stored as an int. Hence the
+ * MSB is useless and needs to be thrown away. The lower 3 bytes are
+ * converted into a byte array MSB to LSB. Hence, using BIG_ENDIAN.
+ *
+ * @see <a href="Bluetooth CoD">https://www.bluetooth.com/specifications/assigned-numbers/baseband</a>
+ *
+ * @hide
+ */
+ public byte[] getClassOfDeviceBytes() {
+ byte[] bytes = ByteBuffer.allocate(4)
+ .order(ByteOrder.BIG_ENDIAN)
+ .putInt(mClass)
+ .array();
+
+ // Discard the top byte
+ return Arrays.copyOfRange(bytes, 1, bytes.length);
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage
+ public static final int PROFILE_HEADSET = 0;
+ /** @hide */
+ @UnsupportedAppUsage
+ public static final int PROFILE_A2DP = 1;
+ /** @hide */
+ public static final int PROFILE_OPP = 2;
+ /** @hide */
+ public static final int PROFILE_HID = 3;
+ /** @hide */
+ public static final int PROFILE_PANU = 4;
+ /** @hide */
+ public static final int PROFILE_NAP = 5;
+ /** @hide */
+ public static final int PROFILE_A2DP_SINK = 6;
+
+ /**
+ * Check class bits for possible bluetooth profile support.
+ * This is a simple heuristic that tries to guess if a device with the
+ * given class bits might support specified profile. It is not accurate for all
+ * devices. It tries to err on the side of false positives.
+ *
+ * @param profile The profile to be checked
+ * @return True if this device might support specified profile.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public boolean doesClassMatch(int profile) {
+ if (profile == PROFILE_A2DP) {
+ if (hasService(Service.RENDER)) {
+ return true;
+ }
+ // By the A2DP spec, sinks must indicate the RENDER service.
+ // However we found some that do not (Chordette). So lets also
+ // match on some other class bits.
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HIFI_AUDIO:
+ case Device.AUDIO_VIDEO_HEADPHONES:
+ case Device.AUDIO_VIDEO_LOUDSPEAKER:
+ case Device.AUDIO_VIDEO_CAR_AUDIO:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_A2DP_SINK) {
+ if (hasService(Service.CAPTURE)) {
+ return true;
+ }
+ // By the A2DP spec, srcs must indicate the CAPTURE service.
+ // However if some device that do not, we try to
+ // match on some other class bits.
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HIFI_AUDIO:
+ case Device.AUDIO_VIDEO_SET_TOP_BOX:
+ case Device.AUDIO_VIDEO_VCR:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_HEADSET) {
+ // The render service class is required by the spec for HFP, so is a
+ // pretty good signal
+ if (hasService(Service.RENDER)) {
+ return true;
+ }
+ // Just in case they forgot the render service class
+ switch (getDeviceClass()) {
+ case Device.AUDIO_VIDEO_HANDSFREE:
+ case Device.AUDIO_VIDEO_WEARABLE_HEADSET:
+ case Device.AUDIO_VIDEO_CAR_AUDIO:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_OPP) {
+ if (hasService(Service.OBJECT_TRANSFER)) {
+ return true;
+ }
+
+ switch (getDeviceClass()) {
+ case Device.COMPUTER_UNCATEGORIZED:
+ case Device.COMPUTER_DESKTOP:
+ case Device.COMPUTER_SERVER:
+ case Device.COMPUTER_LAPTOP:
+ case Device.COMPUTER_HANDHELD_PC_PDA:
+ case Device.COMPUTER_PALM_SIZE_PC_PDA:
+ case Device.COMPUTER_WEARABLE:
+ case Device.PHONE_UNCATEGORIZED:
+ case Device.PHONE_CELLULAR:
+ case Device.PHONE_CORDLESS:
+ case Device.PHONE_SMART:
+ case Device.PHONE_MODEM_OR_GATEWAY:
+ case Device.PHONE_ISDN:
+ return true;
+ default:
+ return false;
+ }
+ } else if (profile == PROFILE_HID) {
+ return getMajorDeviceClass() == Device.Major.PERIPHERAL;
+ } else if (profile == PROFILE_PANU || profile == PROFILE_NAP) {
+ // No good way to distinguish between the two, based on class bits.
+ if (hasService(Service.NETWORKING)) {
+ return true;
+ }
+ return getMajorDeviceClass() == Device.Major.NETWORKING;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothCodecConfig.java b/framework/java/android/bluetooth/BluetoothCodecConfig.java
new file mode 100644
index 0000000000..9a4151adff
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothCodecConfig.java
@@ -0,0 +1,807 @@
+/*
+ * Copyright (C) 2016 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents the codec configuration for a Bluetooth A2DP source device.
+ * <p>Contains the source codec type, the codec priority, the codec sample
+ * rate, the codec bits per sample, and the codec channel mode.
+ * <p>The source codec type values are the same as those supported by the
+ * device hardware.
+ *
+ * {@see BluetoothA2dp}
+ */
+public final class BluetoothCodecConfig implements Parcelable {
+ /** @hide */
+ @IntDef(prefix = "SOURCE_CODEC_TYPE_", value = {
+ SOURCE_CODEC_TYPE_SBC,
+ SOURCE_CODEC_TYPE_AAC,
+ SOURCE_CODEC_TYPE_APTX,
+ SOURCE_CODEC_TYPE_APTX_HD,
+ SOURCE_CODEC_TYPE_LDAC,
+ SOURCE_CODEC_TYPE_INVALID
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SourceCodecType {}
+
+ /**
+ * Source codec type SBC. This is the mandatory source codec
+ * type.
+ */
+ public static final int SOURCE_CODEC_TYPE_SBC = 0;
+
+ /**
+ * Source codec type AAC.
+ */
+ public static final int SOURCE_CODEC_TYPE_AAC = 1;
+
+ /**
+ * Source codec type APTX.
+ */
+ public static final int SOURCE_CODEC_TYPE_APTX = 2;
+
+ /**
+ * Source codec type APTX HD.
+ */
+ public static final int SOURCE_CODEC_TYPE_APTX_HD = 3;
+
+ /**
+ * Source codec type LDAC.
+ */
+ public static final int SOURCE_CODEC_TYPE_LDAC = 4;
+
+ /**
+ * Source codec type invalid. This is the default value used for codec
+ * type.
+ */
+ public static final int SOURCE_CODEC_TYPE_INVALID = 1000 * 1000;
+
+ /**
+ * Represents the count of valid source codec types. Can be accessed via
+ * {@link #getMaxCodecType}.
+ */
+ private static final int SOURCE_CODEC_TYPE_MAX = 5;
+
+ /** @hide */
+ @IntDef(prefix = "CODEC_PRIORITY_", value = {
+ CODEC_PRIORITY_DISABLED,
+ CODEC_PRIORITY_DEFAULT,
+ CODEC_PRIORITY_HIGHEST
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CodecPriority {}
+
+ /**
+ * Codec priority disabled.
+ * Used to indicate that this codec is disabled and should not be used.
+ */
+ public static final int CODEC_PRIORITY_DISABLED = -1;
+
+ /**
+ * Codec priority default.
+ * Default value used for codec priority.
+ */
+ public static final int CODEC_PRIORITY_DEFAULT = 0;
+
+ /**
+ * Codec priority highest.
+ * Used to indicate the highest priority a codec can have.
+ */
+ public static final int CODEC_PRIORITY_HIGHEST = 1000 * 1000;
+
+ /** @hide */
+ @IntDef(prefix = "SAMPLE_RATE_", value = {
+ SAMPLE_RATE_NONE,
+ SAMPLE_RATE_44100,
+ SAMPLE_RATE_48000,
+ SAMPLE_RATE_88200,
+ SAMPLE_RATE_96000,
+ SAMPLE_RATE_176400,
+ SAMPLE_RATE_192000
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SampleRate {}
+
+ /**
+ * Codec sample rate 0 Hz. Default value used for
+ * codec sample rate.
+ */
+ public static final int SAMPLE_RATE_NONE = 0;
+
+ /**
+ * Codec sample rate 44100 Hz.
+ */
+ public static final int SAMPLE_RATE_44100 = 0x1 << 0;
+
+ /**
+ * Codec sample rate 48000 Hz.
+ */
+ public static final int SAMPLE_RATE_48000 = 0x1 << 1;
+
+ /**
+ * Codec sample rate 88200 Hz.
+ */
+ public static final int SAMPLE_RATE_88200 = 0x1 << 2;
+
+ /**
+ * Codec sample rate 96000 Hz.
+ */
+ public static final int SAMPLE_RATE_96000 = 0x1 << 3;
+
+ /**
+ * Codec sample rate 176400 Hz.
+ */
+ public static final int SAMPLE_RATE_176400 = 0x1 << 4;
+
+ /**
+ * Codec sample rate 192000 Hz.
+ */
+ public static final int SAMPLE_RATE_192000 = 0x1 << 5;
+
+ /** @hide */
+ @IntDef(prefix = "BITS_PER_SAMPLE_", value = {
+ BITS_PER_SAMPLE_NONE,
+ BITS_PER_SAMPLE_16,
+ BITS_PER_SAMPLE_24,
+ BITS_PER_SAMPLE_32
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BitsPerSample {}
+
+ /**
+ * Codec bits per sample 0. Default value of the codec
+ * bits per sample.
+ */
+ public static final int BITS_PER_SAMPLE_NONE = 0;
+
+ /**
+ * Codec bits per sample 16.
+ */
+ public static final int BITS_PER_SAMPLE_16 = 0x1 << 0;
+
+ /**
+ * Codec bits per sample 24.
+ */
+ public static final int BITS_PER_SAMPLE_24 = 0x1 << 1;
+
+ /**
+ * Codec bits per sample 32.
+ */
+ public static final int BITS_PER_SAMPLE_32 = 0x1 << 2;
+
+ /** @hide */
+ @IntDef(prefix = "CHANNEL_MODE_", value = {
+ CHANNEL_MODE_NONE,
+ CHANNEL_MODE_MONO,
+ CHANNEL_MODE_STEREO
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ChannelMode {}
+
+ /**
+ * Codec channel mode NONE. Default value of the
+ * codec channel mode.
+ */
+ public static final int CHANNEL_MODE_NONE = 0;
+
+ /**
+ * Codec channel mode MONO.
+ */
+ public static final int CHANNEL_MODE_MONO = 0x1 << 0;
+
+ /**
+ * Codec channel mode STEREO.
+ */
+ public static final int CHANNEL_MODE_STEREO = 0x1 << 1;
+
+ private final @SourceCodecType int mCodecType;
+ private @CodecPriority int mCodecPriority;
+ private final @SampleRate int mSampleRate;
+ private final @BitsPerSample int mBitsPerSample;
+ private final @ChannelMode int mChannelMode;
+ private final long mCodecSpecific1;
+ private final long mCodecSpecific2;
+ private final long mCodecSpecific3;
+ private final long mCodecSpecific4;
+
+ /**
+ * Creates a new BluetoothCodecConfig.
+ *
+ * @param codecType the source codec type
+ * @param codecPriority the priority of this codec
+ * @param sampleRate the codec sample rate
+ * @param bitsPerSample the bits per sample of this codec
+ * @param channelMode the channel mode of this codec
+ * @param codecSpecific1 the specific value 1
+ * @param codecSpecific2 the specific value 2
+ * @param codecSpecific3 the specific value 3
+ * @param codecSpecific4 the specific value 4
+ * values to 0.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public BluetoothCodecConfig(@SourceCodecType int codecType, @CodecPriority int codecPriority,
+ @SampleRate int sampleRate, @BitsPerSample int bitsPerSample,
+ @ChannelMode int channelMode, long codecSpecific1,
+ long codecSpecific2, long codecSpecific3,
+ long codecSpecific4) {
+ mCodecType = codecType;
+ mCodecPriority = codecPriority;
+ mSampleRate = sampleRate;
+ mBitsPerSample = bitsPerSample;
+ mChannelMode = channelMode;
+ mCodecSpecific1 = codecSpecific1;
+ mCodecSpecific2 = codecSpecific2;
+ mCodecSpecific3 = codecSpecific3;
+ mCodecSpecific4 = codecSpecific4;
+ }
+
+ /**
+ * Creates a new BluetoothCodecConfig.
+ * <p> By default, the codec priority will be set
+ * to {@link BluetoothCodecConfig#CODEC_PRIORITY_DEFAULT}, the sample rate to
+ * {@link BluetoothCodecConfig#SAMPLE_RATE_NONE}, the bits per sample to
+ * {@link BluetoothCodecConfig#BITS_PER_SAMPLE_NONE}, the channel mode to
+ * {@link BluetoothCodecConfig#CHANNEL_MODE_NONE}, and all the codec specific
+ * values to 0.
+ *
+ * @param codecType the source codec type
+ */
+ public BluetoothCodecConfig(@SourceCodecType int codecType) {
+ this(codecType, BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_NONE, 0, 0, 0, 0);
+ }
+
+ private BluetoothCodecConfig(Parcel in) {
+ mCodecType = in.readInt();
+ mCodecPriority = in.readInt();
+ mSampleRate = in.readInt();
+ mBitsPerSample = in.readInt();
+ mChannelMode = in.readInt();
+ mCodecSpecific1 = in.readLong();
+ mCodecSpecific2 = in.readLong();
+ mCodecSpecific3 = in.readLong();
+ mCodecSpecific4 = in.readLong();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothCodecConfig) {
+ BluetoothCodecConfig other = (BluetoothCodecConfig) o;
+ return (other.mCodecType == mCodecType
+ && other.mCodecPriority == mCodecPriority
+ && other.mSampleRate == mSampleRate
+ && other.mBitsPerSample == mBitsPerSample
+ && other.mChannelMode == mChannelMode
+ && other.mCodecSpecific1 == mCodecSpecific1
+ && other.mCodecSpecific2 == mCodecSpecific2
+ && other.mCodecSpecific3 == mCodecSpecific3
+ && other.mCodecSpecific4 == mCodecSpecific4);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash representation of this BluetoothCodecConfig
+ * based on all the config values.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCodecType, mCodecPriority, mSampleRate,
+ mBitsPerSample, mChannelMode, mCodecSpecific1,
+ mCodecSpecific2, mCodecSpecific3, mCodecSpecific4);
+ }
+
+ /**
+ * Adds capability string to an existing string.
+ *
+ * @param prevStr the previous string with the capabilities. Can be a {@code null} pointer
+ * @param capStr the capability string to append to prevStr argument
+ * @return the result string in the form "prevStr|capStr"
+ */
+ private static String appendCapabilityToString(@Nullable String prevStr,
+ @NonNull String capStr) {
+ if (prevStr == null) {
+ return capStr;
+ }
+ return prevStr + "|" + capStr;
+ }
+
+ /**
+ * Returns a {@link String} that describes each BluetoothCodecConfig parameter
+ * current value.
+ */
+ @Override
+ public String toString() {
+ String sampleRateStr = null;
+ if (mSampleRate == SAMPLE_RATE_NONE) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "NONE");
+ }
+ if ((mSampleRate & SAMPLE_RATE_44100) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "44100");
+ }
+ if ((mSampleRate & SAMPLE_RATE_48000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "48000");
+ }
+ if ((mSampleRate & SAMPLE_RATE_88200) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "88200");
+ }
+ if ((mSampleRate & SAMPLE_RATE_96000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "96000");
+ }
+ if ((mSampleRate & SAMPLE_RATE_176400) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "176400");
+ }
+ if ((mSampleRate & SAMPLE_RATE_192000) != 0) {
+ sampleRateStr = appendCapabilityToString(sampleRateStr, "192000");
+ }
+
+ String bitsPerSampleStr = null;
+ if (mBitsPerSample == BITS_PER_SAMPLE_NONE) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "NONE");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_16) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "16");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_24) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "24");
+ }
+ if ((mBitsPerSample & BITS_PER_SAMPLE_32) != 0) {
+ bitsPerSampleStr = appendCapabilityToString(bitsPerSampleStr, "32");
+ }
+
+ String channelModeStr = null;
+ if (mChannelMode == CHANNEL_MODE_NONE) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "NONE");
+ }
+ if ((mChannelMode & CHANNEL_MODE_MONO) != 0) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "MONO");
+ }
+ if ((mChannelMode & CHANNEL_MODE_STEREO) != 0) {
+ channelModeStr = appendCapabilityToString(channelModeStr, "STEREO");
+ }
+
+ return "{codecName:" + getCodecName()
+ + ",mCodecType:" + mCodecType
+ + ",mCodecPriority:" + mCodecPriority
+ + ",mSampleRate:" + String.format("0x%x", mSampleRate)
+ + "(" + sampleRateStr + ")"
+ + ",mBitsPerSample:" + String.format("0x%x", mBitsPerSample)
+ + "(" + bitsPerSampleStr + ")"
+ + ",mChannelMode:" + String.format("0x%x", mChannelMode)
+ + "(" + channelModeStr + ")"
+ + ",mCodecSpecific1:" + mCodecSpecific1
+ + ",mCodecSpecific2:" + mCodecSpecific2
+ + ",mCodecSpecific3:" + mCodecSpecific3
+ + ",mCodecSpecific4:" + mCodecSpecific4 + "}";
+ }
+
+ /**
+ * @return 0
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothCodecConfig> CREATOR =
+ new Parcelable.Creator<BluetoothCodecConfig>() {
+ public BluetoothCodecConfig createFromParcel(Parcel in) {
+ return new BluetoothCodecConfig(in);
+ }
+
+ public BluetoothCodecConfig[] newArray(int size) {
+ return new BluetoothCodecConfig[size];
+ }
+ };
+
+ /**
+ * Flattens the object to a parcel
+ *
+ * @param out The Parcel in which the object should be written
+ * @param flags Additional flags about how the object should be written
+ *
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mCodecType);
+ out.writeInt(mCodecPriority);
+ out.writeInt(mSampleRate);
+ out.writeInt(mBitsPerSample);
+ out.writeInt(mChannelMode);
+ out.writeLong(mCodecSpecific1);
+ out.writeLong(mCodecSpecific2);
+ out.writeLong(mCodecSpecific3);
+ out.writeLong(mCodecSpecific4);
+ }
+
+ /**
+ * Returns the codec name converted to {@link String}.
+ * @hide
+ */
+ public @NonNull String getCodecName() {
+ switch (mCodecType) {
+ case SOURCE_CODEC_TYPE_SBC:
+ return "SBC";
+ case SOURCE_CODEC_TYPE_AAC:
+ return "AAC";
+ case SOURCE_CODEC_TYPE_APTX:
+ return "aptX";
+ case SOURCE_CODEC_TYPE_APTX_HD:
+ return "aptX HD";
+ case SOURCE_CODEC_TYPE_LDAC:
+ return "LDAC";
+ case SOURCE_CODEC_TYPE_INVALID:
+ return "INVALID CODEC";
+ default:
+ break;
+ }
+ return "UNKNOWN CODEC(" + mCodecType + ")";
+ }
+
+ /**
+ * Returns the source codec type of this config.
+ */
+ public @SourceCodecType int getCodecType() {
+ return mCodecType;
+ }
+
+ /**
+ * Returns the valid codec types count.
+ */
+ public static int getMaxCodecType() {
+ return SOURCE_CODEC_TYPE_MAX;
+ }
+
+ /**
+ * Checks whether the codec is mandatory.
+ * <p> The actual mandatory codec type for Android Bluetooth audio is SBC.
+ * See {@link #SOURCE_CODEC_TYPE_SBC}.
+ *
+ * @return {@code true} if the codec is mandatory, {@code false} otherwise
+ * @hide
+ */
+ public boolean isMandatoryCodec() {
+ return mCodecType == SOURCE_CODEC_TYPE_SBC;
+ }
+
+ /**
+ * Returns the codec selection priority.
+ * <p>The codec selection priority is relative to other codecs: larger value
+ * means higher priority.
+ */
+ public @CodecPriority int getCodecPriority() {
+ return mCodecPriority;
+ }
+
+ /**
+ * Sets the codec selection priority.
+ * <p>The codec selection priority is relative to other codecs: larger value
+ * means higher priority.
+ *
+ * @param codecPriority the priority this codec should have
+ * @hide
+ */
+ public void setCodecPriority(@CodecPriority int codecPriority) {
+ mCodecPriority = codecPriority;
+ }
+
+ /**
+ * Returns the codec sample rate. The value can be a bitmask with all
+ * supported sample rates.
+ */
+ public @SampleRate int getSampleRate() {
+ return mSampleRate;
+ }
+
+ /**
+ * Returns the codec bits per sample. The value can be a bitmask with all
+ * bits per sample supported.
+ */
+ public @BitsPerSample int getBitsPerSample() {
+ return mBitsPerSample;
+ }
+
+ /**
+ * Returns the codec channel mode. The value can be a bitmask with all
+ * supported channel modes.
+ */
+ public @ChannelMode int getChannelMode() {
+ return mChannelMode;
+ }
+
+ /**
+ * Returns the codec specific value1.
+ */
+ public long getCodecSpecific1() {
+ return mCodecSpecific1;
+ }
+
+ /**
+ * Returns the codec specific value2.
+ */
+ public long getCodecSpecific2() {
+ return mCodecSpecific2;
+ }
+
+ /**
+ * Returns the codec specific value3.
+ */
+ public long getCodecSpecific3() {
+ return mCodecSpecific3;
+ }
+
+ /**
+ * Returns the codec specific value4.
+ */
+ public long getCodecSpecific4() {
+ return mCodecSpecific4;
+ }
+
+ /**
+ * Checks whether a value set presented by a bitmask has zero or single bit
+ *
+ * @param valueSet the value set presented by a bitmask
+ * @return {@code true} if the valueSet contains zero or single bit, {@code false} otherwise
+ * @hide
+ */
+ private static boolean hasSingleBit(int valueSet) {
+ return (valueSet == 0 || (valueSet & (valueSet - 1)) == 0);
+ }
+
+ /**
+ * Returns whether the object contains none or single sample rate.
+ * @hide
+ */
+ public boolean hasSingleSampleRate() {
+ return hasSingleBit(mSampleRate);
+ }
+
+ /**
+ * Returns whether the object contains none or single bits per sample.
+ * @hide
+ */
+ public boolean hasSingleBitsPerSample() {
+ return hasSingleBit(mBitsPerSample);
+ }
+
+ /**
+ * Returns whether the object contains none or single channel mode.
+ * @hide
+ */
+ public boolean hasSingleChannelMode() {
+ return hasSingleBit(mChannelMode);
+ }
+
+ /**
+ * Checks whether the audio feeding parameters are the same.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the audio feeding parameters are same, {@code false} otherwise
+ * @hide
+ */
+ public boolean sameAudioFeedingParameters(BluetoothCodecConfig other) {
+ return (other != null && other.mSampleRate == mSampleRate
+ && other.mBitsPerSample == mBitsPerSample
+ && other.mChannelMode == mChannelMode);
+ }
+
+ /**
+ * Checks whether another codec config has the similar feeding parameters.
+ * Any parameters with NONE value will be considered to be a wildcard matching.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the audio feeding parameters are similar, {@code false} otherwise
+ * @hide
+ */
+ public boolean similarCodecFeedingParameters(BluetoothCodecConfig other) {
+ if (other == null || mCodecType != other.mCodecType) {
+ return false;
+ }
+ int sampleRate = other.mSampleRate;
+ if (mSampleRate == SAMPLE_RATE_NONE
+ || sampleRate == SAMPLE_RATE_NONE) {
+ sampleRate = mSampleRate;
+ }
+ int bitsPerSample = other.mBitsPerSample;
+ if (mBitsPerSample == BITS_PER_SAMPLE_NONE
+ || bitsPerSample == BITS_PER_SAMPLE_NONE) {
+ bitsPerSample = mBitsPerSample;
+ }
+ int channelMode = other.mChannelMode;
+ if (mChannelMode == CHANNEL_MODE_NONE
+ || channelMode == CHANNEL_MODE_NONE) {
+ channelMode = mChannelMode;
+ }
+ return sameAudioFeedingParameters(new BluetoothCodecConfig(
+ mCodecType, /* priority */ 0, sampleRate, bitsPerSample, channelMode,
+ /* specific1 */ 0, /* specific2 */ 0, /* specific3 */ 0,
+ /* specific4 */ 0));
+ }
+
+ /**
+ * Checks whether the codec specific parameters are the same.
+ * <p> Currently, only AAC VBR and LDAC Playback Quality on CodecSpecific1
+ * are compared.
+ *
+ * @param other the codec config to compare against
+ * @return {@code true} if the codec specific parameters are the same, {@code false} otherwise
+ * @hide
+ */
+ public boolean sameCodecSpecificParameters(BluetoothCodecConfig other) {
+ if (other == null && mCodecType != other.mCodecType) {
+ return false;
+ }
+ switch (mCodecType) {
+ case SOURCE_CODEC_TYPE_AAC:
+ case SOURCE_CODEC_TYPE_LDAC:
+ if (mCodecSpecific1 != other.mCodecSpecific1) {
+ return false;
+ }
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Builder for {@link BluetoothCodecConfig}.
+ * <p> By default, the codec type will be set to
+ * {@link BluetoothCodecConfig#SOURCE_CODEC_TYPE_INVALID}, the codec priority
+ * to {@link BluetoothCodecConfig#CODEC_PRIORITY_DEFAULT}, the sample rate to
+ * {@link BluetoothCodecConfig#SAMPLE_RATE_NONE}, the bits per sample to
+ * {@link BluetoothCodecConfig#BITS_PER_SAMPLE_NONE}, the channel mode to
+ * {@link BluetoothCodecConfig#CHANNEL_MODE_NONE}, and all the codec specific
+ * values to 0.
+ */
+ public static final class Builder {
+ private int mCodecType = BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID;
+ private int mCodecPriority = BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT;
+ private int mSampleRate = BluetoothCodecConfig.SAMPLE_RATE_NONE;
+ private int mBitsPerSample = BluetoothCodecConfig.BITS_PER_SAMPLE_NONE;
+ private int mChannelMode = BluetoothCodecConfig.CHANNEL_MODE_NONE;
+ private long mCodecSpecific1 = 0;
+ private long mCodecSpecific2 = 0;
+ private long mCodecSpecific3 = 0;
+ private long mCodecSpecific4 = 0;
+
+ /**
+ * Set codec type for Bluetooth codec config.
+ *
+ * @param codecType of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecType(@SourceCodecType int codecType) {
+ mCodecType = codecType;
+ return this;
+ }
+
+ /**
+ * Set codec priority for Bluetooth codec config.
+ *
+ * @param codecPriority of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecPriority(@CodecPriority int codecPriority) {
+ mCodecPriority = codecPriority;
+ return this;
+ }
+
+ /**
+ * Set sample rate for Bluetooth codec config.
+ *
+ * @param sampleRate of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setSampleRate(@SampleRate int sampleRate) {
+ mSampleRate = sampleRate;
+ return this;
+ }
+
+ /**
+ * Set the bits per sample for Bluetooth codec config.
+ *
+ * @param bitsPerSample of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setBitsPerSample(@BitsPerSample int bitsPerSample) {
+ mBitsPerSample = bitsPerSample;
+ return this;
+ }
+
+ /**
+ * Set the channel mode for Bluetooth codec config.
+ *
+ * @param channelMode of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setChannelMode(@ChannelMode int channelMode) {
+ mChannelMode = channelMode;
+ return this;
+ }
+
+ /**
+ * Set the first codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific1 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific1(long codecSpecific1) {
+ mCodecSpecific1 = codecSpecific1;
+ return this;
+ }
+
+ /**
+ * Set the second codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific2 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific2(long codecSpecific2) {
+ mCodecSpecific2 = codecSpecific2;
+ return this;
+ }
+
+ /**
+ * Set the third codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific3 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific3(long codecSpecific3) {
+ mCodecSpecific3 = codecSpecific3;
+ return this;
+ }
+
+ /**
+ * Set the fourth codec specific values for Bluetooth codec config.
+ *
+ * @param codecSpecific4 codec specific value or 0 if default
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecSpecific4(long codecSpecific4) {
+ mCodecSpecific4 = codecSpecific4;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothCodecConfig}.
+ * @return new BluetoothCodecConfig built
+ */
+ public @NonNull BluetoothCodecConfig build() {
+ return new BluetoothCodecConfig(mCodecType, mCodecPriority,
+ mSampleRate, mBitsPerSample,
+ mChannelMode, mCodecSpecific1,
+ mCodecSpecific2, mCodecSpecific3,
+ mCodecSpecific4);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothCodecStatus.java b/framework/java/android/bluetooth/BluetoothCodecStatus.java
new file mode 100644
index 0000000000..02606feb3b
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothCodecStatus.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2017 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents the codec status (configuration and capability) for a Bluetooth
+ * A2DP source device.
+ *
+ * {@see BluetoothA2dp}
+ */
+public final class BluetoothCodecStatus implements Parcelable {
+ /**
+ * Extra for the codec configuration intents of the individual profiles.
+ *
+ * This extra represents the current codec status of the A2DP
+ * profile.
+ */
+ public static final String EXTRA_CODEC_STATUS =
+ "android.bluetooth.extra.CODEC_STATUS";
+
+ private final @Nullable BluetoothCodecConfig mCodecConfig;
+ private final @Nullable List<BluetoothCodecConfig> mCodecsLocalCapabilities;
+ private final @Nullable List<BluetoothCodecConfig> mCodecsSelectableCapabilities;
+
+ public BluetoothCodecStatus(@Nullable BluetoothCodecConfig codecConfig,
+ @Nullable List<BluetoothCodecConfig> codecsLocalCapabilities,
+ @Nullable List<BluetoothCodecConfig> codecsSelectableCapabilities) {
+ mCodecConfig = codecConfig;
+ mCodecsLocalCapabilities = codecsLocalCapabilities;
+ mCodecsSelectableCapabilities = codecsSelectableCapabilities;
+ }
+
+ private BluetoothCodecStatus(Parcel in) {
+ mCodecConfig = in.readTypedObject(BluetoothCodecConfig.CREATOR);
+ mCodecsLocalCapabilities = in.createTypedArrayList(BluetoothCodecConfig.CREATOR);
+ mCodecsSelectableCapabilities = in.createTypedArrayList(BluetoothCodecConfig.CREATOR);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothCodecStatus) {
+ BluetoothCodecStatus other = (BluetoothCodecStatus) o;
+ return (Objects.equals(other.mCodecConfig, mCodecConfig)
+ && sameCapabilities(other.mCodecsLocalCapabilities, mCodecsLocalCapabilities)
+ && sameCapabilities(other.mCodecsSelectableCapabilities,
+ mCodecsSelectableCapabilities));
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether two lists of capabilities contain same capabilities.
+ * The order of the capabilities in each list is ignored.
+ *
+ * @param c1 the first list of capabilities to compare
+ * @param c2 the second list of capabilities to compare
+ * @return {@code true} if both lists contain same capabilities
+ */
+ private static boolean sameCapabilities(@Nullable List<BluetoothCodecConfig> c1,
+ @Nullable List<BluetoothCodecConfig> c2) {
+ if (c1 == null) {
+ return (c2 == null);
+ }
+ if (c2 == null) {
+ return false;
+ }
+ if (c1.size() != c2.size()) {
+ return false;
+ }
+ return c1.containsAll(c2);
+ }
+
+ /**
+ * Checks whether the codec config matches the selectable capabilities.
+ * Any parameters of the codec config with NONE value will be considered a wildcard matching.
+ *
+ * @param codecConfig the codec config to compare against
+ * @return {@code true} if the codec config matches, {@code false} otherwise
+ */
+ public boolean isCodecConfigSelectable(@Nullable BluetoothCodecConfig codecConfig) {
+ if (codecConfig == null || !codecConfig.hasSingleSampleRate()
+ || !codecConfig.hasSingleBitsPerSample() || !codecConfig.hasSingleChannelMode()) {
+ return false;
+ }
+ for (BluetoothCodecConfig selectableConfig : mCodecsSelectableCapabilities) {
+ if (codecConfig.getCodecType() != selectableConfig.getCodecType()) {
+ continue;
+ }
+ int sampleRate = codecConfig.getSampleRate();
+ if ((sampleRate & selectableConfig.getSampleRate()) == 0
+ && sampleRate != BluetoothCodecConfig.SAMPLE_RATE_NONE) {
+ continue;
+ }
+ int bitsPerSample = codecConfig.getBitsPerSample();
+ if ((bitsPerSample & selectableConfig.getBitsPerSample()) == 0
+ && bitsPerSample != BluetoothCodecConfig.BITS_PER_SAMPLE_NONE) {
+ continue;
+ }
+ int channelMode = codecConfig.getChannelMode();
+ if ((channelMode & selectableConfig.getChannelMode()) == 0
+ && channelMode != BluetoothCodecConfig.CHANNEL_MODE_NONE) {
+ continue;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash based on the codec config and local capabilities.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCodecConfig, mCodecsLocalCapabilities,
+ mCodecsLocalCapabilities);
+ }
+
+ /**
+ * Returns a {@link String} that describes each BluetoothCodecStatus parameter
+ * current value.
+ */
+ @Override
+ public String toString() {
+ return "{mCodecConfig:" + mCodecConfig
+ + ",mCodecsLocalCapabilities:" + mCodecsLocalCapabilities
+ + ",mCodecsSelectableCapabilities:" + mCodecsSelectableCapabilities
+ + "}";
+ }
+
+ /**
+ * @return 0
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothCodecStatus> CREATOR =
+ new Parcelable.Creator<BluetoothCodecStatus>() {
+ public BluetoothCodecStatus createFromParcel(Parcel in) {
+ return new BluetoothCodecStatus(in);
+ }
+
+ public BluetoothCodecStatus[] newArray(int size) {
+ return new BluetoothCodecStatus[size];
+ }
+ };
+
+ /**
+ * Flattens the object to a parcel.
+ *
+ * @param out The Parcel in which the object should be written
+ * @param flags Additional flags about how the object should be written
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeTypedObject(mCodecConfig, 0);
+ out.writeTypedList(mCodecsLocalCapabilities);
+ out.writeTypedList(mCodecsSelectableCapabilities);
+ }
+
+ /**
+ * Returns the current codec configuration.
+ */
+ public @Nullable BluetoothCodecConfig getCodecConfig() {
+ return mCodecConfig;
+ }
+
+ /**
+ * Returns the codecs local capabilities.
+ */
+ public @NonNull List<BluetoothCodecConfig> getCodecsLocalCapabilities() {
+ return (mCodecsLocalCapabilities == null)
+ ? Collections.emptyList() : mCodecsLocalCapabilities;
+ }
+
+ /**
+ * Returns the codecs selectable capabilities.
+ */
+ public @NonNull List<BluetoothCodecConfig> getCodecsSelectableCapabilities() {
+ return (mCodecsSelectableCapabilities == null)
+ ? Collections.emptyList() : mCodecsSelectableCapabilities;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothCsipSetCoordinator.java b/framework/java/android/bluetooth/BluetoothCsipSetCoordinator.java
new file mode 100644
index 0000000000..ba57ec472a
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothCsipSetCoordinator.java
@@ -0,0 +1,555 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.ParcelUuid;
+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.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Bluetooth CSIP set coordinator.
+ *
+ * <p>BluetoothCsipSetCoordinator is a proxy object for controlling the Bluetooth VC
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothCsipSetCoordinator proxy object.
+ *
+ */
+public final class BluetoothCsipSetCoordinator implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothCsipSetCoordinator";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public interface ClientLockCallback {
+ /**
+ * @hide
+ */
+ @SystemApi void onGroupLockSet(int groupId, int opStatus, boolean isLocked);
+ }
+
+ private static class BluetoothCsipSetCoordinatorLockCallbackDelegate
+ extends IBluetoothCsipSetCoordinatorLockCallback.Stub {
+ private final ClientLockCallback mCallback;
+ private final Executor mExecutor;
+
+ BluetoothCsipSetCoordinatorLockCallbackDelegate(
+ Executor executor, ClientLockCallback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onGroupLockSet(int groupId, int opStatus, boolean isLocked) {
+ mExecutor.execute(() -> mCallback.onGroupLockSet(groupId, opStatus, isLocked));
+ }
+ };
+
+ /**
+ * Intent used to broadcast the change in connection state of the CSIS
+ * Client.
+ *
+ * <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}.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.CSIS_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to expose broadcast receiving device.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote Broadcast receiver device. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_SIZE} - Group size. </li>
+ * <li> {@link #EXTRA_CSIS_GROUP_TYPE_UUID} - Group type UUID. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_DEVICE_AVAILABLE =
+ "android.bluetooth.action.CSIS_DEVICE_AVAILABLE";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ * Contains the group id.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CSIS_GROUP_ID = "android.bluetooth.extra.CSIS_GROUP_ID";
+
+ /**
+ * Group size as int extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CSIS_GROUP_SIZE = "android.bluetooth.extra.CSIS_GROUP_SIZE";
+
+ /**
+ * Group type uuid extra field in {@link #ACTION_CSIS_DEVICE_AVAILABLE} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CSIS_GROUP_TYPE_UUID =
+ "android.bluetooth.extra.CSIS_GROUP_TYPE_UUID";
+
+ /**
+ * Intent used to broadcast information about identified set member
+ * ready to connect.
+ *
+ * <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>
+ * <li> {@link #EXTRA_CSIS_GROUP_ID} - Group identifier. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CSIS_SET_MEMBER_AVAILABLE =
+ "android.bluetooth.action.CSIS_SET_MEMBER_AVAILABLE";
+
+ /**
+ * This represents an invalid group ID.
+ *
+ * @hide
+ */
+ public static final int GROUP_ID_INVALID = IBluetoothCsipSetCoordinator.CSIS_GROUP_ID_INVALID;
+
+ /**
+ * Indicating that group was locked with success.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_SUCCESS = 0;
+
+ /**
+ * Indicating that group locked failed due to invalid group ID.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_FAILED_INVALID_GROUP = 1;
+
+ /**
+ * Indicating that group locked failed due to empty group.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_FAILED_GROUP_EMPTY = 2;
+
+ /**
+ * Indicating that group locked failed due to group members being disconnected.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_FAILED_GROUP_NOT_CONNECTED = 3;
+
+ /**
+ * Indicating that group locked failed due to group member being already locked.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_FAILED_LOCKED_BY_OTHER = 4;
+
+ /**
+ * Indicating that group locked failed due to other reason.
+ *
+ * @hide
+ */
+ public static final int GROUP_LOCK_FAILED_OTHER_REASON = 5;
+
+ /**
+ * Indicating that group member in locked state was lost.
+ *
+ * @hide
+ */
+ public static final int LOCKED_GROUP_MEMBER_LOST = 6;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothCsipSetCoordinator> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.CSIP_SET_COORDINATOR, TAG,
+ IBluetoothCsipSetCoordinator.class.getName()) {
+ @Override
+ public IBluetoothCsipSetCoordinator getServiceInterface(IBinder service) {
+ return IBluetoothCsipSetCoordinator.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothCsipSetCoordinator proxy object for interacting with the local
+ * Bluetooth CSIS service.
+ */
+ /*package*/ BluetoothCsipSetCoordinator(Context context, ServiceListener listener, BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * @hide
+ */
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothCsipSetCoordinator getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Lock the set.
+ * @param groupId group ID to lock,
+ * @param executor callback executor,
+ * @param cb callback to report lock and unlock events - stays valid until the app unlocks
+ * using the returned lock identifier or the lock timeouts on the remote side,
+ * as per CSIS specification,
+ * @return unique lock identifier used for unlocking or null if lock has failed.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public
+ @Nullable UUID groupLock(int groupId, @Nullable @CallbackExecutor Executor executor,
+ @Nullable ClientLockCallback cb) {
+ if (VDBG) log("groupLockSet()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final UUID defaultValue = null;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ IBluetoothCsipSetCoordinatorLockCallback delegate = null;
+ if ((executor != null) && (cb != null)) {
+ delegate = new BluetoothCsipSetCoordinatorLockCallbackDelegate(executor, cb);
+ }
+ try {
+ final SynchronousResultReceiver<ParcelUuid> recv = new SynchronousResultReceiver();
+ service.groupLock(groupId, delegate, mAttributionSource, recv);
+ final ParcelUuid ret = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return ret == null ? defaultValue : ret.getUuid();
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Unlock the set.
+ * @param lockUuid unique lock identifier
+ * @return true if unlocked, false on error
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean groupUnlock(@NonNull UUID lockUuid) {
+ if (VDBG) log("groupLockSet()");
+ if (lockUuid == null) {
+ return false;
+ }
+ final IBluetoothCsipSetCoordinator service = getService();
+ 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 recv = new SynchronousResultReceiver();
+ service.groupUnlock(new ParcelUuid(lockUuid), mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ return true;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get device's groups.
+ * @param device the active device
+ * @return Map of groups ids and related UUIDs
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @NonNull Map getGroupUuidMapByDevice(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getGroupUuidMapByDevice()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final Map defaultValue = new HashMap<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Map> recv = new SynchronousResultReceiver();
+ service.getGroupUuidMapByDevice(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 group id for the given UUID
+ * @param uuid
+ * @return list of group IDs
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @NonNull List<Integer> getAllGroupIds(@Nullable ParcelUuid uuid) {
+ if (VDBG) log("getAllGroupIds()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<Integer> defaultValue = new ArrayList<>();
+ 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<Integer>> recv =
+ new SynchronousResultReceiver();
+ service.getAllGroupIds(uuid, 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
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ 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.getConnectedDevices(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
+ public
+ @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates(states=" + Arrays.toString(states) + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ 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 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public
+ @BluetoothProfile.BtProfileState int getConnectionState(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ 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()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getConnectionState(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 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
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean setConnectionPolicy(
+ @Nullable BluetoothDevice device, @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ 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 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
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothCsipSetCoordinator service = getService();
+ 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;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(@Nullable BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothDevice.java b/framework/java/android/bluetooth/BluetoothDevice.java
new file mode 100644
index 0000000000..1edf5cc96b
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothDevice.java
@@ -0,0 +1,2831 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.IntDef;
+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.app.PropertyInvalidatedCache;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.companion.AssociationRequest;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * Represents a remote Bluetooth device. A {@link BluetoothDevice} lets you
+ * create a connection with the respective device or query information about
+ * it, such as the name, address, class, and bonding state.
+ *
+ * <p>This class is really just a thin wrapper for a Bluetooth hardware
+ * address. Objects of this class are immutable. Operations on this class
+ * are performed on the remote Bluetooth hardware address, using the
+ * {@link BluetoothAdapter} that was used to create this {@link
+ * BluetoothDevice}.
+ *
+ * <p>To get a {@link BluetoothDevice}, use
+ * {@link BluetoothAdapter#getRemoteDevice(String)
+ * BluetoothAdapter.getRemoteDevice(String)} to create one representing a device
+ * of a known MAC address (which you can get through device discovery with
+ * {@link BluetoothAdapter}) or get one from the set of bonded devices
+ * returned by {@link BluetoothAdapter#getBondedDevices()
+ * BluetoothAdapter.getBondedDevices()}. You can then open a
+ * {@link BluetoothSocket} for communication with the remote device, using
+ * {@link #createRfcommSocketToServiceRecord(UUID)} over Bluetooth BR/EDR or using
+ * {@link #createL2capChannel(int)} over Bluetooth LE.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about using Bluetooth, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer
+ * guide.
+ * </p>
+ * </div>
+ *
+ * {@see BluetoothAdapter}
+ * {@see BluetoothSocket}
+ */
+public final class BluetoothDevice implements Parcelable, Attributable {
+ private static final String TAG = "BluetoothDevice";
+ private static final boolean DBG = false;
+
+ /**
+ * Connection state bitmask as returned by getConnectionState.
+ */
+ private static final int CONNECTION_STATE_DISCONNECTED = 0;
+ private static final int CONNECTION_STATE_CONNECTED = 1;
+ private static final int CONNECTION_STATE_ENCRYPTED_BREDR = 2;
+ private static final int CONNECTION_STATE_ENCRYPTED_LE = 4;
+
+ /**
+ * Sentinel error value for this class. Guaranteed to not equal any other
+ * integer constant in this class. Provided as a convenience for functions
+ * that require a sentinel error value, for example:
+ * <p><code>Intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ * BluetoothDevice.ERROR)</code>
+ */
+ public static final int ERROR = Integer.MIN_VALUE;
+
+ /**
+ * Broadcast Action: Remote device discovered.
+ * <p>Sent when a remote device is found during discovery.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_CLASS}. Can contain the extra fields {@link #EXTRA_NAME} and/or
+ * {@link #EXTRA_RSSI} and/or {@link #EXTRA_IS_COORDINATED_SET_MEMBER} if they are available.
+ */
+ // TODO: Change API to not broadcast RSSI if not available (incoming connection)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_FOUND =
+ "android.bluetooth.device.action.FOUND";
+
+ /**
+ * Broadcast Action: Bluetooth class of a remote device has changed.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_CLASS}.
+ * {@see BluetoothClass}
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CLASS_CHANGED =
+ "android.bluetooth.device.action.CLASS_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates a low level (ACL) connection has been
+ * established with a remote device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ * <p>ACL connections are managed automatically by the Android Bluetooth
+ * stack.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_CONNECTED =
+ "android.bluetooth.device.action.ACL_CONNECTED";
+
+ /**
+ * Broadcast Action: Indicates that a low level (ACL) disconnection has
+ * been requested for a remote device, and it will soon be disconnected.
+ * <p>This is useful for graceful disconnection. Applications should use
+ * this intent as a hint to immediately terminate higher level connections
+ * (RFCOMM, L2CAP, or profile connections) to the remote device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_DISCONNECT_REQUESTED =
+ "android.bluetooth.device.action.ACL_DISCONNECT_REQUESTED";
+
+ /**
+ * Broadcast Action: Indicates a low level (ACL) disconnection from a
+ * remote device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ * <p>ACL connections are managed automatically by the Android Bluetooth
+ * stack.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACL_DISCONNECTED =
+ "android.bluetooth.device.action.ACL_DISCONNECTED";
+
+ /**
+ * Broadcast Action: Indicates the friendly name of a remote device has
+ * been retrieved for the first time, or changed since the last retrieval.
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_NAME}.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NAME_CHANGED =
+ "android.bluetooth.device.action.NAME_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates the alias of a remote device has been
+ * changed.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ */
+ @SuppressLint("ActionValue")
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ALIAS_CHANGED =
+ "android.bluetooth.device.action.ALIAS_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates a change in the bond state of a remote
+ * device. For example, if a device is bonded (paired).
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE}, {@link
+ * #EXTRA_BOND_STATE} and {@link #EXTRA_PREVIOUS_BOND_STATE}.
+ */
+ // Note: When EXTRA_BOND_STATE is BOND_NONE then this will also
+ // contain a hidden extra field EXTRA_REASON with the result code.
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BOND_STATE_CHANGED =
+ "android.bluetooth.device.action.BOND_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: Indicates the battery level of a remote device has
+ * been retrieved for the first time, or changed since the last retrieval
+ * <p>Always contains the extra fields {@link #EXTRA_DEVICE} and {@link
+ * #EXTRA_BATTERY_LEVEL}.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_BATTERY_LEVEL_CHANGED =
+ "android.bluetooth.device.action.BATTERY_LEVEL_CHANGED";
+
+ /**
+ * Used as an Integer extra field in {@link #ACTION_BATTERY_LEVEL_CHANGED}
+ * intent. It contains the most recently retrieved battery level information
+ * ranging from 0% to 100% for a remote device, {@link #BATTERY_LEVEL_UNKNOWN}
+ * when the valid is unknown or there is an error
+ *
+ * @hide
+ */
+ public static final String EXTRA_BATTERY_LEVEL =
+ "android.bluetooth.device.extra.BATTERY_LEVEL";
+
+ /**
+ * Used as the unknown value for {@link #EXTRA_BATTERY_LEVEL} and {@link #getBatteryLevel()}
+ *
+ * @hide
+ */
+ public static final int BATTERY_LEVEL_UNKNOWN = -1;
+
+ /**
+ * Used as an error value for {@link #getBatteryLevel()} to represent bluetooth is off
+ *
+ * @hide
+ */
+ public static final int BATTERY_LEVEL_BLUETOOTH_OFF = -100;
+
+ /**
+ * Used as a Parcelable {@link BluetoothDevice} extra field in every intent
+ * broadcast by this class. It contains the {@link BluetoothDevice} that
+ * the intent applies to.
+ */
+ public static final String EXTRA_DEVICE = "android.bluetooth.device.extra.DEVICE";
+
+ /**
+ * Used as a String extra field in {@link #ACTION_NAME_CHANGED} and {@link
+ * #ACTION_FOUND} intents. It contains the friendly Bluetooth name.
+ */
+ public static final String EXTRA_NAME = "android.bluetooth.device.extra.NAME";
+
+ /**
+ * Used as an optional short extra field in {@link #ACTION_FOUND} intents.
+ * Contains the RSSI value of the remote device as reported by the
+ * Bluetooth hardware.
+ */
+ public static final String EXTRA_RSSI = "android.bluetooth.device.extra.RSSI";
+
+ /**
+ * Used as an bool extra field in {@link #ACTION_FOUND} intents.
+ * It contains the information if device is discovered as member of a coordinated set or not.
+ * Pairing with device that belongs to a set would trigger pairing with the rest of set members.
+ * See Bluetooth CSIP specification for more details.
+ */
+ public static final String EXTRA_IS_COORDINATED_SET_MEMBER =
+ "android.bluetooth.extra.IS_COORDINATED_SET_MEMBER";
+
+ /**
+ * Used as a Parcelable {@link BluetoothClass} extra field in {@link
+ * #ACTION_FOUND} and {@link #ACTION_CLASS_CHANGED} intents.
+ */
+ public static final String EXTRA_CLASS = "android.bluetooth.device.extra.CLASS";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents.
+ * Contains the bond state of the remote device.
+ * <p>Possible values are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ */
+ public static final String EXTRA_BOND_STATE = "android.bluetooth.device.extra.BOND_STATE";
+ /**
+ * Used as an int extra field in {@link #ACTION_BOND_STATE_CHANGED} intents.
+ * Contains the previous bond state of the remote device.
+ * <p>Possible values are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ */
+ public static final String EXTRA_PREVIOUS_BOND_STATE =
+ "android.bluetooth.device.extra.PREVIOUS_BOND_STATE";
+ /**
+ * Indicates the remote device is not bonded (paired).
+ * <p>There is no shared link key with the remote device, so communication
+ * (if it is allowed at all) will be unauthenticated and unencrypted.
+ */
+ public static final int BOND_NONE = 10;
+ /**
+ * Indicates bonding (pairing) is in progress with the remote device.
+ */
+ public static final int BOND_BONDING = 11;
+ /**
+ * Indicates the remote device is bonded (paired).
+ * <p>A shared link keys exists locally for the remote device, so
+ * communication can be authenticated and encrypted.
+ * <p><i>Being bonded (paired) with a remote device does not necessarily
+ * mean the device is currently connected. It just means that the pending
+ * procedure was completed at some earlier time, and the link key is still
+ * stored locally, ready to use on the next connection.
+ * </i>
+ */
+ public static final int BOND_BONDED = 12;
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents for unbond reason.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static final String EXTRA_REASON = "android.bluetooth.device.extra.REASON";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents to indicate pairing method used. Possible values are:
+ * {@link #PAIRING_VARIANT_PIN},
+ * {@link #PAIRING_VARIANT_PASSKEY_CONFIRMATION},
+ */
+ public static final String EXTRA_PAIRING_VARIANT =
+ "android.bluetooth.device.extra.PAIRING_VARIANT";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents as the value of passkey.
+ */
+ public static final String EXTRA_PAIRING_KEY = "android.bluetooth.device.extra.PAIRING_KEY";
+
+ /**
+ * Used as an int extra field in {@link #ACTION_PAIRING_REQUEST}
+ * intents as the value of passkey.
+ * @hide
+ */
+ public static final String EXTRA_PAIRING_INITIATOR =
+ "android.bluetooth.device.extra.PAIRING_INITIATOR";
+
+ /**
+ * Bluetooth pairing initiator, Foreground App
+ * @hide
+ */
+ public static final int EXTRA_PAIRING_INITIATOR_FOREGROUND = 1;
+
+ /**
+ * Bluetooth pairing initiator, Background
+ * @hide
+ */
+ public static final int EXTRA_PAIRING_INITIATOR_BACKGROUND = 2;
+
+ /**
+ * Bluetooth device type, Unknown
+ */
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * Bluetooth device type, Classic - BR/EDR devices
+ */
+ public static final int DEVICE_TYPE_CLASSIC = 1;
+
+ /**
+ * Bluetooth device type, Low Energy - LE-only
+ */
+ public static final int DEVICE_TYPE_LE = 2;
+
+ /**
+ * Bluetooth device type, Dual Mode - BR/EDR/LE
+ */
+ public static final int DEVICE_TYPE_DUAL = 3;
+
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final String ACTION_SDP_RECORD =
+ "android.bluetooth.device.action.SDP_RECORD";
+
+ /** @hide */
+ @IntDef(prefix = "METADATA_", value = {
+ METADATA_MANUFACTURER_NAME,
+ METADATA_MODEL_NAME,
+ METADATA_SOFTWARE_VERSION,
+ METADATA_HARDWARE_VERSION,
+ METADATA_COMPANION_APP,
+ METADATA_MAIN_ICON,
+ METADATA_IS_UNTETHERED_HEADSET,
+ METADATA_UNTETHERED_LEFT_ICON,
+ METADATA_UNTETHERED_RIGHT_ICON,
+ METADATA_UNTETHERED_CASE_ICON,
+ METADATA_UNTETHERED_LEFT_BATTERY,
+ METADATA_UNTETHERED_RIGHT_BATTERY,
+ METADATA_UNTETHERED_CASE_BATTERY,
+ METADATA_UNTETHERED_LEFT_CHARGING,
+ METADATA_UNTETHERED_RIGHT_CHARGING,
+ METADATA_UNTETHERED_CASE_CHARGING,
+ METADATA_ENHANCED_SETTINGS_UI_URI,
+ METADATA_DEVICE_TYPE,
+ METADATA_MAIN_BATTERY,
+ METADATA_MAIN_CHARGING,
+ METADATA_MAIN_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD,
+ METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MetadataKey{}
+
+ /**
+ * Maximum length of a metadata entry, this is to avoid exploding Bluetooth
+ * disk usage
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAX_LENGTH = 2048;
+
+ /**
+ * Manufacturer name of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MANUFACTURER_NAME = 0;
+
+ /**
+ * Model name of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MODEL_NAME = 1;
+
+ /**
+ * Software version of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_SOFTWARE_VERSION = 2;
+
+ /**
+ * Hardware version of this Bluetooth device
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_HARDWARE_VERSION = 3;
+
+ /**
+ * Package name of the companion app, if any
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_COMPANION_APP = 4;
+
+ /**
+ * URI to the main icon shown on the settings UI
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_ICON = 5;
+
+ /**
+ * Whether this device is an untethered headset with left, right and case
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_IS_UNTETHERED_HEADSET = 6;
+
+ /**
+ * URI to icon of the left headset
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_ICON = 7;
+
+ /**
+ * URI to icon of the right headset
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_ICON = 8;
+
+ /**
+ * URI to icon of the headset charging case
+ * Data type should be {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_ICON = 9;
+
+ /**
+ * Battery level of left headset
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_BATTERY = 10;
+
+ /**
+ * Battery level of rigth headset
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_BATTERY = 11;
+
+ /**
+ * Battery level of the headset charging case
+ * Data type should be {@String} 0-100 as {@link Byte} array, otherwise
+ * as invalid.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_BATTERY = 12;
+
+ /**
+ * Whether the left headset is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_CHARGING = 13;
+
+ /**
+ * Whether the right headset is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_CHARGING = 14;
+
+ /**
+ * Whether the headset charging case is charging
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_CHARGING = 15;
+
+ /**
+ * URI to the enhanced settings UI slice
+ * Data type should be {@String} as {@link Byte} array, null means
+ * the UI does not exist.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_ENHANCED_SETTINGS_UI_URI = 16;
+
+ /**
+ * Type of the Bluetooth device, must be within the list of
+ * BluetoothDevice.DEVICE_TYPE_*
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_DEVICE_TYPE = 17;
+
+ /**
+ * Battery level of the Bluetooth device, use when the Bluetooth device
+ * does not support HFP battery indicator.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_BATTERY = 18;
+
+ /**
+ * Whether the device is charging.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_CHARGING = 19;
+
+ /**
+ * The battery threshold of the Bluetooth device to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_MAIN_LOW_BATTERY_THRESHOLD = 20;
+
+ /**
+ * The battery threshold of the left headset to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_LEFT_LOW_BATTERY_THRESHOLD = 21;
+
+ /**
+ * The battery threshold of the right headset to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD = 22;
+
+ /**
+ * The battery threshold of the case to show low battery icon.
+ * Data type should be {@String} as {@link Byte} array.
+ * @hide
+ */
+ @SystemApi
+ public static final int METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD = 23;
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is a standard Bluetooth accessory or
+ * not listed in METADATA_DEVICE_TYPE_*.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_DEFAULT = "Default";
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is a watch.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_WATCH = "Watch";
+
+ /**
+ * Device type which is used in METADATA_DEVICE_TYPE
+ * Indicates this Bluetooth device is an untethered headset.
+ * @hide
+ */
+ @SystemApi
+ public static final String DEVICE_TYPE_UNTETHERED_HEADSET = "Untethered Headset";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast the {@link UUID}
+ * wrapped as a {@link android.os.ParcelUuid} of the remote device after it
+ * has been fetched. This intent is sent only when the UUIDs of the remote
+ * device are requested to be fetched using Service Discovery Protocol
+ * <p> Always contains the extra field {@link #EXTRA_DEVICE}
+ * <p> Always contains the extra field {@link #EXTRA_UUID}
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_UUID =
+ "android.bluetooth.device.action.UUID";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MAS_INSTANCE =
+ "android.bluetooth.device.action.MAS_INSTANCE";
+
+ /**
+ * Broadcast Action: Indicates a failure to retrieve the name of a remote
+ * device.
+ * <p>Always contains the extra field {@link #EXTRA_DEVICE}.
+ *
+ * @hide
+ */
+ //TODO: is this actually useful?
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NAME_FAILED =
+ "android.bluetooth.device.action.NAME_FAILED";
+
+ /**
+ * Broadcast Action: This intent is used to broadcast PAIRING REQUEST
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PAIRING_REQUEST =
+ "android.bluetooth.device.action.PAIRING_REQUEST";
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @UnsupportedAppUsage
+ public static final String ACTION_PAIRING_CANCEL =
+ "android.bluetooth.device.action.PAIRING_CANCEL";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_ACCESS_REQUEST =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_REQUEST";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_ACCESS_REPLY =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_REPLY";
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_ACCESS_CANCEL =
+ "android.bluetooth.device.action.CONNECTION_ACCESS_CANCEL";
+
+ /**
+ * Intent to broadcast silence mode changed.
+ * Alway contains the extra field {@link #EXTRA_DEVICE}
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_SILENCE_MODE_CHANGED =
+ "android.bluetooth.device.action.SILENCE_MODE_CHANGED";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_ACCESS_REQUEST_TYPE =
+ "android.bluetooth.device.extra.ACCESS_REQUEST_TYPE";
+
+ /** @hide */
+ public static final int REQUEST_TYPE_PROFILE_CONNECTION = 1;
+
+ /** @hide */
+ public static final int REQUEST_TYPE_PHONEBOOK_ACCESS = 2;
+
+ /** @hide */
+ public static final int REQUEST_TYPE_MESSAGE_ACCESS = 3;
+
+ /** @hide */
+ public static final int REQUEST_TYPE_SIM_ACCESS = 4;
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+ * Contains package name to return reply intent to.
+ *
+ * @hide
+ */
+ public static final String EXTRA_PACKAGE_NAME = "android.bluetooth.device.extra.PACKAGE_NAME";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REQUEST} intents,
+ * Contains class name to return reply intent to.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CLASS_NAME = "android.bluetooth.device.extra.CLASS_NAME";
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intent.
+ *
+ * @hide
+ */
+ public static final String EXTRA_CONNECTION_ACCESS_RESULT =
+ "android.bluetooth.device.extra.CONNECTION_ACCESS_RESULT";
+
+ /** @hide */
+ public static final int CONNECTION_ACCESS_YES = 1;
+
+ /** @hide */
+ public static final int CONNECTION_ACCESS_NO = 2;
+
+ /**
+ * Used as an extra field in {@link #ACTION_CONNECTION_ACCESS_REPLY} intents,
+ * Contains boolean to indicate if the allowed response is once-for-all so that
+ * next request will be granted without asking user again.
+ *
+ * @hide
+ */
+ public static final String EXTRA_ALWAYS_ALLOWED =
+ "android.bluetooth.device.extra.ALWAYS_ALLOWED";
+
+ /**
+ * A bond attempt succeeded
+ *
+ * @hide
+ */
+ public static final int BOND_SUCCESS = 0;
+
+ /**
+ * A bond attempt failed because pins did not match, or remote device did
+ * not respond to pin request in time
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_AUTH_FAILED = 1;
+
+ /**
+ * A bond attempt failed because the other side explicitly rejected
+ * bonding
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_AUTH_REJECTED = 2;
+
+ /**
+ * A bond attempt failed because we canceled the bonding process
+ *
+ * @hide
+ */
+ public static final int UNBOND_REASON_AUTH_CANCELED = 3;
+
+ /**
+ * A bond attempt failed because we could not contact the remote device
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_REMOTE_DEVICE_DOWN = 4;
+
+ /**
+ * A bond attempt failed because a discovery is in progress
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_DISCOVERY_IN_PROGRESS = 5;
+
+ /**
+ * A bond attempt failed because of authentication timeout
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_AUTH_TIMEOUT = 6;
+
+ /**
+ * A bond attempt failed because of repeated attempts
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_REPEATED_ATTEMPTS = 7;
+
+ /**
+ * A bond attempt failed because we received an Authentication Cancel
+ * by remote end
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final int UNBOND_REASON_REMOTE_AUTH_CANCELED = 8;
+
+ /**
+ * An existing bond was explicitly revoked
+ *
+ * @hide
+ */
+ public static final int UNBOND_REASON_REMOVED = 9;
+
+ /**
+ * The user will be prompted to enter a pin or
+ * an app will enter a pin for user.
+ */
+ public static final int PAIRING_VARIANT_PIN = 0;
+
+ /**
+ * The user will be prompted to enter a passkey
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_PASSKEY = 1;
+
+ /**
+ * The user will be prompted to confirm the passkey displayed on the screen or
+ * an app will confirm the passkey for the user.
+ */
+ public static final int PAIRING_VARIANT_PASSKEY_CONFIRMATION = 2;
+
+ /**
+ * The user will be prompted to accept or deny the incoming pairing request
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_CONSENT = 3;
+
+ /**
+ * The user will be prompted to enter the passkey displayed on remote device
+ * This is used for Bluetooth 2.1 pairing.
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_DISPLAY_PASSKEY = 4;
+
+ /**
+ * The user will be prompted to enter the PIN displayed on remote device.
+ * This is used for Bluetooth 2.0 pairing.
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_DISPLAY_PIN = 5;
+
+ /**
+ * The user will be prompted to accept or deny the OOB pairing request
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_OOB_CONSENT = 6;
+
+ /**
+ * The user will be prompted to enter a 16 digit pin or
+ * an app will enter a 16 digit pin for user.
+ *
+ * @hide
+ */
+ public static final int PAIRING_VARIANT_PIN_16_DIGITS = 7;
+
+ /**
+ * Used as an extra field in {@link #ACTION_UUID} intents,
+ * Contains the {@link android.os.ParcelUuid}s of the remote device which
+ * is a parcelable version of {@link UUID}.
+ */
+ public static final String EXTRA_UUID = "android.bluetooth.device.extra.UUID";
+
+ /** @hide */
+ public static final String EXTRA_SDP_RECORD =
+ "android.bluetooth.device.extra.SDP_RECORD";
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static final String EXTRA_SDP_SEARCH_STATUS =
+ "android.bluetooth.device.extra.SDP_SEARCH_STATUS";
+
+ /** @hide */
+ @IntDef(prefix = "ACCESS_", value = {ACCESS_UNKNOWN,
+ ACCESS_ALLOWED, ACCESS_REJECTED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AccessPermission{}
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_UNKNOWN = 0;
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_ALLOWED = 1;
+
+ /**
+ * For {@link #getPhonebookAccessPermission}, {@link #setPhonebookAccessPermission},
+ * {@link #getMessageAccessPermission} and {@link #setMessageAccessPermission}.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ACCESS_REJECTED = 2;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "TRANSPORT_" },
+ value = {
+ /** Allow host to automatically select a transport (dual-mode only) */
+ TRANSPORT_AUTO,
+ /** Use Classic or BR/EDR transport.*/
+ TRANSPORT_BREDR,
+ /** Use Low Energy transport.*/
+ TRANSPORT_LE,
+ }
+ )
+ public @interface Transport {}
+
+ /**
+ * No preference of physical transport for GATT connections to remote dual-mode devices
+ */
+ public static final int TRANSPORT_AUTO = 0;
+
+ /**
+ * Prefer BR/EDR transport for GATT connections to remote dual-mode devices
+ */
+ public static final int TRANSPORT_BREDR = 1;
+
+ /**
+ * Prefer LE transport for GATT connections to remote dual-mode devices
+ */
+ public static final int TRANSPORT_LE = 2;
+
+ /**
+ * Bluetooth LE 1M PHY. Used to refer to LE 1M Physical Channel for advertising, scanning or
+ * connection.
+ */
+ public static final int PHY_LE_1M = 1;
+
+ /**
+ * Bluetooth LE 2M PHY. Used to refer to LE 2M Physical Channel for advertising, scanning or
+ * connection.
+ */
+ public static final int PHY_LE_2M = 2;
+
+ /**
+ * Bluetooth LE Coded PHY. Used to refer to LE Coded Physical Channel for advertising, scanning
+ * or connection.
+ */
+ public static final int PHY_LE_CODED = 3;
+
+ /**
+ * Bluetooth LE 1M PHY mask. Used to specify LE 1M Physical Channel as one of many available
+ * options in a bitmask.
+ */
+ public static final int PHY_LE_1M_MASK = 1;
+
+ /**
+ * Bluetooth LE 2M PHY mask. Used to specify LE 2M Physical Channel as one of many available
+ * options in a bitmask.
+ */
+ public static final int PHY_LE_2M_MASK = 2;
+
+ /**
+ * Bluetooth LE Coded PHY mask. Used to specify LE Coded Physical Channel as one of many
+ * available options in a bitmask.
+ */
+ public static final int PHY_LE_CODED_MASK = 4;
+
+ /**
+ * No preferred coding when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_NO_PREFERRED = 0;
+
+ /**
+ * Prefer the S=2 coding to be used when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_S2 = 1;
+
+ /**
+ * Prefer the S=8 coding to be used when transmitting on the LE Coded PHY.
+ */
+ public static final int PHY_OPTION_S8 = 2;
+
+
+ /** @hide */
+ public static final String EXTRA_MAS_INSTANCE =
+ "android.bluetooth.device.extra.MAS_INSTANCE";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "ADDRESS_TYPE_" },
+ value = {
+ /** Hardware MAC Address */
+ ADDRESS_TYPE_PUBLIC,
+ /** Address is either resolvable, non-resolvable or static.*/
+ ADDRESS_TYPE_RANDOM,
+ }
+ )
+ public @interface AddressType {}
+
+ /** Hardware MAC Address of the device */
+ public static final int ADDRESS_TYPE_PUBLIC = 0;
+ /** Address is either resolvable, non-resolvable or static. */
+ public static final int ADDRESS_TYPE_RANDOM = 1;
+
+ private static final String NULL_MAC_ADDRESS = "00:00:00:00:00:00";
+
+ /**
+ * Lazy initialization. Guaranteed final after first object constructed, or
+ * getService() called.
+ * TODO: Unify implementation of sService amongst BluetoothFoo API's
+ */
+ private static volatile IBluetooth sService;
+
+ private final String mAddress;
+ @AddressType private final int mAddressType;
+
+ private AttributionSource mAttributionSource;
+
+ /*package*/
+ @UnsupportedAppUsage
+ static IBluetooth getService() {
+ synchronized (BluetoothDevice.class) {
+ if (sService == null) {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ sService = adapter.getBluetoothService(sStateChangeCallback);
+ }
+ }
+ return sService;
+ }
+
+ static IBluetoothManagerCallback sStateChangeCallback = new IBluetoothManagerCallback.Stub() {
+
+ public void onBluetoothServiceUp(IBluetooth bluetoothService)
+ throws RemoteException {
+ synchronized (BluetoothDevice.class) {
+ if (sService == null) {
+ sService = bluetoothService;
+ }
+ }
+ }
+
+ public void onBluetoothServiceDown()
+ throws RemoteException {
+ synchronized (BluetoothDevice.class) {
+ sService = null;
+ }
+ }
+
+ public void onBrEdrDown() {
+ if (DBG) Log.d(TAG, "onBrEdrDown: reached BLE ON state");
+ }
+
+ public void onOobData(@Transport int transport, OobData oobData) {
+ if (DBG) Log.d(TAG, "onOobData: got data");
+ }
+ };
+
+ /**
+ * Create a new BluetoothDevice
+ * Bluetooth MAC address must be upper case, such as "00:11:22:33:AA:BB",
+ * and is validated in this constructor.
+ *
+ * @param address valid Bluetooth MAC address
+ * @param attributionSource attribution for permission-protected calls
+ * @throws RuntimeException Bluetooth is not available on this platform
+ * @throws IllegalArgumentException address is invalid
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ BluetoothDevice(String address) {
+ getService(); // ensures sService is initialized
+ if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+ throw new IllegalArgumentException(address + " is not a valid Bluetooth address");
+ }
+
+ mAddress = address;
+ mAddressType = ADDRESS_TYPE_PUBLIC;
+ mAttributionSource = AttributionSource.myAttributionSource();
+ }
+
+ /** {@hide} */
+ public void setAttributionSource(@NonNull AttributionSource attributionSource) {
+ mAttributionSource = attributionSource;
+ }
+
+ /** {@hide} */
+ public void prepareToEnterProcess(@NonNull AttributionSource attributionSource) {
+ setAttributionSource(attributionSource);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothDevice) {
+ return mAddress.equals(((BluetoothDevice) o).getAddress());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mAddress.hashCode();
+ }
+
+ /**
+ * Returns a string representation of this BluetoothDevice.
+ * <p>Currently this is the Bluetooth hardware address, for example
+ * "00:11:22:AA:BB:CC". However, you should always use {@link #getAddress}
+ * if you explicitly require the Bluetooth hardware address in case the
+ * {@link #toString} representation changes in the future.
+ *
+ * @return string representation of this BluetoothDevice
+ */
+ @Override
+ public String toString() {
+ return mAddress;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothDevice> CREATOR =
+ new Parcelable.Creator<BluetoothDevice>() {
+ public BluetoothDevice createFromParcel(Parcel in) {
+ return new BluetoothDevice(in.readString());
+ }
+
+ public BluetoothDevice[] newArray(int size) {
+ return new BluetoothDevice[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAddress);
+ }
+
+ /**
+ * Returns the hardware address of this BluetoothDevice.
+ * <p> For example, "00:11:22:AA:BB:CC".
+ *
+ * @return Bluetooth hardware address as string
+ */
+ public String getAddress() {
+ if (DBG) Log.d(TAG, "mAddress: " + mAddress);
+ return mAddress;
+ }
+
+ /**
+ * Returns the anonymized hardware address of this BluetoothDevice. The first three octets
+ * will be suppressed for anonymization.
+ * <p> For example, "XX:XX:XX:AA:BB:CC".
+ *
+ * @return Anonymized bluetooth hardware address as string
+ * @hide
+ */
+ public String getAnonymizedAddress() {
+ return "XX:XX:XX" + getAddress().substring(8);
+ }
+
+ /**
+ * Get the friendly Bluetooth name of the remote device.
+ *
+ * <p>The local adapter will automatically retrieve remote names when
+ * performing a device scan, and will cache them. This method just returns
+ * the name for this device from the cache.
+ *
+ * @return the Bluetooth name, or null if there was a problem.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getName() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device name");
+ return null;
+ }
+ try {
+ String name = service.getRemoteName(this, mAttributionSource);
+ if (name != null) {
+ // remove whitespace characters from the name
+ return name
+ .replace('\t', ' ')
+ .replace('\n', ' ')
+ .replace('\r', ' ');
+ }
+ return null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get the Bluetooth device type of the remote device.
+ *
+ * @return the device type {@link #DEVICE_TYPE_CLASSIC}, {@link #DEVICE_TYPE_LE} {@link
+ * #DEVICE_TYPE_DUAL}. {@link #DEVICE_TYPE_UNKNOWN} if it's not available
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getType() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device type");
+ return DEVICE_TYPE_UNKNOWN;
+ }
+ try {
+ return service.getRemoteType(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return DEVICE_TYPE_UNKNOWN;
+ }
+
+ /**
+ * Get the locally modifiable name (alias) of the remote Bluetooth device.
+ *
+ * @return the Bluetooth alias, the friendly device name if no alias, or
+ * null if there was a problem
+ */
+ @Nullable
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getAlias() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get Remote Device Alias");
+ return null;
+ }
+ try {
+ String alias = service.getRemoteAliasWithAttribution(this, mAttributionSource);
+ if (alias == null) {
+ return getName();
+ }
+ return alias
+ .replace('\t', ' ')
+ .replace('\n', ' ')
+ .replace('\r', ' ');
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED
+ })
+ public @interface SetAliasReturnValues{}
+
+ /**
+ * Sets the locally modifiable name (alias) of the remote Bluetooth device. This method
+ * overwrites the previously stored alias. The new alias is saved in local
+ * storage so that the change is preserved over power cycles.
+ *
+ * <p>This method requires the calling app to be associated with Companion Device Manager (see
+ * {@link android.companion.CompanionDeviceManager#associate(AssociationRequest,
+ * android.companion.CompanionDeviceManager.Callback, Handler)}) and have the
+ * {@link android.Manifest.permission#BLUETOOTH_CONNECT} permission. Alternatively, if the
+ * caller has the {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} permission, they can
+ * bypass the Companion Device Manager association requirement as well as other permission
+ * requirements.
+ *
+ * @param alias is the new locally modifiable name for the remote Bluetooth device which must
+ * be the empty string. If null, we clear the alias.
+ * @return whether the alias was successfully changed
+ * @throws IllegalArgumentException if the alias is the empty string
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @SetAliasReturnValues int setAlias(@Nullable String alias) {
+ if (alias != null && alias.isEmpty()) {
+ throw new IllegalArgumentException("alias cannot be the empty string");
+ }
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot set Remote Device name");
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ try {
+ return service.setRemoteAlias(this, alias, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get the most recent identified battery level of this Bluetooth device
+ *
+ * @return Battery level in percents from 0 to 100, {@link #BATTERY_LEVEL_BLUETOOTH_OFF} if
+ * Bluetooth is disabled or {@link #BATTERY_LEVEL_UNKNOWN} if device is disconnected, or does
+ * not have any battery reporting service, or return value is invalid
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getBatteryLevel() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth disabled. Cannot get remote device battery level");
+ return BATTERY_LEVEL_BLUETOOTH_OFF;
+ }
+ try {
+ return service.getBatteryLevel(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return BATTERY_LEVEL_UNKNOWN;
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device.
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * @return false on immediate error, true if bonding will begin
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBond() {
+ return createBond(TRANSPORT_AUTO);
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device using the
+ * specified transport.
+ *
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * @param transport The transport to use for the pairing procedure.
+ * @return false on immediate error, true if bonding will begin
+ * @throws IllegalArgumentException if an invalid transport was specified
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBond(int transport) {
+ return createBondInternal(transport, null, null);
+ }
+
+ /**
+ * Start the bonding (pairing) process with the remote device using the
+ * Out Of Band mechanism.
+ *
+ * <p>This is an asynchronous call, it will return immediately. Register
+ * for {@link #ACTION_BOND_STATE_CHANGED} intents to be notified when
+ * the bonding process completes, and its result.
+ *
+ * <p>Android system services will handle the necessary user interactions
+ * to confirm and complete the bonding process.
+ *
+ * <p>There are two possible versions of OOB Data. This data can come in as
+ * P192 or P256. This is a reference to the cryptography used to generate the key.
+ * The caller may pass one or both. If both types of data are passed, then the
+ * P256 data will be preferred, and thus used.
+ *
+ * @param transport - Transport to use
+ * @param remoteP192Data - Out Of Band data (P192) or null
+ * @param remoteP256Data - Out Of Band data (P256) or null
+ * @return false on immediate error, true if bonding will begin
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean createBondOutOfBand(int transport, @Nullable OobData remoteP192Data,
+ @Nullable OobData remoteP256Data) {
+ if (remoteP192Data == null && remoteP256Data == null) {
+ throw new IllegalArgumentException(
+ "One or both arguments for the OOB data types are required to not be null."
+ + " Please use createBond() instead if you do not have OOB data to pass.");
+ }
+ return createBondInternal(transport, remoteP192Data, remoteP256Data);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean createBondInternal(int transport, @Nullable OobData remoteP192Data,
+ @Nullable OobData remoteP256Data) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.w(TAG, "BT not enabled, createBondOutOfBand failed");
+ return false;
+ }
+ if (NULL_MAC_ADDRESS.equals(mAddress)) {
+ Log.e(TAG, "Unable to create bond, invalid address " + mAddress);
+ return false;
+ }
+ try {
+ return service.createBond(
+ this, transport, remoteP192Data, remoteP256Data, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Gets whether bonding was initiated locally
+ *
+ * @return true if bonding is initiated locally, false otherwise
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isBondingInitiatedLocally() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.w(TAG, "BT not enabled, isBondingInitiatedLocally failed");
+ return false;
+ }
+ try {
+ return service.isBondingInitiatedLocally(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Cancel an in-progress bonding request started with {@link #createBond}.
+ *
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean cancelBondProcess() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot cancel Remote Device bond");
+ return false;
+ }
+ try {
+ Log.i(TAG, "cancelBondProcess() for device " + getAddress()
+ + " called by pid: " + Process.myPid()
+ + " tid: " + Process.myTid());
+ return service.cancelBondProcess(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Remove bond (pairing) with the remote device.
+ * <p>Delete the link key associated with the remote device, and
+ * immediately terminate connections to that device that require
+ * authentication and encryption.
+ *
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean removeBond() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot remove Remote Device bond");
+ return false;
+ }
+ try {
+ Log.i(TAG, "removeBond() for device " + getAddress()
+ + " called by pid: " + Process.myPid()
+ + " tid: " + Process.myTid());
+ return service.removeBond(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /*
+ private static final String BLUETOOTH_BONDING_CACHE_PROPERTY =
+ "cache_key.bluetooth.get_bond_state";
+ private final PropertyInvalidatedCache<BluetoothDevice, Integer> mBluetoothBondCache =
+ new PropertyInvalidatedCache<BluetoothDevice, Integer>(
+ 8, BLUETOOTH_BONDING_CACHE_PROPERTY) {
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ protected Integer recompute(BluetoothDevice query) {
+ try {
+ return sService.getBondState(query, mAttributionSource);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+ };
+ */
+
+ /** @hide */
+ /* public void disableBluetoothGetBondStateCache() {
+ mBluetoothBondCache.disableLocal();
+ } */
+
+ /** @hide */
+ /*
+ public static void invalidateBluetoothGetBondStateCache() {
+ PropertyInvalidatedCache.invalidateCache(BLUETOOTH_BONDING_CACHE_PROPERTY);
+ }
+ */
+
+ /**
+ * Get the bond state of the remote device.
+ * <p>Possible values for the bond state are:
+ * {@link #BOND_NONE},
+ * {@link #BOND_BONDING},
+ * {@link #BOND_BONDED}.
+ *
+ * @return the bond state
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public int getBondState() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get bond state");
+ return BOND_NONE;
+ }
+ try {
+ //return mBluetoothBondCache.query(this);
+ return sService.getBondState(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to ", e);
+ e.rethrowFromSystemServer();
+ }
+ return BOND_NONE;
+ }
+
+ /**
+ * Checks whether this bluetooth device is associated with CDM and meets the criteria to skip
+ * the bluetooth pairing dialog because it has been already consented by the CDM prompt.
+ *
+ * @return true if we can bond without the dialog, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean canBondWithoutDialog() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot check if we can skip pairing dialog");
+ return false;
+ }
+ try {
+ if (DBG) Log.d(TAG, "canBondWithoutDialog, device: " + this);
+ return service.canBondWithoutDialog(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
+ BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED
+ })
+ public @interface ConnectionReturnValues{}
+
+ /**
+ * Connects all user enabled and supported bluetooth profiles between the local and remote
+ * device. If no profiles are user enabled (e.g. first connection), we connect all supported
+ * profiles. If the device is not already connected, this will page the device before initiating
+ * profile connections. Connection is asynchronous and you should listen to each profile's
+ * broadcast intent ACTION_CONNECTION_STATE_CHANGED to verify whether connection was successful.
+ * For example, to verify a2dp is connected, you would listen for
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * @return whether the messages were successfully sent to try to connect all profiles
+ * @throws IllegalArgumentException if the device address is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.MODIFY_PHONE_STATE,
+ })
+ public @ConnectionReturnValues int connect() {
+ if (!BluetoothAdapter.checkBluetoothAddress(getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+
+ try {
+ if (sService == null) {
+ Log.e(TAG, "BT not enabled. Cannot connect to remote device.");
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ return sService.connectAllEnabledProfiles(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Disconnects all connected bluetooth profiles between the local and remote device.
+ * Disconnection is asynchronous and you should listen to each profile's broadcast intent
+ * ACTION_CONNECTION_STATE_CHANGED to verify whether disconnection was successful. For example,
+ * to verify a2dp is disconnected, you would listen for
+ * {@link BluetoothA2dp#ACTION_CONNECTION_STATE_CHANGED}
+ *
+ * @return whether the messages were successfully sent to try to disconnect all profiles
+ * @throws IllegalArgumentException if the device address is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionReturnValues int disconnect() {
+ if (!BluetoothAdapter.checkBluetoothAddress(getAddress())) {
+ throw new IllegalArgumentException("device cannot have an invalid address");
+ }
+
+ try {
+ if (sService == null) {
+ Log.e(TAG, "BT not enabled. Cannot disconnect from remote device.");
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+ return sService.disconnectAllEnabledProfiles(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether there is an open connection to this device.
+ *
+ * @return True if there is at least one open connection to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isConnected() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ // BT is not enabled, we cannot be connected.
+ return false;
+ }
+ try {
+ return service.getConnectionStateWithAttribution(this, mAttributionSource)
+ != CONNECTION_STATE_DISCONNECTED;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether there is an open connection to this device
+ * that has been encrypted.
+ *
+ * @return True if there is at least one encrypted connection to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isEncrypted() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ // BT is not enabled, we cannot be connected.
+ return false;
+ }
+ try {
+ return service.getConnectionStateWithAttribution(this, mAttributionSource)
+ > CONNECTION_STATE_CONNECTED;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get the Bluetooth class of the remote device.
+ *
+ * @return Bluetooth class object, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothClass getBluetoothClass() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot get Bluetooth Class");
+ return null;
+ }
+ try {
+ int classInt = service.getRemoteClass(this, mAttributionSource);
+ if (classInt == BluetoothClass.ERROR) return null;
+ return new BluetoothClass(classInt);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the supported features (UUIDs) of the remote device.
+ *
+ * <p>This method does not start a service discovery procedure to retrieve the UUIDs
+ * from the remote device. Instead, the local cached copy of the service
+ * UUIDs are returned.
+ * <p>Use {@link #fetchUuidsWithSdp} if fresh UUIDs are desired.
+ *
+ * @return the supported features (UUIDs) of the remote device, or null on error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public ParcelUuid[] getUuids() {
+ final IBluetooth service = sService;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot get remote device Uuids");
+ return null;
+ }
+ try {
+ return service.getRemoteUuids(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the UUIDs supported.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_UUID} intent is sent,
+ * with the UUIDs supported by the remote end. If there is an error
+ * in getting the SDP records or if the process takes a long time, or the device is bonding and
+ * we have its UUIDs cached, {@link #ACTION_UUID} intent is sent with the UUIDs that is
+ * currently present in the cache. Clients should use the {@link #getUuids} to get UUIDs
+ * if service discovery is not to be performed. If there is an ongoing bonding process,
+ * service discovery or device inquiry, the request will be queued.
+ *
+ * @return False if the check fails, True if the process of initiating an ACL connection
+ * to the remote device was started or cached UUIDs will be broadcast.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean fetchUuidsWithSdp() {
+ return fetchUuidsWithSdp(TRANSPORT_AUTO);
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the UUIDs supported with the
+ * specific transport.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_UUID} intent is sent,
+ * with the UUIDs supported by the remote end. If there is an error
+ * in getting the SDP or GATT records or if the process takes a long time, or the device
+ * is bonding and we have its UUIDs cached, {@link #ACTION_UUID} intent is sent with the
+ * UUIDs that is currently present in the cache. Clients should use the {@link #getUuids}
+ * to get UUIDs if service discovery is not to be performed. If there is an ongoing bonding
+ * process, service discovery or device inquiry, the request will be queued.
+ *
+ * @param transport - provide type of transport (e.g. LE or Classic).
+ * @return False if the check fails, True if the process of initiating an ACL connection
+ * to the remote device was started or cached UUIDs will be broadcast with the specific
+ * transport.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean fetchUuidsWithSdp(@Transport int transport) {
+ final IBluetooth service = sService;
+ if (service == null || !isBluetoothEnabled()) {
+ Log.e(TAG, "BT not enabled. Cannot fetchUuidsWithSdp");
+ return false;
+ }
+ try {
+ return service.fetchRemoteUuidsWithAttribution(this, transport, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Perform a service discovery on the remote device to get the SDP records associated
+ * with the specified UUID.
+ *
+ * <p>This API is asynchronous and {@link #ACTION_SDP_RECORD} intent is sent,
+ * with the SDP records found on the remote end. If there is an error
+ * in getting the SDP records or if the process takes a long time,
+ * {@link #ACTION_SDP_RECORD} intent is sent with an status value in
+ * {@link #EXTRA_SDP_SEARCH_STATUS} different from 0.
+ * Detailed status error codes can be found by members of the Bluetooth package in
+ * the AbstractionLayer class.
+ * <p>The SDP record data will be stored in the intent as {@link #EXTRA_SDP_RECORD}.
+ * The object type will match one of the SdpXxxRecord types, depending on the UUID searched
+ * for.
+ *
+ * @return False if the check fails, True if the process
+ * of initiating an ACL connection to the remote device
+ * was started.
+ */
+ /** @hide */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sdpSearch(ParcelUuid uuid) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot query remote device sdp records");
+ return false;
+ }
+ try {
+ return service.sdpSearch(this, uuid, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Set the pin during pairing when the pairing method is {@link #PAIRING_VARIANT_PIN}
+ *
+ * @return true pin has been set false for error
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPin(byte[] pin) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot set Remote Device pin");
+ return false;
+ }
+ try {
+ return service.setPin(this, true, pin.length, pin, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Set the pin during pairing when the pairing method is {@link #PAIRING_VARIANT_PIN}
+ *
+ * @return true pin has been set false for error
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setPin(@NonNull String pin) {
+ byte[] pinBytes = convertPinToBytes(pin);
+ if (pinBytes == null) {
+ return false;
+ }
+ return setPin(pinBytes);
+ }
+
+ /**
+ * Confirm passkey for {@link #PAIRING_VARIANT_PASSKEY_CONFIRMATION} pairing.
+ *
+ * @return true confirmation has been sent out false for error
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPairingConfirmation(boolean confirm) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot set pairing confirmation");
+ return false;
+ }
+ try {
+ return service.setPairingConfirmation(this, confirm, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Cancels pairing to this device
+ *
+ * @return true if pairing cancelled successfully, false otherwise
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean cancelPairing() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "BT not enabled. Cannot cancel pairing");
+ return false;
+ }
+ try {
+ return service.cancelBondProcess(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ boolean isBluetoothEnabled() {
+ boolean ret = false;
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null && adapter.isEnabled()) {
+ ret = true;
+ }
+ return ret;
+ }
+
+ /**
+ * Gets whether the phonebook access is allowed for this bluetooth device
+ *
+ * @return Whether the phonebook access is allowed to this device. Can be {@link
+ * #ACCESS_UNKNOWN}, {@link #ACCESS_ALLOWED} or {@link #ACCESS_REJECTED}.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getPhonebookAccessPermission() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ return ACCESS_UNKNOWN;
+ }
+ try {
+ return service.getPhonebookAccessPermission(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return ACCESS_UNKNOWN;
+ }
+
+ /**
+ * Sets whether the {@link BluetoothDevice} enters silence mode. Audio will not
+ * be routed to the {@link BluetoothDevice} if set to {@code true}.
+ *
+ * When the {@link BluetoothDevice} enters silence mode, and the {@link BluetoothDevice}
+ * is an active device (for A2DP or HFP), the active device for that profile
+ * will be set to null.
+ * If the {@link BluetoothDevice} exits silence mode while the A2DP or HFP
+ * active device is null, the {@link BluetoothDevice} will be set as the
+ * active device for that profile.
+ * If the {@link BluetoothDevice} is disconnected, it exits silence mode.
+ * If the {@link BluetoothDevice} is set as the active device for A2DP or
+ * HFP, while silence mode is enabled, then the device will exit silence mode.
+ * If the {@link BluetoothDevice} is in silence mode, AVRCP position change
+ * event and HFP AG indicators will be disabled.
+ * If the {@link BluetoothDevice} is not connected with A2DP or HFP, it cannot
+ * enter silence mode.
+ *
+ * @param silence true to enter silence mode, false to exit
+ * @return true on success, false on error.
+ * @throws IllegalStateException if Bluetooth is not turned ON.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setSilenceMode(boolean silence) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ throw new IllegalStateException("Bluetooth is not turned ON");
+ }
+ try {
+ return service.setSilenceMode(this, silence, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "setSilenceMode fail", e);
+ return false;
+ }
+ }
+
+ /**
+ * Check whether the {@link BluetoothDevice} is in silence mode
+ *
+ * @return true on device in silence mode, otherwise false.
+ * @throws IllegalStateException if Bluetooth is not turned ON.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean isInSilenceMode() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ throw new IllegalStateException("Bluetooth is not turned ON");
+ }
+ try {
+ return service.getSilenceMode(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "isInSilenceMode fail", e);
+ return false;
+ }
+ }
+
+ /**
+ * Sets whether the phonebook access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN}, {@link #ACCESS_ALLOWED} or {@link
+ * #ACCESS_REJECTED}.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPhonebookAccessPermission(@AccessPermission int value) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ return false;
+ }
+ try {
+ return service.setPhonebookAccessPermission(this, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Gets whether message access is allowed to this bluetooth device
+ *
+ * @return Whether the message access is allowed to this device.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getMessageAccessPermission() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ return ACCESS_UNKNOWN;
+ }
+ try {
+ return service.getMessageAccessPermission(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return ACCESS_UNKNOWN;
+ }
+
+ /**
+ * Sets whether the message access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN} if the device is unbonded,
+ * {@link #ACCESS_ALLOWED} if the permission is being granted, or {@link #ACCESS_REJECTED} if
+ * the permission is not being granted.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setMessageAccessPermission(@AccessPermission int value) {
+ // Validates param value is one of the accepted constants
+ if (value != ACCESS_ALLOWED && value != ACCESS_REJECTED && value != ACCESS_UNKNOWN) {
+ throw new IllegalArgumentException(value + "is not a valid AccessPermission value");
+ }
+ final IBluetooth service = sService;
+ if (service == null) {
+ return false;
+ }
+ try {
+ return service.setMessageAccessPermission(this, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Gets whether sim access is allowed for this bluetooth device
+ *
+ * @return Whether the Sim access is allowed to this device.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @AccessPermission int getSimAccessPermission() {
+ final IBluetooth service = sService;
+ if (service == null) {
+ return ACCESS_UNKNOWN;
+ }
+ try {
+ return service.getSimAccessPermission(this, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return ACCESS_UNKNOWN;
+ }
+
+ /**
+ * Sets whether the Sim access is allowed to this device.
+ *
+ * @param value Can be {@link #ACCESS_UNKNOWN} if the device is unbonded,
+ * {@link #ACCESS_ALLOWED} if the permission is being granted, or {@link #ACCESS_REJECTED} if
+ * the permission is not being granted.
+ * @return Whether the value has been successfully set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setSimAccessPermission(int value) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ return false;
+ }
+ try {
+ return service.setSimAccessPermission(this, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return false;
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link createInsecureRfcommSocket}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid RFCOMM channels are in range 1 to 30.
+ *
+ * @param channel RFCOMM channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createRfcommSocket(int channel) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, channel,
+ null);
+ }
+
+ /**
+ * Create an L2cap {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link createInsecureRfcommSocket}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid L2CAP PSM channels are in range 1 to 2^16.
+ *
+ * @param channel L2cap PSM/channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createL2capSocket(int channel) throws IOException {
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, true, true, this, channel,
+ null);
+ }
+
+ /**
+ * Create an L2cap {@link BluetoothSocket} ready to start an insecure
+ * outgoing connection to this remote device on given channel.
+ * <p>The remote device will be not authenticated and communication on this
+ * socket will not be encrypted.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection.
+ * <p>Valid L2CAP PSM channels are in range 1 to 2^16.
+ *
+ * @param channel L2cap PSM/channel to connect to
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureL2capSocket(int channel) throws IOException {
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP, -1, false, false, this, channel,
+ null);
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} ready to start a secure
+ * outgoing connection to this remote device using SDP lookup of uuid.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingRfcommWithServiceRecord} for peer-peer
+ * Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection. This will also perform an SDP lookup of the given uuid to
+ * determine which channel to connect to.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p> Use this socket only if an authenticated socket link is possible.
+ * Authentication refers to the authentication of the link key to
+ * prevent person-in-the-middle type of attacks.
+ * For example, for Bluetooth 2.1 devices, if any of the devices does not
+ * have an input and output capability or just has the ability to
+ * display a numeric key, a secure socket connection is not possible.
+ * In such a case, use {@link #createInsecureRfcommSocketToServiceRecord}.
+ * For more details, refer to the Security Model section 5.2 (vol 3) of
+ * Bluetooth Core Specification version 2.1 + EDR.
+ * <p>Hint: If you are connecting to a Bluetooth serial board then try
+ * using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB.
+ * However if you are connecting to an Android peer then please generate
+ * your own unique UUID.
+ *
+ * @param uuid service record uuid to lookup RFCOMM channel
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, true, true, this, -1,
+ new ParcelUuid(uuid));
+ }
+
+ /**
+ * Create an RFCOMM {@link BluetoothSocket} socket ready to start an insecure
+ * outgoing connection to this remote device using SDP lookup of uuid.
+ * <p> The communication channel will not have an authenticated link key
+ * i.e it will be subject to person-in-the-middle attacks. For Bluetooth 2.1
+ * devices, the link key will be encrypted, as encryption is mandatory.
+ * For legacy devices (pre Bluetooth 2.1 devices) the link key will
+ * be not be encrypted. Use {@link #createRfcommSocketToServiceRecord} if an
+ * encrypted and authenticated communication channel is desired.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingInsecureRfcommWithServiceRecord} for peer-peer
+ * Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing
+ * connection. This will also perform an SDP lookup of the given uuid to
+ * determine which channel to connect to.
+ * <p>The remote device will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Hint: If you are connecting to a Bluetooth serial board then try
+ * using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB.
+ * However if you are connecting to an Android peer then please generate
+ * your own unique UUID.
+ *
+ * @param uuid service record uuid to lookup RFCOMM channel
+ * @return a RFCOMM BluetoothServerSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureRfcommSocketToServiceRecord(UUID uuid) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, -1,
+ new ParcelUuid(uuid));
+ }
+
+ /**
+ * Construct an insecure RFCOMM socket ready to start an outgoing
+ * connection.
+ * Call #connect on the returned #BluetoothSocket to begin the connection.
+ * The remote device will not be authenticated and communication on this
+ * socket will not be encrypted.
+ *
+ * @param port remote port
+ * @return An RFCOMM BluetoothSocket
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @UnsupportedAppUsage(publicAlternatives = "Use "
+ + "{@link #createInsecureRfcommSocketToServiceRecord} instead.")
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createInsecureRfcommSocket(int port) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_RFCOMM, -1, false, false, this, port,
+ null);
+ }
+
+ /**
+ * Construct a SCO socket ready to start an outgoing connection.
+ * Call #connect on the returned #BluetoothSocket to begin the connection.
+ *
+ * @return a SCO BluetoothSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public BluetoothSocket createScoSocket() throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "Bluetooth is not enabled");
+ throw new IOException();
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_SCO, -1, true, true, this, -1, null);
+ }
+
+ /**
+ * Check that a pin is valid and convert to byte array.
+ *
+ * Bluetooth pin's are 1 to 16 bytes of UTF-8 characters.
+ *
+ * @param pin pin as java String
+ * @return the pin code as a UTF-8 byte array, or null if it is an invalid Bluetooth pin.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static byte[] convertPinToBytes(String pin) {
+ if (pin == null) {
+ return null;
+ }
+ byte[] pinBytes;
+ try {
+ pinBytes = pin.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException uee) {
+ Log.e(TAG, "UTF-8 not supported?!?"); // this should not happen
+ return null;
+ }
+ if (pinBytes.length <= 0 || pinBytes.length > 16) {
+ return null;
+ }
+ return pinBytes;
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @throws IllegalArgumentException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback) {
+ return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @throws IllegalArgumentException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport) {
+ return (connectGatt(context, autoConnect, callback, transport, PHY_LE_1M_MASK));
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @throws NullPointerException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport, int phy) {
+ return connectGatt(context, autoConnect, callback, transport, phy, null);
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, an d{@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @param handler The handler to use for the callback. If {@code null}, callbacks will happen on
+ * an un-specified background thread.
+ * @throws NullPointerException if callback is null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport, int phy,
+ Handler handler) {
+ return connectGatt(context, autoConnect, callback, transport, false, phy, handler);
+ }
+
+ /**
+ * Connect to GATT Server hosted by this device. Caller acts as GATT client.
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as any further GATT client operations.
+ * The method returns a BluetoothGatt instance. You can use BluetoothGatt to conduct
+ * GATT client operations.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param opportunistic Whether this GATT client is opportunistic. An opportunistic GATT client
+ * does not hold a GATT connection. It automatically disconnects when no other GATT connections
+ * are active for the remote device.
+ * @param phy preferred PHY for connections to remote LE device. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, an d{@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}. This option does not take effect if {@code autoConnect}
+ * is set to true.
+ * @param handler The handler to use for the callback. If {@code null}, callbacks will happen on
+ * an un-specified background thread.
+ * @return A BluetoothGatt instance. You can use BluetoothGatt to conduct GATT client
+ * operations.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGatt connectGatt(Context context, boolean autoConnect,
+ BluetoothGattCallback callback, int transport,
+ boolean opportunistic, int phy, Handler handler) {
+ if (callback == null) {
+ throw new NullPointerException("callback is null");
+ }
+
+ // TODO(Bluetooth) check whether platform support BLE
+ // Do the check here or in GattServer?
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ IBluetoothManager managerService = adapter.getBluetoothManager();
+ try {
+ IBluetoothGatt iGatt = managerService.getBluetoothGatt();
+ if (iGatt == null) {
+ // BLE is not supported
+ return null;
+ }
+ BluetoothGatt gatt = new BluetoothGatt(
+ iGatt, this, transport, opportunistic, phy, mAttributionSource);
+ gatt.connect(autoConnect, callback, handler);
+ return gatt;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ return null;
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value. The supported Bluetooth transport is LE only.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingL2capChannel()} for
+ * peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p>The remote device will be authenticated and communication on this socket will be
+ * encrypted.
+ * <p> Use this socket if an authenticated socket link is possible. Authentication refers
+ * to the authentication of the link key to prevent person-in-the-middle type of attacks.
+ *
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public @NonNull BluetoothSocket createL2capChannel(int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createL2capChannel: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (DBG) Log.d(TAG, "createL2capChannel: psm=" + psm);
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, true, true, this, psm,
+ null);
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value. The supported Bluetooth transport is LE only.
+ * <p>This is designed to be used with {@link
+ * BluetoothAdapter#listenUsingInsecureL2capChannel()} for peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p> The communication channel may not have an authenticated link key, i.e. it may be subject
+ * to person-in-the-middle attacks. Use {@link #createL2capChannel(int)} if an encrypted and
+ * authenticated communication channel is possible.
+ *
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public @NonNull BluetoothSocket createInsecureL2capChannel(int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createInsecureL2capChannel: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (DBG) {
+ Log.d(TAG, "createInsecureL2capChannel: psm=" + psm);
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, false, false, this, psm,
+ null);
+ }
+
+ /**
+ * Set a keyed metadata of this {@link BluetoothDevice} to a
+ * {@link String} value.
+ * Only bonded devices's metadata will be persisted across Bluetooth
+ * restart.
+ * Metadata will be removed when the device's bond state is moved to
+ * {@link #BOND_NONE}.
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @param value a byte array data to set for key. Must be less than
+ * {@link BluetoothAdapter#METADATA_MAX_LENGTH} characters in length
+ * @return true on success, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setMetadata(@MetadataKey int key, @NonNull byte[] value) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot set metadata");
+ return false;
+ }
+ if (value.length > METADATA_MAX_LENGTH) {
+ throw new IllegalArgumentException("value length is " + value.length
+ + ", should not over " + METADATA_MAX_LENGTH);
+ }
+ try {
+ return service.setMetadata(this, key, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "setMetadata fail", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get a keyed metadata for this {@link BluetoothDevice} as {@link String}
+ *
+ * @param key must be within the list of BluetoothDevice.METADATA_*
+ * @return Metadata of the key as byte array, null on error or not found
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public byte[] getMetadata(@MetadataKey int key) {
+ final IBluetooth service = sService;
+ if (service == null) {
+ Log.e(TAG, "Bluetooth is not enabled. Cannot get metadata");
+ return null;
+ }
+ try {
+ return service.getMetadata(this, key, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "getMetadata fail", e);
+ return null;
+ }
+ }
+
+ /**
+ * Get the maxinum metadata key ID.
+ *
+ * @return the last supported metadata key
+ * @hide
+ */
+ public static @MetadataKey int getMaxMetadataKey() {
+ return METADATA_UNTETHERED_CASE_LOW_BATTERY_THRESHOLD;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothDevicePicker.java b/framework/java/android/bluetooth/BluetoothDevicePicker.java
new file mode 100644
index 0000000000..26e46573dd
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothDevicePicker.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+
+/**
+ * A helper to show a system "Device Picker" activity to the user.
+ *
+ * @hide
+ */
+public interface BluetoothDevicePicker {
+ public static final String EXTRA_NEED_AUTH =
+ "android.bluetooth.devicepicker.extra.NEED_AUTH";
+ public static final String EXTRA_FILTER_TYPE =
+ "android.bluetooth.devicepicker.extra.FILTER_TYPE";
+ public static final String EXTRA_LAUNCH_PACKAGE =
+ "android.bluetooth.devicepicker.extra.LAUNCH_PACKAGE";
+ public static final String EXTRA_LAUNCH_CLASS =
+ "android.bluetooth.devicepicker.extra.DEVICE_PICKER_LAUNCH_CLASS";
+
+ /**
+ * Broadcast when one BT device is selected from BT device picker screen.
+ * Selected {@link BluetoothDevice} is returned in extra data named
+ * {@link BluetoothDevice#EXTRA_DEVICE}.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DEVICE_SELECTED =
+ "android.bluetooth.devicepicker.action.DEVICE_SELECTED";
+
+ /**
+ * Broadcast when someone want to select one BT device from devices list.
+ * This intent contains below extra data:
+ * - {@link #EXTRA_NEED_AUTH} (boolean): if need authentication
+ * - {@link #EXTRA_FILTER_TYPE} (int): what kinds of device should be
+ * listed
+ * - {@link #EXTRA_LAUNCH_PACKAGE} (string): where(which package) this
+ * intent come from
+ * - {@link #EXTRA_LAUNCH_CLASS} (string): where(which class) this intent
+ * come from
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LAUNCH =
+ "android.bluetooth.devicepicker.action.LAUNCH";
+
+ /** Ask device picker to show all kinds of BT devices */
+ public static final int FILTER_TYPE_ALL = 0;
+ /** Ask device picker to show BT devices that support AUDIO profiles */
+ public static final int FILTER_TYPE_AUDIO = 1;
+ /** Ask device picker to show BT devices that support Object Transfer */
+ public static final int FILTER_TYPE_TRANSFER = 2;
+ /**
+ * Ask device picker to show BT devices that support
+ * Personal Area Networking User (PANU) profile
+ */
+ public static final int FILTER_TYPE_PANU = 3;
+ /** Ask device picker to show BT devices that support Network Access Point (NAP) profile */
+ public static final int FILTER_TYPE_NAP = 4;
+}
diff --git a/framework/java/android/bluetooth/BluetoothGatt.java b/framework/java/android/bluetooth/BluetoothGatt.java
new file mode 100644
index 0000000000..b531829d29
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGatt.java
@@ -0,0 +1,1848 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.os.Build;
+import android.os.Handler;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Public API for the Bluetooth GATT Profile.
+ *
+ * <p>This class provides Bluetooth GATT functionality to enable communication
+ * with Bluetooth Smart or Smart Ready devices.
+ *
+ * <p>To connect to a remote peripheral device, create a {@link BluetoothGattCallback}
+ * and call {@link BluetoothDevice#connectGatt} to get a instance of this class.
+ * GATT capable devices can be discovered using the Bluetooth device discovery or BLE
+ * scan process.
+ */
+public final class BluetoothGatt implements BluetoothProfile {
+ private static final String TAG = "BluetoothGatt";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ @UnsupportedAppUsage
+ private IBluetoothGatt mService;
+ @UnsupportedAppUsage
+ private volatile BluetoothGattCallback mCallback;
+ private Handler mHandler;
+ @UnsupportedAppUsage
+ private int mClientIf;
+ private BluetoothDevice mDevice;
+ @UnsupportedAppUsage
+ private boolean mAutoConnect;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ private int mAuthRetryState;
+ private int mConnState;
+ private final Object mStateLock = new Object();
+ private final Object mDeviceBusyLock = new Object();
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private Boolean mDeviceBusy = false;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ private int mTransport;
+ private int mPhy;
+ private boolean mOpportunistic;
+ private final AttributionSource mAttributionSource;
+
+ private static final int AUTH_RETRY_STATE_IDLE = 0;
+ private static final int AUTH_RETRY_STATE_NO_MITM = 1;
+ private static final int AUTH_RETRY_STATE_MITM = 2;
+
+ private static final int CONN_STATE_IDLE = 0;
+ private static final int CONN_STATE_CONNECTING = 1;
+ private static final int CONN_STATE_CONNECTED = 2;
+ private static final int CONN_STATE_DISCONNECTING = 3;
+ private static final int CONN_STATE_CLOSED = 4;
+
+ private static final int WRITE_CHARACTERISTIC_MAX_RETRIES = 5;
+ private static final int WRITE_CHARACTERISTIC_TIME_TO_WAIT = 10; // milliseconds
+
+ private List<BluetoothGattService> mServices;
+
+ /** A GATT operation completed successfully */
+ public static final int GATT_SUCCESS = 0;
+
+ /** GATT read operation is not permitted */
+ public static final int GATT_READ_NOT_PERMITTED = 0x2;
+
+ /** GATT write operation is not permitted */
+ public static final int GATT_WRITE_NOT_PERMITTED = 0x3;
+
+ /** Insufficient authentication for a given operation */
+ public static final int GATT_INSUFFICIENT_AUTHENTICATION = 0x5;
+
+ /** The given request is not supported */
+ public static final int GATT_REQUEST_NOT_SUPPORTED = 0x6;
+
+ /** Insufficient encryption for a given operation */
+ public static final int GATT_INSUFFICIENT_ENCRYPTION = 0xf;
+
+ /** A read or write operation was requested with an invalid offset */
+ public static final int GATT_INVALID_OFFSET = 0x7;
+
+ /** Insufficient authorization for a given operation */
+ public static final int GATT_INSUFFICIENT_AUTHORIZATION = 0x8;
+
+ /** A write operation exceeds the maximum length of the attribute */
+ public static final int GATT_INVALID_ATTRIBUTE_LENGTH = 0xd;
+
+ /** A remote device connection is congested. */
+ public static final int GATT_CONNECTION_CONGESTED = 0x8f;
+
+ /** A GATT operation failed, errors other than the above */
+ public static final int GATT_FAILURE = 0x101;
+
+ /**
+ * Connection parameter update - Use the connection parameters recommended by the
+ * Bluetooth SIG. This is the default value if no connection parameter update
+ * is requested.
+ */
+ public static final int CONNECTION_PRIORITY_BALANCED = 0;
+
+ /**
+ * Connection parameter update - Request a high priority, low latency connection.
+ * An application should only request high priority connection parameters to transfer large
+ * amounts of data over LE quickly. Once the transfer is complete, the application should
+ * request {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED} connection parameters to reduce
+ * energy use.
+ */
+ public static final int CONNECTION_PRIORITY_HIGH = 1;
+
+ /** Connection parameter update - Request low power, reduced data rate connection parameters. */
+ public static final int CONNECTION_PRIORITY_LOW_POWER = 2;
+
+ /**
+ * No authentication required.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_NONE = 0;
+
+ /**
+ * Authentication requested; no person-in-the-middle protection required.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_NO_MITM = 1;
+
+ /**
+ * Authentication with person-in-the-middle protection requested.
+ *
+ * @hide
+ */
+ /*package*/ static final int AUTHENTICATION_MITM = 2;
+
+ /**
+ * Bluetooth GATT callbacks. Overrides the default BluetoothGattCallback implementation.
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothGattCallback mBluetoothGattCallback =
+ new IBluetoothGattCallback.Stub() {
+ /**
+ * Application interface registered - app is ready to go
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onClientRegistered(int status, int clientIf) {
+ if (DBG) {
+ Log.d(TAG, "onClientRegistered() - status=" + status
+ + " clientIf=" + clientIf);
+ }
+ if (VDBG) {
+ synchronized (mStateLock) {
+ if (mConnState != CONN_STATE_CONNECTING) {
+ Log.e(TAG, "Bad connection state: " + mConnState);
+ }
+ }
+ }
+ mClientIf = clientIf;
+ if (status != GATT_SUCCESS) {
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionStateChange(BluetoothGatt.this,
+ GATT_FAILURE,
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+ }
+ });
+
+ synchronized (mStateLock) {
+ mConnState = CONN_STATE_IDLE;
+ }
+ return;
+ }
+ try {
+ mService.clientConnect(mClientIf, mDevice.getAddress(),
+ !mAutoConnect, mTransport, mOpportunistic,
+ mPhy, mAttributionSource); // autoConnect is inverse of "isDirect"
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Phy update callback
+ * @hide
+ */
+ @Override
+ public void onPhyUpdate(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG, "onPhyUpdate() - status=" + status
+ + " address=" + address + " txPhy=" + txPhy + " rxPhy=" + rxPhy);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onPhyUpdate(BluetoothGatt.this, txPhy, rxPhy, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Phy read callback
+ * @hide
+ */
+ @Override
+ public void onPhyRead(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG, "onPhyRead() - status=" + status
+ + " address=" + address + " txPhy=" + txPhy + " rxPhy=" + rxPhy);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onPhyRead(BluetoothGatt.this, txPhy, rxPhy, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Client connection state changed
+ * @hide
+ */
+ @Override
+ public void onClientConnectionState(int status, int clientIf,
+ boolean connected, String address) {
+ if (DBG) {
+ Log.d(TAG, "onClientConnectionState() - status=" + status
+ + " clientIf=" + clientIf + " device=" + address);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+ int profileState = connected ? BluetoothProfile.STATE_CONNECTED :
+ BluetoothProfile.STATE_DISCONNECTED;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionStateChange(BluetoothGatt.this, status,
+ profileState);
+ }
+ }
+ });
+
+ synchronized (mStateLock) {
+ if (connected) {
+ mConnState = CONN_STATE_CONNECTED;
+ } else {
+ mConnState = CONN_STATE_IDLE;
+ }
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ }
+
+ /**
+ * Remote search has been completed.
+ * The internal object structure should now reflect the state
+ * of the remote device database. Let the application know that
+ * we are done at this point.
+ * @hide
+ */
+ @Override
+ public void onSearchComplete(String address, List<BluetoothGattService> services,
+ int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onSearchComplete() = Device=" + address + " Status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ for (BluetoothGattService s : services) {
+ //services we receive don't have device set properly.
+ s.setDevice(mDevice);
+ }
+
+ mServices.addAll(services);
+
+ // Fix references to included services, as they doesn't point to right objects.
+ for (BluetoothGattService fixedService : mServices) {
+ ArrayList<BluetoothGattService> includedServices =
+ new ArrayList(fixedService.getIncludedServices());
+ fixedService.getIncludedServices().clear();
+
+ for (BluetoothGattService brokenRef : includedServices) {
+ BluetoothGattService includedService = getService(mDevice,
+ brokenRef.getUuid(), brokenRef.getInstanceId());
+ if (includedService != null) {
+ fixedService.addIncludedService(includedService);
+ } else {
+ Log.e(TAG, "Broken GATT database: can't find included service.");
+ }
+ }
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onServicesDiscovered(BluetoothGatt.this, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote characteristic has been read.
+ * Updates the internal value.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCharacteristicRead(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG, "onCharacteristicRead() - Device=" + address
+ + " handle=" + handle + " Status=" + status);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ mService.readCharacteristic(
+ mClientIf, address, handle, authReq, mAttributionSource);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicRead() failed to find characteristic!");
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ if (status == 0) characteristic.setValue(value);
+ callback.onCharacteristicRead(BluetoothGatt.this, characteristic,
+ value, status);
+ // Keep calling deprecated callback to maintain app compatibility
+ callback.onCharacteristicRead(BluetoothGatt.this, characteristic,
+ status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Characteristic has been written to the remote device.
+ * Let the app know how we did...
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onCharacteristicWrite(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG, "onCharacteristicWrite() - Device=" + address
+ + " handle=" + handle + " Status=" + status);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) return;
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN;
+ for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
+ requestStatus = mService.writeCharacteristic(mClientIf, address,
+ handle, characteristic.getWriteType(), authReq,
+ value, mAttributionSource);
+ if (requestStatus
+ != BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY) {
+ break;
+ }
+ try {
+ Thread.sleep(WRITE_CHARACTERISTIC_TIME_TO_WAIT);
+ } catch (InterruptedException e) {
+ }
+ }
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onCharacteristicWrite(BluetoothGatt.this, characteristic,
+ status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote characteristic has been updated.
+ * Updates the internal value.
+ * @hide
+ */
+ @Override
+ public void onNotify(String address, int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onNotify() - Device=" + address + " handle=" + handle);
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice,
+ handle);
+ if (characteristic == null) return;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ characteristic.setValue(value);
+ callback.onCharacteristicChanged(BluetoothGatt.this,
+ characteristic, value);
+ // Keep calling deprecated callback to maintain app compatibility
+ callback.onCharacteristicChanged(BluetoothGatt.this,
+ characteristic);
+ }
+ }
+ });
+ }
+
+ /**
+ * Descriptor has been read.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onDescriptorRead(String address, int status, int handle, byte[] value) {
+ if (VDBG) {
+ Log.d(TAG,
+ "onDescriptorRead() - Device=" + address + " handle=" + handle);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
+ if (descriptor == null) return;
+
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ mService.readDescriptor(
+ mClientIf, address, handle, authReq, mAttributionSource);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ if (status == 0) descriptor.setValue(value);
+ callback.onDescriptorRead(BluetoothGatt.this, descriptor, status,
+ value);
+ // Keep calling deprecated callback to maintain app compatibility
+ callback.onDescriptorRead(BluetoothGatt.this, descriptor, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Descriptor write operation complete.
+ * @hide
+ */
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void onDescriptorWrite(String address, int status, int handle,
+ byte[] value) {
+ if (VDBG) {
+ Log.d(TAG,
+ "onDescriptorWrite() - Device=" + address + " handle=" + handle);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
+ if (descriptor == null) return;
+
+ if ((status == GATT_INSUFFICIENT_AUTHENTICATION
+ || status == GATT_INSUFFICIENT_ENCRYPTION)
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ try {
+ final int authReq = (mAuthRetryState == AUTH_RETRY_STATE_IDLE)
+ ? AUTHENTICATION_NO_MITM : AUTHENTICATION_MITM;
+ mService.writeDescriptor(mClientIf, address, handle,
+ authReq, value, mAttributionSource);
+ mAuthRetryState++;
+ return;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onDescriptorWrite(BluetoothGatt.this, descriptor, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Prepared write transaction completed (or aborted)
+ * @hide
+ */
+ @Override
+ public void onExecuteWrite(String address, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onExecuteWrite() - Device=" + address
+ + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onReliableWriteCompleted(BluetoothGatt.this, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Remote device RSSI has been read
+ * @hide
+ */
+ @Override
+ public void onReadRemoteRssi(String address, int rssi, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onReadRemoteRssi() - Device=" + address
+ + " rssi=" + rssi + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onReadRemoteRssi(BluetoothGatt.this, rssi, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when the MTU for a given connection changes
+ * @hide
+ */
+ @Override
+ public void onConfigureMTU(String address, int mtu, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConfigureMTU() - Device=" + address
+ + " mtu=" + mtu + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onMtuChanged(BluetoothGatt.this, mtu, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when the given connection is updated
+ * @hide
+ */
+ @Override
+ public void onConnectionUpdated(String address, int interval, int latency,
+ int timeout, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConnectionUpdated() - Device=" + address
+ + " interval=" + interval + " latency=" + latency
+ + " timeout=" + timeout + " status=" + status);
+ }
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onConnectionUpdated(BluetoothGatt.this, interval, latency,
+ timeout, status);
+ }
+ }
+ });
+ }
+
+ /**
+ * Callback invoked when service changed event is received
+ * @hide
+ */
+ @Override
+ public void onServiceChanged(String address) {
+ if (DBG) {
+ Log.d(TAG, "onServiceChanged() - Device=" + address);
+ }
+
+ if (!address.equals(mDevice.getAddress())) {
+ return;
+ }
+
+ runOrQueueCallback(new Runnable() {
+ @Override
+ public void run() {
+ final BluetoothGattCallback callback = mCallback;
+ if (callback != null) {
+ callback.onServiceChanged(BluetoothGatt.this);
+ }
+ }
+ });
+ }
+ };
+
+ /* package */ BluetoothGatt(IBluetoothGatt iGatt, BluetoothDevice device, int transport,
+ boolean opportunistic, int phy, AttributionSource attributionSource) {
+ mService = iGatt;
+ mDevice = device;
+ mTransport = transport;
+ mPhy = phy;
+ mOpportunistic = opportunistic;
+ mAttributionSource = attributionSource;
+ mServices = new ArrayList<BluetoothGattService>();
+
+ mConnState = CONN_STATE_IDLE;
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ }
+
+ /**
+ * Close this Bluetooth GATT client.
+ *
+ * Application should call this method as early as possible after it is done with
+ * this GATT client.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void close() {
+ if (DBG) Log.d(TAG, "close()");
+
+ unregisterApp();
+ mConnState = CONN_STATE_CLOSED;
+ mAuthRetryState = AUTH_RETRY_STATE_IDLE;
+ }
+
+ /**
+ * Returns a service by UUID, instance and type.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService getService(BluetoothDevice device, UUID uuid,
+ int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ if (svc.getDevice().equals(device)
+ && svc.getInstanceId() == instanceId
+ && svc.getUuid().equals(uuid)) {
+ return svc;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns a characteristic with id equal to instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristicById(BluetoothDevice device,
+ int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ if (charac.getInstanceId() == instanceId) {
+ return charac;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a descriptor with id equal to instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptorById(BluetoothDevice device, int instanceId) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ for (BluetoothGattDescriptor desc : charac.getDescriptors()) {
+ if (desc.getInstanceId() == instanceId) {
+ return desc;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Queue the runnable on a {@link Handler} provided by the user, or execute the runnable
+ * immediately if no Handler was provided.
+ */
+ private void runOrQueueCallback(final Runnable cb) {
+ if (mHandler == null) {
+ try {
+ cb.run();
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ } else {
+ mHandler.post(cb);
+ }
+ }
+
+ /**
+ * Register an application callback to start using GATT.
+ *
+ * <p>This is an asynchronous call. The callback {@link BluetoothGattCallback#onAppRegistered}
+ * is used to notify success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @return If true, the callback will be called to notify success or failure, false on immediate
+ * error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean registerApp(BluetoothGattCallback callback, Handler handler) {
+ return registerApp(callback, handler, false);
+ }
+
+ /**
+ * Register an application callback to start using GATT.
+ *
+ * <p>This is an asynchronous call. The callback {@link BluetoothGattCallback#onAppRegistered}
+ * is used to notify success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param eatt_support indicate to allow for eatt support
+ * @return If true, the callback will be called to notify success or failure, false on immediate
+ * error
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean registerApp(BluetoothGattCallback callback, Handler handler,
+ boolean eatt_support) {
+ if (DBG) Log.d(TAG, "registerApp()");
+ if (mService == null) return false;
+
+ mCallback = callback;
+ mHandler = handler;
+ UUID uuid = UUID.randomUUID();
+ if (DBG) Log.d(TAG, "registerApp() - UUID=" + uuid);
+
+ try {
+ mService.registerClient(
+ new ParcelUuid(uuid), mBluetoothGattCallback, eatt_support, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Unregister the current application and callbacks.
+ */
+ @UnsupportedAppUsage
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void unregisterApp() {
+ if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf);
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ mCallback = null;
+ mService.unregisterClient(mClientIf, mAttributionSource);
+ mClientIf = 0;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Initiate a connection to a Bluetooth GATT capable device.
+ *
+ * <p>The connection may not be established right away, but will be
+ * completed when the remote device is available. A
+ * {@link BluetoothGattCallback#onConnectionStateChange} callback will be
+ * invoked when the connection state changes as a result of this function.
+ *
+ * <p>The autoConnect parameter determines whether to actively connect to
+ * the remote device, or rather passively scan and finalize the connection
+ * when the remote device is in range/available. Generally, the first ever
+ * connection to a device should be direct (autoConnect set to false) and
+ * subsequent connections to known devices should be invoked with the
+ * autoConnect parameter set to true.
+ *
+ * @param device Remote device to connect to
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean connect(Boolean autoConnect, BluetoothGattCallback callback,
+ Handler handler) {
+ if (DBG) {
+ Log.d(TAG,
+ "connect() - device: " + mDevice.getAddress() + ", auto: " + autoConnect);
+ }
+ synchronized (mStateLock) {
+ if (mConnState != CONN_STATE_IDLE) {
+ throw new IllegalStateException("Not idle");
+ }
+ mConnState = CONN_STATE_CONNECTING;
+ }
+
+ mAutoConnect = autoConnect;
+
+ if (!registerApp(callback, handler)) {
+ synchronized (mStateLock) {
+ mConnState = CONN_STATE_IDLE;
+ }
+ Log.e(TAG, "Failed to register callback");
+ return false;
+ }
+
+ // The connection will continue in the onClientRegistered callback
+ return true;
+ }
+
+ /**
+ * Disconnects an established connection, or cancels a connection attempt
+ * currently in progress.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void disconnect() {
+ if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ mService.clientDisconnect(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Connect back to remote device.
+ *
+ * <p>This method is used to re-connect to a remote device after the
+ * connection has been dropped. If the device is not in range, the
+ * re-connection will be triggered once the device is back in range.
+ *
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect() {
+ try {
+ // autoConnect is inverse of "isDirect"
+ mService.clientConnect(mClientIf, mDevice.getAddress(), false, mTransport,
+ mOpportunistic, mPhy, mAttributionSource);
+ return true;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ }
+
+ /**
+ * Set the preferred connection PHY for this app. Please note that this is just a
+ * recommendation, whether the PHY change will happen depends on other applications preferences,
+ * local and remote controller capabilities. Controller can override these settings.
+ * <p>
+ * {@link BluetoothGattCallback#onPhyUpdate} will be triggered as a result of this call, even
+ * if no PHY change happens. It is also triggered when remote device updates the PHY.
+ *
+ * @param txPhy preferred transmitter PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param rxPhy preferred receiver PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
+ * of {@link BluetoothDevice#PHY_OPTION_NO_PREFERRED}, {@link BluetoothDevice#PHY_OPTION_S2} or
+ * {@link BluetoothDevice#PHY_OPTION_S8}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) {
+ try {
+ mService.clientSetPreferredPhy(mClientIf, mDevice.getAddress(), txPhy, rxPhy,
+ phyOptions, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Read the current transmitter PHY and receiver PHY of the connection. The values are returned
+ * in {@link BluetoothGattCallback#onPhyRead}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void readPhy() {
+ try {
+ mService.clientReadPhy(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Return the remote bluetooth device this GATT client targets to
+ *
+ * @return remote bluetooth device
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Discovers services offered by a remote device as well as their
+ * characteristics and descriptors.
+ *
+ * <p>This is an asynchronous operation. Once service discovery is completed,
+ * the {@link BluetoothGattCallback#onServicesDiscovered} callback is
+ * triggered. If the discovery was successful, the remote services can be
+ * retrieved using the {@link #getServices} function.
+ *
+ * @return true, if the remote service discovery has been started
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean discoverServices() {
+ if (DBG) Log.d(TAG, "discoverServices() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ mServices.clear();
+
+ try {
+ mService.discoverServices(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Discovers a service by UUID. This is exposed only for passing PTS tests.
+ * It should never be used by real applications. The service is not searched
+ * for characteristics and descriptors, or returned in any callback.
+ *
+ * @return true, if the remote service discovery has been started
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean discoverServiceByUuid(UUID uuid) {
+ if (DBG) Log.d(TAG, "discoverServiceByUuid() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ mServices.clear();
+
+ try {
+ mService.discoverServiceByUuid(
+ mClientIf, mDevice.getAddress(), new ParcelUuid(uuid), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns a list of GATT services offered by the remote device.
+ *
+ * <p>This function requires that service discovery has been completed
+ * for the given device.
+ *
+ * @return List of services on the remote device. Returns an empty list if service discovery has
+ * not yet been performed.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public List<BluetoothGattService> getServices() {
+ List<BluetoothGattService> result =
+ new ArrayList<BluetoothGattService>();
+
+ for (BluetoothGattService service : mServices) {
+ if (service.getDevice().equals(mDevice)) {
+ result.add(service);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a {@link BluetoothGattService}, if the requested UUID is
+ * supported by the remote device.
+ *
+ * <p>This function requires that service discovery has been completed
+ * for the given device.
+ *
+ * <p>If multiple instances of the same service (as identified by UUID)
+ * exist, the first instance of the service is returned.
+ *
+ * @param uuid UUID of the requested service
+ * @return BluetoothGattService if supported, or null if the requested service is not offered by
+ * the remote device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public BluetoothGattService getService(UUID uuid) {
+ for (BluetoothGattService service : mServices) {
+ if (service.getDevice().equals(mDevice) && service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads the requested characteristic from the associated remote device.
+ *
+ * <p>This is an asynchronous operation. The result of the read operation
+ * is reported by the {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} callback.
+ *
+ * @param characteristic Characteristic to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) {
+ return false;
+ }
+
+ if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid());
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ mService.readCharacteristic(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), AUTHENTICATION_NONE, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Reads the characteristic using its UUID from the associated remote device.
+ *
+ * <p>This is an asynchronous operation. The result of the read operation
+ * is reported by the {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} callback.
+ *
+ * @param uuid UUID of characteristic to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) {
+ if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid);
+ if (mService == null || mClientIf == 0) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ mService.readUsingCharacteristicUuid(mClientIf, mDevice.getAddress(),
+ new ParcelUuid(uuid), startHandle, endHandle, AUTHENTICATION_NONE,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Writes a given characteristic and its values to the associated remote device.
+ *
+ * <p>Once the write operation has been completed, the
+ * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
+ * reporting the result of the operation.
+ *
+ * @param characteristic Characteristic to write on the remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if characteristic or its value are null
+ *
+ * @deprecated Use {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[],
+ * int)} as this is not memory safe.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic) {
+ try {
+ return writeCharacteristic(characteristic, characteristic.getValue(),
+ characteristic.getWriteType()) == BluetoothStatusCodes.SUCCESS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface WriteOperationReturnValues{}
+
+ /**
+ * Writes a given characteristic and its values to the associated remote device.
+ *
+ * <p>Once the write operation has been completed, the
+ * {@link BluetoothGattCallback#onCharacteristicWrite} callback is invoked,
+ * reporting the result of the operation.
+ *
+ * @param characteristic Characteristic to write on the remote device
+ * @return whether the characteristic was successfully written to
+ * @throws IllegalArgumentException if characteristic or value are null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @WriteOperationReturnValues
+ public int writeCharacteristic(@NonNull BluetoothGattCharacteristic characteristic,
+ @NonNull byte[] value, int writeType) {
+ if (characteristic == null) {
+ throw new IllegalArgumentException("characteristic must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null");
+ }
+ if (VDBG) Log.d(TAG, "writeCharacteristic() - uuid: " + characteristic.getUuid());
+ if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) == 0
+ && (characteristic.getProperties()
+ & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) == 0) {
+ return BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED;
+ }
+ if (mService == null || mClientIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) {
+ throw new IllegalArgumentException("Service must have a non-null device");
+ }
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) {
+ return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
+ }
+ mDeviceBusy = true;
+ }
+
+ int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN;
+ try {
+ for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
+ requestStatus = mService.writeCharacteristic(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), writeType, AUTHENTICATION_NONE, value,
+ mAttributionSource);
+ if (requestStatus != BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY) {
+ break;
+ }
+ try {
+ Thread.sleep(WRITE_CHARACTERISTIC_TIME_TO_WAIT);
+ } catch (InterruptedException e) {
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ throw e.rethrowFromSystemServer();
+ }
+
+ return requestStatus;
+ }
+
+ /**
+ * Reads the value for a given descriptor from the associated remote device.
+ *
+ * <p>Once the read operation has been completed, the
+ * {@link BluetoothGattCallback#onDescriptorRead} callback is
+ * triggered, signaling the result of the operation.
+ *
+ * @param descriptor Descriptor value to read from the remote device
+ * @return true, if the read operation was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
+ if (VDBG) Log.d(TAG, "readDescriptor() - uuid: " + descriptor.getUuid());
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ mService.readDescriptor(mClientIf, device.getAddress(),
+ descriptor.getInstanceId(), AUTHENTICATION_NONE, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Write the value of a given descriptor to the associated remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onDescriptorWrite} callback is triggered to report the
+ * result of the write operation.
+ *
+ * @param descriptor Descriptor to write to the associated remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if descriptor or its value are null
+ *
+ * @deprecated Use {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor, byte[])} as
+ * this is not memory safe.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean writeDescriptor(BluetoothGattDescriptor descriptor) {
+ try {
+ return writeDescriptor(descriptor, descriptor.getValue())
+ == BluetoothStatusCodes.SUCCESS;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Write the value of a given descriptor to the associated remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onDescriptorWrite} callback is triggered to report the
+ * result of the write operation.
+ *
+ * @param descriptor Descriptor to write to the associated remote device
+ * @return true, if the write operation was initiated successfully
+ * @throws IllegalArgumentException if descriptor or value are null
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @WriteOperationReturnValues
+ public int writeDescriptor(@NonNull BluetoothGattDescriptor descriptor,
+ @NonNull byte[] value) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("value must not be null");
+ }
+ if (VDBG) Log.d(TAG, "writeDescriptor() - uuid: " + descriptor.getUuid());
+ if (mService == null || mClientIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
+ if (characteristic == null) {
+ throw new IllegalArgumentException("Descriptor must have a non-null characteristic");
+ }
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) {
+ throw new IllegalArgumentException("Service must have a non-null device");
+ }
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
+ mDeviceBusy = true;
+ }
+
+ try {
+ return mService.writeDescriptor(mClientIf, device.getAddress(),
+ descriptor.getInstanceId(), AUTHENTICATION_NONE, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ e.rethrowFromSystemServer();
+ }
+ return BluetoothStatusCodes.ERROR_UNKNOWN;
+ }
+
+ /**
+ * Initiates a reliable write transaction for a given remote device.
+ *
+ * <p>Once a reliable write transaction has been initiated, all calls
+ * to {@link #writeCharacteristic} are sent to the remote device for
+ * verification and queued up for atomic execution. The application will
+ * receive a {@link BluetoothGattCallback#onCharacteristicWrite} callback in response to every
+ * {@link #writeCharacteristic(BluetoothGattCharacteristic, byte[], int)} call and is
+ * responsible for verifying if the value has been transmitted accurately.
+ *
+ * <p>After all characteristics have been queued up and verified,
+ * {@link #executeReliableWrite} will execute all writes. If a characteristic
+ * was not written correctly, calling {@link #abortReliableWrite} will
+ * cancel the current transaction without committing any values on the
+ * remote device.
+ *
+ * @return true, if the reliable write transaction has been initiated
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean beginReliableWrite() {
+ if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.beginReliableWrite(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Executes a reliable write transaction for a given remote device.
+ *
+ * <p>This function will commit all queued up characteristic write
+ * operations for a given remote device.
+ *
+ * <p>A {@link BluetoothGattCallback#onReliableWriteCompleted} callback is
+ * invoked to indicate whether the transaction has been executed correctly.
+ *
+ * @return true, if the request to execute the transaction has been sent
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean executeReliableWrite() {
+ if (VDBG) Log.d(TAG, "executeReliableWrite() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ synchronized (mDeviceBusyLock) {
+ if (mDeviceBusy) return false;
+ mDeviceBusy = true;
+ }
+
+ try {
+ mService.endReliableWrite(mClientIf, mDevice.getAddress(), true, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mDeviceBusyLock) {
+ mDeviceBusy = false;
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Cancels a reliable write transaction for a given device.
+ *
+ * <p>Calling this function will discard all queued characteristic write
+ * operations for a given remote device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void abortReliableWrite() {
+ if (VDBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return;
+
+ try {
+ mService.endReliableWrite(mClientIf, mDevice.getAddress(), false, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #abortReliableWrite()}
+ */
+ @Deprecated
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void abortReliableWrite(BluetoothDevice mDevice) {
+ abortReliableWrite();
+ }
+
+ /**
+ * Enable or disable notifications/indications for a given characteristic.
+ *
+ * <p>Once notifications are enabled for a characteristic, a
+ * {@link BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[])} callback will be triggered if the remote device
+ * indicates that the given characteristic has changed.
+ *
+ * @param characteristic The characteristic for which to enable notifications
+ * @param enable Set to true to enable notifications/indications
+ * @return true, if the requested notification status was set successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
+ boolean enable) {
+ if (DBG) {
+ Log.d(TAG, "setCharacteristicNotification() - uuid: " + characteristic.getUuid()
+ + " enable: " + enable);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) return false;
+
+ BluetoothDevice device = service.getDevice();
+ if (device == null) return false;
+
+ try {
+ mService.registerForNotification(mClientIf, device.getAddress(),
+ characteristic.getInstanceId(), enable, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Clears the internal cache and forces a refresh of the services from the
+ * remote device.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean refresh() {
+ if (DBG) Log.d(TAG, "refresh() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.refreshDevice(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Read the RSSI for a connected remote device.
+ *
+ * <p>The {@link BluetoothGattCallback#onReadRemoteRssi} callback will be
+ * invoked when the RSSI value has been read.
+ *
+ * @return true, if the RSSI value has been requested successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean readRemoteRssi() {
+ if (DBG) Log.d(TAG, "readRssi() - device: " + mDevice.getAddress());
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.readRemoteRssi(mClientIf, mDevice.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request an MTU size used for a given connection.
+ *
+ * <p>When performing a write request operation (write without response),
+ * the data sent is truncated to the MTU size. This function may be used
+ * to request a larger MTU size to be able to send more data at once.
+ *
+ * <p>A {@link BluetoothGattCallback#onMtuChanged} callback will indicate
+ * whether this operation was successful.
+ *
+ * @return true, if the new MTU value has been requested successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestMtu(int mtu) {
+ if (DBG) {
+ Log.d(TAG, "configureMTU() - device: " + mDevice.getAddress()
+ + " mtu: " + mtu);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.configureMTU(mClientIf, mDevice.getAddress(), mtu, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request a connection parameter update.
+ *
+ * <p>This function will send a connection parameter update request to the
+ * remote device.
+ *
+ * @param connectionPriority Request a specific connection priority. Must be one of {@link
+ * BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+ * or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
+ * @throws IllegalArgumentException If the parameters are outside of their specified range.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestConnectionPriority(int connectionPriority) {
+ if (connectionPriority < CONNECTION_PRIORITY_BALANCED
+ || connectionPriority > CONNECTION_PRIORITY_LOW_POWER) {
+ throw new IllegalArgumentException("connectionPriority not within valid range");
+ }
+
+ if (DBG) Log.d(TAG, "requestConnectionPriority() - params: " + connectionPriority);
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.connectionParameterUpdate(
+ mClientIf, mDevice.getAddress(), connectionPriority, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Request an LE connection parameter update.
+ *
+ * <p>This function will send an LE connection parameters update request to the remote device.
+ *
+ * @return true, if the request is send to the Bluetooth stack.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean requestLeConnectionUpdate(int minConnectionInterval, int maxConnectionInterval,
+ int slaveLatency, int supervisionTimeout,
+ int minConnectionEventLen, int maxConnectionEventLen) {
+ if (DBG) {
+ Log.d(TAG, "requestLeConnectionUpdate() - min=(" + minConnectionInterval
+ + ")" + (1.25 * minConnectionInterval)
+ + "msec, max=(" + maxConnectionInterval + ")"
+ + (1.25 * maxConnectionInterval) + "msec, latency=" + slaveLatency
+ + ", timeout=" + supervisionTimeout + "msec" + ", min_ce="
+ + minConnectionEventLen + ", max_ce=" + maxConnectionEventLen);
+ }
+ if (mService == null || mClientIf == 0) return false;
+
+ try {
+ mService.leConnectionUpdate(mClientIf, mDevice.getAddress(),
+ minConnectionInterval, maxConnectionInterval,
+ slaveLatency, supervisionTimeout,
+ minConnectionEventLen, maxConnectionEventLen,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @deprecated Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public int getConnectionState(BluetoothDevice device) {
+ throw new UnsupportedOperationException("Use BluetoothManager#getConnectionState instead.");
+ }
+
+ /**
+ * @deprecated Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getConnectedDevices instead.");
+ }
+
+ /**
+ * @deprecated Not supported - please use
+ * {@link BluetoothManager#getDevicesMatchingConnectionStates(int, int[])}
+ * with {@link BluetoothProfile#GATT} as first argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ @Deprecated
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getDevicesMatchingConnectionStates instead.");
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattCallback.java b/framework/java/android/bluetooth/BluetoothGattCallback.java
new file mode 100644
index 0000000000..d0a5a1e729
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattCallback.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2017 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 android.annotation.NonNull;
+
+/**
+ * This abstract class is used to implement {@link BluetoothGatt} callbacks.
+ */
+public abstract class BluetoothGattCallback {
+
+ /**
+ * Callback triggered as result of {@link BluetoothGatt#setPreferredPhy}, or as a result of
+ * remote device changing the PHY.
+ *
+ * @param gatt GATT client
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param status Status of the PHY update operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGatt#readPhy}
+ *
+ * @param gatt GATT client
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}.
+ * @param status Status of the PHY read operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback indicating when GATT client has connected/disconnected to/from a remote
+ * GATT server.
+ *
+ * @param gatt GATT client
+ * @param status Status of the connect or disconnect operation. {@link
+ * BluetoothGatt#GATT_SUCCESS} if the operation succeeds.
+ * @param newState Returns the new connection state. Can be one of {@link
+ * BluetoothProfile#STATE_DISCONNECTED} or {@link BluetoothProfile#STATE_CONNECTED}
+ */
+ public void onConnectionStateChange(BluetoothGatt gatt, int status,
+ int newState) {
+ }
+
+ /**
+ * Callback invoked when the list of remote services, characteristics and descriptors
+ * for the remote device have been updated, ie new services have been discovered.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#discoverServices}
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the remote device has been explored
+ * successfully.
+ */
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ }
+
+ /**
+ * Callback reporting the result of a characteristic read operation.
+ *
+ * @param gatt GATT client invoked
+ * {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}
+ * @param characteristic Characteristic that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully.
+ * @deprecated Use {@link BluetoothGattCallback#onCharacteristicRead(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[], int)} as it is memory safe
+ */
+ @Deprecated
+ public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic,
+ int status) {
+ }
+
+ /**
+ * Callback reporting the result of a characteristic read operation.
+ *
+ * @param gatt GATT client invoked
+ * {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)}
+ * @param characteristic Characteristic that was read from the associated remote device.
+ * @param value the value of the characteristic
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully.
+ */
+ public void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull
+ BluetoothGattCharacteristic characteristic, @NonNull byte[] value, int status) {
+ }
+
+ /**
+ * Callback indicating the result of a characteristic write operation.
+ *
+ * <p>If this callback is invoked while a reliable write transaction is
+ * in progress, the value of the characteristic represents the value
+ * reported by the remote device. An application should compare this
+ * value to the desired value to be written. If the values don't match,
+ * the application must abort the reliable write transaction.
+ *
+ * @param gatt GATT client that invoked
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic,
+ * byte[], int)}
+ * @param characteristic Characteristic that was written to the associated remote device.
+ * @param status The result of the write operation {@link BluetoothGatt#GATT_SUCCESS} if
+ * the
+ * operation succeeds.
+ */
+ public void onCharacteristicWrite(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic, int status) {
+ }
+
+ /**
+ * Callback triggered as a result of a remote characteristic notification.
+ *
+ * @param gatt GATT client the characteristic is associated with
+ * @param characteristic Characteristic that has been updated as a result of a remote
+ * notification event.
+ * @deprecated Use {@link BluetoothGattCallback#onCharacteristicChanged(BluetoothGatt,
+ * BluetoothGattCharacteristic, byte[])} as it is memory safe by providing the characteristic
+ * value at the time of notification.
+ */
+ @Deprecated
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ }
+
+ /**
+ * Callback triggered as a result of a remote characteristic notification. Note that the value
+ * within the characteristic object may have changed since receiving the remote characteristic
+ * notification, so check the parameter value for the value at the time of notification.
+ *
+ * @param gatt GATT client the characteristic is associated with
+ * @param characteristic Characteristic that has been updated as a result of a remote
+ * notification event.
+ * @param value notified characteristic value
+ */
+ public void onCharacteristicChanged(@NonNull BluetoothGatt gatt,
+ @NonNull BluetoothGattCharacteristic characteristic, @NonNull byte[] value) {
+ }
+
+ /**
+ * Callback reporting the result of a descriptor read operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readDescriptor}
+ * @param descriptor Descriptor that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully
+ * @deprecated Use {@link BluetoothGattCallback#onDescriptorRead(BluetoothGatt,
+ * BluetoothGattDescriptor, int, byte[])} as it is memory safe by providing the descriptor
+ * value at the time it was read.
+ */
+ @Deprecated
+ public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ }
+
+ /**
+ * Callback reporting the result of a descriptor read operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readDescriptor}
+ * @param descriptor Descriptor that was read from the associated remote device.
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the read operation was completed
+ * successfully
+ * @param value the descriptor value at the time of the read operation
+ */
+ public void onDescriptorRead(@NonNull BluetoothGatt gatt,
+ @NonNull BluetoothGattDescriptor descriptor, int status, @NonNull byte[] value) {
+ }
+
+ /**
+ * Callback indicating the result of a descriptor write operation.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#writeDescriptor}
+ * @param descriptor Descriptor that was writte to the associated remote device.
+ * @param status The result of the write operation {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,
+ int status) {
+ }
+
+ /**
+ * Callback invoked when a reliable write transaction has been completed.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#executeReliableWrite}
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the reliable write transaction was
+ * executed successfully
+ */
+ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
+ }
+
+ /**
+ * Callback reporting the RSSI for a remote device connection.
+ *
+ * This callback is triggered in response to the
+ * {@link BluetoothGatt#readRemoteRssi} function.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#readRemoteRssi}
+ * @param rssi The RSSI value for the remote device
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the RSSI was read successfully
+ */
+ public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+ }
+
+ /**
+ * Callback indicating the MTU for a given device connection has changed.
+ *
+ * This callback is triggered in response to the
+ * {@link BluetoothGatt#requestMtu} function, or in response to a connection
+ * event.
+ *
+ * @param gatt GATT client invoked {@link BluetoothGatt#requestMtu}
+ * @param mtu The new MTU size
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the MTU has been changed successfully
+ */
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ }
+
+ /**
+ * Callback indicating the connection parameters were updated.
+ *
+ * @param gatt GATT client involved
+ * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from
+ * 6 (7.5ms) to 3200 (4000ms).
+ * @param latency Worker latency for the connection in number of connection events. Valid range
+ * is from 0 to 499
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated
+ * successfully
+ * @hide
+ */
+ public void onConnectionUpdated(BluetoothGatt gatt, int interval, int latency, int timeout,
+ int status) {
+ }
+
+ /**
+ * Callback indicating service changed event is received
+ *
+ * <p>Receiving this event means that the GATT database is out of sync with
+ * the remote device. {@link BluetoothGatt#discoverServices} should be
+ * called to re-discover the services.
+ *
+ * @param gatt GATT client involved
+ */
+ public void onServiceChanged(@NonNull BluetoothGatt gatt) {
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattCharacteristic.java b/framework/java/android/bluetooth/BluetoothGattCharacteristic.java
new file mode 100644
index 0000000000..c5e986e895
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattCharacteristic.java
@@ -0,0 +1,806 @@
+/*
+ * Copyright (C) 2013 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 android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Characteristic
+ *
+ * <p>A GATT characteristic is a basic data element used to construct a GATT service,
+ * {@link BluetoothGattService}. The characteristic contains a value as well as
+ * additional information and optional GATT descriptors, {@link BluetoothGattDescriptor}.
+ */
+public class BluetoothGattCharacteristic implements Parcelable {
+
+ /**
+ * Characteristic proprty: Characteristic is broadcastable.
+ */
+ public static final int PROPERTY_BROADCAST = 0x01;
+
+ /**
+ * Characteristic property: Characteristic is readable.
+ */
+ public static final int PROPERTY_READ = 0x02;
+
+ /**
+ * Characteristic property: Characteristic can be written without response.
+ */
+ public static final int PROPERTY_WRITE_NO_RESPONSE = 0x04;
+
+ /**
+ * Characteristic property: Characteristic can be written.
+ */
+ public static final int PROPERTY_WRITE = 0x08;
+
+ /**
+ * Characteristic property: Characteristic supports notification
+ */
+ public static final int PROPERTY_NOTIFY = 0x10;
+
+ /**
+ * Characteristic property: Characteristic supports indication
+ */
+ public static final int PROPERTY_INDICATE = 0x20;
+
+ /**
+ * Characteristic property: Characteristic supports write with signature
+ */
+ public static final int PROPERTY_SIGNED_WRITE = 0x40;
+
+ /**
+ * Characteristic property: Characteristic has extended properties
+ */
+ public static final int PROPERTY_EXTENDED_PROPS = 0x80;
+
+ /**
+ * Characteristic read permission
+ */
+ public static final int PERMISSION_READ = 0x01;
+
+ /**
+ * Characteristic permission: Allow encrypted read operations
+ */
+ public static final int PERMISSION_READ_ENCRYPTED = 0x02;
+
+ /**
+ * Characteristic permission: Allow reading with person-in-the-middle protection
+ */
+ public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;
+
+ /**
+ * Characteristic write permission
+ */
+ public static final int PERMISSION_WRITE = 0x10;
+
+ /**
+ * Characteristic permission: Allow encrypted writes
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;
+
+ /**
+ * Characteristic permission: Allow encrypted writes with person-in-the-middle
+ * protection
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;
+
+ /**
+ * Characteristic permission: Allow signed write operations
+ */
+ public static final int PERMISSION_WRITE_SIGNED = 0x80;
+
+ /**
+ * Characteristic permission: Allow signed write operations with
+ * person-in-the-middle protection
+ */
+ public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;
+
+ /**
+ * Write characteristic, requesting acknoledgement by the remote device
+ */
+ public static final int WRITE_TYPE_DEFAULT = 0x02;
+
+ /**
+ * Write characteristic without requiring a response by the remote device
+ */
+ public static final int WRITE_TYPE_NO_RESPONSE = 0x01;
+
+ /**
+ * Write characteristic including authentication signature
+ */
+ public static final int WRITE_TYPE_SIGNED = 0x04;
+
+ /**
+ * Characteristic value format type uint8
+ */
+ public static final int FORMAT_UINT8 = 0x11;
+
+ /**
+ * Characteristic value format type uint16
+ */
+ public static final int FORMAT_UINT16 = 0x12;
+
+ /**
+ * Characteristic value format type uint32
+ */
+ public static final int FORMAT_UINT32 = 0x14;
+
+ /**
+ * Characteristic value format type sint8
+ */
+ public static final int FORMAT_SINT8 = 0x21;
+
+ /**
+ * Characteristic value format type sint16
+ */
+ public static final int FORMAT_SINT16 = 0x22;
+
+ /**
+ * Characteristic value format type sint32
+ */
+ public static final int FORMAT_SINT32 = 0x24;
+
+ /**
+ * Characteristic value format type sfloat (16-bit float)
+ */
+ public static final int FORMAT_SFLOAT = 0x32;
+
+ /**
+ * Characteristic value format type float (32-bit float)
+ */
+ public static final int FORMAT_FLOAT = 0x34;
+
+
+ /**
+ * The UUID of this characteristic.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this characteristic.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected int mInstance;
+
+ /**
+ * Characteristic properties.
+ *
+ * @hide
+ */
+ protected int mProperties;
+
+ /**
+ * Characteristic permissions.
+ *
+ * @hide
+ */
+ protected int mPermissions;
+
+ /**
+ * Key size (default = 16).
+ *
+ * @hide
+ */
+ protected int mKeySize = 16;
+
+ /**
+ * Write type for this characteristic.
+ * See WRITE_TYPE_* constants.
+ *
+ * @hide
+ */
+ protected int mWriteType;
+
+ /**
+ * Back-reference to the service this characteristic belongs to.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothGattService mService;
+
+ /**
+ * The cached value of this characteristic.
+ *
+ * @hide
+ */
+ protected byte[] mValue;
+
+ /**
+ * List of descriptors included in this characteristic.
+ */
+ protected List<BluetoothGattDescriptor> mDescriptors;
+
+ /**
+ * Create a new BluetoothGattCharacteristic.
+ *
+ * @param uuid The UUID for this characteristic
+ * @param properties Properties of this characteristic
+ * @param permissions Permissions for this characteristic
+ */
+ public BluetoothGattCharacteristic(UUID uuid, int properties, int permissions) {
+ initCharacteristic(null, uuid, 0, properties, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattCharacteristic
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic(BluetoothGattService service,
+ UUID uuid, int instanceId,
+ int properties, int permissions) {
+ initCharacteristic(service, uuid, instanceId, properties, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattCharacteristic
+ *
+ * @hide
+ */
+ public BluetoothGattCharacteristic(UUID uuid, int instanceId,
+ int properties, int permissions) {
+ initCharacteristic(null, uuid, instanceId, properties, permissions);
+ }
+
+ private void initCharacteristic(BluetoothGattService service,
+ UUID uuid, int instanceId,
+ int properties, int permissions) {
+ mUuid = uuid;
+ mInstance = instanceId;
+ mProperties = properties;
+ mPermissions = permissions;
+ mService = service;
+ mValue = null;
+ mDescriptors = new ArrayList<BluetoothGattDescriptor>();
+
+ if ((mProperties & PROPERTY_WRITE_NO_RESPONSE) != 0) {
+ mWriteType = WRITE_TYPE_NO_RESPONSE;
+ } else {
+ mWriteType = WRITE_TYPE_DEFAULT;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstance);
+ out.writeInt(mProperties);
+ out.writeInt(mPermissions);
+ out.writeInt(mKeySize);
+ out.writeInt(mWriteType);
+ out.writeTypedList(mDescriptors);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothGattCharacteristic> CREATOR =
+ new Parcelable.Creator<BluetoothGattCharacteristic>() {
+ public BluetoothGattCharacteristic createFromParcel(Parcel in) {
+ return new BluetoothGattCharacteristic(in);
+ }
+
+ public BluetoothGattCharacteristic[] newArray(int size) {
+ return new BluetoothGattCharacteristic[size];
+ }
+ };
+
+ private BluetoothGattCharacteristic(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstance = in.readInt();
+ mProperties = in.readInt();
+ mPermissions = in.readInt();
+ mKeySize = in.readInt();
+ mWriteType = in.readInt();
+
+ mDescriptors = new ArrayList<BluetoothGattDescriptor>();
+
+ ArrayList<BluetoothGattDescriptor> descs =
+ in.createTypedArrayList(BluetoothGattDescriptor.CREATOR);
+ if (descs != null) {
+ for (BluetoothGattDescriptor desc : descs) {
+ desc.setCharacteristic(this);
+ mDescriptors.add(desc);
+ }
+ }
+ }
+
+ /**
+ * Returns the desired key size.
+ *
+ * @hide
+ */
+ public int getKeySize() {
+ return mKeySize;
+ }
+
+ /**
+ * Adds a descriptor to this characteristic.
+ *
+ * @param descriptor Descriptor to be added to this characteristic.
+ * @return true, if the descriptor was added to the characteristic
+ */
+ public boolean addDescriptor(BluetoothGattDescriptor descriptor) {
+ mDescriptors.add(descriptor);
+ descriptor.setCharacteristic(this);
+ return true;
+ }
+
+ /**
+ * Get a descriptor by UUID and isntance id.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptor(UUID uuid, int instanceId) {
+ for (BluetoothGattDescriptor descriptor : mDescriptors) {
+ if (descriptor.getUuid().equals(uuid)
+ && descriptor.getInstanceId() == instanceId) {
+ return descriptor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the service this characteristic belongs to.
+ *
+ * @return The asscociated service
+ */
+ public BluetoothGattService getService() {
+ return mService;
+ }
+
+ /**
+ * Sets the service associated with this device.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ void setService(BluetoothGattService service) {
+ mService = service;
+ }
+
+ /**
+ * Returns the UUID of this characteristic
+ *
+ * @return UUID of this characteristic
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this characteristic.
+ *
+ * <p>If a remote device offers multiple characteristics with the same UUID,
+ * the instance ID is used to distuinguish between characteristics.
+ *
+ * @return Instance ID of this characteristic
+ */
+ public int getInstanceId() {
+ return mInstance;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ public void setInstanceId(int instanceId) {
+ mInstance = instanceId;
+ }
+
+ /**
+ * Returns the properties of this characteristic.
+ *
+ * <p>The properties contain a bit mask of property flags indicating
+ * the features of this characteristic.
+ *
+ * @return Properties of this characteristic
+ */
+ public int getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * Returns the permissions for this characteristic.
+ *
+ * @return Permissions of this characteristic
+ */
+ public int getPermissions() {
+ return mPermissions;
+ }
+
+ /**
+ * Gets the write type for this characteristic.
+ *
+ * @return Write type for this characteristic
+ */
+ public int getWriteType() {
+ return mWriteType;
+ }
+
+ /**
+ * Set the write type for this characteristic
+ *
+ * <p>Setting the write type of a characteristic determines how the
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)} function
+ * write this characteristic.
+ *
+ * @param writeType The write type to for this characteristic. Can be one of: {@link
+ * #WRITE_TYPE_DEFAULT}, {@link #WRITE_TYPE_NO_RESPONSE} or {@link #WRITE_TYPE_SIGNED}.
+ */
+ public void setWriteType(int writeType) {
+ mWriteType = writeType;
+ }
+
+ /**
+ * Set the desired key size.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setKeySize(int keySize) {
+ mKeySize = keySize;
+ }
+
+ /**
+ * Returns a list of descriptors for this characteristic.
+ *
+ * @return Descriptors for this characteristic
+ */
+ public List<BluetoothGattDescriptor> getDescriptors() {
+ return mDescriptors;
+ }
+
+ /**
+ * Returns a descriptor with a given UUID out of the list of
+ * descriptors for this characteristic.
+ *
+ * @return GATT descriptor object or null if no descriptor with the given UUID was found.
+ */
+ public BluetoothGattDescriptor getDescriptor(UUID uuid) {
+ for (BluetoothGattDescriptor descriptor : mDescriptors) {
+ if (descriptor.getUuid().equals(uuid)) {
+ return descriptor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the stored value for this characteristic.
+ *
+ * <p>This function returns the stored value for this characteristic as
+ * retrieved by calling {@link BluetoothGatt#readCharacteristic}. The cached
+ * value of the characteristic is updated as a result of a read characteristic
+ * operation or if a characteristic update notification has been received.
+ *
+ * @return Cached value of the characteristic
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} instead
+ */
+ @Deprecated
+ public byte[] getValue() {
+ return mValue;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ *
+ * <p>The formatType parameter determines how the characteristic value
+ * is to be interpreted. For example, settting formatType to
+ * {@link #FORMAT_UINT16} specifies that the first two bytes of the
+ * characteristic value at the given offset are interpreted to generate the
+ * return value.
+ *
+ * @param formatType The format type used to interpret the characteristic value.
+ * @param offset Offset at which the integer value can be found.
+ * @return Cached value of the characteristic or null of offset exceeds value size.
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public Integer getIntValue(int formatType, int offset) {
+ if ((offset + getTypeLen(formatType)) > mValue.length) return null;
+
+ switch (formatType) {
+ case FORMAT_UINT8:
+ return unsignedByteToInt(mValue[offset]);
+
+ case FORMAT_UINT16:
+ return unsignedBytesToInt(mValue[offset], mValue[offset + 1]);
+
+ case FORMAT_UINT32:
+ return unsignedBytesToInt(mValue[offset], mValue[offset + 1],
+ mValue[offset + 2], mValue[offset + 3]);
+ case FORMAT_SINT8:
+ return unsignedToSigned(unsignedByteToInt(mValue[offset]), 8);
+
+ case FORMAT_SINT16:
+ return unsignedToSigned(unsignedBytesToInt(mValue[offset],
+ mValue[offset + 1]), 16);
+
+ case FORMAT_SINT32:
+ return unsignedToSigned(unsignedBytesToInt(mValue[offset],
+ mValue[offset + 1], mValue[offset + 2], mValue[offset + 3]), 32);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ * <p>See {@link #getValue} for details.
+ *
+ * @param formatType The format type used to interpret the characteristic value.
+ * @param offset Offset at which the float value can be found.
+ * @return Cached value of the characteristic at a given offset or null if the requested offset
+ * exceeds the value size.
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public Float getFloatValue(int formatType, int offset) {
+ if ((offset + getTypeLen(formatType)) > mValue.length) return null;
+
+ switch (formatType) {
+ case FORMAT_SFLOAT:
+ return bytesToFloat(mValue[offset], mValue[offset + 1]);
+
+ case FORMAT_FLOAT:
+ return bytesToFloat(mValue[offset], mValue[offset + 1],
+ mValue[offset + 2], mValue[offset + 3]);
+ }
+
+ return null;
+ }
+
+ /**
+ * Return the stored value of this characteristic.
+ * <p>See {@link #getValue} for details.
+ *
+ * @param offset Offset at which the string value can be found.
+ * @return Cached value of the characteristic
+ *
+ * @deprecated Use {@link BluetoothGatt#readCharacteristic(BluetoothGattCharacteristic)} to get
+ * the characteristic value
+ */
+ @Deprecated
+ public String getStringValue(int offset) {
+ if (mValue == null || offset > mValue.length) return null;
+ byte[] strBytes = new byte[mValue.length - offset];
+ for (int i = 0; i != (mValue.length - offset); ++i) strBytes[i] = mValue[offset + i];
+ return new String(strBytes);
+ }
+
+ /**
+ * Updates the locally stored value of this characteristic.
+ *
+ * <p>This function modifies the locally stored cached value of this
+ * characteristic. To send the value to the remote device, call
+ * {@link BluetoothGatt#writeCharacteristic} to send the value to the
+ * remote device.
+ *
+ * @param value New value for this characteristic
+ * @return true if the locally stored value has been set, false if the requested value could not
+ * be stored locally.
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(byte[] value) {
+ mValue = value;
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param value New value for this characteristic
+ * @param formatType Integer format type used to transform the value parameter
+ * @param offset Offset at which the value should be placed
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(int value, int formatType, int offset) {
+ int len = offset + getTypeLen(formatType);
+ if (mValue == null) mValue = new byte[len];
+ if (len > mValue.length) return false;
+
+ switch (formatType) {
+ case FORMAT_SINT8:
+ value = intToSignedBits(value, 8);
+ // Fall-through intended
+ case FORMAT_UINT8:
+ mValue[offset] = (byte) (value & 0xFF);
+ break;
+
+ case FORMAT_SINT16:
+ value = intToSignedBits(value, 16);
+ // Fall-through intended
+ case FORMAT_UINT16:
+ mValue[offset++] = (byte) (value & 0xFF);
+ mValue[offset] = (byte) ((value >> 8) & 0xFF);
+ break;
+
+ case FORMAT_SINT32:
+ value = intToSignedBits(value, 32);
+ // Fall-through intended
+ case FORMAT_UINT32:
+ mValue[offset++] = (byte) (value & 0xFF);
+ mValue[offset++] = (byte) ((value >> 8) & 0xFF);
+ mValue[offset++] = (byte) ((value >> 16) & 0xFF);
+ mValue[offset] = (byte) ((value >> 24) & 0xFF);
+ break;
+
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param mantissa Mantissa for this characteristic
+ * @param exponent exponent value for this characteristic
+ * @param formatType Float format type used to transform the value parameter
+ * @param offset Offset at which the value should be placed
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(int mantissa, int exponent, int formatType, int offset) {
+ int len = offset + getTypeLen(formatType);
+ if (mValue == null) mValue = new byte[len];
+ if (len > mValue.length) return false;
+
+ switch (formatType) {
+ case FORMAT_SFLOAT:
+ mantissa = intToSignedBits(mantissa, 12);
+ exponent = intToSignedBits(exponent, 4);
+ mValue[offset++] = (byte) (mantissa & 0xFF);
+ mValue[offset] = (byte) ((mantissa >> 8) & 0x0F);
+ mValue[offset] += (byte) ((exponent & 0x0F) << 4);
+ break;
+
+ case FORMAT_FLOAT:
+ mantissa = intToSignedBits(mantissa, 24);
+ exponent = intToSignedBits(exponent, 8);
+ mValue[offset++] = (byte) (mantissa & 0xFF);
+ mValue[offset++] = (byte) ((mantissa >> 8) & 0xFF);
+ mValue[offset++] = (byte) ((mantissa >> 16) & 0xFF);
+ mValue[offset] += (byte) (exponent & 0xFF);
+ break;
+
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Set the locally stored value of this characteristic.
+ * <p>See {@link #setValue(byte[])} for details.
+ *
+ * @param value New value for this characteristic
+ * @return true if the locally stored value has been set
+ *
+ * @deprecated Pass the characteristic value directly into
+ * {@link BluetoothGatt#writeCharacteristic(BluetoothGattCharacteristic, byte[], int)}
+ */
+ @Deprecated
+ public boolean setValue(String value) {
+ mValue = value.getBytes();
+ return true;
+ }
+
+ /**
+ * Returns the size of a give value type.
+ */
+ private int getTypeLen(int formatType) {
+ return formatType & 0xF;
+ }
+
+ /**
+ * Convert a signed byte to an unsigned int.
+ */
+ private int unsignedByteToInt(byte b) {
+ return b & 0xFF;
+ }
+
+ /**
+ * Convert signed bytes to a 16-bit unsigned int.
+ */
+ private int unsignedBytesToInt(byte b0, byte b1) {
+ return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8));
+ }
+
+ /**
+ * Convert signed bytes to a 32-bit unsigned int.
+ */
+ private int unsignedBytesToInt(byte b0, byte b1, byte b2, byte b3) {
+ return (unsignedByteToInt(b0) + (unsignedByteToInt(b1) << 8))
+ + (unsignedByteToInt(b2) << 16) + (unsignedByteToInt(b3) << 24);
+ }
+
+ /**
+ * Convert signed bytes to a 16-bit short float value.
+ */
+ private float bytesToFloat(byte b0, byte b1) {
+ int mantissa = unsignedToSigned(unsignedByteToInt(b0)
+ + ((unsignedByteToInt(b1) & 0x0F) << 8), 12);
+ int exponent = unsignedToSigned(unsignedByteToInt(b1) >> 4, 4);
+ return (float) (mantissa * Math.pow(10, exponent));
+ }
+
+ /**
+ * Convert signed bytes to a 32-bit short float value.
+ */
+ private float bytesToFloat(byte b0, byte b1, byte b2, byte b3) {
+ int mantissa = unsignedToSigned(unsignedByteToInt(b0)
+ + (unsignedByteToInt(b1) << 8)
+ + (unsignedByteToInt(b2) << 16), 24);
+ return (float) (mantissa * Math.pow(10, b3));
+ }
+
+ /**
+ * Convert an unsigned integer value to a two's-complement encoded
+ * signed value.
+ */
+ private int unsignedToSigned(int unsigned, int size) {
+ if ((unsigned & (1 << size - 1)) != 0) {
+ unsigned = -1 * ((1 << size - 1) - (unsigned & ((1 << size - 1) - 1)));
+ }
+ return unsigned;
+ }
+
+ /**
+ * Convert an integer into the signed bits of a given length.
+ */
+ private int intToSignedBits(int i, int size) {
+ if (i < 0) {
+ i = (1 << size - 1) + (i & ((1 << size - 1) - 1));
+ }
+ return i;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattDescriptor.java b/framework/java/android/bluetooth/BluetoothGattDescriptor.java
new file mode 100644
index 0000000000..a35d5b99fd
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattDescriptor.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 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 android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Descriptor
+ *
+ * <p> GATT Descriptors contain additional information and attributes of a GATT
+ * characteristic, {@link BluetoothGattCharacteristic}. They can be used to describe
+ * the characteristic's features or to control certain behaviours of the characteristic.
+ */
+public class BluetoothGattDescriptor implements Parcelable {
+
+ /**
+ * Value used to enable notification for a client configuration descriptor
+ */
+ public static final byte[] ENABLE_NOTIFICATION_VALUE = {0x01, 0x00};
+
+ /**
+ * Value used to enable indication for a client configuration descriptor
+ */
+ public static final byte[] ENABLE_INDICATION_VALUE = {0x02, 0x00};
+
+ /**
+ * Value used to disable notifications or indicatinos
+ */
+ public static final byte[] DISABLE_NOTIFICATION_VALUE = {0x00, 0x00};
+
+ /**
+ * Descriptor read permission
+ */
+ public static final int PERMISSION_READ = 0x01;
+
+ /**
+ * Descriptor permission: Allow encrypted read operations
+ */
+ public static final int PERMISSION_READ_ENCRYPTED = 0x02;
+
+ /**
+ * Descriptor permission: Allow reading with person-in-the-middle protection
+ */
+ public static final int PERMISSION_READ_ENCRYPTED_MITM = 0x04;
+
+ /**
+ * Descriptor write permission
+ */
+ public static final int PERMISSION_WRITE = 0x10;
+
+ /**
+ * Descriptor permission: Allow encrypted writes
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED = 0x20;
+
+ /**
+ * Descriptor permission: Allow encrypted writes with person-in-the-middle
+ * protection
+ */
+ public static final int PERMISSION_WRITE_ENCRYPTED_MITM = 0x40;
+
+ /**
+ * Descriptor permission: Allow signed write operations
+ */
+ public static final int PERMISSION_WRITE_SIGNED = 0x80;
+
+ /**
+ * Descriptor permission: Allow signed write operations with
+ * person-in-the-middle protection
+ */
+ public static final int PERMISSION_WRITE_SIGNED_MITM = 0x100;
+
+ /**
+ * The UUID of this descriptor.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this descriptor.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected int mInstance;
+
+ /**
+ * Permissions for this descriptor
+ *
+ * @hide
+ */
+ protected int mPermissions;
+
+ /**
+ * Back-reference to the characteristic this descriptor belongs to.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothGattCharacteristic mCharacteristic;
+
+ /**
+ * The value for this descriptor.
+ *
+ * @hide
+ */
+ protected byte[] mValue;
+
+ /**
+ * Create a new BluetoothGattDescriptor.
+ *
+ * @param uuid The UUID for this descriptor
+ * @param permissions Permissions for this descriptor
+ */
+ public BluetoothGattDescriptor(UUID uuid, int permissions) {
+ initDescriptor(null, uuid, 0, permissions);
+ }
+
+ /**
+ * Create a new BluetoothGattDescriptor.
+ *
+ * @param characteristic The characteristic this descriptor belongs to
+ * @param uuid The UUID for this descriptor
+ * @param permissions Permissions for this descriptor
+ */
+ /*package*/ BluetoothGattDescriptor(BluetoothGattCharacteristic characteristic, UUID uuid,
+ int instance, int permissions) {
+ initDescriptor(characteristic, uuid, instance, permissions);
+ }
+
+ /**
+ * @hide
+ */
+ public BluetoothGattDescriptor(UUID uuid, int instance, int permissions) {
+ initDescriptor(null, uuid, instance, permissions);
+ }
+
+ private void initDescriptor(BluetoothGattCharacteristic characteristic, UUID uuid,
+ int instance, int permissions) {
+ mCharacteristic = characteristic;
+ mUuid = uuid;
+ mInstance = instance;
+ mPermissions = permissions;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstance);
+ out.writeInt(mPermissions);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothGattDescriptor> CREATOR =
+ new Parcelable.Creator<BluetoothGattDescriptor>() {
+ public BluetoothGattDescriptor createFromParcel(Parcel in) {
+ return new BluetoothGattDescriptor(in);
+ }
+
+ public BluetoothGattDescriptor[] newArray(int size) {
+ return new BluetoothGattDescriptor[size];
+ }
+ };
+
+ private BluetoothGattDescriptor(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstance = in.readInt();
+ mPermissions = in.readInt();
+ }
+
+ /**
+ * Returns the characteristic this descriptor belongs to.
+ *
+ * @return The characteristic.
+ */
+ public BluetoothGattCharacteristic getCharacteristic() {
+ return mCharacteristic;
+ }
+
+ /**
+ * Set the back-reference to the associated characteristic
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /*package*/ void setCharacteristic(BluetoothGattCharacteristic characteristic) {
+ mCharacteristic = characteristic;
+ }
+
+ /**
+ * Returns the UUID of this descriptor.
+ *
+ * @return UUID of this descriptor
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this descriptor.
+ *
+ * <p>If a remote device offers multiple descriptors with the same UUID,
+ * the instance ID is used to distuinguish between descriptors.
+ *
+ * @return Instance ID of this descriptor
+ * @hide
+ */
+ public int getInstanceId() {
+ return mInstance;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ public void setInstanceId(int instanceId) {
+ mInstance = instanceId;
+ }
+
+ /**
+ * Returns the permissions for this descriptor.
+ *
+ * @return Permissions of this descriptor
+ */
+ public int getPermissions() {
+ return mPermissions;
+ }
+
+ /**
+ * Returns the stored value for this descriptor
+ *
+ * <p>This function returns the stored value for this descriptor as
+ * retrieved by calling {@link BluetoothGatt#readDescriptor}. The cached
+ * value of the descriptor is updated as a result of a descriptor read
+ * operation.
+ *
+ * @return Cached value of the descriptor
+ *
+ * @deprecated Use {@link BluetoothGatt#readDescriptor(BluetoothGattDescriptor)} instead
+ */
+ @Deprecated
+ public byte[] getValue() {
+ return mValue;
+ }
+
+ /**
+ * Updates the locally stored value of this descriptor.
+ *
+ * <p>This function modifies the locally stored cached value of this
+ * descriptor. To send the value to the remote device, call
+ * {@link BluetoothGatt#writeDescriptor} to send the value to the
+ * remote device.
+ *
+ * @param value New value for this descriptor
+ * @return true if the locally stored value has been set, false if the requested value could not
+ * be stored locally.
+ *
+ * @deprecated Pass the descriptor value directly into
+ * {@link BluetoothGatt#writeDescriptor(BluetoothGattDescriptor, byte[])}
+ */
+ @Deprecated
+ public boolean setValue(byte[] value) {
+ mValue = value;
+ return true;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattIncludedService.java b/framework/java/android/bluetooth/BluetoothGattIncludedService.java
new file mode 100644
index 0000000000..5580619033
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattIncludedService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2016 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 android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Included Service
+ *
+ * @hide
+ */
+public class BluetoothGattIncludedService implements Parcelable {
+
+ /**
+ * The UUID of this service.
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this service.
+ */
+ protected int mInstanceId;
+
+ /**
+ * Service type (Primary/Secondary).
+ */
+ protected int mServiceType;
+
+ /**
+ * Create a new BluetoothGattIncludedService
+ */
+ public BluetoothGattIncludedService(UUID uuid, int instanceId, int serviceType) {
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstanceId);
+ out.writeInt(mServiceType);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothGattIncludedService> CREATOR =
+ new Parcelable.Creator<BluetoothGattIncludedService>() {
+ public BluetoothGattIncludedService createFromParcel(Parcel in) {
+ return new BluetoothGattIncludedService(in);
+ }
+
+ public BluetoothGattIncludedService[] newArray(int size) {
+ return new BluetoothGattIncludedService[size];
+ }
+ };
+
+ private BluetoothGattIncludedService(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstanceId = in.readInt();
+ mServiceType = in.readInt();
+ }
+
+ /**
+ * Returns the UUID of this service
+ *
+ * @return UUID of this service
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this service
+ *
+ * <p>If a remote device offers multiple services with the same UUID
+ * (ex. multiple battery services for different batteries), the instance
+ * ID is used to distuinguish services.
+ *
+ * @return Instance ID of this service
+ */
+ public int getInstanceId() {
+ return mInstanceId;
+ }
+
+ /**
+ * Get the type of this service (primary/secondary)
+ */
+ public int getType() {
+ return mServiceType;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattServer.java b/framework/java/android/bluetooth/BluetoothGattServer.java
new file mode 100644
index 0000000000..08e0178403
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattServer.java
@@ -0,0 +1,954 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Public API for the Bluetooth GATT Profile server role.
+ *
+ * <p>This class provides Bluetooth GATT server role functionality,
+ * allowing applications to create Bluetooth Smart services and
+ * characteristics.
+ *
+ * <p>BluetoothGattServer is a proxy object for controlling the Bluetooth Service
+ * via IPC. Use {@link BluetoothManager#openGattServer} to get an instance
+ * of this class.
+ */
+public final class BluetoothGattServer implements BluetoothProfile {
+ private static final String TAG = "BluetoothGattServer";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private final IBluetoothGatt mService;
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+
+ private BluetoothGattServerCallback mCallback;
+
+ private Object mServerIfLock = new Object();
+ private int mServerIf;
+ private int mTransport;
+ private BluetoothGattService mPendingService;
+ private List<BluetoothGattService> mServices;
+
+ private static final int CALLBACK_REG_TIMEOUT = 10000;
+
+ /**
+ * Bluetooth GATT interface callbacks
+ */
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private final IBluetoothGattServerCallback mBluetoothGattServerCallback =
+ new IBluetoothGattServerCallback.Stub() {
+ /**
+ * Application interface registered - app is ready to go
+ * @hide
+ */
+ @Override
+ public void onServerRegistered(int status, int serverIf) {
+ if (DBG) {
+ Log.d(TAG, "onServerRegistered() - status=" + status
+ + " serverIf=" + serverIf);
+ }
+ synchronized (mServerIfLock) {
+ if (mCallback != null) {
+ mServerIf = serverIf;
+ mServerIfLock.notify();
+ } else {
+ // registration timeout
+ Log.e(TAG, "onServerRegistered: mCallback is null");
+ }
+ }
+ }
+
+ /**
+ * Server connection state changed
+ * @hide
+ */
+ @Override
+ public void onServerConnectionState(int status, int serverIf,
+ boolean connected, String address) {
+ if (DBG) {
+ Log.d(TAG, "onServerConnectionState() - status=" + status
+ + " serverIf=" + serverIf + " device=" + address);
+ }
+ try {
+ mCallback.onConnectionStateChange(mAdapter.getRemoteDevice(address), status,
+ connected ? BluetoothProfile.STATE_CONNECTED :
+ BluetoothProfile.STATE_DISCONNECTED);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Service has been added
+ * @hide
+ */
+ @Override
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ if (DBG) {
+ Log.d(TAG, "onServiceAdded() - handle=" + service.getInstanceId()
+ + " uuid=" + service.getUuid() + " status=" + status);
+ }
+
+ if (mPendingService == null) {
+ return;
+ }
+
+ BluetoothGattService tmp = mPendingService;
+ mPendingService = null;
+
+ // Rewrite newly assigned handles to existing service.
+ tmp.setInstanceId(service.getInstanceId());
+ List<BluetoothGattCharacteristic> temp_chars = tmp.getCharacteristics();
+ List<BluetoothGattCharacteristic> svc_chars = service.getCharacteristics();
+ for (int i = 0; i < svc_chars.size(); i++) {
+ BluetoothGattCharacteristic temp_char = temp_chars.get(i);
+ BluetoothGattCharacteristic svc_char = svc_chars.get(i);
+
+ temp_char.setInstanceId(svc_char.getInstanceId());
+
+ List<BluetoothGattDescriptor> temp_descs = temp_char.getDescriptors();
+ List<BluetoothGattDescriptor> svc_descs = svc_char.getDescriptors();
+ for (int j = 0; j < svc_descs.size(); j++) {
+ temp_descs.get(j).setInstanceId(svc_descs.get(j).getInstanceId());
+ }
+ }
+
+ mServices.add(tmp);
+
+ try {
+ mCallback.onServiceAdded((int) status, tmp);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client characteristic read request.
+ * @hide
+ */
+ @Override
+ public void onCharacteristicReadRequest(String address, int transId,
+ int offset, boolean isLong, int handle) {
+ if (VDBG) Log.d(TAG, "onCharacteristicReadRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattCharacteristic characteristic = getCharacteristicByHandle(handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicReadRequest() no char for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onCharacteristicReadRequest(device, transId, offset,
+ characteristic);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client descriptor read request.
+ * @hide
+ */
+ @Override
+ public void onDescriptorReadRequest(String address, int transId,
+ int offset, boolean isLong, int handle) {
+ if (VDBG) Log.d(TAG, "onCharacteristicReadRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattDescriptor descriptor = getDescriptorByHandle(handle);
+ if (descriptor == null) {
+ Log.w(TAG, "onDescriptorReadRequest() no desc for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onDescriptorReadRequest(device, transId, offset, descriptor);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Remote client characteristic write request.
+ * @hide
+ */
+ @Override
+ public void onCharacteristicWriteRequest(String address, int transId,
+ int offset, int length, boolean isPrep, boolean needRsp,
+ int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onCharacteristicWriteRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattCharacteristic characteristic = getCharacteristicByHandle(handle);
+ if (characteristic == null) {
+ Log.w(TAG, "onCharacteristicWriteRequest() no char for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onCharacteristicWriteRequest(device, transId, characteristic,
+ isPrep, needRsp, offset, value);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+
+ }
+
+ /**
+ * Remote client descriptor write request.
+ * @hide
+ */
+ @Override
+ public void onDescriptorWriteRequest(String address, int transId, int offset,
+ int length, boolean isPrep, boolean needRsp, int handle, byte[] value) {
+ if (VDBG) Log.d(TAG, "onDescriptorWriteRequest() - handle=" + handle);
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ BluetoothGattDescriptor descriptor = getDescriptorByHandle(handle);
+ if (descriptor == null) {
+ Log.w(TAG, "onDescriptorWriteRequest() no desc for handle " + handle);
+ return;
+ }
+
+ try {
+ mCallback.onDescriptorWriteRequest(device, transId, descriptor,
+ isPrep, needRsp, offset, value);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * Execute pending writes.
+ * @hide
+ */
+ @Override
+ public void onExecuteWrite(String address, int transId,
+ boolean execWrite) {
+ if (DBG) {
+ Log.d(TAG, "onExecuteWrite() - "
+ + "device=" + address + ", transId=" + transId
+ + "execWrite=" + execWrite);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onExecuteWrite(device, transId, execWrite);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception in callback", ex);
+ }
+ }
+
+ /**
+ * A notification/indication has been sent.
+ * @hide
+ */
+ @Override
+ public void onNotificationSent(String address, int status) {
+ if (VDBG) {
+ Log.d(TAG, "onNotificationSent() - "
+ + "device=" + address + ", status=" + status);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onNotificationSent(device, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The MTU for a connection has changed
+ * @hide
+ */
+ @Override
+ public void onMtuChanged(String address, int mtu) {
+ if (DBG) {
+ Log.d(TAG, "onMtuChanged() - "
+ + "device=" + address + ", mtu=" + mtu);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onMtuChanged(device, mtu);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The PHY for a connection was updated
+ * @hide
+ */
+ @Override
+ public void onPhyUpdate(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onPhyUpdate() - " + "device=" + address + ", txPHy=" + txPhy
+ + ", rxPHy=" + rxPhy);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onPhyUpdate(device, txPhy, rxPhy, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * The PHY for a connection was read
+ * @hide
+ */
+ @Override
+ public void onPhyRead(String address, int txPhy, int rxPhy, int status) {
+ if (DBG) {
+ Log.d(TAG,
+ "onPhyUpdate() - " + "device=" + address + ", txPHy=" + txPhy
+ + ", rxPHy=" + rxPhy);
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onPhyRead(device, txPhy, rxPhy, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ /**
+ * Callback invoked when the given connection is updated
+ * @hide
+ */
+ @Override
+ public void onConnectionUpdated(String address, int interval, int latency,
+ int timeout, int status) {
+ if (DBG) {
+ Log.d(TAG, "onConnectionUpdated() - Device=" + address
+ + " interval=" + interval + " latency=" + latency
+ + " timeout=" + timeout + " status=" + status);
+ }
+ BluetoothDevice device = mAdapter.getRemoteDevice(address);
+ if (device == null) return;
+
+ try {
+ mCallback.onConnectionUpdated(device, interval, latency,
+ timeout, status);
+ } catch (Exception ex) {
+ Log.w(TAG, "Unhandled exception: " + ex);
+ }
+ }
+
+ };
+
+ /**
+ * Create a BluetoothGattServer proxy object.
+ */
+ /* package */ BluetoothGattServer(IBluetoothGatt iGatt, int transport,
+ BluetoothAdapter adapter) {
+ mService = iGatt;
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mCallback = null;
+ mServerIf = 0;
+ mTransport = transport;
+ mServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Returns a characteristic with given handle.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristicByHandle(int handle) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ if (charac.getInstanceId() == handle) {
+ return charac;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a descriptor with given handle.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattDescriptor getDescriptorByHandle(int handle) {
+ for (BluetoothGattService svc : mServices) {
+ for (BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
+ for (BluetoothGattDescriptor desc : charac.getDescriptors()) {
+ if (desc.getInstanceId() == handle) {
+ return desc;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Close this GATT server instance.
+ *
+ * Application should call this method as early as possible after it is done with
+ * this GATT server.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void close() {
+ if (DBG) Log.d(TAG, "close()");
+ unregisterCallback();
+ }
+
+ /**
+ * Register an application callback to start using GattServer.
+ *
+ * <p>This is an asynchronous call. The callback is used to notify
+ * success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @return true, the callback will be called to notify success or failure, false on immediate
+ * error
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean registerCallback(BluetoothGattServerCallback callback) {
+ return registerCallback(callback, false);
+ }
+
+ /**
+ * Register an application callback to start using GattServer.
+ *
+ * <p>This is an asynchronous call. The callback is used to notify
+ * success or failure if the function returns true.
+ *
+ * @param callback GATT callback handler that will receive asynchronous callbacks.
+ * @param eatt_support indicates if server can use eatt
+ * @return true, the callback will be called to notify success or failure, false on immediate
+ * error
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ boolean registerCallback(BluetoothGattServerCallback callback,
+ boolean eatt_support) {
+ if (DBG) Log.d(TAG, "registerCallback()");
+ if (mService == null) {
+ Log.e(TAG, "GATT service not available");
+ return false;
+ }
+ UUID uuid = UUID.randomUUID();
+ if (DBG) Log.d(TAG, "registerCallback() - UUID=" + uuid);
+
+ synchronized (mServerIfLock) {
+ if (mCallback != null) {
+ Log.e(TAG, "App can register callback only once");
+ return false;
+ }
+
+ mCallback = callback;
+ try {
+ mService.registerServer(new ParcelUuid(uuid), mBluetoothGattServerCallback,
+ eatt_support, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ mCallback = null;
+ return false;
+ }
+
+ try {
+ mServerIfLock.wait(CALLBACK_REG_TIMEOUT);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "" + e);
+ mCallback = null;
+ }
+
+ if (mServerIf == 0) {
+ mCallback = null;
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Unregister the current application and callbacks.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void unregisterCallback() {
+ if (DBG) Log.d(TAG, "unregisterCallback() - mServerIf=" + mServerIf);
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ mCallback = null;
+ mService.unregisterServer(mServerIf, mAttributionSource);
+ mServerIf = 0;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Returns a service by UUID, instance and type.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService getService(UUID uuid, int instanceId, int type) {
+ for (BluetoothGattService svc : mServices) {
+ if (svc.getType() == type
+ && svc.getInstanceId() == instanceId
+ && svc.getUuid().equals(uuid)) {
+ return svc;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Initiate a connection to a Bluetooth GATT capable device.
+ *
+ * <p>The connection may not be established right away, but will be
+ * completed when the remote device is available. A
+ * {@link BluetoothGattServerCallback#onConnectionStateChange} callback will be
+ * invoked when the connection state changes as a result of this function.
+ *
+ * <p>The autoConnect parameter determines whether to actively connect to
+ * the remote device, or rather passively scan and finalize the connection
+ * when the remote device is in range/available. Generally, the first ever
+ * connection to a device should be direct (autoConnect set to false) and
+ * subsequent connections to known devices should be invoked with the
+ * autoConnect parameter set to true.
+ *
+ * @param autoConnect Whether to directly connect to the remote device (false) or to
+ * automatically connect as soon as the remote device becomes available (true).
+ * @return true, if the connection attempt was initiated successfully
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(BluetoothDevice device, boolean autoConnect) {
+ if (DBG) {
+ Log.d(TAG,
+ "connect() - device: " + device.getAddress() + ", auto: " + autoConnect);
+ }
+ if (mService == null || mServerIf == 0) return false;
+
+ try {
+ // autoConnect is inverse of "isDirect"
+ mService.serverConnect(
+ mServerIf, device.getAddress(), !autoConnect, mTransport, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnects an established connection, or cancels a connection attempt
+ * currently in progress.
+ *
+ * @param device Remote device
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void cancelConnection(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "cancelConnection() - device: " + device.getAddress());
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ mService.serverDisconnect(mServerIf, device.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Set the preferred connection PHY for this app. Please note that this is just a
+ * recommendation, whether the PHY change will happen depends on other applications peferences,
+ * local and remote controller capabilities. Controller can override these settings. <p> {@link
+ * BluetoothGattServerCallback#onPhyUpdate} will be triggered as a result of this call, even if
+ * no PHY change happens. It is also triggered when remote device updates the PHY.
+ *
+ * @param device The remote device to send this response to
+ * @param txPhy preferred transmitter PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param rxPhy preferred receiver PHY. Bitwise OR of any of {@link
+ * BluetoothDevice#PHY_LE_1M_MASK}, {@link BluetoothDevice#PHY_LE_2M_MASK}, and {@link
+ * BluetoothDevice#PHY_LE_CODED_MASK}.
+ * @param phyOptions preferred coding to use when transmitting on the LE Coded PHY. Can be one
+ * of {@link BluetoothDevice#PHY_OPTION_NO_PREFERRED}, {@link BluetoothDevice#PHY_OPTION_S2} or
+ * {@link BluetoothDevice#PHY_OPTION_S8}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setPreferredPhy(BluetoothDevice device, int txPhy, int rxPhy, int phyOptions) {
+ try {
+ mService.serverSetPreferredPhy(mServerIf, device.getAddress(), txPhy, rxPhy,
+ phyOptions, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Read the current transmitter PHY and receiver PHY of the connection. The values are returned
+ * in {@link BluetoothGattServerCallback#onPhyRead}
+ *
+ * @param device The remote device to send this response to
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void readPhy(BluetoothDevice device) {
+ try {
+ mService.serverReadPhy(mServerIf, device.getAddress(), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Send a response to a read or write request to a remote device.
+ *
+ * <p>This function must be invoked in when a remote read/write request
+ * is received by one of these callback methods:
+ *
+ * <ul>
+ * <li>{@link BluetoothGattServerCallback#onCharacteristicReadRequest}
+ * <li>{@link BluetoothGattServerCallback#onCharacteristicWriteRequest}
+ * <li>{@link BluetoothGattServerCallback#onDescriptorReadRequest}
+ * <li>{@link BluetoothGattServerCallback#onDescriptorWriteRequest}
+ * </ul>
+ *
+ * @param device The remote device to send this response to
+ * @param requestId The ID of the request that was received with the callback
+ * @param status The status of the request to be sent to the remote devices
+ * @param offset Value offset for partial read/write response
+ * @param value The value of the attribute that was read/written (optional)
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendResponse(BluetoothDevice device, int requestId,
+ int status, int offset, byte[] value) {
+ if (VDBG) Log.d(TAG, "sendResponse() - device: " + device.getAddress());
+ if (mService == null || mServerIf == 0) return false;
+
+ try {
+ mService.sendResponse(mServerIf, device.getAddress(), requestId,
+ status, offset, value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Send a notification or indication that a local characteristic has been
+ * updated.
+ *
+ * <p>A notification or indication is sent to the remote device to signal
+ * that the characteristic has been updated. This function should be invoked
+ * for every client that requests notifications/indications by writing
+ * to the "Client Configuration" descriptor for the given characteristic.
+ *
+ * @param device The remote device to receive the notification/indication
+ * @param characteristic The local characteristic that has been updated
+ * @param confirm true to request confirmation from the client (indication), false to send a
+ * notification
+ * @return true, if the notification has been triggered successfully
+ * @throws IllegalArgumentException
+ *
+ * @deprecated Use {@link BluetoothGattServer#notifyCharacteristicChanged(BluetoothDevice,
+ * BluetoothGattCharacteristic, boolean, byte[])} as this is not memory safe.
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean notifyCharacteristicChanged(BluetoothDevice device,
+ BluetoothGattCharacteristic characteristic, boolean confirm) {
+ return notifyCharacteristicChanged(device, characteristic, confirm,
+ characteristic.getValue()) == BluetoothStatusCodes.SUCCESS;
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ BluetoothStatusCodes.SUCCESS,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
+ BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION,
+ BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED,
+ BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED,
+ BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY,
+ BluetoothStatusCodes.ERROR_UNKNOWN
+ })
+ public @interface NotifyCharacteristicReturnValues{}
+
+ /**
+ * Send a notification or indication that a local characteristic has been
+ * updated.
+ *
+ * <p>A notification or indication is sent to the remote device to signal
+ * that the characteristic has been updated. This function should be invoked
+ * for every client that requests notifications/indications by writing
+ * to the "Client Configuration" descriptor for the given characteristic.
+ *
+ * @param device the remote device to receive the notification/indication
+ * @param characteristic the local characteristic that has been updated
+ * @param confirm {@code true} to request confirmation from the client (indication) or
+ * {@code false} to send a notification
+ * @param value the characteristic value
+ * @return whether the notification has been triggered successfully
+ * @throws IllegalArgumentException if the characteristic value or service is null
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @NotifyCharacteristicReturnValues
+ public int notifyCharacteristicChanged(@NonNull BluetoothDevice device,
+ @NonNull BluetoothGattCharacteristic characteristic, boolean confirm,
+ @NonNull byte[] value) {
+ if (VDBG) Log.d(TAG, "notifyCharacteristicChanged() - device: " + device.getAddress());
+ if (mService == null || mServerIf == 0) {
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
+ }
+
+ if (characteristic == null) {
+ throw new IllegalArgumentException("characteristic must not be null");
+ }
+ if (device == null) {
+ throw new IllegalArgumentException("device must not be null");
+ }
+ BluetoothGattService service = characteristic.getService();
+ if (service == null) {
+ throw new IllegalArgumentException("Characteristic must have a non-null service");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("Characteristic value must not be null");
+ }
+
+ try {
+ return mService.sendNotification(mServerIf, device.getAddress(),
+ characteristic.getInstanceId(), confirm,
+ value, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Add a service to the list of services to be hosted.
+ *
+ * <p>Once a service has been addded to the list, the service and its
+ * included characteristics will be provided by the local device.
+ *
+ * <p>If the local device has already exposed services when this function
+ * is called, a service update notification will be sent to all clients.
+ *
+ * <p>The {@link BluetoothGattServerCallback#onServiceAdded} callback will indicate
+ * whether this service has been added successfully. Do not add another service
+ * before this callback.
+ *
+ * @param service Service to be added to the list of services provided by this device.
+ * @return true, if the request to add service has been initiated
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean addService(BluetoothGattService service) {
+ if (DBG) Log.d(TAG, "addService() - service: " + service.getUuid());
+ if (mService == null || mServerIf == 0) return false;
+
+ mPendingService = service;
+
+ try {
+ mService.addService(mServerIf, service, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Removes a service from the list of services to be provided.
+ *
+ * @param service Service to be removed.
+ * @return true, if the service has been removed
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean removeService(BluetoothGattService service) {
+ if (DBG) Log.d(TAG, "removeService() - service: " + service.getUuid());
+ if (mService == null || mServerIf == 0) return false;
+
+ BluetoothGattService intService = getService(service.getUuid(),
+ service.getInstanceId(), service.getType());
+ if (intService == null) return false;
+
+ try {
+ mService.removeService(mServerIf, service.getInstanceId(), mAttributionSource);
+ mServices.remove(intService);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove all services from the list of provided services.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void clearServices() {
+ if (DBG) Log.d(TAG, "clearServices()");
+ if (mService == null || mServerIf == 0) return;
+
+ try {
+ mService.clearServices(mServerIf, mAttributionSource);
+ mServices.clear();
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ /**
+ * Returns a list of GATT services offered by this device.
+ *
+ * <p>An application must call {@link #addService} to add a serice to the
+ * list of services offered by this device.
+ *
+ * @return List of services. Returns an empty list if no services have been added yet.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public List<BluetoothGattService> getServices() {
+ return mServices;
+ }
+
+ /**
+ * Returns a {@link BluetoothGattService} from the list of services offered
+ * by this device.
+ *
+ * <p>If multiple instances of the same service (as identified by UUID)
+ * exist, the first instance of the service is returned.
+ *
+ * @param uuid UUID of the requested service
+ * @return BluetoothGattService if supported, or null if the requested service is not offered by
+ * this device.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresNoPermission
+ public BluetoothGattService getService(UUID uuid) {
+ for (BluetoothGattService service : mServices) {
+ if (service.getUuid().equals(uuid)) {
+ return service;
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public int getConnectionState(BluetoothDevice device) {
+ throw new UnsupportedOperationException("Use BluetoothManager#getConnectionState instead.");
+ }
+
+ /**
+ * Not supported - please use {@link BluetoothManager#getConnectedDevices(int)}
+ * with {@link BluetoothProfile#GATT} as argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getConnectedDevices instead.");
+ }
+
+ /**
+ * Not supported - please use
+ * {@link BluetoothManager#getDevicesMatchingConnectionStates(int, int[])}
+ * with {@link BluetoothProfile#GATT} as first argument
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ @RequiresNoPermission
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ throw new UnsupportedOperationException(
+ "Use BluetoothManager#getDevicesMatchingConnectionStates instead.");
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattServerCallback.java b/framework/java/android/bluetooth/BluetoothGattServerCallback.java
new file mode 100644
index 0000000000..0ead5f57e8
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattServerCallback.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * This abstract class is used to implement {@link BluetoothGattServer} callbacks.
+ */
+public abstract class BluetoothGattServerCallback {
+
+ /**
+ * Callback indicating when a remote device has been connected or disconnected.
+ *
+ * @param device Remote device that has been connected or disconnected.
+ * @param status Status of the connect or disconnect operation.
+ * @param newState Returns the new connection state. Can be one of {@link
+ * BluetoothProfile#STATE_DISCONNECTED} or {@link BluetoothProfile#STATE_CONNECTED}
+ */
+ public void onConnectionStateChange(BluetoothDevice device, int status,
+ int newState) {
+ }
+
+ /**
+ * Indicates whether a local service has been added successfully.
+ *
+ * @param status Returns {@link BluetoothGatt#GATT_SUCCESS} if the service was added
+ * successfully.
+ * @param service The service that has been added
+ */
+ public void onServiceAdded(int status, BluetoothGattService service) {
+ }
+
+ /**
+ * A remote client has requested to read a local characteristic.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the read operation
+ * @param requestId The Id of the request
+ * @param offset Offset into the value of the characteristic
+ * @param characteristic Characteristic to be read
+ */
+ public void onCharacteristicReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattCharacteristic characteristic) {
+ }
+
+ /**
+ * A remote client has requested to write to a local characteristic.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operation
+ * @param requestId The Id of the request
+ * @param characteristic Characteristic to be written to.
+ * @param preparedWrite true, if this write operation should be queued for later execution.
+ * @param responseNeeded true, if the remote device requires a response
+ * @param offset The offset given for the value
+ * @param value The value the client wants to assign to the characteristic
+ */
+ public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattCharacteristic characteristic,
+ boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value) {
+ }
+
+ /**
+ * A remote client has requested to read a local descriptor.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the read operation
+ * @param requestId The Id of the request
+ * @param offset Offset into the value of the characteristic
+ * @param descriptor Descriptor to be read
+ */
+ public void onDescriptorReadRequest(BluetoothDevice device, int requestId,
+ int offset, BluetoothGattDescriptor descriptor) {
+ }
+
+ /**
+ * A remote client has requested to write to a local descriptor.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operation
+ * @param requestId The Id of the request
+ * @param descriptor Descriptor to be written to.
+ * @param preparedWrite true, if this write operation should be queued for later execution.
+ * @param responseNeeded true, if the remote device requires a response
+ * @param offset The offset given for the value
+ * @param value The value the client wants to assign to the descriptor
+ */
+ public void onDescriptorWriteRequest(BluetoothDevice device, int requestId,
+ BluetoothGattDescriptor descriptor,
+ boolean preparedWrite, boolean responseNeeded,
+ int offset, byte[] value) {
+ }
+
+ /**
+ * Execute all pending write operations for this device.
+ *
+ * <p>An application must call {@link BluetoothGattServer#sendResponse}
+ * to complete the request.
+ *
+ * @param device The remote device that has requested the write operations
+ * @param requestId The Id of the request
+ * @param execute Whether the pending writes should be executed (true) or cancelled (false)
+ */
+ public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
+ }
+
+ /**
+ * Callback invoked when a notification or indication has been sent to
+ * a remote device.
+ *
+ * <p>When multiple notifications are to be sent, an application must
+ * wait for this callback to be received before sending additional
+ * notifications.
+ *
+ * @param device The remote device the notification has been sent to
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the operation was successful
+ */
+ public void onNotificationSent(BluetoothDevice device, int status) {
+ }
+
+ /**
+ * Callback indicating the MTU for a given device connection has changed.
+ *
+ * <p>This callback will be invoked if a remote client has requested to change
+ * the MTU for a given connection.
+ *
+ * @param device The remote device that requested the MTU change
+ * @param mtu The new MTU size
+ */
+ public void onMtuChanged(BluetoothDevice device, int mtu) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGattServer#setPreferredPhy}, or as a result
+ * of remote device changing the PHY.
+ *
+ * @param device The remote device
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param status Status of the PHY update operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback triggered as result of {@link BluetoothGattServer#readPhy}
+ *
+ * @param device The remote device that requested the PHY read
+ * @param txPhy the transmitter PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param rxPhy the receiver PHY in use. One of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_2M}, and {@link BluetoothDevice#PHY_LE_CODED}
+ * @param status Status of the PHY read operation. {@link BluetoothGatt#GATT_SUCCESS} if the
+ * operation succeeds.
+ */
+ public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
+ }
+
+ /**
+ * Callback indicating the connection parameters were updated.
+ *
+ * @param device The remote device involved
+ * @param interval Connection interval used on this connection, 1.25ms unit. Valid range is from
+ * 6 (7.5ms) to 3200 (4000ms).
+ * @param latency Worker latency for the connection in number of connection events. Valid range
+ * is from 0 to 499
+ * @param timeout Supervision timeout for this connection, in 10ms unit. Valid range is from 10
+ * (0.1s) to 3200 (32s)
+ * @param status {@link BluetoothGatt#GATT_SUCCESS} if the connection has been updated
+ * successfully
+ * @hide
+ */
+ public void onConnectionUpdated(BluetoothDevice device, int interval, int latency, int timeout,
+ int status) {
+ }
+
+}
diff --git a/framework/java/android/bluetooth/BluetoothGattService.java b/framework/java/android/bluetooth/BluetoothGattService.java
new file mode 100644
index 0000000000..f64d09fc30
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothGattService.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.RequiresPermission;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a Bluetooth GATT Service
+ *
+ * <p> Gatt Service contains a collection of {@link BluetoothGattCharacteristic},
+ * as well as referenced services.
+ */
+public class BluetoothGattService implements Parcelable {
+
+ /**
+ * Primary service
+ */
+ public static final int SERVICE_TYPE_PRIMARY = 0;
+
+ /**
+ * Secondary service (included by primary services)
+ */
+ public static final int SERVICE_TYPE_SECONDARY = 1;
+
+
+ /**
+ * The remote device this service is associated with.
+ * This applies to client applications only.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ protected BluetoothDevice mDevice;
+
+ /**
+ * The UUID of this service.
+ *
+ * @hide
+ */
+ protected UUID mUuid;
+
+ /**
+ * Instance ID for this service.
+ *
+ * @hide
+ */
+ protected int mInstanceId;
+
+ /**
+ * Handle counter override (for conformance testing).
+ *
+ * @hide
+ */
+ protected int mHandles = 0;
+
+ /**
+ * Service type (Primary/Secondary).
+ *
+ * @hide
+ */
+ protected int mServiceType;
+
+ /**
+ * List of characteristics included in this service.
+ */
+ protected List<BluetoothGattCharacteristic> mCharacteristics;
+
+ /**
+ * List of included services for this service.
+ */
+ protected List<BluetoothGattService> mIncludedServices;
+
+ /**
+ * Whether the service uuid should be advertised.
+ */
+ private boolean mAdvertisePreferred;
+
+ /**
+ * Create a new BluetoothGattService.
+ *
+ * @param uuid The UUID for this service
+ * @param serviceType The type of this service,
+ * {@link BluetoothGattService#SERVICE_TYPE_PRIMARY}
+ * or {@link BluetoothGattService#SERVICE_TYPE_SECONDARY}
+ */
+ public BluetoothGattService(UUID uuid, int serviceType) {
+ mDevice = null;
+ mUuid = uuid;
+ mInstanceId = 0;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Create a new BluetoothGattService
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattService(BluetoothDevice device, UUID uuid,
+ int instanceId, int serviceType) {
+ mDevice = device;
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * Create a new BluetoothGattService
+ *
+ * @hide
+ */
+ public BluetoothGattService(UUID uuid, int instanceId, int serviceType) {
+ mDevice = null;
+ mUuid = uuid;
+ mInstanceId = instanceId;
+ mServiceType = serviceType;
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+ }
+
+ /**
+ * @hide
+ */
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeInt(mInstanceId);
+ out.writeInt(mServiceType);
+ out.writeTypedList(mCharacteristics);
+
+ ArrayList<BluetoothGattIncludedService> includedServices =
+ new ArrayList<BluetoothGattIncludedService>(mIncludedServices.size());
+ for (BluetoothGattService s : mIncludedServices) {
+ includedServices.add(new BluetoothGattIncludedService(s.getUuid(),
+ s.getInstanceId(), s.getType()));
+ }
+ out.writeTypedList(includedServices);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothGattService> CREATOR =
+ new Parcelable.Creator<BluetoothGattService>() {
+ public BluetoothGattService createFromParcel(Parcel in) {
+ return new BluetoothGattService(in);
+ }
+
+ public BluetoothGattService[] newArray(int size) {
+ return new BluetoothGattService[size];
+ }
+ };
+
+ private BluetoothGattService(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mInstanceId = in.readInt();
+ mServiceType = in.readInt();
+
+ mCharacteristics = new ArrayList<BluetoothGattCharacteristic>();
+
+ ArrayList<BluetoothGattCharacteristic> chrcs =
+ in.createTypedArrayList(BluetoothGattCharacteristic.CREATOR);
+ if (chrcs != null) {
+ for (BluetoothGattCharacteristic chrc : chrcs) {
+ chrc.setService(this);
+ mCharacteristics.add(chrc);
+ }
+ }
+
+ mIncludedServices = new ArrayList<BluetoothGattService>();
+
+ ArrayList<BluetoothGattIncludedService> inclSvcs =
+ in.createTypedArrayList(BluetoothGattIncludedService.CREATOR);
+ if (chrcs != null) {
+ for (BluetoothGattIncludedService isvc : inclSvcs) {
+ mIncludedServices.add(new BluetoothGattService(null, isvc.getUuid(),
+ isvc.getInstanceId(), isvc.getType()));
+ }
+ }
+ }
+
+ /**
+ * Returns the device associated with this service.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the device associated with this service.
+ *
+ * @hide
+ */
+ /*package*/ void setDevice(BluetoothDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * Add an included service to this service.
+ *
+ * @param service The service to be added
+ * @return true, if the included service was added to the service
+ */
+ @RequiresLegacyBluetoothPermission
+ public boolean addService(BluetoothGattService service) {
+ mIncludedServices.add(service);
+ return true;
+ }
+
+ /**
+ * Add a characteristic to this service.
+ *
+ * @param characteristic The characteristics to be added
+ * @return true, if the characteristic was added to the service
+ */
+ @RequiresLegacyBluetoothPermission
+ public boolean addCharacteristic(BluetoothGattCharacteristic characteristic) {
+ mCharacteristics.add(characteristic);
+ characteristic.setService(this);
+ return true;
+ }
+
+ /**
+ * Get characteristic by UUID and instanceId.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothGattCharacteristic getCharacteristic(UUID uuid, int instanceId) {
+ for (BluetoothGattCharacteristic characteristic : mCharacteristics) {
+ if (uuid.equals(characteristic.getUuid())
+ && characteristic.getInstanceId() == instanceId) {
+ return characteristic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Force the instance ID.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public void setInstanceId(int instanceId) {
+ mInstanceId = instanceId;
+ }
+
+ /**
+ * Get the handle count override (conformance testing.
+ *
+ * @hide
+ */
+ /*package*/ int getHandles() {
+ return mHandles;
+ }
+
+ /**
+ * Force the number of handles to reserve for this service.
+ * This is needed for conformance testing only.
+ *
+ * @hide
+ */
+ public void setHandles(int handles) {
+ mHandles = handles;
+ }
+
+ /**
+ * Add an included service to the internal map.
+ *
+ * @hide
+ */
+ public void addIncludedService(BluetoothGattService includedService) {
+ mIncludedServices.add(includedService);
+ }
+
+ /**
+ * Returns the UUID of this service
+ *
+ * @return UUID of this service
+ */
+ public UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns the instance ID for this service
+ *
+ * <p>If a remote device offers multiple services with the same UUID
+ * (ex. multiple battery services for different batteries), the instance
+ * ID is used to distuinguish services.
+ *
+ * @return Instance ID of this service
+ */
+ public int getInstanceId() {
+ return mInstanceId;
+ }
+
+ /**
+ * Get the type of this service (primary/secondary)
+ */
+ public int getType() {
+ return mServiceType;
+ }
+
+ /**
+ * Get the list of included GATT services for this service.
+ *
+ * @return List of included services or empty list if no included services were discovered.
+ */
+ public List<BluetoothGattService> getIncludedServices() {
+ return mIncludedServices;
+ }
+
+ /**
+ * Returns a list of characteristics included in this service.
+ *
+ * @return Characteristics included in this service
+ */
+ public List<BluetoothGattCharacteristic> getCharacteristics() {
+ return mCharacteristics;
+ }
+
+ /**
+ * Returns a characteristic with a given UUID out of the list of
+ * characteristics offered by this service.
+ *
+ * <p>This is a convenience function to allow access to a given characteristic
+ * without enumerating over the list returned by {@link #getCharacteristics}
+ * manually.
+ *
+ * <p>If a remote service offers multiple characteristics with the same
+ * UUID, the first instance of a characteristic with the given UUID
+ * is returned.
+ *
+ * @return GATT characteristic object or null if no characteristic with the given UUID was
+ * found.
+ */
+ public BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
+ for (BluetoothGattCharacteristic characteristic : mCharacteristics) {
+ if (uuid.equals(characteristic.getUuid())) {
+ return characteristic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the uuid of the service should be advertised.
+ *
+ * @hide
+ */
+ public boolean isAdvertisePreferred() {
+ return mAdvertisePreferred;
+ }
+
+ /**
+ * Set whether the service uuid should be advertised.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public void setAdvertisePreferred(boolean advertisePreferred) {
+ mAdvertisePreferred = advertisePreferred;
+ }
+}
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;
+ }
+ }
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/BluetoothHeadsetClient.java b/framework/java/android/bluetooth/BluetoothHeadsetClient.java
new file mode 100644
index 0000000000..7d7a7f798b
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHeadsetClient.java
@@ -0,0 +1,1356 @@
+/*
+ * Copyright (C) 2014 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.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+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 to control Hands Free Profile (HFP role only).
+ * <p>
+ * This class defines methods that shall be used by application to manage profile
+ * connection, calls states and calls actions.
+ * <p>
+ *
+ * @hide
+ */
+public final class BluetoothHeadsetClient implements BluetoothProfile {
+ private static final String TAG = "BluetoothHeadsetClient";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent sent whenever connection to remote changes.
+ *
+ * <p>It includes two extras:
+ * <code>BluetoothProfile.EXTRA_PREVIOUS_STATE</code>
+ * and <code>BluetoothProfile.EXTRA_STATE</code>, which
+ * are mandatory.
+ * <p>There are also non mandatory feature extras:
+ * {@link #EXTRA_AG_FEATURE_3WAY_CALLING},
+ * {@link #EXTRA_AG_FEATURE_VOICE_RECOGNITION},
+ * {@link #EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT},
+ * {@link #EXTRA_AG_FEATURE_REJECT_CALL},
+ * {@link #EXTRA_AG_FEATURE_ECC},
+ * {@link #EXTRA_AG_FEATURE_RESPONSE_AND_HOLD},
+ * {@link #EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL},
+ * {@link #EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL},
+ * {@link #EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT},
+ * {@link #EXTRA_AG_FEATURE_MERGE},
+ * {@link #EXTRA_AG_FEATURE_MERGE_AND_DETACH},
+ * sent as boolean values only when <code>EXTRA_STATE</code>
+ * is set to <code>STATE_CONNECTED</code>.</p>
+ *
+ * <p>Note that features supported by AG are being sent as
+ * booleans with value <code>true</code>,
+ * and not supported ones are <strong>not</strong> being sent at all.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent sent whenever audio state changes.
+ *
+ * <p>It includes two mandatory extras:
+ * {@link BluetoothProfile#EXTRA_STATE},
+ * {@link BluetoothProfile#EXTRA_PREVIOUS_STATE},
+ * with possible values:
+ * {@link #STATE_AUDIO_CONNECTING},
+ * {@link #STATE_AUDIO_CONNECTED},
+ * {@link #STATE_AUDIO_DISCONNECTED}</p>
+ * <p>When <code>EXTRA_STATE</code> is set
+ * to </code>STATE_AUDIO_CONNECTED</code>,
+ * it also includes {@link #EXTRA_AUDIO_WBS}
+ * indicating wide band speech support.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AUDIO_STATE_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.AUDIO_STATE_CHANGED";
+
+ /**
+ * Intent sending updates of the Audio Gateway state.
+ * Each extra is being sent only when value it
+ * represents has been changed recently on AG.
+ * <p>It can contain one or more of the following extras:
+ * {@link #EXTRA_NETWORK_STATUS},
+ * {@link #EXTRA_NETWORK_SIGNAL_STRENGTH},
+ * {@link #EXTRA_NETWORK_ROAMING},
+ * {@link #EXTRA_BATTERY_LEVEL},
+ * {@link #EXTRA_OPERATOR_NAME},
+ * {@link #EXTRA_VOICE_RECOGNITION},
+ * {@link #EXTRA_IN_BAND_RING}</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AG_EVENT =
+ "android.bluetooth.headsetclient.profile.action.AG_EVENT";
+
+ /**
+ * Intent sent whenever state of a call changes.
+ *
+ * <p>It includes:
+ * {@link #EXTRA_CALL},
+ * with value of {@link BluetoothHeadsetClientCall} instance,
+ * representing actual call state.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CALL_CHANGED =
+ "android.bluetooth.headsetclient.profile.action.AG_CALL_CHANGED";
+
+ /**
+ * Intent that notifies about the result of the last issued action.
+ * Please note that not every action results in explicit action result code being sent.
+ * Instead other notifications about new Audio Gateway state might be sent,
+ * like <code>ACTION_AG_EVENT</code> with <code>EXTRA_VOICE_RECOGNITION</code> value
+ * when for example user started voice recognition from HF unit.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_RESULT =
+ "android.bluetooth.headsetclient.profile.action.RESULT";
+
+ /**
+ * Intent that notifies about vendor specific event arrival. Events not defined in
+ * HFP spec will be matched with supported vendor event list and this intent will
+ * be broadcasted upon a match. Supported vendor events are of format of
+ * of "+eventCode" or "+eventCode=xxxx" or "+eventCode:=xxxx".
+ * Vendor event can be a response to an vendor specific command or unsolicited.
+ *
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_VENDOR_SPECIFIC_HEADSETCLIENT_EVENT =
+ "android.bluetooth.headsetclient.profile.action.VENDOR_SPECIFIC_EVENT";
+
+ /**
+ * Intent that notifies about the number attached to the last voice tag
+ * recorded on AG.
+ *
+ * <p>It contains:
+ * {@link #EXTRA_NUMBER},
+ * with a <code>String</code> value representing phone number.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LAST_VTAG =
+ "android.bluetooth.headsetclient.profile.action.LAST_VTAG";
+
+ public static final int STATE_AUDIO_DISCONNECTED = 0;
+ public static final int STATE_AUDIO_CONNECTING = 1;
+ public static final int STATE_AUDIO_CONNECTED = 2;
+
+ /**
+ * Extra with information if connected audio is WBS.
+ * <p>Possible values: <code>true</code>,
+ * <code>false</code>.</p>
+ */
+ public static final String EXTRA_AUDIO_WBS =
+ "android.bluetooth.headsetclient.extra.AUDIO_WBS";
+
+ /**
+ * Extra for AG_EVENT indicates network status.
+ * <p>Value: 0 - network unavailable,
+ * 1 - network available </p>
+ */
+ public static final String EXTRA_NETWORK_STATUS =
+ "android.bluetooth.headsetclient.extra.NETWORK_STATUS";
+ /**
+ * Extra for AG_EVENT intent indicates network signal strength.
+ * <p>Value: <code>Integer</code> representing signal strength.</p>
+ */
+ public static final String EXTRA_NETWORK_SIGNAL_STRENGTH =
+ "android.bluetooth.headsetclient.extra.NETWORK_SIGNAL_STRENGTH";
+ /**
+ * Extra for AG_EVENT intent indicates roaming state.
+ * <p>Value: 0 - no roaming
+ * 1 - active roaming</p>
+ */
+ public static final String EXTRA_NETWORK_ROAMING =
+ "android.bluetooth.headsetclient.extra.NETWORK_ROAMING";
+ /**
+ * Extra for AG_EVENT intent indicates the battery level.
+ * <p>Value: <code>Integer</code> representing signal strength.</p>
+ */
+ public static final String EXTRA_BATTERY_LEVEL =
+ "android.bluetooth.headsetclient.extra.BATTERY_LEVEL";
+ /**
+ * Extra for AG_EVENT intent indicates operator name.
+ * <p>Value: <code>String</code> representing operator name.</p>
+ */
+ public static final String EXTRA_OPERATOR_NAME =
+ "android.bluetooth.headsetclient.extra.OPERATOR_NAME";
+ /**
+ * Extra for AG_EVENT intent indicates voice recognition state.
+ * <p>Value:
+ * 0 - voice recognition stopped,
+ * 1 - voice recognition started.</p>
+ */
+ public static final String EXTRA_VOICE_RECOGNITION =
+ "android.bluetooth.headsetclient.extra.VOICE_RECOGNITION";
+ /**
+ * Extra for AG_EVENT intent indicates in band ring state.
+ * <p>Value:
+ * 0 - in band ring tone not supported, or
+ * 1 - in band ring tone supported.</p>
+ */
+ public static final String EXTRA_IN_BAND_RING =
+ "android.bluetooth.headsetclient.extra.IN_BAND_RING";
+
+ /**
+ * Extra for AG_EVENT intent indicates subscriber info.
+ * <p>Value: <code>String</code> containing subscriber information.</p>
+ */
+ public static final String EXTRA_SUBSCRIBER_INFO =
+ "android.bluetooth.headsetclient.extra.SUBSCRIBER_INFO";
+
+ /**
+ * Extra for AG_CALL_CHANGED intent indicates the
+ * {@link BluetoothHeadsetClientCall} object that has changed.
+ */
+ public static final String EXTRA_CALL =
+ "android.bluetooth.headsetclient.extra.CALL";
+
+ /**
+ * Extra for ACTION_LAST_VTAG intent.
+ * <p>Value: <code>String</code> representing phone number
+ * corresponding to last voice tag recorded on AG</p>
+ */
+ public static final String EXTRA_NUMBER =
+ "android.bluetooth.headsetclient.extra.NUMBER";
+
+ /**
+ * Extra for ACTION_RESULT intent that shows the result code of
+ * last issued action.
+ * <p>Possible results:
+ * {@link #ACTION_RESULT_OK},
+ * {@link #ACTION_RESULT_ERROR},
+ * {@link #ACTION_RESULT_ERROR_NO_CARRIER},
+ * {@link #ACTION_RESULT_ERROR_BUSY},
+ * {@link #ACTION_RESULT_ERROR_NO_ANSWER},
+ * {@link #ACTION_RESULT_ERROR_DELAYED},
+ * {@link #ACTION_RESULT_ERROR_BLACKLISTED},
+ * {@link #ACTION_RESULT_ERROR_CME}</p>
+ */
+ public static final String EXTRA_RESULT_CODE =
+ "android.bluetooth.headsetclient.extra.RESULT_CODE";
+
+ /**
+ * Extra for ACTION_RESULT intent that shows the extended result code of
+ * last issued action.
+ * <p>Value: <code>Integer</code> - error code.</p>
+ */
+ public static final String EXTRA_CME_CODE =
+ "android.bluetooth.headsetclient.extra.CME_CODE";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * indicates vendor ID.
+ */
+ public static final String EXTRA_VENDOR_ID =
+ "android.bluetooth.headsetclient.extra.VENDOR_ID";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * indicates vendor event code.
+ */
+ public static final String EXTRA_VENDOR_EVENT_CODE =
+ "android.bluetooth.headsetclient.extra.VENDOR_EVENT_CODE";
+
+ /**
+ * Extra for VENDOR_SPECIFIC_HEADSETCLIENT_EVENT intent that
+ * contains full vendor event including event code and full arguments.
+ */
+ public static final String EXTRA_VENDOR_EVENT_FULL_ARGS =
+ "android.bluetooth.headsetclient.extra.VENDOR_EVENT_FULL_ARGS";
+
+
+ /* Extras for AG_FEATURES, extras type is boolean */
+ // TODO verify if all of those are actually useful
+ /**
+ * AG feature: three way calling.
+ */
+ public static final String EXTRA_AG_FEATURE_3WAY_CALLING =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_3WAY_CALLING";
+ /**
+ * AG feature: voice recognition.
+ */
+ public static final String EXTRA_AG_FEATURE_VOICE_RECOGNITION =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_VOICE_RECOGNITION";
+ /**
+ * AG feature: fetching phone number for voice tagging procedure.
+ */
+ public static final String EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT";
+ /**
+ * AG feature: ability to reject incoming call.
+ */
+ public static final String EXTRA_AG_FEATURE_REJECT_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_REJECT_CALL";
+ /**
+ * AG feature: enhanced call handling (terminate specific call, private consultation).
+ */
+ public static final String EXTRA_AG_FEATURE_ECC =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ECC";
+ /**
+ * AG feature: response and hold.
+ */
+ public static final String EXTRA_AG_FEATURE_RESPONSE_AND_HOLD =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RESPONSE_AND_HOLD";
+ /**
+ * AG call handling feature: accept held or waiting call in three way calling scenarios.
+ */
+ public static final String EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_ACCEPT_HELD_OR_WAITING_CALL";
+ /**
+ * AG call handling feature: release held or waiting call in three way calling scenarios.
+ */
+ public static final String EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RELEASE_HELD_OR_WAITING_CALL";
+ /**
+ * AG call handling feature: release active call and accept held or waiting call in three way
+ * calling scenarios.
+ */
+ public static final String EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_RELEASE_AND_ACCEPT";
+ /**
+ * AG call handling feature: merge two calls, held and active - multi party conference mode.
+ */
+ public static final String EXTRA_AG_FEATURE_MERGE =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_MERGE";
+ /**
+ * AG call handling feature: merge calls and disconnect from multi party
+ * conversation leaving peers connected to each other.
+ * Note that this feature needs to be supported by mobile network operator
+ * as it requires connection and billing transfer.
+ */
+ public static final String EXTRA_AG_FEATURE_MERGE_AND_DETACH =
+ "android.bluetooth.headsetclient.extra.EXTRA_AG_FEATURE_MERGE_AND_DETACH";
+
+ /* Action result codes */
+ public static final int ACTION_RESULT_OK = 0;
+ public static final int ACTION_RESULT_ERROR = 1;
+ public static final int ACTION_RESULT_ERROR_NO_CARRIER = 2;
+ public static final int ACTION_RESULT_ERROR_BUSY = 3;
+ public static final int ACTION_RESULT_ERROR_NO_ANSWER = 4;
+ public static final int ACTION_RESULT_ERROR_DELAYED = 5;
+ public static final int ACTION_RESULT_ERROR_BLACKLISTED = 6;
+ public static final int ACTION_RESULT_ERROR_CME = 7;
+
+ /* Detailed CME error codes */
+ public static final int CME_PHONE_FAILURE = 0;
+ public static final int CME_NO_CONNECTION_TO_PHONE = 1;
+ public static final int CME_OPERATION_NOT_ALLOWED = 3;
+ public static final int CME_OPERATION_NOT_SUPPORTED = 4;
+ public static final int CME_PHSIM_PIN_REQUIRED = 5;
+ public static final int CME_PHFSIM_PIN_REQUIRED = 6;
+ public static final int CME_PHFSIM_PUK_REQUIRED = 7;
+ public static final int CME_SIM_NOT_INSERTED = 10;
+ public static final int CME_SIM_PIN_REQUIRED = 11;
+ public static final int CME_SIM_PUK_REQUIRED = 12;
+ public static final int CME_SIM_FAILURE = 13;
+ public static final int CME_SIM_BUSY = 14;
+ public static final int CME_SIM_WRONG = 15;
+ public static final int CME_INCORRECT_PASSWORD = 16;
+ public static final int CME_SIM_PIN2_REQUIRED = 17;
+ public static final int CME_SIM_PUK2_REQUIRED = 18;
+ public static final int CME_MEMORY_FULL = 20;
+ public static final int CME_INVALID_INDEX = 21;
+ public static final int CME_NOT_FOUND = 22;
+ public static final int CME_MEMORY_FAILURE = 23;
+ public static final int CME_TEXT_STRING_TOO_LONG = 24;
+ public static final int CME_INVALID_CHARACTER_IN_TEXT_STRING = 25;
+ public static final int CME_DIAL_STRING_TOO_LONG = 26;
+ public static final int CME_INVALID_CHARACTER_IN_DIAL_STRING = 27;
+ public static final int CME_NO_NETWORK_SERVICE = 30;
+ public static final int CME_NETWORK_TIMEOUT = 31;
+ public static final int CME_EMERGENCY_SERVICE_ONLY = 32;
+ public static final int CME_NO_SIMULTANOUS_VOIP_CS_CALLS = 33;
+ public static final int CME_NOT_SUPPORTED_FOR_VOIP = 34;
+ public static final int CME_SIP_RESPONSE_CODE = 35;
+ public static final int CME_NETWORK_PERSONALIZATION_PIN_REQUIRED = 40;
+ public static final int CME_NETWORK_PERSONALIZATION_PUK_REQUIRED = 41;
+ public static final int CME_NETWORK_SUBSET_PERSONALIZATION_PIN_REQUIRED = 42;
+ public static final int CME_NETWORK_SUBSET_PERSONALIZATION_PUK_REQUIRED = 43;
+ public static final int CME_SERVICE_PROVIDER_PERSONALIZATION_PIN_REQUIRED = 44;
+ public static final int CME_SERVICE_PROVIDER_PERSONALIZATION_PUK_REQUIRED = 45;
+ public static final int CME_CORPORATE_PERSONALIZATION_PIN_REQUIRED = 46;
+ public static final int CME_CORPORATE_PERSONALIZATION_PUK_REQUIRED = 47;
+ public static final int CME_HIDDEN_KEY_REQUIRED = 48;
+ public static final int CME_EAP_NOT_SUPPORTED = 49;
+ public static final int CME_INCORRECT_PARAMETERS = 50;
+
+ /* Action policy for other calls when accepting call */
+ public static final int CALL_ACCEPT_NONE = 0;
+ public static final int CALL_ACCEPT_HOLD = 1;
+ public static final int CALL_ACCEPT_TERMINATE = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHeadsetClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HEADSET_CLIENT,
+ "BluetoothHeadsetClient", IBluetoothHeadsetClient.class.getName()) {
+ @Override
+ public IBluetoothHeadsetClient getServiceInterface(IBinder service) {
+ return IBluetoothHeadsetClient.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHeadsetClient proxy object.
+ */
+ /* package */ BluetoothHeadsetClient(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothHeadsetClient will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ */
+ /*package*/ void close() {
+ if (VDBG) log("close()");
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHeadsetClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Connects to remote device.
+ *
+ * Currently, the system supports only 1 connection. So, in case of the
+ * second connection, this implementation will disconnect already connected
+ * device automatically and will process the new one.
+ *
+ * @param device a remote device we want connect to
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CONNECTION_STATE_CHANGED} intent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ 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.connect(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;
+ }
+
+ /**
+ * Disconnects remote device
+ *
+ * @param device a remote device we want disconnect
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CONNECTION_STATE_CHANGED} intent.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ 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.disconnect(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;
+ }
+
+ /**
+ * Return the list of connected remote devices
+ *
+ * @return list of connected devices; empty list if nothing is connected.
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.getConnectedDevices(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;
+ }
+
+ /**
+ * Returns list of remote devices in a particular state
+ *
+ * @param states collection of states
+ * @return list of devices that state matches the states listed in <code>states</code>; empty
+ * list if nothing matches the <code>states</code>
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHeadsetClient service = getService();
+ 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;
+ }
+
+ /**
+ * Returns state of the <code>device</code>
+ *
+ * @param device a remote device
+ * @return the state of connection of the device
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (VDBG) log("getConnectionState(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ 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.getConnectionState(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 #PRIORITY_ON} or {@link #PRIORITY_OFF}
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothHeadsetClient service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @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
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothHeadsetClient service = getService();
+ final @ConnectionPolicy 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;
+ }
+
+ /**
+ * Starts voice recognition.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AG_EVENT} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_VOICE_RECOGNITION}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean startVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("startVoiceRecognition()");
+ final IBluetoothHeadsetClient service = getService();
+ 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;
+ }
+
+ /**
+ * Send vendor specific AT command.
+ *
+ * @param device remote device
+ * @param vendorId vendor number by Bluetooth SIG
+ * @param atCommand command to be sent. It start with + prefix and only one command at one time.
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
+ if (DBG) log("sendVendorSpecificCommand()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.sendVendorAtCommand(device, vendorId, atCommand, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Stops voice recognition.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AG_EVENT} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_VOICE_RECOGNITION}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean stopVoiceRecognition(BluetoothDevice device) {
+ if (DBG) log("stopVoiceRecognition()");
+ final IBluetoothHeadsetClient service = getService();
+ 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;
+ }
+
+ /**
+ * Returns list of all calls in any state.
+ *
+ * @param device remote device
+ * @return list of calls; empty list if none call exists
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothHeadsetClientCall> getCurrentCalls(BluetoothDevice device) {
+ if (DBG) log("getCurrentCalls()");
+ final IBluetoothHeadsetClient service = getService();
+ final List<BluetoothHeadsetClientCall> defaultValue = null;
+ 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<List<BluetoothHeadsetClientCall>> recv =
+ new SynchronousResultReceiver();
+ service.getCurrentCalls(device, 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;
+ }
+
+ /**
+ * Returns list of current values of AG indicators.
+ *
+ * @param device remote device
+ * @return bundle of AG indicators; null if device is not in CONNECTED state
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Bundle getCurrentAgEvents(BluetoothDevice device) {
+ if (DBG) log("getCurrentAgEvents()");
+ final IBluetoothHeadsetClient service = getService();
+ final Bundle defaultValue = null;
+ 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<Bundle> recv = new SynchronousResultReceiver();
+ service.getCurrentAgEvents(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;
+ }
+
+ /**
+ * Accepts a call
+ *
+ * @param device remote device
+ * @param flag action policy while accepting a call. Possible values {@link #CALL_ACCEPT_NONE},
+ * {@link #CALL_ACCEPT_HOLD}, {@link #CALL_ACCEPT_TERMINATE}
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean acceptCall(BluetoothDevice device, int flag) {
+ if (DBG) log("acceptCall()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.acceptCall(device, flag, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Holds a call.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean holdCall(BluetoothDevice device) {
+ if (DBG) log("holdCall()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.holdCall(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;
+ }
+
+ /**
+ * Rejects a call.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_REJECT_CALL}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean rejectCall(BluetoothDevice device) {
+ if (DBG) log("rejectCall()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.rejectCall(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;
+ }
+
+ /**
+ * Terminates a specified call.
+ *
+ * Works only when Extended Call Control is supported by Audio Gateway.
+ *
+ * @param device remote device
+ * @param call Handle of call obtained in {@link #dial(BluetoothDevice, String)} or obtained via
+ * {@link #ACTION_CALL_CHANGED}. {@code call} may be null in which case we will hangup all active
+ * calls.
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ECC}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean terminateCall(BluetoothDevice device, BluetoothHeadsetClientCall call) {
+ if (DBG) log("terminateCall()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.terminateCall(device, call, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Enters private mode with a specified call.
+ *
+ * Works only when Extended Call Control is supported by Audio Gateway.
+ *
+ * @param device remote device
+ * @param index index of the call to connect in private mode
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ECC}. This method invocation will fail silently when feature is not
+ * supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean enterPrivateMode(BluetoothDevice device, int index) {
+ if (DBG) log("enterPrivateMode()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.enterPrivateMode(device, index, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Performs explicit call transfer.
+ *
+ * That means connect other calls and disconnect.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_CALL_CHANGED} intent.
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_MERGE_AND_DETACH}. This method invocation will fail silently when feature
+ * is not supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean explicitCallTransfer(BluetoothDevice device) {
+ if (DBG) log("explicitCallTransfer()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.explicitCallTransfer(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;
+ }
+
+ /**
+ * Places a call with specified number.
+ *
+ * @param device remote device
+ * @param number valid phone number
+ * @return <code>{@link BluetoothHeadsetClientCall} call</code> if command has been issued
+ * successfully; <code>{@link null}</code> otherwise; upon completion HFP sends {@link
+ * #ACTION_CALL_CHANGED} intent in case of success; {@link #ACTION_RESULT} is sent otherwise;
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) {
+ if (DBG) log("dial()");
+ final IBluetoothHeadsetClient service = getService();
+ final BluetoothHeadsetClientCall defaultValue = null;
+ 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<BluetoothHeadsetClientCall> recv =
+ new SynchronousResultReceiver();
+ service.dial(device, number, 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;
+ }
+
+ /**
+ * Sends DTMF code.
+ *
+ * Possible code values : 0,1,2,3,4,5,6,7,8,9,A,B,C,D,*,#
+ *
+ * @param device remote device
+ * @param code ASCII code
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_RESULT} intent;
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendDTMF(BluetoothDevice device, byte code) {
+ if (DBG) log("sendDTMF()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.sendDTMF(device, code, 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 a number corresponding to last voice tag recorded on AG.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_LAST_VTAG} or {@link #ACTION_RESULT}
+ * intent;
+ *
+ * <p>Feature required for successful execution is being reported by: {@link
+ * #EXTRA_AG_FEATURE_ATTACH_NUMBER_TO_VT}. This method invocation will fail silently when
+ * feature is not supported.</p>
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getLastVoiceTagNumber(BluetoothDevice device) {
+ if (DBG) log("getLastVoiceTagNumber()");
+ final IBluetoothHeadsetClient service = getService();
+ 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.getLastVoiceTagNumber(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;
+ }
+
+ /**
+ * Returns current audio state of Audio Gateway.
+ *
+ * Note: This is an internal function and shouldn't be exposed
+ */
+ @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 IBluetoothHeadsetClient service = getService();
+ final int defaultValue = BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ 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()));
+ }
+ } else {
+ return defaultValue;
+ }
+ return BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED;
+ }
+
+ /**
+ * Sets whether audio routing is allowed.
+ *
+ * @param device remote device
+ * @param allowed if routing is allowed to the device Note: This is an internal function and
+ * shouldn't be exposed
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setAudioRouteAllowed(BluetoothDevice device, boolean allowed) {
+ if (VDBG) log("setAudioRouteAllowed");
+ final IBluetoothHeadsetClient service = getService();
+ 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(device, 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.
+ *
+ * @param device remote device
+ * @return whether the command succeeded Note: This is an internal function and shouldn't be
+ * exposed
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getAudioRouteAllowed(BluetoothDevice device) {
+ if (VDBG) log("getAudioRouteAllowed");
+ final IBluetoothHeadsetClient service = getService();
+ 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(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;
+ }
+
+ /**
+ * Initiates a connection of audio channel.
+ *
+ * It setup SCO channel with remote connected Handsfree AG device.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AUDIO_STATE_CHANGED} intent;
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connectAudio(BluetoothDevice device) {
+ if (VDBG) log("connectAudio");
+ final IBluetoothHeadsetClient service = getService();
+ 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(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;
+ }
+
+ /**
+ * Disconnects audio channel.
+ *
+ * It tears down the SCO channel from remote AG device.
+ *
+ * @param device remote device
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise; upon completion HFP sends {@link #ACTION_AUDIO_STATE_CHANGED} intent;
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnectAudio(BluetoothDevice device) {
+ if (VDBG) log("disconnectAudio");
+ final IBluetoothHeadsetClient service = getService();
+ 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(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 Audio Gateway features
+ *
+ * @param device remote device
+ * @return bundle of AG features; null if no service or AG not connected
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public Bundle getCurrentAgFeatures(BluetoothDevice device) {
+ if (VDBG) log("getCurrentAgFeatures");
+ final IBluetoothHeadsetClient service = getService();
+ final Bundle 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<Bundle> recv = new SynchronousResultReceiver();
+ service.getCurrentAgFeatures(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;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHeadsetClientCall.java b/framework/java/android/bluetooth/BluetoothHeadsetClientCall.java
new file mode 100644
index 0000000000..032b507f5d
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2014 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 android.annotation.NonNull;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+
+import java.util.UUID;
+
+/**
+ * This class represents a single call, its state and properties.
+ * It implements {@link Parcelable} for inter-process message passing.
+ *
+ * @hide
+ */
+public final class BluetoothHeadsetClientCall implements Parcelable, Attributable {
+
+ /* Call state */
+ /**
+ * Call is active.
+ */
+ public static final int CALL_STATE_ACTIVE = 0;
+ /**
+ * Call is in held state.
+ */
+ public static final int CALL_STATE_HELD = 1;
+ /**
+ * Outgoing call that is being dialed right now.
+ */
+ public static final int CALL_STATE_DIALING = 2;
+ /**
+ * Outgoing call that remote party has already been alerted about.
+ */
+ public static final int CALL_STATE_ALERTING = 3;
+ /**
+ * Incoming call that can be accepted or rejected.
+ */
+ public static final int CALL_STATE_INCOMING = 4;
+ /**
+ * Waiting call state when there is already an active call.
+ */
+ public static final int CALL_STATE_WAITING = 5;
+ /**
+ * Call that has been held by response and hold
+ * (see Bluetooth specification for further references).
+ */
+ public static final int CALL_STATE_HELD_BY_RESPONSE_AND_HOLD = 6;
+ /**
+ * Call that has been already terminated and should not be referenced as a valid call.
+ */
+ public static final int CALL_STATE_TERMINATED = 7;
+
+ private final BluetoothDevice mDevice;
+ private final int mId;
+ private int mState;
+ private String mNumber;
+ private boolean mMultiParty;
+ private final boolean mOutgoing;
+ private final UUID mUUID;
+ private final long mCreationElapsedMilli;
+ private final boolean mInBandRing;
+
+ /**
+ * Creates BluetoothHeadsetClientCall instance.
+ */
+ public BluetoothHeadsetClientCall(BluetoothDevice device, int id, int state, String number,
+ boolean multiParty, boolean outgoing, boolean inBandRing) {
+ this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing, inBandRing);
+ }
+
+ public BluetoothHeadsetClientCall(BluetoothDevice device, int id, UUID uuid, int state,
+ String number, boolean multiParty, boolean outgoing, boolean inBandRing) {
+ mDevice = device;
+ mId = id;
+ mUUID = uuid;
+ mState = state;
+ mNumber = number != null ? number : "";
+ mMultiParty = multiParty;
+ mOutgoing = outgoing;
+ mInBandRing = inBandRing;
+ mCreationElapsedMilli = SystemClock.elapsedRealtime();
+ }
+
+ /** {@hide} */
+ public void setAttributionSource(@NonNull AttributionSource attributionSource) {
+ Attributable.setAttributionSource(mDevice, attributionSource);
+ }
+
+ /**
+ * Sets call's state.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param state new call state.
+ */
+ public void setState(int state) {
+ mState = state;
+ }
+
+ /**
+ * Sets call's number.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param number String representing phone number.
+ */
+ public void setNumber(String number) {
+ mNumber = number;
+ }
+
+ /**
+ * Sets this call as multi party call.
+ *
+ * <p>Note: This is an internal function and shouldn't be exposed</p>
+ *
+ * @param multiParty if <code>true</code> sets this call as a part of multi party conference.
+ */
+ public void setMultiParty(boolean multiParty) {
+ mMultiParty = multiParty;
+ }
+
+ /**
+ * Gets call's device.
+ *
+ * @return call device.
+ */
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Gets call's Id.
+ *
+ * @return call id.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Gets call's UUID.
+ *
+ * @return call uuid
+ * @hide
+ */
+ public UUID getUUID() {
+ return mUUID;
+ }
+
+ /**
+ * Gets call's current state.
+ *
+ * @return state of this particular phone call.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Gets call's number.
+ *
+ * @return string representing phone number.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public String getNumber() {
+ return mNumber;
+ }
+
+ /**
+ * Gets call's creation time in millis since epoch.
+ *
+ * @return long representing the creation time.
+ */
+ public long getCreationElapsedMilli() {
+ return mCreationElapsedMilli;
+ }
+
+ /**
+ * Checks if call is an active call in a conference mode (aka multi party).
+ *
+ * @return <code>true</code> if call is a multi party call, <code>false</code> otherwise.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isMultiParty() {
+ return mMultiParty;
+ }
+
+ /**
+ * Checks if this call is an outgoing call.
+ *
+ * @return <code>true</code> if its outgoing call, <code>false</code> otherwise.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean isOutgoing() {
+ return mOutgoing;
+ }
+
+ /**
+ * Checks if the ringtone will be generated by the connected phone
+ *
+ * @return <code>true</code> if in band ring is enabled, <code>false</code> otherwise.
+ */
+ public boolean isInBandRing() {
+ return mInBandRing;
+ }
+
+
+ @Override
+ public String toString() {
+ return toString(false);
+ }
+
+ /**
+ * Generate a log string for this call
+ * @param loggable whether device address should be logged
+ * @return log string
+ */
+ public String toString(boolean loggable) {
+ StringBuilder builder = new StringBuilder("BluetoothHeadsetClientCall{mDevice: ");
+ builder.append(loggable ? mDevice : mDevice.hashCode());
+ builder.append(", mId: ");
+ builder.append(mId);
+ builder.append(", mUUID: ");
+ builder.append(mUUID);
+ builder.append(", mState: ");
+ switch (mState) {
+ case CALL_STATE_ACTIVE:
+ builder.append("ACTIVE");
+ break;
+ case CALL_STATE_HELD:
+ builder.append("HELD");
+ break;
+ case CALL_STATE_DIALING:
+ builder.append("DIALING");
+ break;
+ case CALL_STATE_ALERTING:
+ builder.append("ALERTING");
+ break;
+ case CALL_STATE_INCOMING:
+ builder.append("INCOMING");
+ break;
+ case CALL_STATE_WAITING:
+ builder.append("WAITING");
+ break;
+ case CALL_STATE_HELD_BY_RESPONSE_AND_HOLD:
+ builder.append("HELD_BY_RESPONSE_AND_HOLD");
+ break;
+ case CALL_STATE_TERMINATED:
+ builder.append("TERMINATED");
+ break;
+ default:
+ builder.append(mState);
+ break;
+ }
+ builder.append(", mNumber: ");
+ builder.append(loggable ? mNumber : mNumber.hashCode());
+ builder.append(", mMultiParty: ");
+ builder.append(mMultiParty);
+ builder.append(", mOutgoing: ");
+ builder.append(mOutgoing);
+ builder.append(", mInBandRing: ");
+ builder.append(mInBandRing);
+ builder.append("}");
+ return builder.toString();
+ }
+
+ /**
+ * {@link Parcelable.Creator} interface implementation.
+ */
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothHeadsetClientCall> CREATOR =
+ new Parcelable.Creator<BluetoothHeadsetClientCall>() {
+ @Override
+ public BluetoothHeadsetClientCall createFromParcel(Parcel in) {
+ return new BluetoothHeadsetClientCall((BluetoothDevice) in.readParcelable(null),
+ in.readInt(), UUID.fromString(in.readString()), in.readInt(),
+ in.readString(), in.readInt() == 1, in.readInt() == 1,
+ in.readInt() == 1);
+ }
+
+ @Override
+ public BluetoothHeadsetClientCall[] newArray(int size) {
+ return new BluetoothHeadsetClientCall[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeParcelable(mDevice, 0);
+ out.writeInt(mId);
+ out.writeString(mUUID.toString());
+ out.writeInt(mState);
+ out.writeString(mNumber);
+ out.writeInt(mMultiParty ? 1 : 0);
+ out.writeInt(mOutgoing ? 1 : 0);
+ out.writeInt(mInBandRing ? 1 : 0);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHealth.java b/framework/java/android/bluetooth/BluetoothHealth.java
new file mode 100644
index 0000000000..65f68a943e
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHealth.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2011 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 android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Public API for Bluetooth Health Profile.
+ *
+ * <p>BluetoothHealth is a proxy object for controlling the Bluetooth
+ * Service via IPC.
+ *
+ * <p> How to connect to a health device which is acting in the source role.
+ * <li> Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHealth proxy object. </li>
+ * <li> Create an {@link BluetoothHealth} callback and call
+ * {@link #registerSinkAppConfiguration} to register an application
+ * configuration </li>
+ * <li> Pair with the remote device. This currently needs to be done manually
+ * from Bluetooth Settings </li>
+ * <li> Connect to a health device using {@link #connectChannelToSource}. Some
+ * devices will connect the channel automatically. The {@link BluetoothHealth}
+ * callback will inform the application of channel state change. </li>
+ * <li> Use the file descriptor provided with a connected channel to read and
+ * write data to the health channel. </li>
+ * <li> The received data needs to be interpreted using a health manager which
+ * implements the IEEE 11073-xxxxx specifications.
+ * <li> When done, close the health channel by calling {@link #disconnectChannel}
+ * and unregister the application configuration calling
+ * {@link #unregisterAppConfiguration}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New apps
+ * should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public final class BluetoothHealth implements BluetoothProfile {
+ private static final String TAG = "BluetoothHealth";
+ /**
+ * Health Profile Source Role - the health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int SOURCE_ROLE = 1 << 0;
+
+ /**
+ * Health Profile Sink Role the device talking to the health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int SINK_ROLE = 1 << 1;
+
+ /**
+ * Health Profile - Channel Type used - Reliable
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int CHANNEL_TYPE_RELIABLE = 10;
+
+ /**
+ * Health Profile - Channel Type used - Streaming
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int CHANNEL_TYPE_STREAMING = 11;
+
+ /**
+ * Hide auto-created default constructor
+ * @hide
+ */
+ BluetoothHealth() {}
+
+ /**
+ * Register an application configuration that acts as a Health SINK.
+ * This is the configuration that will be used to communicate with health devices
+ * which will act as the {@link #SOURCE_ROLE}. This is an asynchronous call and so
+ * the callback is used to notify success or failure if the function returns true.
+ *
+ * @param name The friendly name associated with the application or configuration.
+ * @param dataType The dataType of the Source role of Health Profile to which the sink wants to
+ * connect to.
+ * @param callback A callback to indicate success or failure of the registration and all
+ * operations done on this application configuration.
+ * @return If true, callback will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean registerSinkAppConfiguration(String name, int dataType,
+ BluetoothHealthCallback callback) {
+ Log.e(TAG, "registerSinkAppConfiguration(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Unregister an application configuration that has been registered using
+ * {@link #registerSinkAppConfiguration}
+ *
+ * @param config The health app configuration
+ * @return Success or failure.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean unregisterAppConfiguration(BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "unregisterAppConfiguration(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Connect to a health device which has the {@link #SOURCE_ROLE}.
+ * This is an asynchronous call. If this function returns true, the callback
+ * associated with the application configuration will be called.
+ *
+ * @param device The remote Bluetooth device.
+ * @param config The application configuration which has been registered using {@link
+ * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
+ * @return If true, the callback associated with the application config will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean connectChannelToSource(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "connectChannelToSource(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Disconnect a connected health channel.
+ * This is an asynchronous call. If this function returns true, the callback
+ * associated with the application configuration will be called.
+ *
+ * @param device The remote Bluetooth device.
+ * @param config The application configuration which has been registered using {@link
+ * #registerSinkAppConfiguration(String, int, BluetoothHealthCallback) }
+ * @param channelId The channel id associated with the channel
+ * @return If true, the callback associated with the application config will be called.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public boolean disconnectChannel(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config, int channelId) {
+ Log.e(TAG, "disconnectChannel(): BluetoothHealth is deprecated");
+ return false;
+ }
+
+ /**
+ * Get the file descriptor of the main channel associated with the remote device
+ * and application configuration.
+ *
+ * <p> Its the responsibility of the caller to close the ParcelFileDescriptor
+ * when done.
+ *
+ * @param device The remote Bluetooth health device
+ * @param config The application configuration
+ * @return null on failure, ParcelFileDescriptor on success.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public ParcelFileDescriptor getMainChannelFd(BluetoothDevice device,
+ BluetoothHealthAppConfiguration config) {
+ Log.e(TAG, "getMainChannelFd(): BluetoothHealth is deprecated");
+ return null;
+ }
+
+ /**
+ * Get the current connection state of the profile.
+ *
+ * This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter with the remote device. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @param device Remote bluetooth device.
+ * @return State of the profile connection. One of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public int getConnectionState(BluetoothDevice device) {
+ Log.e(TAG, "getConnectionState(): BluetoothHealth is deprecated");
+ return STATE_DISCONNECTED;
+ }
+
+ /**
+ * Get connected devices for the health profile.
+ *
+ * <p> Return the set of devices which are in state {@link #STATE_CONNECTED}
+ *
+ * This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter for this profile. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @return List of devices. The list will be empty on error.
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public List<BluetoothDevice> getConnectedDevices() {
+ Log.e(TAG, "getConnectedDevices(): BluetoothHealth is deprecated");
+ return new ArrayList<>();
+ }
+
+ /**
+ * Get a list of devices that match any of the given connection
+ * states.
+ *
+ * <p> If none of the devices match any of the given states,
+ * an empty list will be returned.
+ *
+ * <p>This is not specific to any application configuration but represents the connection
+ * state of the local Bluetooth adapter for this profile. This can be used
+ * by applications like status bar which would just like to know the state of the
+ * local adapter.
+ *
+ * @param states Array of states. States can be one of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING},
+ * @return List of devices. The list will be empty on error.
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ Log.e(TAG, "getDevicesMatchingConnectionStates(): BluetoothHealth is deprecated");
+ return new ArrayList<>();
+ }
+
+ /** Health Channel Connection State - Disconnected
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_DISCONNECTED = 0;
+ /** Health Channel Connection State - Connecting
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_CONNECTING = 1;
+ /** Health Channel Connection State - Connected
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_CONNECTED = 2;
+ /** Health Channel Connection State - Disconnecting
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int STATE_CHANNEL_DISCONNECTING = 3;
+
+ /** Health App Configuration registration success
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_REGISTRATION_SUCCESS = 0;
+ /** Health App Configuration registration failure
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_REGISTRATION_FAILURE = 1;
+ /** Health App Configuration un-registration success
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_UNREGISTRATION_SUCCESS = 2;
+ /** Health App Configuration un-registration failure
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final int APP_CONFIG_UNREGISTRATION_FAILURE = 3;
+}
diff --git a/framework/java/android/bluetooth/BluetoothHealthAppConfiguration.java b/framework/java/android/bluetooth/BluetoothHealthAppConfiguration.java
new file mode 100644
index 0000000000..2f66df258b
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHealthAppConfiguration.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2011 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The Bluetooth Health Application Configuration that is used in conjunction with
+ * the {@link BluetoothHealth} class. This class represents an application configuration
+ * that the Bluetooth Health third party application will register to communicate with the
+ * remote Bluetooth health device.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public final class BluetoothHealthAppConfiguration implements Parcelable {
+
+ /**
+ * Hide auto-created default constructor
+ * @hide
+ */
+ BluetoothHealthAppConfiguration() {}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Return the data type associated with this application configuration.
+ *
+ * @return dataType
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public int getDataType() {
+ return 0;
+ }
+
+ /**
+ * Return the name of the application configuration.
+ *
+ * @return String name
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public String getName() {
+ return null;
+ }
+
+ /**
+ * Return the role associated with this application configuration.
+ *
+ * @return One of {@link BluetoothHealth#SOURCE_ROLE} or {@link BluetoothHealth#SINK_ROLE}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public int getRole() {
+ return 0;
+ }
+
+ /**
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothHealthAppConfiguration> CREATOR =
+ new Parcelable.Creator<BluetoothHealthAppConfiguration>() {
+ @Override
+ public BluetoothHealthAppConfiguration createFromParcel(Parcel in) {
+ return new BluetoothHealthAppConfiguration();
+ }
+
+ @Override
+ public BluetoothHealthAppConfiguration[] newArray(int size) {
+ return new BluetoothHealthAppConfiguration[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {}
+}
diff --git a/framework/java/android/bluetooth/BluetoothHealthCallback.java b/framework/java/android/bluetooth/BluetoothHealthCallback.java
new file mode 100644
index 0000000000..4769212c53
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHealthCallback.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2011 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 android.annotation.BinderThread;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+/**
+ * This abstract class is used to implement {@link BluetoothHealth} callbacks.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+@Deprecated
+public abstract class BluetoothHealthCallback {
+ private static final String TAG = "BluetoothHealthCallback";
+
+ /**
+ * Callback to inform change in registration state of the health
+ * application.
+ * <p> This callback is called on the binder thread (not on the UI thread)
+ *
+ * @param config Bluetooth Health app configuration
+ * @param status Success or failure of the registration or unregistration calls. Can be one of
+ * {@link BluetoothHealth#APP_CONFIG_REGISTRATION_SUCCESS} or {@link
+ * BluetoothHealth#APP_CONFIG_REGISTRATION_FAILURE} or
+ * {@link BluetoothHealth#APP_CONFIG_UNREGISTRATION_SUCCESS}
+ * or {@link BluetoothHealth#APP_CONFIG_UNREGISTRATION_FAILURE}
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @BinderThread
+ @Deprecated
+ public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
+ int status) {
+ Log.d(TAG, "onHealthAppConfigurationStatusChange: " + config + "Status: " + status);
+ }
+
+ /**
+ * Callback to inform change in channel state.
+ * <p> Its the responsibility of the implementor of this callback to close the
+ * parcel file descriptor when done. This callback is called on the Binder
+ * thread (not the UI thread)
+ *
+ * @param config The Health app configutation
+ * @param device The Bluetooth Device
+ * @param prevState The previous state of the channel
+ * @param newState The new state of the channel.
+ * @param fd The Parcel File Descriptor when the channel state is connected.
+ * @param channelId The id associated with the channel. This id will be used in future calls
+ * like when disconnecting the channel.
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()(int)}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @BinderThread
+ @Deprecated
+ public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
+ BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,
+ int channelId) {
+ Log.d(TAG, "onHealthChannelStateChange: " + config + "Device: " + device
+ + "prevState:" + prevState + "newState:" + newState + "ParcelFd:" + fd
+ + "ChannelId:" + channelId);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHearingAid.java b/framework/java/android/bluetooth/BluetoothHearingAid.java
new file mode 100644
index 0000000000..339a75fe0f
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHearingAid.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright 2018 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.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.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the public APIs to control the Hearing Aid profile.
+ *
+ * <p>BluetoothHearingAid is a proxy object for controlling the Bluetooth Hearing Aid
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHearingAid proxy object.
+ *
+ * <p> Android only supports one set of connected Bluetooth Hearing Aid device at a time. Each
+ * method is protected with its appropriate permission.
+ */
+public final class BluetoothHearingAid implements BluetoothProfile {
+ private static final String TAG = "BluetoothHearingAid";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Hearing Aid
+ * profile. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own connection state changes.S
+ *
+ * <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.hearingaid.profile.action.CONNECTION_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)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.hearingaid.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * This device represents Left Hearing Aid.
+ *
+ * @hide
+ */
+ public static final int SIDE_LEFT = IBluetoothHearingAid.SIDE_LEFT;
+
+ /**
+ * This device represents Right Hearing Aid.
+ *
+ * @hide
+ */
+ public static final int SIDE_RIGHT = IBluetoothHearingAid.SIDE_RIGHT;
+
+ /**
+ * This device is Monaural.
+ *
+ * @hide
+ */
+ public static final int MODE_MONAURAL = IBluetoothHearingAid.MODE_MONAURAL;
+
+ /**
+ * This device is Binaural (should receive only left or right audio).
+ *
+ * @hide
+ */
+ public static final int MODE_BINAURAL = IBluetoothHearingAid.MODE_BINAURAL;
+
+ /**
+ * Indicates the HiSyncID could not be read and is unavailable.
+ *
+ * @hide
+ */
+ public static final long HI_SYNC_ID_INVALID = IBluetoothHearingAid.HI_SYNC_ID_INVALID;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHearingAid> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HEARING_AID,
+ "BluetoothHearingAid", IBluetoothHearingAid.class.getName()) {
+ @Override
+ public IBluetoothHearingAid getServiceInterface(IBinder service) {
+ return IBluetoothHearingAid.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHearingAid proxy object for interacting with the local
+ * Bluetooth Hearing Aid service.
+ */
+ /* package */ BluetoothHearingAid(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /*package*/ void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHearingAid getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ 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.connect(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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ 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.disconnect(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 @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHearingAid service = getService();
+ 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.getConnectedDevices(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 @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
+ @NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHearingAid service = getService();
+ 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 @BluetoothProfile.BtProfileState int getConnectionState(
+ @NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ 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.getConnectionState(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;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, Hearing Aid audio
+ * streaming is to the active Hearing Aid device. 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 the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ 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 physical Hearing Aid devices that are active
+ *
+ * @return the list of active devices. The first element is the left active
+ * device; the second element is the right active device. If either or both side
+ * is not active, it will be null on that position. Returns empty list on error.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getActiveDevices() {
+ if (VDBG) log("getActiveDevices()");
+ final IBluetoothHearingAid service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ 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.getActiveDevices(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;
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ verifyDeviceNotNull(device, "setConnectionPolicy");
+ final IBluetoothHearingAid service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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 + ")");
+ verifyDeviceNotNull(device, "getConnectionPolicy");
+ final IBluetoothHearingAid service = getService();
+ 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;
+ }
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ /**
+ * Tells remote device to set an absolute volume.
+ *
+ * @param volume Absolute volume to be set on remote
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void setVolume(int volume) {
+ if (DBG) Log.d(TAG, "setVolume(" + volume + ")");
+ final IBluetoothHearingAid service = getService();
+ 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.setVolume(volume, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Get the HiSyncId (unique hearing aid device identifier) of the device.
+ *
+ * <a href=https://source.android.com/devices/bluetooth/asha#hisyncid>HiSyncId documentation
+ * can be found here</a>
+ *
+ * @param device Bluetooth device
+ * @return the HiSyncId of the device
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public long getHiSyncId(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getHiSyncId(" + device + ")");
+ verifyDeviceNotNull(device, "getConnectionPolicy");
+ final IBluetoothHearingAid service = getService();
+ final long defaultValue = HI_SYNC_ID_INVALID;
+ 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<Long> recv = new SynchronousResultReceiver();
+ service.getHiSyncId(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 side of the device.
+ *
+ * @param device Bluetooth device.
+ * @return SIDE_LEFT or SIDE_RIGHT
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getDeviceSide(BluetoothDevice device) {
+ if (VDBG) log("getDeviceSide(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = SIDE_LEFT;
+ 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.getDeviceSide(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 mode of the device.
+ *
+ * @param device Bluetooth device
+ * @return MODE_MONAURAL or MODE_BINAURAL
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getDeviceMode(BluetoothDevice device) {
+ if (VDBG) log("getDeviceMode(" + device + ")");
+ final IBluetoothHearingAid service = getService();
+ final int defaultValue = MODE_MONAURAL;
+ 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.getDeviceMode(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;
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private void verifyDeviceNotNull(BluetoothDevice device, String methodName) {
+ if (device == null) {
+ Log.e(TAG, methodName + ": device param is null");
+ throw new IllegalArgumentException("Device cannot be null");
+ }
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHidDevice.java b/framework/java/android/bluetooth/BluetoothHidDevice.java
new file mode 100644
index 0000000000..44a355b5f7
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHidDevice.java
@@ -0,0 +1,848 @@
+/*
+ * Copyright (C) 2016 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Provides the public APIs to control the Bluetooth HID Device profile.
+ *
+ * <p>BluetoothHidDevice is a proxy object for controlling the Bluetooth HID Device Service via IPC.
+ * Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothHidDevice proxy object.
+ */
+public final class BluetoothHidDevice implements BluetoothProfile {
+ private static final String TAG = BluetoothHidDevice.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Input Host profile.
+ *
+ * <p>This intent will have 3 extras:
+ *
+ * <ul>
+ * <li>{@link #EXTRA_STATE} - The current state of the profile.
+ * <li>{@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.
+ * <li>{@link BluetoothDevice#EXTRA_DEVICE} - The remote device.
+ * </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.hiddevice.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Constant representing unspecified HID device subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS1_NONE = (byte) 0x00;
+ /**
+ * Constant representing keyboard subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS1_KEYBOARD = (byte) 0x40;
+ /**
+ * Constant representing mouse subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS1_MOUSE = (byte) 0x80;
+ /**
+ * Constant representing combo keyboard and mouse subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS1_COMBO = (byte) 0xC0;
+
+ /**
+ * Constant representing uncategorized HID device subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_UNCATEGORIZED = (byte) 0x00;
+ /**
+ * Constant representing joystick subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_JOYSTICK = (byte) 0x01;
+ /**
+ * Constant representing gamepad subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_GAMEPAD = (byte) 0x02;
+ /**
+ * Constant representing remote control subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_REMOTE_CONTROL = (byte) 0x03;
+ /**
+ * Constant representing sensing device subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_SENSING_DEVICE = (byte) 0x04;
+ /**
+ * Constant representing digitizer tablet subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_DIGITIZER_TABLET = (byte) 0x05;
+ /**
+ * Constant representing card reader subclass.
+ *
+ * @see #registerApp (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)
+ */
+ public static final byte SUBCLASS2_CARD_READER = (byte) 0x06;
+
+ /**
+ * Constant representing HID Input Report type.
+ *
+ * @see Callback#onGetReport(BluetoothDevice, byte, byte, int)
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ * @see Callback#onInterruptData(BluetoothDevice, byte, byte[])
+ */
+ public static final byte REPORT_TYPE_INPUT = (byte) 1;
+ /**
+ * Constant representing HID Output Report type.
+ *
+ * @see Callback#onGetReport(BluetoothDevice, byte, byte, int)
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ * @see Callback#onInterruptData(BluetoothDevice, byte, byte[])
+ */
+ public static final byte REPORT_TYPE_OUTPUT = (byte) 2;
+ /**
+ * Constant representing HID Feature Report type.
+ *
+ * @see Callback#onGetReport(BluetoothDevice, byte, byte, int)
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ * @see Callback#onInterruptData(BluetoothDevice, byte, byte[])
+ */
+ public static final byte REPORT_TYPE_FEATURE = (byte) 3;
+
+ /**
+ * Constant representing success response for Set Report.
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_SUCCESS = (byte) 0;
+ /**
+ * Constant representing error response for Set Report due to "not ready".
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_NOT_READY = (byte) 1;
+ /**
+ * Constant representing error response for Set Report due to "invalid report ID".
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_INVALID_RPT_ID = (byte) 2;
+ /**
+ * Constant representing error response for Set Report due to "unsupported request".
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_UNSUPPORTED_REQ = (byte) 3;
+ /**
+ * Constant representing error response for Set Report due to "invalid parameter".
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_INVALID_PARAM = (byte) 4;
+ /**
+ * Constant representing error response for Set Report with unknown reason.
+ *
+ * @see Callback#onSetReport(BluetoothDevice, byte, byte, byte[])
+ */
+ public static final byte ERROR_RSP_UNKNOWN = (byte) 14;
+
+ /**
+ * Constant representing boot protocol mode used set by host. Default is always {@link
+ * #PROTOCOL_REPORT_MODE} unless notified otherwise.
+ *
+ * @see Callback#onSetProtocol(BluetoothDevice, byte)
+ */
+ public static final byte PROTOCOL_BOOT_MODE = (byte) 0;
+ /**
+ * Constant representing report protocol mode used set by host. Default is always {@link
+ * #PROTOCOL_REPORT_MODE} unless notified otherwise.
+ *
+ * @see Callback#onSetProtocol(BluetoothDevice, byte)
+ */
+ public static final byte PROTOCOL_REPORT_MODE = (byte) 1;
+
+ /**
+ * The template class that applications use to call callback functions on events from the HID
+ * host. Callback functions are wrapped in this class and registered to the Android system
+ * during app registration.
+ */
+ public abstract static class Callback {
+
+ private static final String TAG = "BluetoothHidDevCallback";
+
+ /**
+ * Callback called when application registration state changes. Usually it's called due to
+ * either {@link BluetoothHidDevice#registerApp (String, String, String, byte, byte[],
+ * Executor, Callback)} or {@link BluetoothHidDevice#unregisterApp()} , but can be also
+ * unsolicited in case e.g. Bluetooth was turned off in which case application is
+ * unregistered automatically.
+ *
+ * @param pluggedDevice {@link BluetoothDevice} object which represents host that currently
+ * has Virtual Cable established with device. Only valid when application is registered,
+ * can be <code>null</code>.
+ * @param registered <code>true</code> if application is registered, <code>false</code>
+ * otherwise.
+ */
+ public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
+ Log.d(
+ TAG,
+ "onAppStatusChanged: pluggedDevice="
+ + pluggedDevice
+ + " registered="
+ + registered);
+ }
+
+ /**
+ * Callback called when connection state with remote host was changed. Application can
+ * assume than Virtual Cable is established when called with {@link
+ * BluetoothProfile#STATE_CONNECTED} <code>state</code>.
+ *
+ * @param device {@link BluetoothDevice} object representing host device which connection
+ * state was changed.
+ * @param state Connection state as defined in {@link BluetoothProfile}.
+ */
+ public void onConnectionStateChanged(BluetoothDevice device, int state) {
+ Log.d(TAG, "onConnectionStateChanged: device=" + device + " state=" + state);
+ }
+
+ /**
+ * Callback called when GET_REPORT is received from remote host. Should be replied by
+ * application using {@link BluetoothHidDevice#replyReport(BluetoothDevice, byte, byte,
+ * byte[])}.
+ *
+ * @param type Requested Report Type.
+ * @param id Requested Report Id, can be 0 if no Report Id are defined in descriptor.
+ * @param bufferSize Requested buffer size, application shall respond with at least given
+ * number of bytes.
+ */
+ public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) {
+ Log.d(
+ TAG,
+ "onGetReport: device="
+ + device
+ + " type="
+ + type
+ + " id="
+ + id
+ + " bufferSize="
+ + bufferSize);
+ }
+
+ /**
+ * Callback called when SET_REPORT is received from remote host. In case received data are
+ * invalid, application shall respond with {@link
+ * BluetoothHidDevice#reportError(BluetoothDevice, byte)}.
+ *
+ * @param type Report Type.
+ * @param id Report Id.
+ * @param data Report data.
+ */
+ public void onSetReport(BluetoothDevice device, byte type, byte id, byte[] data) {
+ Log.d(TAG, "onSetReport: device=" + device + " type=" + type + " id=" + id);
+ }
+
+ /**
+ * Callback called when SET_PROTOCOL is received from remote host. Application shall use
+ * this information to send only reports valid for given protocol mode. By default, {@link
+ * BluetoothHidDevice#PROTOCOL_REPORT_MODE} shall be assumed.
+ *
+ * @param protocol Protocol Mode.
+ */
+ public void onSetProtocol(BluetoothDevice device, byte protocol) {
+ Log.d(TAG, "onSetProtocol: device=" + device + " protocol=" + protocol);
+ }
+
+ /**
+ * Callback called when report data is received over interrupt channel. Report Type is
+ * assumed to be {@link BluetoothHidDevice#REPORT_TYPE_OUTPUT}.
+ *
+ * @param reportId Report Id.
+ * @param data Report data.
+ */
+ public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) {
+ Log.d(TAG, "onInterruptData: device=" + device + " reportId=" + reportId);
+ }
+
+ /**
+ * Callback called when Virtual Cable is removed. After this callback is received connection
+ * will be disconnected automatically.
+ */
+ public void onVirtualCableUnplug(BluetoothDevice device) {
+ Log.d(TAG, "onVirtualCableUnplug: device=" + device);
+ }
+ }
+
+ private static class CallbackWrapper extends IBluetoothHidDeviceCallback.Stub {
+
+ private final Executor mExecutor;
+ private final Callback mCallback;
+ private final AttributionSource mAttributionSource;
+
+ CallbackWrapper(Executor executor, Callback callback, AttributionSource attributionSource) {
+ mExecutor = executor;
+ mCallback = callback;
+ mAttributionSource = attributionSource;
+ }
+
+ @Override
+ public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
+ Attributable.setAttributionSource(pluggedDevice, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onAppStatusChanged(pluggedDevice, registered));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onConnectionStateChanged(BluetoothDevice device, int state) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onConnectionStateChanged(device, state));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onGetReport(BluetoothDevice device, byte type, byte id, int bufferSize) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onGetReport(device, type, id, bufferSize));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onSetReport(BluetoothDevice device, byte type, byte id, byte[] data) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onSetReport(device, type, id, data));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onSetProtocol(BluetoothDevice device, byte protocol) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onSetProtocol(device, protocol));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onInterruptData(BluetoothDevice device, byte reportId, byte[] data) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onInterruptData(device, reportId, data));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void onVirtualCableUnplug(BluetoothDevice device) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ final long token = clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onVirtualCableUnplug(device));
+ } finally {
+ restoreCallingIdentity(token);
+ }
+ }
+ }
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHidDevice> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HID_DEVICE,
+ "BluetoothHidDevice", IBluetoothHidDevice.class.getName()) {
+ @Override
+ public IBluetoothHidDevice getServiceInterface(IBinder service) {
+ return IBluetoothHidDevice.Stub.asInterface(service);
+ }
+ };
+
+ BluetoothHidDevice(Context context, ServiceListener listener, BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHidDevice getService() {
+ return mProfileConnector.getService();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ final IBluetoothHidDevice service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ 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.getConnectedDevices(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(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ final IBluetoothHidDevice service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ 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(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ final IBluetoothHidDevice service = getService();
+ final int defaultValue = STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getConnectionState(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;
+ }
+
+ /**
+ * Registers application to be used for HID device. Connections to HID Device are only possible
+ * when application is registered. Only one application can be registered at one time. When an
+ * application is registered, the HID Host service will be disabled until it is unregistered.
+ * When no longer used, application should be unregistered using {@link #unregisterApp()}. The
+ * app will be automatically unregistered if it is not foreground. The registration status
+ * should be tracked by the application by handling callback from Callback#onAppStatusChanged.
+ * The app registration status is not related to the return value of this method.
+ *
+ * @param sdp {@link BluetoothHidDeviceAppSdpSettings} object of HID Device SDP record. The HID
+ * Device SDP record is required.
+ * @param inQos {@link BluetoothHidDeviceAppQosSettings} object of Incoming QoS Settings. The
+ * Incoming QoS Settings is not required. Use null or default
+ * BluetoothHidDeviceAppQosSettings.Builder for default values.
+ * @param outQos {@link BluetoothHidDeviceAppQosSettings} object of Outgoing QoS Settings. The
+ * Outgoing QoS Settings is not required. Use null or default
+ * BluetoothHidDeviceAppQosSettings.Builder for default values.
+ * @param executor {@link Executor} object on which callback will be executed. The Executor
+ * object is required.
+ * @param callback {@link Callback} object to which callback messages will be sent. The Callback
+ * object is required.
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean registerApp(
+ BluetoothHidDeviceAppSdpSettings sdp,
+ BluetoothHidDeviceAppQosSettings inQos,
+ BluetoothHidDeviceAppQosSettings outQos,
+ Executor executor,
+ Callback callback) {
+ boolean result = false;
+
+ if (sdp == null) {
+ throw new IllegalArgumentException("sdp parameter cannot be null");
+ }
+
+ if (executor == null) {
+ throw new IllegalArgumentException("executor parameter cannot be null");
+ }
+
+ if (callback == null) {
+ throw new IllegalArgumentException("callback parameter cannot be null");
+ }
+
+ final IBluetoothHidDevice service = getService();
+ final boolean defaultValue = result;
+ 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();
+ CallbackWrapper cbw = new CallbackWrapper(executor, callback, mAttributionSource);
+ service.registerApp(sdp, inQos, outQos, cbw, mAttributionSource, recv);
+ result = recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Unregisters application. Active connection will be disconnected and no new connections will
+ * be allowed until registered again using {@link #registerApp
+ * (BluetoothHidDeviceAppQosSettings, BluetoothHidDeviceAppQosSettings,
+ * BluetoothHidDeviceAppQosSettings, Executor, Callback)}. The registration status should be
+ * tracked by the application by handling callback from Callback#onAppStatusChanged. The app
+ * registration status is not related to the return value of this method.
+ *
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean unregisterApp() {
+ final IBluetoothHidDevice service = getService();
+ 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.unregisterApp(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sends report to remote host using interrupt channel.
+ *
+ * @param id Report Id, as defined in descriptor. Can be 0 in case Report Id are not defined in
+ * descriptor.
+ * @param data Report data, not including Report Id.
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendReport(BluetoothDevice device, int id, byte[] data) {
+ final IBluetoothHidDevice service = getService();
+ 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.sendReport(device, id, data, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sends report to remote host as reply for GET_REPORT request from {@link
+ * Callback#onGetReport(BluetoothDevice, byte, byte, int)}.
+ *
+ * @param type Report Type, as in request.
+ * @param id Report Id, as in request.
+ * @param data Report data, not including Report Id.
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean replyReport(BluetoothDevice device, byte type, byte id, byte[] data) {
+ final IBluetoothHidDevice service = getService();
+ 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.replyReport(device, type, id, data, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sends error handshake message as reply for invalid SET_REPORT request from {@link
+ * Callback#onSetReport(BluetoothDevice, byte, byte, byte[])}.
+ *
+ * @param error Error to be sent for SET_REPORT via HANDSHAKE.
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean reportError(BluetoothDevice device, byte error) {
+ final IBluetoothHidDevice service = getService();
+ 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.reportError(device, error, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the application name of the current HidDeviceService user.
+ *
+ * @return the current user name, or empty string if cannot get the name
+ * {@hide}
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public String getUserAppName() {
+ final IBluetoothHidDevice service = getService();
+ final String defaultValue = "";
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<String> recv = new SynchronousResultReceiver();
+ service.getUserAppName(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 connection to host which is currently paired with this device. If the application
+ * is not registered, #connect(BluetoothDevice) will fail. The connection state should be
+ * tracked by the application by handling callback from Callback#onConnectionStateChanged. The
+ * connection state is not related to the return value of this method.
+ *
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(BluetoothDevice device) {
+ final IBluetoothHidDevice service = getService();
+ 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.connect(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;
+ }
+
+ /**
+ * Disconnects from currently connected host. The connection state should be tracked by the
+ * application by handling callback from Callback#onConnectionStateChanged. The connection state
+ * is not related to the return value of this method.
+ *
+ * @return true if the command is successfully sent; otherwise false.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ final IBluetoothHidDevice service = getService();
+ 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.disconnect(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;
+ }
+
+ /**
+ * Connects Hid Device if connectionPolicy is {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}
+ * and disconnects Hid device if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}.
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of:
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
+ * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy determines whether hid device should be connected or disconnected
+ * @return true if hid device is connected or disconnected, false otherwise
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothHidDevice service = getService();
+ 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;
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java b/framework/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java
new file mode 100644
index 0000000000..b21ebe59d8
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHidDeviceAppQosSettings.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2016 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the Quality of Service (QoS) settings for a Bluetooth HID Device application.
+ *
+ * <p>The BluetoothHidDevice framework will update the L2CAP QoS settings for the app during
+ * registration.
+ *
+ * <p>{@see BluetoothHidDevice}
+ */
+public final class BluetoothHidDeviceAppQosSettings implements Parcelable {
+
+ private final int mServiceType;
+ private final int mTokenRate;
+ private final int mTokenBucketSize;
+ private final int mPeakBandwidth;
+ private final int mLatency;
+ private final int mDelayVariation;
+
+ public static final int SERVICE_NO_TRAFFIC = 0x00;
+ public static final int SERVICE_BEST_EFFORT = 0x01;
+ public static final int SERVICE_GUARANTEED = 0x02;
+
+ public static final int MAX = (int) 0xffffffff;
+
+ /**
+ * Create a BluetoothHidDeviceAppQosSettings object for the Bluetooth L2CAP channel. The QoS
+ * Settings is optional. Please refer to Bluetooth HID Specfication v1.1.1 Section 5.2 and
+ * Appendix D for parameters.
+ *
+ * @param serviceType L2CAP service type, default = SERVICE_BEST_EFFORT
+ * @param tokenRate L2CAP token rate, default = 0
+ * @param tokenBucketSize L2CAP token bucket size, default = 0
+ * @param peakBandwidth L2CAP peak bandwidth, default = 0
+ * @param latency L2CAP latency, default = MAX
+ * @param delayVariation L2CAP delay variation, default = MAX
+ */
+ public BluetoothHidDeviceAppQosSettings(
+ int serviceType,
+ int tokenRate,
+ int tokenBucketSize,
+ int peakBandwidth,
+ int latency,
+ int delayVariation) {
+ mServiceType = serviceType;
+ mTokenRate = tokenRate;
+ mTokenBucketSize = tokenBucketSize;
+ mPeakBandwidth = peakBandwidth;
+ mLatency = latency;
+ mDelayVariation = delayVariation;
+ }
+
+ public int getServiceType() {
+ return mServiceType;
+ }
+
+ public int getTokenRate() {
+ return mTokenRate;
+ }
+
+ public int getTokenBucketSize() {
+ return mTokenBucketSize;
+ }
+
+ public int getPeakBandwidth() {
+ return mPeakBandwidth;
+ }
+
+ public int getLatency() {
+ return mLatency;
+ }
+
+ public int getDelayVariation() {
+ return mDelayVariation;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothHidDeviceAppQosSettings> CREATOR =
+ new Parcelable.Creator<BluetoothHidDeviceAppQosSettings>() {
+
+ @Override
+ public BluetoothHidDeviceAppQosSettings createFromParcel(Parcel in) {
+
+ return new BluetoothHidDeviceAppQosSettings(
+ in.readInt(),
+ in.readInt(),
+ in.readInt(),
+ in.readInt(),
+ in.readInt(),
+ in.readInt());
+ }
+
+ @Override
+ public BluetoothHidDeviceAppQosSettings[] newArray(int size) {
+ return new BluetoothHidDeviceAppQosSettings[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mServiceType);
+ out.writeInt(mTokenRate);
+ out.writeInt(mTokenBucketSize);
+ out.writeInt(mPeakBandwidth);
+ out.writeInt(mLatency);
+ out.writeInt(mDelayVariation);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java b/framework/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java
new file mode 100644
index 0000000000..4e1a2aaedc
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHidDeviceAppSdpSettings.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 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 android.os.Parcel;
+import android.os.Parcelable;
+import android.util.EventLog;
+
+
+/**
+ * Represents the Service Discovery Protocol (SDP) settings for a Bluetooth HID Device application.
+ *
+ * <p>The BluetoothHidDevice framework adds the SDP record during app registration, so that the
+ * Android device can be discovered as a Bluetooth HID Device.
+ *
+ * <p>{@see BluetoothHidDevice}
+ */
+public final class BluetoothHidDeviceAppSdpSettings implements Parcelable {
+
+ private static final int MAX_DESCRIPTOR_SIZE = 2048;
+
+ private final String mName;
+ private final String mDescription;
+ private final String mProvider;
+ private final byte mSubclass;
+ private final byte[] mDescriptors;
+
+ /**
+ * Create a BluetoothHidDeviceAppSdpSettings object for the Bluetooth SDP record.
+ *
+ * @param name Name of this Bluetooth HID device. Maximum length is 50 bytes.
+ * @param description Description for this Bluetooth HID device. Maximum length is 50 bytes.
+ * @param provider Provider of this Bluetooth HID device. Maximum length is 50 bytes.
+ * @param subclass Subclass of this Bluetooth HID device. See <a
+ * href="www.usb.org/developers/hidpage/HID1_11.pdf">
+ * www.usb.org/developers/hidpage/HID1_11.pdf Section 4.2</a>
+ * @param descriptors Descriptors of this Bluetooth HID device. See <a
+ * href="www.usb.org/developers/hidpage/HID1_11.pdf">
+ * www.usb.org/developers/hidpage/HID1_11.pdf Chapter 6</a> Maximum length is 2048 bytes.
+ */
+ public BluetoothHidDeviceAppSdpSettings(
+ String name, String description, String provider, byte subclass, byte[] descriptors) {
+ mName = name;
+ mDescription = description;
+ mProvider = provider;
+ mSubclass = subclass;
+
+ if (descriptors == null || descriptors.length > MAX_DESCRIPTOR_SIZE) {
+ EventLog.writeEvent(0x534e4554, "119819889", -1, "");
+ throw new IllegalArgumentException("descriptors must be not null and shorter than "
+ + MAX_DESCRIPTOR_SIZE);
+ }
+ mDescriptors = descriptors.clone();
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getDescription() {
+ return mDescription;
+ }
+
+ public String getProvider() {
+ return mProvider;
+ }
+
+ public byte getSubclass() {
+ return mSubclass;
+ }
+
+ public byte[] getDescriptors() {
+ return mDescriptors;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothHidDeviceAppSdpSettings> CREATOR =
+ new Parcelable.Creator<BluetoothHidDeviceAppSdpSettings>() {
+
+ @Override
+ public BluetoothHidDeviceAppSdpSettings createFromParcel(Parcel in) {
+
+ return new BluetoothHidDeviceAppSdpSettings(
+ in.readString(),
+ in.readString(),
+ in.readString(),
+ in.readByte(),
+ in.createByteArray());
+ }
+
+ @Override
+ public BluetoothHidDeviceAppSdpSettings[] newArray(int size) {
+ return new BluetoothHidDeviceAppSdpSettings[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mName);
+ out.writeString(mDescription);
+ out.writeString(mProvider);
+ out.writeByte(mSubclass);
+ out.writeByteArray(mDescriptors);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHidHost.java b/framework/java/android/bluetooth/BluetoothHidHost.java
new file mode 100644
index 0000000000..ecbeddf2b8
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHidHost.java
@@ -0,0 +1,831 @@
+/*
+ * Copyright (C) 2011 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.Manifest;
+import android.annotation.NonNull;
+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.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * This class provides the public APIs to control the Bluetooth Input
+ * Device Profile.
+ *
+ * <p>BluetoothHidHost is a proxy object for controlling the Bluetooth
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothHidHost proxy object.
+ *
+ * <p>Each method is protected with its appropriate permission.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothHidHost implements BluetoothProfile {
+ private static final String TAG = "BluetoothHidHost";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Input
+ * Device 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}.
+ */
+ @SuppressLint("ActionValue")
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PROTOCOL_MODE_CHANGED =
+ "android.bluetooth.input.profile.action.PROTOCOL_MODE_CHANGED";
+
+ /**
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HANDSHAKE =
+ "android.bluetooth.input.profile.action.HANDSHAKE";
+
+ /**
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_REPORT =
+ "android.bluetooth.input.profile.action.REPORT";
+
+ /**
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_VIRTUAL_UNPLUG_STATUS =
+ "android.bluetooth.input.profile.action.VIRTUAL_UNPLUG_STATUS";
+
+ /**
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_IDLE_TIME_CHANGED =
+ "android.bluetooth.input.profile.action.IDLE_TIME_CHANGED";
+
+ /**
+ * Return codes for the connect and disconnect Bluez / Dbus calls.
+ *
+ * @hide
+ */
+ public static final int INPUT_DISCONNECT_FAILED_NOT_CONNECTED = 5000;
+
+ /**
+ * @hide
+ */
+ public static final int INPUT_CONNECT_FAILED_ALREADY_CONNECTED = 5001;
+
+ /**
+ * @hide
+ */
+ public static final int INPUT_CONNECT_FAILED_ATTEMPT_FAILED = 5002;
+
+ /**
+ * @hide
+ */
+ public static final int INPUT_OPERATION_GENERIC_FAILURE = 5003;
+
+ /**
+ * @hide
+ */
+ public static final int INPUT_OPERATION_SUCCESS = 5004;
+
+ /**
+ * @hide
+ */
+ public static final int PROTOCOL_REPORT_MODE = 0;
+
+ /**
+ * @hide
+ */
+ public static final int PROTOCOL_BOOT_MODE = 1;
+
+ /**
+ * @hide
+ */
+ public static final int PROTOCOL_UNSUPPORTED_MODE = 255;
+
+ /* int reportType, int reportType, int bufferSize */
+ /**
+ * @hide
+ */
+ public static final byte REPORT_TYPE_INPUT = 1;
+
+ /**
+ * @hide
+ */
+ public static final byte REPORT_TYPE_OUTPUT = 2;
+
+ /**
+ * @hide
+ */
+ public static final byte REPORT_TYPE_FEATURE = 3;
+
+ /**
+ * @hide
+ */
+ public static final int VIRTUAL_UNPLUG_STATUS_SUCCESS = 0;
+
+ /**
+ * @hide
+ */
+ public static final int VIRTUAL_UNPLUG_STATUS_FAIL = 1;
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_PROTOCOL_MODE =
+ "android.bluetooth.BluetoothHidHost.extra.PROTOCOL_MODE";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REPORT_TYPE =
+ "android.bluetooth.BluetoothHidHost.extra.REPORT_TYPE";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REPORT_ID =
+ "android.bluetooth.BluetoothHidHost.extra.REPORT_ID";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REPORT_BUFFER_SIZE =
+ "android.bluetooth.BluetoothHidHost.extra.REPORT_BUFFER_SIZE";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_REPORT = "android.bluetooth.BluetoothHidHost.extra.REPORT";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_STATUS = "android.bluetooth.BluetoothHidHost.extra.STATUS";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_VIRTUAL_UNPLUG_STATUS =
+ "android.bluetooth.BluetoothHidHost.extra.VIRTUAL_UNPLUG_STATUS";
+
+ /**
+ * @hide
+ */
+ public static final String EXTRA_IDLE_TIME =
+ "android.bluetooth.BluetoothHidHost.extra.IDLE_TIME";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHidHost> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HID_HOST,
+ "BluetoothHidHost", IBluetoothHidHost.class.getName()) {
+ @Override
+ public IBluetoothHidHost getServiceInterface(IBinder service) {
+ return IBluetoothHidHost.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHidHost proxy object for interacting with the local
+ * Bluetooth Service which handles the InputDevice profile
+ */
+ /* package */ BluetoothHidHost(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /*package*/ void close() {
+ if (VDBG) log("close()");
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHidHost getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <p> The system supports connection to multiple input devices.
+ *
+ * <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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.connect(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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.disconnect(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}
+ *
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothHidHost service = getService();
+ 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.getConnectedDevices(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}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothHidHost service = getService();
+ 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}
+ *
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ if (device == null) {
+ throw new IllegalArgumentException("device must not be null");
+ }
+ final IBluetoothHidHost service = getService();
+ 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.getConnectionState(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 #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ if (device == null) {
+ throw new IllegalArgumentException("device must not be null");
+ }
+ final IBluetoothHidHost service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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 + ")");
+ if (device == null) {
+ throw new IllegalArgumentException("device must not be null");
+ }
+ final IBluetoothHidHost service = getService();
+ 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;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ /**
+ * Initiate virtual unplug for a HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean virtualUnplug(BluetoothDevice device) {
+ if (DBG) log("virtualUnplug(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.virtualUnplug(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;
+ }
+
+ /**
+ * Send Get_Protocol_Mode command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getProtocolMode(BluetoothDevice device) {
+ if (VDBG) log("getProtocolMode(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.getProtocolMode(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;
+ }
+
+ /**
+ * Send Set_Protocol_Mode command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setProtocolMode(BluetoothDevice device, int protocolMode) {
+ if (DBG) log("setProtocolMode(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.setProtocolMode(device, protocolMode, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Get_Report command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @param reportType Report type
+ * @param reportId Report ID
+ * @param bufferSize Report receiving buffer size
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getReport(BluetoothDevice device, byte reportType, byte reportId,
+ int bufferSize) {
+ if (VDBG) {
+ log("getReport(" + device + "), reportType=" + reportType + " reportId=" + reportId
+ + "bufferSize=" + bufferSize);
+ }
+ final IBluetoothHidHost service = getService();
+ 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.getReport(device, reportType, reportId, bufferSize, mAttributionSource,
+ recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Set_Report command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @param reportType Report type
+ * @param report Report receiving buffer size
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setReport(BluetoothDevice device, byte reportType, String report) {
+ if (VDBG) log("setReport(" + device + "), reportType=" + reportType + " report=" + report);
+ final IBluetoothHidHost service = getService();
+ 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.setReport(device, reportType, report, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Send_Data command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @param report Report to send
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean sendData(BluetoothDevice device, String report) {
+ if (DBG) log("sendData(" + device + "), report=" + report);
+ final IBluetoothHidHost service = getService();
+ 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.sendData(device, report, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Send Get_Idle_Time command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getIdleTime(BluetoothDevice device) {
+ if (DBG) log("getIdletime(" + device + ")");
+ final IBluetoothHidHost service = getService();
+ 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.getIdleTime(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;
+ }
+
+ /**
+ * Send Set_Idle_Time command to the connected HID input device.
+ *
+ * @param device Remote Bluetooth Device
+ * @param idleTime Idle time to be set on HID Device
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setIdleTime(BluetoothDevice device, byte idleTime) {
+ if (DBG) log("setIdletime(" + device + "), idleTime=" + idleTime);
+ final IBluetoothHidHost service = getService();
+ 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.setIdleTime(device, idleTime, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothInputStream.java b/framework/java/android/bluetooth/BluetoothInputStream.java
new file mode 100644
index 0000000000..95f9229f04
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothInputStream.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.SuppressLint;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * BluetoothInputStream.
+ *
+ * Used to write to a Bluetooth socket.
+ *
+ * @hide
+ */
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+/*package*/ final class BluetoothInputStream extends InputStream {
+ private BluetoothSocket mSocket;
+
+ /*package*/ BluetoothInputStream(BluetoothSocket s) {
+ mSocket = s;
+ }
+
+ /**
+ * Return number of bytes available before this stream will block.
+ */
+ public int available() throws IOException {
+ return mSocket.available();
+ }
+
+ public void close() throws IOException {
+ mSocket.close();
+ }
+
+ /**
+ * Reads a single byte from this stream and returns it as an integer in the
+ * range from 0 to 255. Returns -1 if the end of the stream has been
+ * reached. Blocks until one byte has been read, the end of the source
+ * stream is detected or an exception is thrown.
+ *
+ * @return the byte read or -1 if the end of stream has been reached.
+ * @throws IOException if the stream is closed or another IOException occurs.
+ * @since Android 1.5
+ */
+ public int read() throws IOException {
+ byte[] b = new byte[1];
+ int ret = mSocket.read(b, 0, 1);
+ if (ret == 1) {
+ return (int) b[0] & 0xff;
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Reads at most {@code length} bytes from this stream and stores them in
+ * the byte array {@code b} starting at {@code offset}.
+ *
+ * @param b the byte array in which to store the bytes read.
+ * @param offset the initial position in {@code buffer} to store the bytes read from this
+ * stream.
+ * @param length the maximum number of bytes to store in {@code b}.
+ * @return the number of bytes actually read or -1 if the end of the stream has been reached.
+ * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code length < 0}, or if {@code
+ * offset + length} is greater than the length of {@code b}.
+ * @throws IOException if the stream is closed or another IOException occurs.
+ * @since Android 1.5
+ */
+ public int read(byte[] b, int offset, int length) throws IOException {
+ if (b == null) {
+ throw new NullPointerException("byte array is null");
+ }
+ if ((offset | length) < 0 || length > b.length - offset) {
+ throw new ArrayIndexOutOfBoundsException("invalid offset or length");
+ }
+ return mSocket.read(b, offset, length);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeAudio.java b/framework/java/android/bluetooth/BluetoothLeAudio.java
new file mode 100644
index 0000000000..15db686b3b
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeAudio.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+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;
+
+/**
+ * This class provides the public APIs to control the LeAudio profile.
+ *
+ * <p>BluetoothLeAudio is a proxy object for controlling the Bluetooth LE Audio
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothLeAudio proxy object.
+ *
+ * <p> Android only supports one set of connected Bluetooth LeAudio device at a time. Each
+ * method is protected with its appropriate permission.
+ */
+public final class BluetoothLeAudio implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothLeAudio";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * Intent used to broadcast the change in connection state of the LeAudio
+ * profile. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own connection state changes.
+ *
+ * <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_LE_AUDIO_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_CONNECTION_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)
+ public static final String ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED";
+
+ /**
+ * Intent used to broadcast group node status information.
+ *
+ * <p>This intent will have 3 extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_GROUP_ID} - Group id. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_GROUP_NODE_STATUS} - Group node status. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LE_AUDIO_GROUP_NODE_STATUS_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_GROUP_NODE_STATUS_CHANGED";
+
+
+ /**
+ * Intent used to broadcast group status information.
+ *
+ * <p>This intent will have 4 extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_GROUP_ID} - Group id. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_GROUP_STATUS} - Group status. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LE_AUDIO_GROUP_STATUS_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_GROUP_STATUS_CHANGED";
+
+ /**
+ * Intent used to broadcast group audio configuration changed information.
+ *
+ * <p>This intent will have 5 extra:
+ * <ul>
+ * <li> {@link #EXTRA_LE_AUDIO_GROUP_ID} - Group id. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_DIRECTION} - Direction as bit mask. </li>
+ * <li> {@link #EXTRA_LE_AUDIO_SINK_LOCATION} - Sink location as per Bluetooth Assigned
+ * Numbers </li>
+ * <li> {@link #EXTRA_LE_AUDIO_SOURCE_LOCATION} - Source location as per Bluetooth Assigned
+ * Numbers </li>
+ * <li> {@link #EXTRA_LE_AUDIO_AVAILABLE_CONTEXTS} - Available contexts for group as per
+ * Bluetooth Assigned Numbers </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_LE_AUDIO_CONF_CHANGED =
+ "android.bluetooth.action.LE_AUDIO_CONF_CHANGED";
+
+ /**
+ * Indicates unspecified audio content.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_UNSPECIFIED = 0x0001;
+
+ /**
+ * Indicates conversation between humans as, for example, in telephony or video calls.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_COMMUNICATION = 0x0002;
+
+ /**
+ * Indicates media as, for example, in music, public radio, podcast or video soundtrack.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_MEDIA = 0x0004;
+
+ /**
+ * Indicates instructional audio as, for example, in navigation, traffic announcements
+ * or user guidance.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_INSTRUCTIONAL = 0x0008;
+
+ /**
+ * Indicates attention seeking audio as, for example, in beeps signalling arrival of a message
+ * or keyboard clicks.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_ATTENTION_SEEKING = 0x0010;
+
+ /**
+ * Indicates immediate alerts as, for example, in a low battery alarm, timer expiry or alarm
+ * clock.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_IMMEDIATE_ALERT = 0x0020;
+
+ /**
+ * Indicates man machine communication as, for example, with voice recognition or virtual
+ * assistant.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_MAN_MACHINE = 0x0040;
+
+ /**
+ * Indicates emergency alerts as, for example, with fire alarms or other urgent alerts.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_EMERGENCY_ALERT = 0x0080;
+
+ /**
+ * Indicates ringtone as in a call alert.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_RINGTONE = 0x0100;
+
+ /**
+ * Indicates audio associated with a television program and/or with metadata conforming to the
+ * Bluetooth Broadcast TV profile.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_TV = 0x0200;
+
+ /**
+ * Indicates audio associated with a low latency live audio stream.
+ *
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_LIVE = 0x0400;
+
+ /**
+ * Indicates audio associated with a video game stream.
+ * @hide
+ */
+ public static final int CONTEXT_TYPE_GAME = 0x0800;
+
+ /**
+ * This represents an invalid group ID.
+ *
+ * @hide
+ */
+ public static final int GROUP_ID_INVALID = IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
+
+ /**
+ * Contains group id.
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_GROUP_ID =
+ "android.bluetooth.extra.LE_AUDIO_GROUP_ID";
+
+ /**
+ * Contains group node status, can be any of
+ * <p>
+ * <ul>
+ * <li> {@link #GROUP_NODE_ADDED} </li>
+ * <li> {@link #GROUP_NODE_REMOVED} </li>
+ * </ul>
+ * <p>
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_GROUP_NODE_STATUS =
+ "android.bluetooth.extra.LE_AUDIO_GROUP_NODE_STATUS";
+
+ /**
+ * Contains group status, can be any of
+ *
+ * <p>
+ * <ul>
+ * <li> {@link #GROUP_STATUS_ACTIVE} </li>
+ * <li> {@link #GROUP_STATUS_INACTIVE} </li>
+ * </ul>
+ * <p>
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_GROUP_STATUS =
+ "android.bluetooth.extra.LE_AUDIO_GROUP_STATUS";
+
+ /**
+ * Contains bit mask for direction, bit 0 set when Sink, bit 1 set when Source.
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_DIRECTION =
+ "android.bluetooth.extra.LE_AUDIO_DIRECTION";
+
+ /**
+ * Contains source location as per Bluetooth Assigned Numbers
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_SOURCE_LOCATION =
+ "android.bluetooth.extra.LE_AUDIO_SOURCE_LOCATION";
+
+ /**
+ * Contains sink location as per Bluetooth Assigned Numbers
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_SINK_LOCATION =
+ "android.bluetooth.extra.LE_AUDIO_SINK_LOCATION";
+
+ /**
+ * Contains available context types for group as per Bluetooth Assigned Numbers
+ * @hide
+ */
+ public static final String EXTRA_LE_AUDIO_AVAILABLE_CONTEXTS =
+ "android.bluetooth.extra.LE_AUDIO_AVAILABLE_CONTEXTS";
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ /**
+ * Indicating that group is Active ( Audio device is available )
+ * @hide
+ */
+ public static final int GROUP_STATUS_ACTIVE = IBluetoothLeAudio.GROUP_STATUS_ACTIVE;
+
+ /**
+ * Indicating that group is Inactive ( Audio device is not available )
+ * @hide
+ */
+ public static final int GROUP_STATUS_INACTIVE = IBluetoothLeAudio.GROUP_STATUS_INACTIVE;
+
+ /**
+ * Indicating that node has been added to the group.
+ * @hide
+ */
+ public static final int GROUP_NODE_ADDED = IBluetoothLeAudio.GROUP_NODE_ADDED;
+
+ /**
+ * Indicating that node has been removed from the group.
+ * @hide
+ */
+ public static final int GROUP_NODE_REMOVED = IBluetoothLeAudio.GROUP_NODE_REMOVED;
+
+ private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO, "BluetoothLeAudio",
+ IBluetoothLeAudio.class.getName()) {
+ @Override
+ public IBluetoothLeAudio getServiceInterface(IBinder service) {
+ return IBluetoothLeAudio.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothLeAudio proxy object for interacting with the local
+ * Bluetooth LeAudio service.
+ */
+ /* package */ BluetoothLeAudio(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothLeAudio getService() {
+ return mProfileConnector.getService();
+ }
+
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean connect(@Nullable BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.connect(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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(@Nullable BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.disconnect(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 @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothLeAudio service = getService();
+ 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 (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ new SynchronousResultReceiver();
+ service.getConnectedDevices(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 @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
+ @NonNull int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothLeAudio service = getService();
+ 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 (mAdapter.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
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothLeAudio service = getService();
+ 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 (mAdapter.isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getConnectionState(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;
+ }
+
+ /**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, LeAudio audio
+ * streaming is to the active LeAudio device. 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_LE_AUDIO_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.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 LeAudio devices that are active
+ *
+ * @return the list of active devices. Returns empty list on error.
+ * @hide
+ */
+ @NonNull
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getActiveDevices() {
+ if (VDBG) log("getActiveDevice()");
+ final IBluetoothLeAudio service = getService();
+ 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 (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ new SynchronousResultReceiver();
+ service.getActiveDevices(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;
+ }
+
+ /**
+ * Get device group id. Devices with same group id belong to same group (i.e left and right
+ * earbud)
+ * @param device LE Audio capable device
+ * @return group id that this device currently belongs to
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getGroupId(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getGroupId()");
+ final IBluetoothLeAudio service = getService();
+ final int defaultValue = GROUP_ID_INVALID;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getGroupId(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 volume for the streaming devices
+ *
+ * @param volume volume to set
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED})
+ public void setVolume(int volume) {
+ if (VDBG) log("setVolume(vol: " + volume + " )");
+ final IBluetoothLeAudio service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver recv = new SynchronousResultReceiver();
+ service.setVolume(volume, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Add device to the given group.
+ * @param group_id group ID the device is being added to
+ * @param device the active device
+ * @return true on success, otherwise false
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean groupAddNode(int group_id, @NonNull BluetoothDevice device) {
+ if (VDBG) log("groupAddNode()");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupAddNode(group_id, 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;
+ }
+
+ /**
+ * Remove device from a given group.
+ * @param group_id group ID the device is being removed from
+ * @param device the active device
+ * @return true on success, otherwise false
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ public boolean groupRemoveNode(int group_id, @NonNull BluetoothDevice device) {
+ if (VDBG) log("groupRemoveNode()");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupRemoveNode(group_id, 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 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothLeAudio service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.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 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothLeAudio service = getService();
+ 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 (mAdapter.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;
+ }
+
+
+ /**
+ * Helper for converting a state to a string.
+ *
+ * For debug use only - strings are not internationalized.
+ *
+ * @hide
+ */
+ public static String stateToString(int state) {
+ switch (state) {
+ case STATE_DISCONNECTED:
+ return "disconnected";
+ case STATE_CONNECTING:
+ return "connecting";
+ case STATE_CONNECTED:
+ return "connected";
+ case STATE_DISCONNECTING:
+ return "disconnecting";
+ default:
+ return "<unknown state " + state + ">";
+ }
+ }
+
+ private boolean isValidDevice(@Nullable BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeAudioCodecConfig.java b/framework/java/android/bluetooth/BluetoothLeAudioCodecConfig.java
new file mode 100644
index 0000000000..dcaf4b682f
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeAudioCodecConfig.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2021 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents the codec configuration for a Bluetooth LE Audio source device.
+ * <p>Contains the source codec type.
+ * <p>The source codec type values are the same as those supported by the
+ * device hardware.
+ *
+ * {@see BluetoothLeAudioCodecConfig}
+ */
+public final class BluetoothLeAudioCodecConfig {
+ // Add an entry for each source codec here.
+
+ /** @hide */
+ @IntDef(prefix = "SOURCE_CODEC_TYPE_", value = {
+ SOURCE_CODEC_TYPE_LC3,
+ SOURCE_CODEC_TYPE_INVALID
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SourceCodecType {};
+
+ public static final int SOURCE_CODEC_TYPE_LC3 = 0;
+ public static final int SOURCE_CODEC_TYPE_INVALID = 1000 * 1000;
+
+ /**
+ * Represents the count of valid source codec types. Can be accessed via
+ * {@link #getMaxCodecType}.
+ */
+ private static final int SOURCE_CODEC_TYPE_MAX = 1;
+
+ private final @SourceCodecType int mCodecType;
+
+ /**
+ * Creates a new BluetoothLeAudioCodecConfig.
+ *
+ * @param codecType the source codec type
+ */
+ private BluetoothLeAudioCodecConfig(@SourceCodecType int codecType) {
+ mCodecType = codecType;
+ }
+
+ @Override
+ public String toString() {
+ return "{codecName:" + getCodecName() + "}";
+ }
+
+ /**
+ * Gets the codec type.
+ *
+ * @return the codec type
+ */
+ public @SourceCodecType int getCodecType() {
+ return mCodecType;
+ }
+
+ /**
+ * Returns the valid codec types count.
+ */
+ public static int getMaxCodecType() {
+ return SOURCE_CODEC_TYPE_MAX;
+ }
+
+ /**
+ * Gets the codec name.
+ *
+ * @return the codec name
+ */
+ public @NonNull String getCodecName() {
+ switch (mCodecType) {
+ case SOURCE_CODEC_TYPE_LC3:
+ return "LC3";
+ case SOURCE_CODEC_TYPE_INVALID:
+ return "INVALID CODEC";
+ default:
+ break;
+ }
+ return "UNKNOWN CODEC(" + mCodecType + ")";
+ }
+
+ /**
+ * Builder for {@link BluetoothLeAudioCodecConfig}.
+ * <p> By default, the codec type will be set to
+ * {@link BluetoothLeAudioCodecConfig#SOURCE_CODEC_TYPE_INVALID}
+ */
+ public static final class Builder {
+ private int mCodecType = BluetoothLeAudioCodecConfig.SOURCE_CODEC_TYPE_INVALID;
+
+ /**
+ * Set codec type for Bluetooth codec config.
+ *
+ * @param codecType of this codec
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setCodecType(@SourceCodecType int codecType) {
+ mCodecType = codecType;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothLeAudioCodecConfig}.
+ * @return new BluetoothLeAudioCodecConfig built
+ */
+ public @NonNull BluetoothLeAudioCodecConfig build() {
+ return new BluetoothLeAudioCodecConfig(mCodecType);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcast.java b/framework/java/android/bluetooth/BluetoothLeBroadcast.java
new file mode 100644
index 0000000000..fed9f911d5
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeBroadcast.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2021 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 android.annotation.IntDef;
+import android.content.Context;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * This class provides the public APIs to control the Bluetooth LE Broadcast Source profile.
+ *
+ * <p>BluetoothLeBroadcast is a proxy object for controlling the Bluetooth LE Broadcast
+ * Source Service via IPC. Use {@link BluetoothAdapter#getProfileProxy}
+ * to get the BluetoothLeBroadcast proxy object.
+ *
+ * @hide
+ */
+public final class BluetoothLeBroadcast implements BluetoothProfile {
+ private static final String TAG = "BluetoothLeBroadcast";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Constants used by the LE Audio Broadcast profile for the Broadcast state
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"LE_AUDIO_BROADCAST_STATE_"}, value = {
+ LE_AUDIO_BROADCAST_STATE_DISABLED,
+ LE_AUDIO_BROADCAST_STATE_ENABLING,
+ LE_AUDIO_BROADCAST_STATE_ENABLED,
+ LE_AUDIO_BROADCAST_STATE_DISABLING,
+ LE_AUDIO_BROADCAST_STATE_PLAYING,
+ LE_AUDIO_BROADCAST_STATE_NOT_PLAYING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioBroadcastState {}
+
+ /**
+ * Indicates that LE Audio Broadcast mode is currently disabled
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_DISABLED = 10;
+
+ /**
+ * Indicates that LE Audio Broadcast mode is being enabled
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_ENABLING = 11;
+
+ /**
+ * Indicates that LE Audio Broadcast mode is currently enabled
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_ENABLED = 12;
+ /**
+ * Indicates that LE Audio Broadcast mode is being disabled
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_DISABLING = 13;
+
+ /**
+ * Indicates that an LE Audio Broadcast mode is currently playing
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_PLAYING = 14;
+
+ /**
+ * Indicates that LE Audio Broadcast is currently not playing
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_STATE_NOT_PLAYING = 15;
+
+ /**
+ * Constants used by the LE Audio Broadcast profile for encryption key length
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"LE_AUDIO_BROADCAST_ENCRYPTION_KEY_"}, value = {
+ LE_AUDIO_BROADCAST_ENCRYPTION_KEY_32BIT,
+ LE_AUDIO_BROADCAST_ENCRYPTION_KEY_128BIT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioEncryptionKeyLength {}
+
+ /**
+ * Indicates that the LE Audio Broadcast encryption key size is 32 bits.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_ENCRYPTION_KEY_32BIT = 16;
+
+ /**
+ * Indicates that the LE Audio Broadcast encryption key size is 128 bits.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_ENCRYPTION_KEY_128BIT = 17;
+
+ /**
+ * Interface for receiving events related to broadcasts
+ */
+ public interface Callback {
+ /**
+ * Called when broadcast state has changed
+ *
+ * @param prevState broadcast state before the change
+ * @param newState broadcast state after the change
+ */
+ @LeAudioBroadcastState
+ void onBroadcastStateChange(int prevState, int newState);
+ /**
+ * Called when encryption key has been updated
+ *
+ * @param success true if the key was updated successfully, false otherwise
+ */
+ void onEncryptionKeySet(boolean success);
+ }
+
+ /**
+ * Create a BluetoothLeBroadcast proxy object for interacting with the local
+ * LE Audio Broadcast Source service.
+ *
+ * @hide
+ */
+ /*package*/ BluetoothLeBroadcast(Context context,
+ BluetoothProfile.ServiceListener listener) {
+ }
+
+ /**
+ * Not supported since LE Audio Broadcasts do not establish a connection
+ *
+ * @throws UnsupportedOperationException
+ *
+ * @hide
+ */
+ @Override
+ public int getConnectionState(BluetoothDevice device) {
+ throw new UnsupportedOperationException(
+ "LE Audio Broadcasts are not connection-oriented.");
+ }
+
+ /**
+ * Not supported since LE Audio Broadcasts do not establish a connection
+ *
+ * @throws UnsupportedOperationException
+ *
+ * @hide
+ */
+ @Override
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ throw new UnsupportedOperationException(
+ "LE Audio Broadcasts are not connection-oriented.");
+ }
+
+ /**
+ * Not supported since LE Audio Broadcasts do not establish a connection
+ *
+ * @throws UnsupportedOperationException
+ *
+ * @hide
+ */
+ @Override
+ public List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException(
+ "LE Audio Broadcasts are not connection-oriented.");
+ }
+
+ /**
+ * Enable LE Audio Broadcast mode.
+ *
+ * Generates a new broadcast ID and enables sending of encrypted or unencrypted
+ * isochronous PDUs
+ *
+ * @hide
+ */
+ public int enableBroadcastMode() {
+ if (DBG) log("enableBroadcastMode");
+ return BluetoothStatusCodes.ERROR_LE_AUDIO_BROADCAST_SOURCE_SET_BROADCAST_MODE_FAILED;
+ }
+
+ /**
+ * Disable LE Audio Broadcast mode.
+ *
+ * @hide
+ */
+ public int disableBroadcastMode() {
+ if (DBG) log("disableBroadcastMode");
+ return BluetoothStatusCodes.ERROR_LE_AUDIO_BROADCAST_SOURCE_SET_BROADCAST_MODE_FAILED;
+ }
+
+ /**
+ * Get the current LE Audio broadcast state
+ *
+ * @hide
+ */
+ @LeAudioBroadcastState
+ public int getBroadcastState() {
+ if (DBG) log("getBroadcastState");
+ return LE_AUDIO_BROADCAST_STATE_DISABLED;
+ }
+
+ /**
+ * Enable LE Audio broadcast encryption
+ *
+ * @param keyLength if useExisting is true, this specifies the length of the key that should
+ * be generated
+ * @param useExisting true, if an existing key should be used
+ * false, if a new key should be generated
+ *
+ * @hide
+ */
+ @LeAudioEncryptionKeyLength
+ public int enableEncryption(boolean useExisting, int keyLength) {
+ if (DBG) log("enableEncryption useExisting=" + useExisting + " keyLength=" + keyLength);
+ return BluetoothStatusCodes.ERROR_LE_AUDIO_BROADCAST_SOURCE_ENABLE_ENCRYPTION_FAILED;
+ }
+
+ /**
+ * Disable LE Audio broadcast encryption
+ *
+ * @param removeExisting true, if the existing key should be removed
+ * false, otherwise
+ *
+ * @hide
+ */
+ public int disableEncryption(boolean removeExisting) {
+ if (DBG) log("disableEncryption removeExisting=" + removeExisting);
+ return BluetoothStatusCodes.ERROR_LE_AUDIO_BROADCAST_SOURCE_DISABLE_ENCRYPTION_FAILED;
+ }
+
+ /**
+ * Enable or disable LE Audio broadcast encryption
+ *
+ * @param key use the provided key if non-null, generate a new key if null
+ * @param keyLength 0 if encryption is disabled, 4 bytes (low security),
+ * 16 bytes (high security)
+ *
+ * @hide
+ */
+ @LeAudioEncryptionKeyLength
+ public int setEncryptionKey(byte[] key, int keyLength) {
+ if (DBG) log("setEncryptionKey key=" + key + " keyLength=" + keyLength);
+ return BluetoothStatusCodes.ERROR_LE_AUDIO_BROADCAST_SOURCE_SET_ENCRYPTION_KEY_FAILED;
+ }
+
+
+ /**
+ * Get the encryption key that was set before
+ *
+ * @return encryption key as a byte array or null if no encryption key was set
+ *
+ * @hide
+ */
+ public byte[] getEncryptionKey() {
+ if (DBG) log("getEncryptionKey");
+ return null;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java
new file mode 100644
index 0000000000..b866cce224
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeBroadcastAssistantCallback.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2021 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.bluetooth.le.ScanResult;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class provides a set of callbacks that are invoked when scanning for Broadcast Sources is
+ * offloaded to a Broadcast Assistant.
+ *
+ * <p>An LE Audio Broadcast Assistant can help a Broadcast Sink to scan for available Broadcast
+ * Sources. The Broadcast Sink achieves this by offloading the scan to a Broadcast Assistant. This
+ * is facilitated by the Broadcast Audio Scan Service (BASS). A BASS server is a GATT server that is
+ * part of the Scan Delegator on a Broadcast Sink. A BASS client instead runs on the Broadcast
+ * Assistant.
+ *
+ * <p>Once a GATT connection is established between the BASS client and the BASS server, the
+ * Broadcast Sink can offload the scans to the Broadcast Assistant. Upon finding new Broadcast
+ * Sources, the Broadcast Assistant then notifies the Broadcast Sink about these over the
+ * established GATT connection. The Scan Delegator on the Broadcast Sink can also notify the
+ * Assistant about changes such as addition and removal of Broadcast Sources.
+ *
+ * @hide
+ */
+public abstract class BluetoothLeBroadcastAssistantCallback {
+
+ /**
+ * Broadcast Audio Scan Service (BASS) codes returned by a BASS Server
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = "BASS_STATUS_",
+ value = {
+ BASS_STATUS_SUCCESS,
+ BASS_STATUS_FAILURE,
+ BASS_STATUS_INVALID_GATT_HANDLE,
+ BASS_STATUS_TXN_TIMEOUT,
+ BASS_STATUS_INVALID_SOURCE_ID,
+ BASS_STATUS_COLOCATED_SRC_UNAVAILABLE,
+ BASS_STATUS_INVALID_SOURCE_SELECTED,
+ BASS_STATUS_SOURCE_UNAVAILABLE,
+ BASS_STATUS_DUPLICATE_ADDITION,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BassStatus {}
+
+ public static final int BASS_STATUS_SUCCESS = 0x00;
+ public static final int BASS_STATUS_FAILURE = 0x01;
+ public static final int BASS_STATUS_INVALID_GATT_HANDLE = 0x02;
+ public static final int BASS_STATUS_TXN_TIMEOUT = 0x03;
+
+ public static final int BASS_STATUS_INVALID_SOURCE_ID = 0x04;
+ public static final int BASS_STATUS_COLOCATED_SRC_UNAVAILABLE = 0x05;
+ public static final int BASS_STATUS_INVALID_SOURCE_SELECTED = 0x06;
+ public static final int BASS_STATUS_SOURCE_UNAVAILABLE = 0x07;
+ public static final int BASS_STATUS_DUPLICATE_ADDITION = 0x08;
+ public static final int BASS_STATUS_NO_EMPTY_SLOT = 0x09;
+ public static final int BASS_STATUS_INVALID_GROUP_OP = 0x10;
+
+ /**
+ * Callback invoked when a new LE Audio Broadcast Source is found.
+ *
+ * @param result {@link ScanResult} scan result representing a Broadcast Source
+ */
+ public void onBluetoothLeBroadcastSourceFound(@NonNull ScanResult result) {}
+
+ /**
+ * Callback invoked when the Broadcast Assistant synchronizes with Periodic Advertisements (PAs)
+ * of an LE Audio Broadcast Source.
+ *
+ * @param source the selected Broadcast Source
+ */
+ public void onBluetoothLeBroadcastSourceSelected(
+ @NonNull BluetoothLeBroadcastSourceInfo source, @BassStatus int status) {}
+
+ /**
+ * Callback invoked when the Broadcast Assistant loses synchronization with an LE Audio
+ * Broadcast Source.
+ *
+ * @param source the Broadcast Source with which synchronization was lost
+ */
+ public void onBluetoothLeBroadcastSourceLost(
+ @NonNull BluetoothLeBroadcastSourceInfo source, @BassStatus int status) {}
+
+ /**
+ * Callback invoked when a new LE Audio Broadcast Source has been successfully added to the Scan
+ * Delegator (within a Broadcast Sink, for example).
+ *
+ * @param sink Scan Delegator device on which a new Broadcast Source has been added
+ * @param source the added Broadcast Source
+ */
+ public void onBluetoothLeBroadcastSourceAdded(
+ @NonNull BluetoothDevice sink,
+ @NonNull BluetoothLeBroadcastSourceInfo source,
+ @BassStatus int status) {}
+
+ /**
+ * Callback invoked when an existing LE Audio Broadcast Source within a remote Scan Delegator
+ * has been updated.
+ *
+ * @param sink Scan Delegator device on which a Broadcast Source has been updated
+ * @param source the updated Broadcast Source
+ */
+ public void onBluetoothLeBroadcastSourceUpdated(
+ @NonNull BluetoothDevice sink,
+ @NonNull BluetoothLeBroadcastSourceInfo source,
+ @BassStatus int status) {}
+
+ /**
+ * Callback invoked when an LE Audio Broadcast Source has been successfully removed from the
+ * Scan Delegator (within a Broadcast Sink, for example).
+ *
+ * @param sink Scan Delegator device from which a Broadcast Source has been removed
+ * @param source the removed Broadcast Source
+ */
+ public void onBluetoothLeBroadcastSourceRemoved(
+ @NonNull BluetoothDevice sink,
+ @NonNull BluetoothLeBroadcastSourceInfo source,
+ @BassStatus int status) {}
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastSourceInfo.java b/framework/java/android/bluetooth/BluetoothLeBroadcastSourceInfo.java
new file mode 100644
index 0000000000..cb47280acc
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeBroadcastSourceInfo.java
@@ -0,0 +1,788 @@
+/*
+ * Copyright 2021 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class represents an LE Audio Broadcast Source and the associated information that is needed
+ * by Broadcast Audio Scan Service (BASS) residing on a Scan Delegator.
+ *
+ * <p>For example, the Scan Delegator on an LE Audio Broadcast Sink can use the information
+ * contained within an instance of this class to synchronize with an LE Audio Broadcast Source in
+ * order to listen to a Broadcast Audio Stream.
+ *
+ * <p>BroadcastAssistant has a BASS client which facilitates scanning and discovery of Broadcast
+ * Sources on behalf of say a Broadcast Sink. Upon successful discovery of one or more Broadcast
+ * sources, this information needs to be communicated to the BASS Server residing within the Scan
+ * Delegator on a Broadcast Sink. This is achieved using the Periodic Advertising Synchronization
+ * Transfer (PAST) procedure. This procedure uses information contained within an instance of this
+ * class.
+ *
+ * @hide
+ */
+public final class BluetoothLeBroadcastSourceInfo implements Parcelable {
+ private static final String TAG = "BluetoothLeBroadcastSourceInfo";
+ private static final boolean DBG = true;
+
+ /**
+ * Constants representing Broadcast Source address types
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = "LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_",
+ value = {
+ LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_PUBLIC,
+ LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_RANDOM,
+ LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_INVALID
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioBroadcastSourceAddressType {}
+
+ /**
+ * Represents a public address used by an LE Audio Broadcast Source
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_PUBLIC = 0;
+
+ /**
+ * Represents a random address used by an LE Audio Broadcast Source
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_RANDOM = 1;
+
+ /**
+ * Represents an invalid address used by an LE Audio Broadcast Seurce
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_INVALID = 0xFFFF;
+
+ /**
+ * Periodic Advertising Synchronization state
+ *
+ * <p>Periodic Advertising (PA) enables the LE Audio Broadcast Assistant to discover broadcast
+ * audio streams as well as the audio stream configuration on behalf of an LE Audio Broadcast
+ * Sink. This information can then be transferred to the LE Audio Broadcast Sink using the
+ * Periodic Advertising Synchronizaton Transfer (PAST) procedure.
+ *
+ * @hide
+ */
+ @IntDef(
+ prefix = "LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_",
+ value = {
+ LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_IDLE,
+ LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_SYNCINFO_REQ,
+ LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_IN_SYNC,
+ LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_SYNC_FAIL,
+ LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_NO_PAST
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioBroadcastSinkPaSyncState {}
+
+ /**
+ * Indicates that the Broadcast Sink is not synchronized with the Periodic Advertisements (PA)
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_IDLE = 0;
+
+ /**
+ * Indicates that the Broadcast Sink requested the Broadcast Assistant to synchronize with the
+ * Periodic Advertisements (PA).
+ *
+ * <p>This is also known as scan delegation or scan offloading.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_SYNCINFO_REQ = 1;
+
+ /**
+ * Indicates that the Broadcast Sink is synchronized with the Periodic Advertisements (PA).
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_IN_SYNC = 2;
+
+ /**
+ * Indicates that the Broadcast Sink was unable to synchronize with the Periodic Advertisements
+ * (PA).
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_SYNC_FAIL = 3;
+
+ /**
+ * Indicates that the Broadcast Sink should be synchronized with the Periodic Advertisements
+ * (PA) using the Periodic Advertisements Synchronization Transfert (PAST) procedure.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_NO_PAST = 4;
+
+ /**
+ * Indicates that the Broadcast Sink synchornization state is invalid.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_INVALID = 0xFFFF;
+
+ /** @hide */
+ @IntDef(
+ prefix = "LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_",
+ value = {
+ LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_NOT_SYNCHRONIZED,
+ LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_SYNCHRONIZED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioBroadcastSinkAudioSyncState {}
+
+ /**
+ * Indicates that the Broadcast Sink is not synchronized with a Broadcast Audio Stream.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_NOT_SYNCHRONIZED = 0;
+
+ /**
+ * Indicates that the Broadcast Sink is synchronized with a Broadcast Audio Stream.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_SYNCHRONIZED = 1;
+
+ /**
+ * Indicates that the Broadcast Sink audio synchronization state is invalid.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_INVALID = 0xFFFF;
+
+ /** @hide */
+ @IntDef(
+ prefix = "LE_AUDIO_BROADCAST_SINK_ENC_STATE_",
+ value = {
+ LE_AUDIO_BROADCAST_SINK_ENC_STATE_NOT_ENCRYPTED,
+ LE_AUDIO_BROADCAST_SINK_ENC_STATE_CODE_REQUIRED,
+ LE_AUDIO_BROADCAST_SINK_ENC_STATE_DECRYPTING,
+ LE_AUDIO_BROADCAST_SINK_ENC_STATE_BAD_CODE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LeAudioBroadcastSinkEncryptionState {}
+
+ /**
+ * Indicates that the Broadcast Sink is synchronized with an unencrypted audio stream.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_ENC_STATE_NOT_ENCRYPTED = 0;
+
+ /**
+ * Indicates that the Broadcast Sink needs a Broadcast Code to synchronize with the audio
+ * stream.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_ENC_STATE_CODE_REQUIRED = 1;
+
+ /**
+ * Indicates that the Broadcast Sink is synchronized with an encrypted audio stream.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_ENC_STATE_DECRYPTING = 2;
+
+ /**
+ * Indicates that the Broadcast Sink is unable to decrypt an audio stream due to an incorrect
+ * Broadcast Code
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_ENC_STATE_BAD_CODE = 3;
+
+ /**
+ * Indicates that the Broadcast Sink encryption state is invalid.
+ *
+ * @hide
+ */
+ public static final int LE_AUDIO_BROADCAST_SINK_ENC_STATE_INVALID = 0xFF;
+
+ /**
+ * Represents an invalid LE Audio Broadcast Source ID
+ *
+ * @hide
+ */
+ public static final byte LE_AUDIO_BROADCAST_SINK_INVALID_SOURCE_ID = (byte) 0x00;
+
+ /**
+ * Represents an invalid Broadcast ID of a Broadcast Source
+ *
+ * @hide
+ */
+ public static final int INVALID_BROADCAST_ID = 0xFFFFFF;
+
+ private byte mSourceId;
+ private @LeAudioBroadcastSourceAddressType int mSourceAddressType;
+ private BluetoothDevice mSourceDevice;
+ private byte mSourceAdvSid;
+ private int mBroadcastId;
+ private @LeAudioBroadcastSinkPaSyncState int mPaSyncState;
+ private @LeAudioBroadcastSinkEncryptionState int mEncryptionStatus;
+ private @LeAudioBroadcastSinkAudioSyncState int mAudioSyncState;
+ private byte[] mBadBroadcastCode;
+ private byte mNumSubGroups;
+ private Map<Integer, Integer> mSubgroupBisSyncState = new HashMap<Integer, Integer>();
+ private Map<Integer, byte[]> mSubgroupMetadata = new HashMap<Integer, byte[]>();
+
+ private String mBroadcastCode;
+ private static final int BIS_NO_PREF = 0xFFFFFFFF;
+ private static final int BROADCAST_CODE_SIZE = 16;
+
+ /**
+ * Constructor to create an Empty object of {@link BluetoothLeBroadcastSourceInfo } with the
+ * given Source Id.
+ *
+ * <p>This is mainly used to represent the Empty Broadcast Source entries
+ *
+ * @param sourceId Source Id for this Broadcast Source info object
+ * @hide
+ */
+ public BluetoothLeBroadcastSourceInfo(byte sourceId) {
+ mSourceId = sourceId;
+ mSourceAddressType = LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_INVALID;
+ mSourceDevice = null;
+ mSourceAdvSid = (byte) 0x00;
+ mBroadcastId = INVALID_BROADCAST_ID;
+ mPaSyncState = LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_INVALID;
+ mAudioSyncState = LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_INVALID;
+ mEncryptionStatus = LE_AUDIO_BROADCAST_SINK_ENC_STATE_INVALID;
+ mBadBroadcastCode = null;
+ mNumSubGroups = 0;
+ mBroadcastCode = null;
+ }
+
+ /*package*/ BluetoothLeBroadcastSourceInfo(
+ byte sourceId,
+ @LeAudioBroadcastSourceAddressType int addressType,
+ @NonNull BluetoothDevice device,
+ byte advSid,
+ int broadcastId,
+ @LeAudioBroadcastSinkPaSyncState int paSyncstate,
+ @LeAudioBroadcastSinkEncryptionState int encryptionStatus,
+ @LeAudioBroadcastSinkAudioSyncState int audioSyncstate,
+ @Nullable byte[] badCode,
+ byte numSubGroups,
+ @NonNull Map<Integer, Integer> bisSyncState,
+ @Nullable Map<Integer, byte[]> subgroupMetadata,
+ @NonNull String broadcastCode) {
+ mSourceId = sourceId;
+ mSourceAddressType = addressType;
+ mSourceDevice = device;
+ mSourceAdvSid = advSid;
+ mBroadcastId = broadcastId;
+ mPaSyncState = paSyncstate;
+ mEncryptionStatus = encryptionStatus;
+ mAudioSyncState = audioSyncstate;
+
+ if (badCode != null && badCode.length != 0) {
+ mBadBroadcastCode = new byte[badCode.length];
+ System.arraycopy(badCode, 0, mBadBroadcastCode, 0, badCode.length);
+ }
+ mNumSubGroups = numSubGroups;
+ mSubgroupBisSyncState = new HashMap<Integer, Integer>(bisSyncState);
+ mSubgroupMetadata = new HashMap<Integer, byte[]>(subgroupMetadata);
+ mBroadcastCode = broadcastCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof BluetoothLeBroadcastSourceInfo) {
+ BluetoothLeBroadcastSourceInfo other = (BluetoothLeBroadcastSourceInfo) o;
+ return (other.mSourceId == mSourceId
+ && other.mSourceAddressType == mSourceAddressType
+ && other.mSourceDevice == mSourceDevice
+ && other.mSourceAdvSid == mSourceAdvSid
+ && other.mBroadcastId == mBroadcastId
+ && other.mPaSyncState == mPaSyncState
+ && other.mEncryptionStatus == mEncryptionStatus
+ && other.mAudioSyncState == mAudioSyncState
+ && Arrays.equals(other.mBadBroadcastCode, mBadBroadcastCode)
+ && other.mNumSubGroups == mNumSubGroups
+ && mSubgroupBisSyncState.equals(other.mSubgroupBisSyncState)
+ && mSubgroupMetadata.equals(other.mSubgroupMetadata)
+ && other.mBroadcastCode == mBroadcastCode);
+ }
+ return false;
+ }
+
+ /**
+ * Checks if an instance of {@link BluetoothLeBroadcastSourceInfo} is empty.
+ *
+ * @hide
+ */
+ public boolean isEmpty() {
+ boolean ret = false;
+ if (mSourceAddressType == LE_AUDIO_BROADCAST_SOURCE_ADDRESS_TYPE_INVALID
+ && mSourceDevice == null
+ && mSourceAdvSid == (byte) 0
+ && mPaSyncState == LE_AUDIO_BROADCAST_SINK_PA_SYNC_STATE_INVALID
+ && mEncryptionStatus == LE_AUDIO_BROADCAST_SINK_ENC_STATE_INVALID
+ && mAudioSyncState == LE_AUDIO_BROADCAST_SINK_AUDIO_SYNC_STATE_INVALID
+ && mBadBroadcastCode == null
+ && mNumSubGroups == 0
+ && mSubgroupBisSyncState.size() == 0
+ && mSubgroupMetadata.size() == 0
+ && mBroadcastCode == null) {
+ ret = true;
+ }
+ return ret;
+ }
+
+ /**
+ * Compares an instance of {@link BluetoothLeBroadcastSourceInfo} with the provided instance.
+ *
+ * @hide
+ */
+ public boolean matches(BluetoothLeBroadcastSourceInfo srcInfo) {
+ boolean ret = false;
+ if (srcInfo == null) {
+ ret = false;
+ } else {
+ if (mSourceDevice == null) {
+ if (mSourceAdvSid == srcInfo.getAdvertisingSid()
+ && mSourceAddressType == srcInfo.getAdvAddressType()) {
+ ret = true;
+ }
+ } else {
+ if (mSourceDevice.equals(srcInfo.getSourceDevice())
+ && mSourceAdvSid == srcInfo.getAdvertisingSid()
+ && mSourceAddressType == srcInfo.getAdvAddressType()
+ && mBroadcastId == srcInfo.getBroadcastId()) {
+ ret = true;
+ }
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mSourceId,
+ mSourceAddressType,
+ mSourceDevice,
+ mSourceAdvSid,
+ mBroadcastId,
+ mPaSyncState,
+ mEncryptionStatus,
+ mAudioSyncState,
+ mBadBroadcastCode,
+ mNumSubGroups,
+ mSubgroupBisSyncState,
+ mSubgroupMetadata,
+ mBroadcastCode);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "{BluetoothLeBroadcastSourceInfo : mSourceId"
+ + mSourceId
+ + " addressType: "
+ + mSourceAddressType
+ + " sourceDevice: "
+ + mSourceDevice
+ + " mSourceAdvSid:"
+ + mSourceAdvSid
+ + " mBroadcastId:"
+ + mBroadcastId
+ + " mPaSyncState:"
+ + mPaSyncState
+ + " mEncryptionStatus:"
+ + mEncryptionStatus
+ + " mAudioSyncState:"
+ + mAudioSyncState
+ + " mBadBroadcastCode:"
+ + mBadBroadcastCode
+ + " mNumSubGroups:"
+ + mNumSubGroups
+ + " mSubgroupBisSyncState:"
+ + mSubgroupBisSyncState
+ + " mSubgroupMetadata:"
+ + mSubgroupMetadata
+ + " mBroadcastCode:"
+ + mBroadcastCode
+ + "}";
+ }
+
+ /**
+ * Get the Source Id
+ *
+ * @return byte representing the Source Id, {@link
+ * #LE_AUDIO_BROADCAST_ASSISTANT_INVALID_SOURCE_ID} if invalid
+ * @hide
+ */
+ public byte getSourceId() {
+ return mSourceId;
+ }
+
+ /**
+ * Set the Source Id
+ *
+ * @param sourceId source Id
+ * @hide
+ */
+ public void setSourceId(byte sourceId) {
+ mSourceId = sourceId;
+ }
+
+ /**
+ * Set the Broadcast Source device
+ *
+ * @param sourceDevice the Broadcast Source BluetoothDevice
+ * @hide
+ */
+ public void setSourceDevice(@NonNull BluetoothDevice sourceDevice) {
+ mSourceDevice = sourceDevice;
+ }
+
+ /**
+ * Get the Broadcast Source BluetoothDevice
+ *
+ * @return Broadcast Source BluetoothDevice
+ * @hide
+ */
+ public @NonNull BluetoothDevice getSourceDevice() {
+ return mSourceDevice;
+ }
+
+ /**
+ * Set the address type of the Broadcast Source advertisements
+ *
+ * @hide
+ */
+ public void setAdvAddressType(@LeAudioBroadcastSourceAddressType int addressType) {
+ mSourceAddressType = addressType;
+ }
+
+ /**
+ * Get the address type used by advertisements from the Broadcast Source.
+ * BluetoothLeBroadcastSourceInfo Object
+ *
+ * @hide
+ */
+ @LeAudioBroadcastSourceAddressType
+ public int getAdvAddressType() {
+ return mSourceAddressType;
+ }
+
+ /**
+ * Set the advertising SID of the Broadcast Source advertisement.
+ *
+ * @param advSid advertising SID of the Broadcast Source
+ * @hide
+ */
+ public void setAdvertisingSid(byte advSid) {
+ mSourceAdvSid = advSid;
+ }
+
+ /**
+ * Get the advertising SID of the Broadcast Source advertisement.
+ *
+ * @return advertising SID of the Broadcast Source
+ * @hide
+ */
+ public byte getAdvertisingSid() {
+ return mSourceAdvSid;
+ }
+
+ /**
+ * Get the Broadcast ID of the Broadcast Source.
+ *
+ * @return broadcast ID
+ * @hide
+ */
+ public int getBroadcastId() {
+ return mBroadcastId;
+ }
+
+ /**
+ * Set the Periodic Advertising (PA) Sync State.
+ *
+ * @hide
+ */
+ /*package*/ void setPaSyncState(@LeAudioBroadcastSinkPaSyncState int paSyncState) {
+ mPaSyncState = paSyncState;
+ }
+
+ /**
+ * Get the Periodic Advertising (PA) Sync State
+ *
+ * @hide
+ */
+ public @LeAudioBroadcastSinkPaSyncState int getMetadataSyncState() {
+ return mPaSyncState;
+ }
+
+ /**
+ * Set the audio sync state
+ *
+ * @hide
+ */
+ /*package*/ void setAudioSyncState(@LeAudioBroadcastSinkAudioSyncState int audioSyncState) {
+ mAudioSyncState = audioSyncState;
+ }
+
+ /**
+ * Get the audio sync state
+ *
+ * @hide
+ */
+ public @LeAudioBroadcastSinkAudioSyncState int getAudioSyncState() {
+ return mAudioSyncState;
+ }
+
+ /**
+ * Set the encryption status
+ *
+ * @hide
+ */
+ /*package*/ void setEncryptionStatus(
+ @LeAudioBroadcastSinkEncryptionState int encryptionStatus) {
+ mEncryptionStatus = encryptionStatus;
+ }
+
+ /**
+ * Get the encryption status
+ *
+ * @hide
+ */
+ public @LeAudioBroadcastSinkEncryptionState int getEncryptionStatus() {
+ return mEncryptionStatus;
+ }
+
+ /**
+ * Get the incorrect broadcast code that the Scan delegator used to decrypt the Broadcast Audio
+ * Stream and failed.
+ *
+ * <p>This code is valid only if {@link #getEncryptionStatus} returns {@link
+ * #LE_AUDIO_BROADCAST_SINK_ENC_STATE_BAD_CODE}
+ *
+ * @return byte array containing bad broadcast value, null if the current encryption status is
+ * not {@link #LE_AUDIO_BROADCAST_SINK_ENC_STATE_BAD_CODE}
+ * @hide
+ */
+ public @Nullable byte[] getBadBroadcastCode() {
+ return mBadBroadcastCode;
+ }
+
+ /**
+ * Get the number of subgroups.
+ *
+ * @return number of subgroups
+ * @hide
+ */
+ public byte getNumberOfSubGroups() {
+ return mNumSubGroups;
+ }
+
+ public @NonNull Map<Integer, Integer> getSubgroupBisSyncState() {
+ return mSubgroupBisSyncState;
+ }
+
+ public void setSubgroupBisSyncState(@NonNull Map<Integer, Integer> bisSyncState) {
+ mSubgroupBisSyncState = new HashMap<Integer, Integer>(bisSyncState);
+ }
+
+ /*package*/ void setBroadcastCode(@NonNull String broadcastCode) {
+ mBroadcastCode = broadcastCode;
+ }
+
+ /**
+ * Get the broadcast code
+ *
+ * @return
+ * @hide
+ */
+ public @NonNull String getBroadcastCode() {
+ return mBroadcastCode;
+ }
+
+ /**
+ * Set the broadcast ID
+ *
+ * @param broadcastId broadcast ID of the Broadcast Source
+ * @hide
+ */
+ public void setBroadcastId(int broadcastId) {
+ mBroadcastId = broadcastId;
+ }
+
+ private void writeSubgroupBisSyncStateToParcel(
+ @NonNull Parcel dest, @NonNull Map<Integer, Integer> subgroupBisSyncState) {
+ dest.writeInt(subgroupBisSyncState.size());
+ for (Map.Entry<Integer, Integer> entry : subgroupBisSyncState.entrySet()) {
+ dest.writeInt(entry.getKey());
+ dest.writeInt(entry.getValue());
+ }
+ }
+
+ private static void readSubgroupBisSyncStateFromParcel(
+ @NonNull Parcel in, @NonNull Map<Integer, Integer> subgroupBisSyncState) {
+ int size = in.readInt();
+
+ for (int i = 0; i < size; i++) {
+ Integer key = in.readInt();
+ Integer value = in.readInt();
+ subgroupBisSyncState.put(key, value);
+ }
+ }
+
+ private void writeSubgroupMetadataToParcel(
+ @NonNull Parcel dest, @Nullable Map<Integer, byte[]> subgroupMetadata) {
+ if (subgroupMetadata == null) {
+ dest.writeInt(0);
+ return;
+ }
+
+ dest.writeInt(subgroupMetadata.size());
+ for (Map.Entry<Integer, byte[]> entry : subgroupMetadata.entrySet()) {
+ dest.writeInt(entry.getKey());
+ byte[] metadata = entry.getValue();
+ if (metadata != null) {
+ dest.writeInt(metadata.length);
+ dest.writeByteArray(metadata);
+ }
+ }
+ }
+
+ private static void readSubgroupMetadataFromParcel(
+ @NonNull Parcel in, @NonNull Map<Integer, byte[]> subgroupMetadata) {
+ int size = in.readInt();
+
+ for (int i = 0; i < size; i++) {
+ Integer key = in.readInt();
+ Integer metaDataLen = in.readInt();
+ byte[] metadata = null;
+ if (metaDataLen != 0) {
+ metadata = new byte[metaDataLen];
+ in.readByteArray(metadata);
+ }
+ subgroupMetadata.put(key, metadata);
+ }
+ }
+
+ public static final @NonNull Parcelable.Creator<BluetoothLeBroadcastSourceInfo> CREATOR =
+ new Parcelable.Creator<BluetoothLeBroadcastSourceInfo>() {
+ public @NonNull BluetoothLeBroadcastSourceInfo createFromParcel(
+ @NonNull Parcel in) {
+ final byte sourceId = in.readByte();
+ final int sourceAddressType = in.readInt();
+ final BluetoothDevice sourceDevice =
+ in.readTypedObject(BluetoothDevice.CREATOR);
+ final byte sourceAdvSid = in.readByte();
+ final int broadcastId = in.readInt();
+ final int paSyncState = in.readInt();
+ final int audioSyncState = in.readInt();
+ final int encryptionStatus = in.readInt();
+ final int badBroadcastLen = in.readInt();
+ byte[] badBroadcastCode = null;
+
+ if (badBroadcastLen > 0) {
+ badBroadcastCode = new byte[badBroadcastLen];
+ in.readByteArray(badBroadcastCode);
+ }
+ final byte numSubGroups = in.readByte();
+ final String broadcastCode = in.readString();
+ Map<Integer, Integer> subgroupBisSyncState = new HashMap<Integer, Integer>();
+ readSubgroupBisSyncStateFromParcel(in, subgroupBisSyncState);
+ Map<Integer, byte[]> subgroupMetadata = new HashMap<Integer, byte[]>();
+ readSubgroupMetadataFromParcel(in, subgroupMetadata);
+
+ BluetoothLeBroadcastSourceInfo srcInfo =
+ new BluetoothLeBroadcastSourceInfo(
+ sourceId,
+ sourceAddressType,
+ sourceDevice,
+ sourceAdvSid,
+ broadcastId,
+ paSyncState,
+ encryptionStatus,
+ audioSyncState,
+ badBroadcastCode,
+ numSubGroups,
+ subgroupBisSyncState,
+ subgroupMetadata,
+ broadcastCode);
+ return srcInfo;
+ }
+
+ public @NonNull BluetoothLeBroadcastSourceInfo[] newArray(int size) {
+ return new BluetoothLeBroadcastSourceInfo[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeByte(mSourceId);
+ out.writeInt(mSourceAddressType);
+ out.writeTypedObject(mSourceDevice, 0);
+ out.writeByte(mSourceAdvSid);
+ out.writeInt(mBroadcastId);
+ out.writeInt(mPaSyncState);
+ out.writeInt(mAudioSyncState);
+ out.writeInt(mEncryptionStatus);
+
+ if (mBadBroadcastCode != null) {
+ out.writeInt(mBadBroadcastCode.length);
+ out.writeByteArray(mBadBroadcastCode);
+ } else {
+ // zero indicates that there is no "bad broadcast code"
+ out.writeInt(0);
+ }
+ out.writeByte(mNumSubGroups);
+ out.writeString(mBroadcastCode);
+ writeSubgroupBisSyncStateToParcel(out, mSubgroupBisSyncState);
+ writeSubgroupMetadataToParcel(out, mSubgroupMetadata);
+ }
+
+ private static void log(@NonNull String msg) {
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
+ }
+}
+;
diff --git a/framework/java/android/bluetooth/BluetoothLeCall.java b/framework/java/android/bluetooth/BluetoothLeCall.java
new file mode 100644
index 0000000000..fb7789db25
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeCall.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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 android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelUuid;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Representation of Call
+ *
+ * @hide
+ */
+public final class BluetoothLeCall implements Parcelable {
+
+ /** @hide */
+ @IntDef(prefix = "STATE_", value = {
+ STATE_INCOMING,
+ STATE_DIALING,
+ STATE_ALERTING,
+ STATE_ACTIVE,
+ STATE_LOCALLY_HELD,
+ STATE_REMOTELY_HELD,
+ STATE_LOCALLY_AND_REMOTELY_HELD
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {
+ }
+
+ /**
+ * A remote party is calling (incoming call).
+ *
+ * @hide
+ */
+ public static final int STATE_INCOMING = 0x00;
+
+ /**
+ * The process to call the remote party has started but the remote party is not
+ * being alerted (outgoing call).
+ *
+ * @hide
+ */
+ public static final int STATE_DIALING = 0x01;
+
+ /**
+ * A remote party is being alerted (outgoing call).
+ *
+ * @hide
+ */
+ public static final int STATE_ALERTING = 0x02;
+
+ /**
+ * The call is in an active conversation.
+ *
+ * @hide
+ */
+ public static final int STATE_ACTIVE = 0x03;
+
+ /**
+ * The call is connected but held locally. “Locally Held” implies that either
+ * the server or the client can affect the state.
+ *
+ * @hide
+ */
+ public static final int STATE_LOCALLY_HELD = 0x04;
+
+ /**
+ * The call is connected but held remotely. “Remotely Held” means that the state
+ * is controlled by the remote party of a call.
+ *
+ * @hide
+ */
+ public static final int STATE_REMOTELY_HELD = 0x05;
+
+ /**
+ * The call is connected but held both locally and remotely.
+ *
+ * @hide
+ */
+ public static final int STATE_LOCALLY_AND_REMOTELY_HELD = 0x06;
+
+ /**
+ * Whether the call direction is outgoing.
+ *
+ * @hide
+ */
+ public static final int FLAG_OUTGOING_CALL = 0x00000001;
+
+ /**
+ * Whether the call URI and Friendly Name are withheld by server.
+ *
+ * @hide
+ */
+ public static final int FLAG_WITHHELD_BY_SERVER = 0x00000002;
+
+ /**
+ * Whether the call URI and Friendly Name are withheld by network.
+ *
+ * @hide
+ */
+ public static final int FLAG_WITHHELD_BY_NETWORK = 0x00000004;
+
+ /** Unique UUID that identifies this call */
+ private UUID mUuid;
+
+ /** Remote Caller URI */
+ private String mUri;
+
+ /** Caller friendly name */
+ private String mFriendlyName;
+
+ /** Call state */
+ private @State int mState;
+
+ /** Call flags */
+ private int mCallFlags;
+
+ /** @hide */
+ public BluetoothLeCall(@NonNull BluetoothLeCall that) {
+ mUuid = new UUID(that.getUuid().getMostSignificantBits(),
+ that.getUuid().getLeastSignificantBits());
+ mUri = that.mUri;
+ mFriendlyName = that.mFriendlyName;
+ mState = that.mState;
+ mCallFlags = that.mCallFlags;
+ }
+
+ /** @hide */
+ public BluetoothLeCall(@NonNull UUID uuid, @NonNull String uri, @NonNull String friendlyName,
+ @State int state, int callFlags) {
+ mUuid = uuid;
+ mUri = uri;
+ mFriendlyName = friendlyName;
+ mState = state;
+ mCallFlags = callFlags;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ BluetoothLeCall that = (BluetoothLeCall) o;
+ return mUuid.equals(that.mUuid) && mUri.equals(that.mUri)
+ && mFriendlyName.equals(that.mFriendlyName) && mState == that.mState
+ && mCallFlags == that.mCallFlags;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUuid, mUri, mFriendlyName, mState, mCallFlags);
+ }
+
+ /**
+ * Returns a string representation of this BluetoothLeCall.
+ *
+ * <p>
+ * Currently this is the UUID.
+ *
+ * @return string representation of this BluetoothLeCall
+ */
+ @Override
+ public String toString() {
+ return mUuid.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeParcelable(new ParcelUuid(mUuid), 0);
+ out.writeString(mUri);
+ out.writeString(mFriendlyName);
+ out.writeInt(mState);
+ out.writeInt(mCallFlags);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothLeCall> CREATOR =
+ new Parcelable.Creator<BluetoothLeCall>() {
+ public BluetoothLeCall createFromParcel(Parcel in) {
+ return new BluetoothLeCall(in);
+ }
+
+ public BluetoothLeCall[] newArray(int size) {
+ return new BluetoothLeCall[size];
+ }
+ };
+
+ private BluetoothLeCall(Parcel in) {
+ mUuid = ((ParcelUuid) in.readParcelable(null)).getUuid();
+ mUri = in.readString();
+ mFriendlyName = in.readString();
+ mState = in.readInt();
+ mCallFlags = in.readInt();
+ }
+
+ /**
+ * Returns an UUID of this BluetoothLeCall.
+ *
+ * <p>
+ * An UUID is unique identifier of a BluetoothLeCall.
+ *
+ * @return UUID of this BluetoothLeCall
+ * @hide
+ */
+ public @NonNull UUID getUuid() {
+ return mUuid;
+ }
+
+ /**
+ * Returns a URI of the remote party of this BluetoothLeCall.
+ *
+ * @return string representation of this BluetoothLeCall
+ * @hide
+ */
+ public @NonNull String getUri() {
+ return mUri;
+ }
+
+ /**
+ * Returns a friendly name of the call.
+ *
+ * @return friendly name representation of this BluetoothLeCall
+ * @hide
+ */
+ public @NonNull String getFriendlyName() {
+ return mFriendlyName;
+ }
+
+ /**
+ * Returns the call state.
+ *
+ * @return the state of this BluetoothLeCall
+ * @hide
+ */
+ public @State int getState() {
+ return mState;
+ }
+
+ /**
+ * Returns the call flags.
+ *
+ * @return call flags
+ * @hide
+ */
+ public int getCallFlags() {
+ return mCallFlags;
+ }
+
+ /**
+ * Whether the call direction is incoming.
+ *
+ * @return true if incoming call, false otherwise
+ * @hide
+ */
+ public boolean isIncomingCall() {
+ return (mCallFlags & FLAG_OUTGOING_CALL) == 0;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothLeCallControl.java b/framework/java/android/bluetooth/BluetoothLeCallControl.java
new file mode 100644
index 0000000000..fb080c9ec3
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothLeCallControl.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright 2019 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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 android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+import android.annotation.SuppressLint;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides the APIs to control the Call Control profile.
+ *
+ * <p>
+ * This class provides Bluetooth Telephone Bearer Service functionality,
+ * allowing applications to expose a GATT Service based interface to control the
+ * state of the calls by remote devices such as LE audio devices.
+ *
+ * <p>
+ * BluetoothLeCallControl is a proxy object for controlling the Bluetooth Telephone Bearer
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the
+ * BluetoothLeCallControl proxy object.
+ *
+ * @hide
+ */
+public final class BluetoothLeCallControl implements BluetoothProfile {
+ private static final String TAG = "BluetoothLeCallControl";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /** @hide */
+ @IntDef(prefix = "RESULT_", value = {
+ RESULT_SUCCESS,
+ RESULT_ERROR_UNKNOWN_CALL_ID,
+ RESULT_ERROR_INVALID_URI,
+ RESULT_ERROR_APPLICATION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Result {
+ }
+
+ /**
+ * Opcode write was successful.
+ *
+ * @hide
+ */
+ public static final int RESULT_SUCCESS = 0;
+
+ /**
+ * Unknown call Id has been used in the operation.
+ *
+ * @hide
+ */
+ public static final int RESULT_ERROR_UNKNOWN_CALL_ID = 1;
+
+ /**
+ * The URI provided in {@link Callback#onPlaceCallRequest} is invalid.
+ *
+ * @hide
+ */
+ public static final int RESULT_ERROR_INVALID_URI = 2;
+
+ /**
+ * Application internal error.
+ *
+ * @hide
+ */
+ public static final int RESULT_ERROR_APPLICATION = 3;
+
+ /** @hide */
+ @IntDef(prefix = "TERMINATION_REASON_", value = {
+ TERMINATION_REASON_INVALID_URI,
+ TERMINATION_REASON_FAIL,
+ TERMINATION_REASON_REMOTE_HANGUP,
+ TERMINATION_REASON_SERVER_HANGUP,
+ TERMINATION_REASON_LINE_BUSY,
+ TERMINATION_REASON_NETWORK_CONGESTION,
+ TERMINATION_REASON_CLIENT_HANGUP,
+ TERMINATION_REASON_NO_SERVICE,
+ TERMINATION_REASON_NO_ANSWER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TerminationReason {
+ }
+
+ /**
+ * Remote Caller ID value used to place a call was formed improperly.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_INVALID_URI = 0x00;
+
+ /**
+ * Call fail.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_FAIL = 0x01;
+
+ /**
+ * Remote party ended call.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_REMOTE_HANGUP = 0x02;
+
+ /**
+ * Call ended from the server.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_SERVER_HANGUP = 0x03;
+
+ /**
+ * Line busy.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_LINE_BUSY = 0x04;
+
+ /**
+ * Network congestion.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_NETWORK_CONGESTION = 0x05;
+
+ /**
+ * Client terminated.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_CLIENT_HANGUP = 0x06;
+
+ /**
+ * No service.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_NO_SERVICE = 0x07;
+
+ /**
+ * No answer.
+ *
+ * @hide
+ */
+ public static final int TERMINATION_REASON_NO_ANSWER = 0x08;
+
+ /*
+ * Flag indicating support for hold/unhold call feature.
+ *
+ * @hide
+ */
+ public static final int CAPABILITY_HOLD_CALL = 0x00000001;
+
+ /**
+ * Flag indicating support for joining calls feature.
+ *
+ * @hide
+ */
+ public static final int CAPABILITY_JOIN_CALLS = 0x00000002;
+
+ private static final int MESSAGE_TBS_SERVICE_CONNECTED = 102;
+ private static final int MESSAGE_TBS_SERVICE_DISCONNECTED = 103;
+
+ private static final int REG_TIMEOUT = 10000;
+
+ /**
+ * The template class is used to call callback functions on events from the TBS
+ * server. Callback functions are wrapped in this class and registered to the
+ * Android system during app registration.
+ *
+ * @hide
+ */
+ public abstract static class Callback {
+
+ private static final String TAG = "BluetoothLeCallControl.Callback";
+
+ /**
+ * Called when a remote client requested to accept the call.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callId The call Id requested to be accepted
+ * @hide
+ */
+ public abstract void onAcceptCall(int requestId, @NonNull UUID callId);
+
+ /**
+ * A remote client has requested to terminate the call.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callId The call Id requested to terminate
+ * @hide
+ */
+ public abstract void onTerminateCall(int requestId, @NonNull UUID callId);
+
+ /**
+ * A remote client has requested to hold the call.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callId The call Id requested to be put on hold
+ * @hide
+ */
+ public void onHoldCall(int requestId, @NonNull UUID callId) {
+ Log.e(TAG, "onHoldCall: unimplemented, however CAPABILITY_HOLD_CALL is set!");
+ }
+
+ /**
+ * A remote client has requested to unhold the call.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callId The call Id requested to unhold
+ * @hide
+ */
+ public void onUnholdCall(int requestId, @NonNull UUID callId) {
+ Log.e(TAG, "onUnholdCall: unimplemented, however CAPABILITY_HOLD_CALL is set!");
+ }
+
+ /**
+ * A remote client has requested to place a call.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callId The Id to be assigned for the new call
+ * @param uri The caller URI requested
+ * @hide
+ */
+ public abstract void onPlaceCall(int requestId, @NonNull UUID callId, @NonNull String uri);
+
+ /**
+ * A remote client has requested to join the calls.
+ *
+ * <p>
+ * An application must call {@link BluetoothLeCallControl#requestResult} to complete the
+ * request.
+ *
+ * @param requestId The Id of the request
+ * @param callIds The call Id list requested to join
+ * @hide
+ */
+ public void onJoinCalls(int requestId, @NonNull List<UUID> callIds) {
+ Log.e(TAG, "onJoinCalls: unimplemented, however CAPABILITY_JOIN_CALLS is set!");
+ }
+ }
+
+ private class CallbackWrapper extends IBluetoothLeCallControlCallback.Stub {
+
+ private final Executor mExecutor;
+ private final Callback mCallback;
+
+ CallbackWrapper(Executor executor, Callback callback) {
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void onBearerRegistered(int ccid) {
+ if (mCallback != null) {
+ mCcid = ccid;
+ } else {
+ // registration timeout
+ Log.e(TAG, "onBearerRegistered: mCallback is null");
+ }
+ }
+
+ @Override
+ public void onAcceptCall(int requestId, ParcelUuid uuid) {
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onAcceptCall(requestId, uuid.getUuid()));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onTerminateCall(int requestId, ParcelUuid uuid) {
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onTerminateCall(requestId, uuid.getUuid()));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onHoldCall(int requestId, ParcelUuid uuid) {
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onHoldCall(requestId, uuid.getUuid()));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onUnholdCall(int requestId, ParcelUuid uuid) {
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onUnholdCall(requestId, uuid.getUuid()));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onPlaceCall(int requestId, ParcelUuid uuid, String uri) {
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onPlaceCall(requestId, uuid.getUuid(), uri));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+
+ @Override
+ public void onJoinCalls(int requestId, List<ParcelUuid> parcelUuids) {
+ List<UUID> uuids = new ArrayList<>();
+ for (ParcelUuid parcelUuid : parcelUuids) {
+ uuids.add(parcelUuid.getUuid());
+ }
+
+ final long identityToken = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.onJoinCalls(requestId, uuids));
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ }
+ };
+
+ private Context mContext;
+ private ServiceListener mServiceListener;
+ private volatile IBluetoothLeCallControl mService;
+ private BluetoothAdapter mAdapter;
+ private int mCcid = 0;
+ private String mToken;
+ private Callback mCallback = null;
+
+ 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 BluetoothLeCallControl proxy object for interacting with the local Bluetooth
+ * telephone bearer service.
+ */
+ /* package */ BluetoothLeCallControl(Context context, ServiceListener listener) {
+ mContext = context;
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mServiceListener = listener;
+
+ IBluetoothManager mgr = mAdapter.getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+
+ doBind();
+ }
+
+ private boolean doBind() {
+ synchronized (mConnection) {
+ if (mService == null) {
+ if (VDBG)
+ Log.d(TAG, "Binding service...");
+ try {
+ return mAdapter.getBluetoothManager().
+ bindBluetoothProfileService(BluetoothProfile.LE_CALL_CONTROL,
+ mConnection);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to bind TelephoneBearerService", e);
+ }
+ }
+ }
+ return false;
+ }
+
+ private void doUnbind() {
+ synchronized (mConnection) {
+ if (mService != null) {
+ if (VDBG)
+ Log.d(TAG, "Unbinding service...");
+ try {
+ mAdapter.getBluetoothManager().
+ unbindBluetoothProfileService(BluetoothProfile.LE_CALL_CONTROL,
+ mConnection);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to unbind TelephoneBearerService", e);
+ } finally {
+ mService = null;
+ }
+ }
+ }
+ }
+
+ /* package */ void close() {
+ if (VDBG)
+ log("close()");
+ unregisterBearer();
+
+ IBluetoothManager mgr = mAdapter.getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException re) {
+ Log.e(TAG, "", re);
+ }
+ }
+ mServiceListener = null;
+ doUnbind();
+ }
+
+ private IBluetoothLeCallControl getService() {
+ return mService;
+ }
+
+ /**
+ * Not supported
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public int getConnectionState(@Nullable BluetoothDevice device) {
+ throw new UnsupportedOperationException("not supported");
+ }
+
+ /**
+ * Not supported
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ throw new UnsupportedOperationException("not supported");
+ }
+
+ /**
+ * Not supported
+ *
+ * @throws UnsupportedOperationException
+ */
+ @Override
+ public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
+ @NonNull int[] states) {
+ throw new UnsupportedOperationException("not supported");
+ }
+
+ /**
+ * Register Telephone Bearer exposing the interface that allows remote devices
+ * to track and control the call states.
+ *
+ * <p>
+ * This is an asynchronous call. The callback is used to notify success or
+ * failure if the function returns true.
+ *
+ * <p>
+ * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
+ *
+ * <!-- The UCI is a String identifier of the telephone bearer as defined at
+ * https://www.bluetooth.com/specifications/assigned-numbers/uniform-caller-identifiers
+ * (login required). -->
+ *
+ * <!-- The examples of common URI schemes can be found in
+ * https://iana.org/assignments/uri-schemes/uri-schemes.xhtml -->
+ *
+ * <!-- The Technology is an integer value. The possible values are defined at
+ * https://www.bluetooth.com/specifications/assigned-numbers (login required).
+ * -->
+ *
+ * @param uci Bearer Unique Client Identifier
+ * @param uriSchemes URI Schemes supported list
+ * @param capabilities bearer capabilities
+ * @param provider Network provider name
+ * @param technology Network technology
+ * @param executor {@link Executor} object on which callback will be
+ * executed. The Executor object is required.
+ * @param callback {@link Callback} object to which callback messages will
+ * be sent. The Callback object is required.
+ * @return true on success, false otherwise
+ * @hide
+ */
+ @SuppressLint("ExecutorRegistration")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean registerBearer(@Nullable String uci,
+ @NonNull List<String> uriSchemes, int capabilities,
+ @NonNull String provider, int technology,
+ @NonNull Executor executor, @NonNull Callback callback) {
+ if (DBG) {
+ Log.d(TAG, "registerBearer");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("null parameter: " + callback);
+ }
+ if (mCcid != 0) {
+ return false;
+ }
+
+ mToken = uci;
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ if (mCallback != null) {
+ Log.e(TAG, "Bearer can be opened only once");
+ return false;
+ }
+
+ mCallback = callback;
+ try {
+ CallbackWrapper callbackWrapper = new CallbackWrapper(executor, callback);
+ service.registerBearer(mToken, callbackWrapper, uci, uriSchemes, capabilities,
+ provider, technology);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ mCallback = null;
+ return false;
+ }
+
+ if (mCcid == 0) {
+ mCallback = null;
+ return false;
+ }
+
+ return true;
+ }
+
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+
+ return false;
+ }
+
+ /**
+ * Unregister Telephone Bearer Service and destroy all the associated data.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void unregisterBearer() {
+ if (DBG) {
+ Log.d(TAG, "unregisterBearer");
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ int ccid = mCcid;
+ mCcid = 0;
+ mCallback = null;
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.unregisterBearer(mToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ }
+
+ /**
+ * Get the Content Control ID (CCID) value.
+ *
+ * @return ccid Content Control ID value
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public int getContentControlId() {
+ return mCcid;
+ }
+
+ /**
+ * Notify about the newly added call.
+ *
+ * <p>
+ * This shall be called as early as possible after the call has been added.
+ *
+ * <p>
+ * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
+ *
+ * @param call Newly added call
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void onCallAdded(@NonNull BluetoothLeCall call) {
+ if (DBG) {
+ Log.d(TAG, "onCallAdded: call=" + call);
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.callAdded(mCcid, call);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ }
+
+ /**
+ * Notify about the removed call.
+ *
+ * <p>
+ * This shall be called as early as possible after the call has been removed.
+ *
+ * <p>
+ * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
+ *
+ * @param callId The Id of a call that has been removed
+ * @param reason Call termination reason
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void onCallRemoved(@NonNull UUID callId, @TerminationReason int reason) {
+ if (DBG) {
+ Log.d(TAG, "callRemoved: callId=" + callId);
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.callRemoved(mCcid, new ParcelUuid(callId), reason);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ }
+
+ /**
+ * Notify the call state change
+ *
+ * <p>
+ * This shall be called as early as possible after the state of the call has
+ * changed.
+ *
+ * <p>
+ * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
+ *
+ * @param callId The call Id that state has been changed
+ * @param state Call state
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void onCallStateChanged(@NonNull UUID callId, @BluetoothLeCall.State int state) {
+ if (DBG) {
+ Log.d(TAG, "callStateChanged: callId=" + callId + " state=" + state);
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.callStateChanged(mCcid, new ParcelUuid(callId), state);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ }
+
+ /**
+ * Provide the current calls list
+ *
+ * <p>
+ * This function must be invoked after registration if application has any
+ * calls.
+ *
+ * @param calls current calls list
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void currentCallsList(@NonNull List<BluetoothLeCall> calls) {
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.currentCallsList(mCcid, calls);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+
+ /**
+ * Provide the network current status
+ *
+ * <p>
+ * This function must be invoked on change of network state.
+ *
+ * <p>
+ * Requires {@link android.Manifest.permission#BLUETOOTH} permission.
+ *
+ * <!-- The Technology is an integer value. The possible values are defined at
+ * https://www.bluetooth.com/specifications/assigned-numbers (login required).
+ * -->
+ *
+ * @param provider Network provider name
+ * @param technology Network technology
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void networkStateChanged(@NonNull String provider, int technology) {
+ if (DBG) {
+ Log.d(TAG, "networkStateChanged: provider=" + provider + ", technology=" + technology);
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.networkStateChanged(mCcid, provider, technology);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ }
+
+ /**
+ * Send a response to a call control request to a remote device.
+ *
+ * <p>
+ * This function must be invoked in when a request is received by one of these
+ * callback methods:
+ *
+ * <ul>
+ * <li>{@link Callback#onAcceptCall}
+ * <li>{@link Callback#onTerminateCall}
+ * <li>{@link Callback#onHoldCall}
+ * <li>{@link Callback#onUnholdCall}
+ * <li>{@link Callback#onPlaceCall}
+ * <li>{@link Callback#onJoinCalls}
+ * </ul>
+ *
+ * @param requestId The ID of the request that was received with the callback
+ * @param result The result of the request to be sent to the remote devices
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void requestResult(int requestId, @Result int result) {
+ if (DBG) {
+ Log.d(TAG, "requestResult: requestId=" + requestId + " result=" + result);
+ }
+ if (mCcid == 0) {
+ return;
+ }
+
+ final IBluetoothLeCallControl service = getService();
+ if (service != null) {
+ try {
+ service.requestResult(mCcid, requestId, result);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+ }
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ private static boolean isValidDevice(@Nullable BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private final IBluetoothProfileServiceConnection mConnection =
+ new IBluetoothProfileServiceConnection.Stub() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (DBG) {
+ Log.d(TAG, "Proxy object connected");
+ }
+ mService = IBluetoothLeCallControl.Stub.asInterface(Binder.allowBlocking(service));
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_TBS_SERVICE_CONNECTED));
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ if (DBG) {
+ Log.d(TAG, "Proxy object disconnected");
+ }
+ doUnbind();
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_TBS_SERVICE_DISCONNECTED));
+ }
+ };
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_TBS_SERVICE_CONNECTED: {
+ if (mServiceListener != null) {
+ mServiceListener.onServiceConnected(BluetoothProfile.LE_CALL_CONTROL,
+ BluetoothLeCallControl.this);
+ }
+ break;
+ }
+ case MESSAGE_TBS_SERVICE_DISCONNECTED: {
+ if (mServiceListener != null) {
+ mServiceListener.onServiceDisconnected(BluetoothProfile.LE_CALL_CONTROL);
+ }
+ break;
+ }
+ }
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/BluetoothManager.java b/framework/java/android/bluetooth/BluetoothManager.java
new file mode 100644
index 0000000000..fef6f225dd
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothManager.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.RequiresFeature;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemService;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * High level manager used to obtain an instance of an {@link BluetoothAdapter}
+ * and to conduct overall Bluetooth Management.
+ * <p>
+ * Use {@link android.content.Context#getSystemService(java.lang.String)}
+ * with {@link Context#BLUETOOTH_SERVICE} to create an {@link BluetoothManager},
+ * then call {@link #getAdapter} to obtain the {@link BluetoothAdapter}.
+ * </p>
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>
+ * For more information about using BLUETOOTH, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer
+ * guide.
+ * </p>
+ * </div>
+ *
+ * @see Context#getSystemService
+ * @see BluetoothAdapter#getDefaultAdapter()
+ */
+@SystemService(Context.BLUETOOTH_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_BLUETOOTH)
+public final class BluetoothManager {
+ private static final String TAG = "BluetoothManager";
+ private static final boolean DBG = false;
+
+ private final AttributionSource mAttributionSource;
+ private final BluetoothAdapter mAdapter;
+
+ /**
+ * @hide
+ */
+ public BluetoothManager(Context context) {
+ mAttributionSource = (context != null) ? context.getAttributionSource() :
+ AttributionSource.myAttributionSource();
+ mAdapter = BluetoothAdapter.createAdapter(mAttributionSource);
+ }
+
+ /**
+ * Get the BLUETOOTH Adapter for this device.
+ *
+ * @return the BLUETOOTH Adapter
+ */
+ @RequiresNoPermission
+ public BluetoothAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Get the current connection state of the profile to the remote device.
+ *
+ * <p>This is not specific to any application configuration but represents
+ * the connection state of the local Bluetooth adapter for certain profile.
+ * This can be used by applications like status bar which would just like
+ * to know the state of Bluetooth.
+ *
+ * @param device Remote bluetooth device.
+ * @param profile GATT or GATT_SERVER
+ * @return State of the profile connection. One of {@link BluetoothProfile#STATE_CONNECTED},
+ * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_DISCONNECTED},
+ * {@link BluetoothProfile#STATE_DISCONNECTING}
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device, int profile) {
+ if (DBG) Log.d(TAG, "getConnectionState()");
+
+ List<BluetoothDevice> connectedDevices = getConnectedDevices(profile);
+ for (BluetoothDevice connectedDevice : connectedDevices) {
+ if (device.equals(connectedDevice)) {
+ return BluetoothProfile.STATE_CONNECTED;
+ }
+ }
+
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ /**
+ * Get connected devices for the specified profile.
+ *
+ * <p> Return the set of devices which are in state {@link BluetoothProfile#STATE_CONNECTED}
+ *
+ * <p>This is not specific to any application configuration but represents
+ * the connection state of Bluetooth for this profile.
+ * This can be used by applications like status bar which would just like
+ * to know the state of Bluetooth.
+ *
+ * @param profile GATT or GATT_SERVER
+ * @return List of devices. The list will be empty on error.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices(int profile) {
+ if (DBG) Log.d(TAG, "getConnectedDevices");
+ return getDevicesMatchingConnectionStates(profile, new int[] {
+ BluetoothProfile.STATE_CONNECTED
+ });
+ }
+
+ /**
+ * Get a list of devices that match any of the given connection
+ * states.
+ *
+ * <p> If none of the devices match any of the given states,
+ * an empty list will be returned.
+ *
+ * <p>This is not specific to any application configuration but represents
+ * the connection state of the local Bluetooth adapter for this profile.
+ * This can be used by applications like status bar which would just like
+ * to know the state of the local adapter.
+ *
+ * @param profile GATT or GATT_SERVER
+ * @param states Array of states. States can be one of {@link BluetoothProfile#STATE_CONNECTED},
+ * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_DISCONNECTED},
+ * {@link BluetoothProfile#STATE_DISCONNECTING},
+ * @return List of devices. The list will be empty on error.
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int profile, int[] states) {
+ if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates");
+
+ if (profile != BluetoothProfile.GATT && profile != BluetoothProfile.GATT_SERVER) {
+ throw new IllegalArgumentException("Profile not supported: " + profile);
+ }
+
+ List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
+
+ try {
+ IBluetoothManager managerService = mAdapter.getBluetoothManager();
+ IBluetoothGatt iGatt = managerService.getBluetoothGatt();
+ if (iGatt == null) return devices;
+ devices = Attributable.setAttributionSource(
+ iGatt.getDevicesMatchingConnectionStates(states, mAttributionSource),
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ }
+
+ return devices;
+ }
+
+ /**
+ * Open a GATT Server
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as the results of any other GATT server operations.
+ * The method returns a BluetoothGattServer instance. You can use BluetoothGattServer
+ * to conduct GATT server operations.
+ *
+ * @param context App context
+ * @param callback GATT server callback handler that will receive asynchronous callbacks.
+ * @return BluetoothGattServer instance
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGattServer openGattServer(Context context,
+ BluetoothGattServerCallback callback) {
+
+ return (openGattServer(context, callback, BluetoothDevice.TRANSPORT_AUTO));
+ }
+
+ /**
+ * Open a GATT Server
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as the results of any other GATT server operations.
+ * The method returns a BluetoothGattServer instance. You can use BluetoothGattServer
+ * to conduct GATT server operations.
+ *
+ * @param context App context
+ * @param callback GATT server callback handler that will receive asynchronous callbacks.
+ * @param eatt_support idicates if server should use eatt channel for notifications.
+ * @return BluetoothGattServer instance
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGattServer openGattServer(Context context,
+ BluetoothGattServerCallback callback, boolean eatt_support) {
+ return (openGattServer(context, callback, BluetoothDevice.TRANSPORT_AUTO, eatt_support));
+ }
+
+ /**
+ * Open a GATT Server
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as the results of any other GATT server operations.
+ * The method returns a BluetoothGattServer instance. You can use BluetoothGattServer
+ * to conduct GATT server operations.
+ *
+ * @param context App context
+ * @param callback GATT server callback handler that will receive asynchronous callbacks.
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @return BluetoothGattServer instance
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGattServer openGattServer(Context context,
+ BluetoothGattServerCallback callback, int transport) {
+ return (openGattServer(context, callback, transport, false));
+ }
+
+ /**
+ * Open a GATT Server
+ * The callback is used to deliver results to Caller, such as connection status as well
+ * as the results of any other GATT server operations.
+ * The method returns a BluetoothGattServer instance. You can use BluetoothGattServer
+ * to conduct GATT server operations.
+ *
+ * @param context App context
+ * @param callback GATT server callback handler that will receive asynchronous callbacks.
+ * @param transport preferred transport for GATT connections to remote dual-mode devices {@link
+ * BluetoothDevice#TRANSPORT_AUTO} or {@link BluetoothDevice#TRANSPORT_BREDR} or {@link
+ * BluetoothDevice#TRANSPORT_LE}
+ * @param eatt_support idicates if server should use eatt channel for notifications.
+ * @return BluetoothGattServer instance
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothGattServer openGattServer(Context context,
+ BluetoothGattServerCallback callback, int transport, boolean eatt_support) {
+ if (context == null || callback == null) {
+ throw new IllegalArgumentException("null parameter: " + context + " " + callback);
+ }
+
+ // TODO(Bluetooth) check whether platform support BLE
+ // Do the check here or in GattServer?
+
+ try {
+ IBluetoothManager managerService = mAdapter.getBluetoothManager();
+ IBluetoothGatt iGatt = managerService.getBluetoothGatt();
+ if (iGatt == null) {
+ Log.e(TAG, "Fail to get GATT Server connection");
+ return null;
+ }
+ BluetoothGattServer mGattServer =
+ new BluetoothGattServer(iGatt, transport, mAdapter);
+ Boolean regStatus = mGattServer.registerCallback(callback, eatt_support);
+ return regStatus ? mGattServer : null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ return null;
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothMap.java b/framework/java/android/bluetooth/BluetoothMap.java
new file mode 100644
index 0000000000..56e4972624
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothMap.java
@@ -0,0 +1,513 @@
+/*
+ * 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresNoPermission;
+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.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+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;
+
+/**
+ * This class provides the APIs to control the Bluetooth MAP
+ * Profile.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothMap implements BluetoothProfile, AutoCloseable {
+
+ private static final String TAG = "BluetoothMap";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /** @hide */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.map.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * There was an error trying to obtain the state
+ *
+ * @hide
+ */
+ public static final int STATE_ERROR = -1;
+
+ /** @hide */
+ public static final int RESULT_FAILURE = 0;
+ /** @hide */
+ public static final int RESULT_SUCCESS = 1;
+ /**
+ * Connection canceled before completion.
+ *
+ * @hide
+ */
+ public static final int RESULT_CANCELED = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothMap> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.MAP,
+ "BluetoothMap", IBluetoothMap.class.getName()) {
+ @Override
+ public IBluetoothMap getServiceInterface(IBinder service) {
+ return IBluetoothMap.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothMap proxy object.
+ */
+ /* package */ BluetoothMap(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ if (DBG) Log.d(TAG, "Create BluetoothMap proxy object");
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothMap will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ *
+ * @hide
+ */
+ @SystemApi
+ public void close() {
+ if (VDBG) log("close()");
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothMap getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Get the current state of the BluetoothMap service.
+ *
+ * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not
+ * connected to the Map service.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getState() {
+ if (VDBG) log("getState()");
+ final IBluetoothMap service = getService();
+ final int defaultValue = BluetoothMap.STATE_ERROR;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getState(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 currently connected remote Bluetooth device (PCE).
+ *
+ * @return The remote Bluetooth device, or null if not in connected or connecting state, or if
+ * this proxy object is not connected to the Map service.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothDevice getClient() {
+ if (VDBG) log("getClient()");
+ final IBluetoothMap service = getService();
+ 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.getClient(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;
+ }
+
+ /**
+ * Returns true if the specified Bluetooth device is connected.
+ * Returns false if not connected, or if this proxy object is not
+ * currently connected to the Map service.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isConnected(BluetoothDevice device) {
+ if (VDBG) log("isConnected(" + device + ")");
+ final IBluetoothMap service = getService();
+ 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.isConnected(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 connection. Initiation of outgoing connections is not
+ * supported for MAP server.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")" + "not supported for MAPS");
+ return false;
+ }
+
+ /**
+ * Initiate disconnect.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on error, true otherwise
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothMap service = getService();
+ 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.disconnect(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 class bits for possible Map support.
+ * This is a simple heuristic that tries to guess if a device with the
+ * given class bits might support Map. It is not accurate for all
+ * devices. It tries to err on the side of false positives.
+ *
+ * @return True if this device might support Map.
+ *
+ * @hide
+ */
+ public static boolean doesClassMatchSink(BluetoothClass btClass) {
+ // TODO optimize the rule
+ switch (btClass.getDeviceClass()) {
+ case BluetoothClass.Device.COMPUTER_DESKTOP:
+ case BluetoothClass.Device.COMPUTER_LAPTOP:
+ case BluetoothClass.Device.COMPUTER_SERVER:
+ case BluetoothClass.Device.COMPUTER_UNCATEGORIZED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Get the list of connected devices. Currently at most one.
+ *
+ * @return list of connected devices
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (DBG) log("getConnectedDevices()");
+ final IBluetoothMap service = getService();
+ 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.getConnectedDevices(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;
+ }
+
+ /**
+ * Get the list of devices matching specified states. Currently at most one.
+ *
+ * @return list of matching devices
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) log("getDevicesMatchingStates()");
+ final IBluetoothMap service = getService();
+ 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;
+ }
+
+ /**
+ * Get connection state of device
+ *
+ * @return device connection state
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (DBG) log("getConnectionState(" + device + ")");
+ final IBluetoothMap service = getService();
+ 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.getConnectionState(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 #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothMap service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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 IBluetoothMap service = getService();
+ 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;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.isEnabled();
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothMapClient.java b/framework/java/android/bluetooth/BluetoothMapClient.java
new file mode 100644
index 0000000000..03536f9aad
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothMapClient.java
@@ -0,0 +1,686 @@
+/*
+ * Copyright (C) 2016 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the APIs to control the Bluetooth MAP MCE Profile.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothMapClient implements BluetoothProfile {
+
+ private static final String TAG = "BluetoothMapClient";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.mapmce.profile.action.CONNECTION_STATE_CHANGED";
+ /** @hide */
+ @RequiresPermission(android.Manifest.permission.RECEIVE_SMS)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MESSAGE_RECEIVED =
+ "android.bluetooth.mapmce.profile.action.MESSAGE_RECEIVED";
+ /* Actions to be used for pending intents */
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MESSAGE_SENT_SUCCESSFULLY =
+ "android.bluetooth.mapmce.profile.action.MESSAGE_SENT_SUCCESSFULLY";
+ /** @hide */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MESSAGE_DELIVERED_SUCCESSFULLY =
+ "android.bluetooth.mapmce.profile.action.MESSAGE_DELIVERED_SUCCESSFULLY";
+
+ /**
+ * Action to notify read status changed
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MESSAGE_READ_STATUS_CHANGED =
+ "android.bluetooth.mapmce.profile.action.MESSAGE_READ_STATUS_CHANGED";
+
+ /**
+ * Action to notify deleted status changed
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_MESSAGE_DELETED_STATUS_CHANGED =
+ "android.bluetooth.mapmce.profile.action.MESSAGE_DELETED_STATUS_CHANGED";
+
+ /**
+ * Extras used in ACTION_MESSAGE_RECEIVED intent.
+ * NOTE: HANDLE is only valid for a single session with the device.
+ */
+ /** @hide */
+ public static final String EXTRA_MESSAGE_HANDLE =
+ "android.bluetooth.mapmce.profile.extra.MESSAGE_HANDLE";
+ /** @hide */
+ public static final String EXTRA_MESSAGE_TIMESTAMP =
+ "android.bluetooth.mapmce.profile.extra.MESSAGE_TIMESTAMP";
+ /** @hide */
+ public static final String EXTRA_MESSAGE_READ_STATUS =
+ "android.bluetooth.mapmce.profile.extra.MESSAGE_READ_STATUS";
+ /** @hide */
+ public static final String EXTRA_SENDER_CONTACT_URI =
+ "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_URI";
+ /** @hide */
+ public static final String EXTRA_SENDER_CONTACT_NAME =
+ "android.bluetooth.mapmce.profile.extra.SENDER_CONTACT_NAME";
+
+ /**
+ * Used as a boolean extra in ACTION_MESSAGE_DELETED_STATUS_CHANGED
+ * Contains the MAP message deleted status
+ * Possible values are:
+ * true: deleted
+ * false: undeleted
+ *
+ * @hide
+ */
+ public static final String EXTRA_MESSAGE_DELETED_STATUS =
+ "android.bluetooth.mapmce.profile.extra.MESSAGE_DELETED_STATUS";
+
+ /**
+ * Extra used in ACTION_MESSAGE_READ_STATUS_CHANGED or ACTION_MESSAGE_DELETED_STATUS_CHANGED
+ * Possible values are:
+ * 0: failure
+ * 1: success
+ *
+ * @hide
+ */
+ public static final String EXTRA_RESULT_CODE =
+ "android.bluetooth.device.extra.RESULT_CODE";
+
+ /**
+ * There was an error trying to obtain the state
+ * @hide
+ */
+ public static final int STATE_ERROR = -1;
+
+ /** @hide */
+ public static final int RESULT_FAILURE = 0;
+ /** @hide */
+ public static final int RESULT_SUCCESS = 1;
+ /**
+ * Connection canceled before completion.
+ * @hide
+ */
+ public static final int RESULT_CANCELED = 2;
+ /** @hide */
+ private static final int UPLOADING_FEATURE_BITMASK = 0x08;
+
+ /*
+ * UNREAD, READ, UNDELETED, DELETED are passed as parameters
+ * to setMessageStatus to indicate the messages new state.
+ */
+
+ /** @hide */
+ public static final int UNREAD = 0;
+ /** @hide */
+ public static final int READ = 1;
+ /** @hide */
+ public static final int UNDELETED = 2;
+ /** @hide */
+ public static final int DELETED = 3;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothMapClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.MAP_CLIENT,
+ "BluetoothMapClient", IBluetoothMapClient.class.getName()) {
+ @Override
+ public IBluetoothMapClient getServiceInterface(IBinder service) {
+ return IBluetoothMapClient.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothMapClient proxy object.
+ */
+ /* package */ BluetoothMapClient(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ if (DBG) Log.d(TAG, "Create BluetoothMapClient proxy object");
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothMap will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ * @hide
+ */
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothMapClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Returns true if the specified Bluetooth device is connected.
+ * Returns false if not connected, or if this proxy object is not
+ * currently connected to the Map service.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isConnected(BluetoothDevice device) {
+ if (VDBG) Log.d(TAG, "isConnected(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.isConnected(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 connection. Initiation of outgoing connections is not
+ * supported for MAP server.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "connect(" + device + ")" + "for MAPS MCE");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.connect(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 disconnect.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on error, true otherwise
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "disconnect(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.disconnect(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 list of connected devices. Currently at most one.
+ *
+ * @return list of connected devices
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (DBG) Log.d(TAG, "getConnectedDevices()");
+ final IBluetoothMapClient service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List<BluetoothDevice>> recv =
+ new SynchronousResultReceiver();
+ service.getConnectedDevices(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;
+ }
+
+ /**
+ * Get the list of devices matching specified states. Currently at most one.
+ *
+ * @return list of matching devices
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) Log.d(TAG, "getDevicesMatchingStates()");
+ final IBluetoothMapClient service = getService();
+ final List<BluetoothDevice> defaultValue = new ArrayList<>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, 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;
+ }
+
+ /**
+ * Get connection state of device
+ *
+ * @return device connection state
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getConnectionState(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver<>();
+ service.getConnectionState(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 #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) Log.d(TAG, "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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) Log.d(TAG, "setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) Log.d(TAG, "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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(@NonNull BluetoothDevice device) {
+ if (VDBG) Log.d(TAG, "getConnectionPolicy(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, 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;
+ }
+
+ /**
+ * Send a message.
+ *
+ * Send an SMS message to either the contacts primary number or the telephone number specified.
+ *
+ * @param device Bluetooth device
+ * @param contacts Uri Collection of the contacts
+ * @param message Message to be sent
+ * @param sentIntent intent issued when message is sent
+ * @param deliveredIntent intent issued when message is delivered
+ * @return true if the message is enqueued, false on error
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.SEND_SMS,
+ })
+ public boolean sendMessage(@NonNull BluetoothDevice device, @NonNull Collection<Uri> contacts,
+ @NonNull String message, @Nullable PendingIntent sentIntent,
+ @Nullable PendingIntent deliveredIntent) {
+ return sendMessage(device, contacts.toArray(new Uri[contacts.size()]), message, sentIntent,
+ deliveredIntent);
+ }
+
+ /**
+ * Send a message.
+ *
+ * Send an SMS message to either the contacts primary number or the telephone number specified.
+ *
+ * @param device Bluetooth device
+ * @param contacts Uri[] of the contacts
+ * @param message Message to be sent
+ * @param sentIntent intent issued when message is sent
+ * @param deliveredIntent intent issued when message is delivered
+ * @return true if the message is enqueued, false on error
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.SEND_SMS,
+ })
+ public boolean sendMessage(BluetoothDevice device, Uri[] contacts, String message,
+ PendingIntent sentIntent, PendingIntent deliveredIntent) {
+ if (DBG) Log.d(TAG, "sendMessage(" + device + ", " + contacts + ", " + message);
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.sendMessage(device, contacts, message, sentIntent, deliveredIntent,
+ 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 unread messages. Unread messages will be published via {@link #ACTION_MESSAGE_RECEIVED}.
+ *
+ * @param device Bluetooth device
+ * @return true if the message is enqueued, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.READ_SMS,
+ })
+ public boolean getUnreadMessages(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "getUnreadMessages(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.getUnreadMessages(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;
+ }
+
+ /**
+ * Returns the "Uploading" feature bit value from the SDP record's
+ * MapSupportedFeatures field (see Bluetooth MAP 1.4 spec, page 114).
+ * @param device The Bluetooth device to get this value for.
+ * @return Returns true if the Uploading bit value in SDP record's
+ * MapSupportedFeatures field is set. False is returned otherwise.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isUploadingSupported(BluetoothDevice device) {
+ if (DBG) Log.d(TAG, "isUploadingSupported(" + device + ")");
+ final IBluetoothMapClient service = getService();
+ final int defaultValue = 0;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getSupportedFeatures(device, mAttributionSource, recv);
+ return (recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue)
+ & UPLOADING_FEATURE_BITMASK) > 0;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Set message status of message on MSE
+ * <p>
+ * When read status changed, the result will be published via
+ * {@link #ACTION_MESSAGE_READ_STATUS_CHANGED}
+ * When deleted status changed, the result will be published via
+ * {@link #ACTION_MESSAGE_DELETED_STATUS_CHANGED}
+ *
+ * @param device Bluetooth device
+ * @param handle message handle
+ * @param status <code>UNREAD</code> for "unread", <code>READ</code> for
+ * "read", <code>UNDELETED</code> for "undeleted", <code>DELETED</code> for
+ * "deleted", otherwise return error
+ * @return <code>true</code> if request has been sent, <code>false</code> on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.READ_SMS,
+ })
+ public boolean setMessageStatus(BluetoothDevice device, String handle, int status) {
+ if (DBG) Log.d(TAG, "setMessageStatus(" + device + ", " + handle + ", " + status + ")");
+ final IBluetoothMapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device) && handle != null && (status == READ
+ || status == UNREAD || status == UNDELETED || status == DELETED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.setMessageStatus(device, handle, status, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.isEnabled();
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothMasInstance.java b/framework/java/android/bluetooth/BluetoothMasInstance.java
new file mode 100644
index 0000000000..eeaf085451
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothMasInstance.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 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 android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public final class BluetoothMasInstance implements Parcelable {
+ private final int mId;
+ private final String mName;
+ private final int mChannel;
+ private final int mMsgTypes;
+
+ public BluetoothMasInstance(int id, String name, int channel, int msgTypes) {
+ mId = id;
+ mName = name;
+ mChannel = channel;
+ mMsgTypes = msgTypes;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (o instanceof BluetoothMasInstance) {
+ return mId == ((BluetoothMasInstance) o).mId;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mId + (mChannel << 8) + (mMsgTypes << 16);
+ }
+
+ @Override
+ public String toString() {
+ return Integer.toString(mId) + ":" + mName + ":" + mChannel + ":"
+ + Integer.toHexString(mMsgTypes);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<BluetoothMasInstance> CREATOR =
+ new Parcelable.Creator<BluetoothMasInstance>() {
+ public BluetoothMasInstance createFromParcel(Parcel in) {
+ return new BluetoothMasInstance(in.readInt(), in.readString(),
+ in.readInt(), in.readInt());
+ }
+
+ public BluetoothMasInstance[] newArray(int size) {
+ return new BluetoothMasInstance[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mId);
+ out.writeString(mName);
+ out.writeInt(mChannel);
+ out.writeInt(mMsgTypes);
+ }
+
+ public static final class MessageType {
+ public static final int EMAIL = 0x01;
+ public static final int SMS_GSM = 0x02;
+ public static final int SMS_CDMA = 0x04;
+ public static final int MMS = 0x08;
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public int getChannel() {
+ return mChannel;
+ }
+
+ public int getMsgTypes() {
+ return mMsgTypes;
+ }
+
+ public boolean msgSupported(int msg) {
+ return (mMsgTypes & msg) != 0;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothOutputStream.java b/framework/java/android/bluetooth/BluetoothOutputStream.java
new file mode 100644
index 0000000000..ac2b3edb0e
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothOutputStream.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.SuppressLint;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * BluetoothOutputStream.
+ *
+ * Used to read from a Bluetooth socket.
+ *
+ * @hide
+ */
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+/*package*/ final class BluetoothOutputStream extends OutputStream {
+ private BluetoothSocket mSocket;
+
+ /*package*/ BluetoothOutputStream(BluetoothSocket s) {
+ mSocket = s;
+ }
+
+ /**
+ * Close this output stream and the socket associated with it.
+ */
+ public void close() throws IOException {
+ mSocket.close();
+ }
+
+ /**
+ * Writes a single byte to this stream. Only the least significant byte of
+ * the integer {@code oneByte} is written to the stream.
+ *
+ * @param oneByte the byte to be written.
+ * @throws IOException if an error occurs while writing to this stream.
+ * @since Android 1.0
+ */
+ public void write(int oneByte) throws IOException {
+ byte[] b = new byte[1];
+ b[0] = (byte) oneByte;
+ mSocket.write(b, 0, 1);
+ }
+
+ /**
+ * Writes {@code count} bytes from the byte array {@code buffer} starting
+ * at position {@code offset} to this stream.
+ *
+ * @param b the buffer to be written.
+ * @param offset the start position in {@code buffer} from where to get bytes.
+ * @param count the number of bytes from {@code buffer} to write to this stream.
+ * @throws IOException if an error occurs while writing to this stream.
+ * @throws IndexOutOfBoundsException if {@code offset < 0} or {@code count < 0}, or if {@code
+ * offset + count} is bigger than the length of {@code buffer}.
+ * @since Android 1.0
+ */
+ public void write(byte[] b, int offset, int count) throws IOException {
+ if (b == null) {
+ throw new NullPointerException("buffer is null");
+ }
+ if ((offset | count) < 0 || count > b.length - offset) {
+ throw new IndexOutOfBoundsException("invalid offset or length");
+ }
+ mSocket.write(b, offset, count);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothPan.java b/framework/java/android/bluetooth/BluetoothPan.java
new file mode 100644
index 0000000000..d4ad4ef47a
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothPan.java
@@ -0,0 +1,525 @@
+/*
+ * 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.IntDef;
+import android.annotation.NonNull;
+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.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the APIs to control the Bluetooth Pan
+ * Profile.
+ *
+ * <p>BluetoothPan is a proxy object for controlling the Bluetooth
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothPan proxy object.
+ *
+ * <p>Each method is protected with its appropriate permission.
+ *
+ * @hide
+ */
+@SystemApi
+public final class BluetoothPan implements BluetoothProfile {
+ private static final String TAG = "BluetoothPan";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Pan
+ * profile.
+ *
+ * <p>This intent will have 4 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>
+ * <li> {@link #EXTRA_LOCAL_ROLE} - Which local role the remote device is
+ * bound to. </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}.
+ *
+ * <p> {@link #EXTRA_LOCAL_ROLE} can be one of {@link #LOCAL_NAP_ROLE} or
+ * {@link #LOCAL_PANU_ROLE}
+ */
+ @SuppressLint("ActionValue")
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * Extra for {@link #ACTION_CONNECTION_STATE_CHANGED} intent
+ * The local role of the PAN profile that the remote device is bound to.
+ * It can be one of {@link #LOCAL_NAP_ROLE} or {@link #LOCAL_PANU_ROLE}.
+ */
+ @SuppressLint("ActionValue")
+ public static final String EXTRA_LOCAL_ROLE = "android.bluetooth.pan.extra.LOCAL_ROLE";
+
+ /**
+ * Intent used to broadcast the change in tethering state of the Pan
+ * Profile
+ *
+ * <p>This intent will have 1 extra:
+ * <ul>
+ * <li> {@link #EXTRA_TETHERING_STATE} - The current state of Bluetooth
+ * tethering. </li>
+ * </ul>
+ *
+ * <p> {@link #EXTRA_TETHERING_STATE} can be any of {@link #TETHERING_STATE_OFF} or
+ * {@link #TETHERING_STATE_ON}
+ */
+ @RequiresLegacyBluetoothPermission
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TETHERING_STATE_CHANGED =
+ "android.bluetooth.action.TETHERING_STATE_CHANGED";
+
+ /**
+ * Extra for {@link #ACTION_TETHERING_STATE_CHANGED} intent
+ * The tethering state of the PAN profile.
+ * It can be one of {@link #TETHERING_STATE_OFF} or {@link #TETHERING_STATE_ON}.
+ */
+ public static final String EXTRA_TETHERING_STATE =
+ "android.bluetooth.extra.TETHERING_STATE";
+
+ /** @hide */
+ @IntDef({PAN_ROLE_NONE, LOCAL_NAP_ROLE, LOCAL_PANU_ROLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LocalPanRole {}
+
+ public static final int PAN_ROLE_NONE = 0;
+ /**
+ * The local device is acting as a Network Access Point.
+ */
+ public static final int LOCAL_NAP_ROLE = 1;
+
+ /**
+ * The local device is acting as a PAN User.
+ */
+ public static final int LOCAL_PANU_ROLE = 2;
+
+ /** @hide */
+ @IntDef({PAN_ROLE_NONE, REMOTE_NAP_ROLE, REMOTE_PANU_ROLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RemotePanRole {}
+
+ public static final int REMOTE_NAP_ROLE = 1;
+
+ public static final int REMOTE_PANU_ROLE = 2;
+
+ /** @hide **/
+ @IntDef({TETHERING_STATE_OFF, TETHERING_STATE_ON})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TetheringState{}
+
+ public static final int TETHERING_STATE_OFF = 1;
+
+ public static final int TETHERING_STATE_ON = 2;
+ /**
+ * Return codes for the connect and disconnect Bluez / Dbus calls.
+ *
+ * @hide
+ */
+ public static final int PAN_DISCONNECT_FAILED_NOT_CONNECTED = 1000;
+
+ /**
+ * @hide
+ */
+ public static final int PAN_CONNECT_FAILED_ALREADY_CONNECTED = 1001;
+
+ /**
+ * @hide
+ */
+ public static final int PAN_CONNECT_FAILED_ATTEMPT_FAILED = 1002;
+
+ /**
+ * @hide
+ */
+ public static final int PAN_OPERATION_GENERIC_FAILURE = 1003;
+
+ /**
+ * @hide
+ */
+ public static final int PAN_OPERATION_SUCCESS = 1004;
+
+ private final Context mContext;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothPan> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.PAN,
+ "BluetoothPan", IBluetoothPan.class.getName()) {
+ @Override
+ public IBluetoothPan getServiceInterface(IBinder service) {
+ return IBluetoothPan.Stub.asInterface(service);
+ }
+ };
+
+
+ /**
+ * Create a BluetoothPan proxy object for interacting with the local
+ * Bluetooth Service which handles the Pan profile
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ /* package */ BluetoothPan(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mContext = context;
+ mProfileConnector.connect(context, listener);
+ }
+
+ /**
+ * Closes the connection to the service and unregisters callbacks
+ */
+ @UnsupportedAppUsage
+ void close() {
+ if (VDBG) log("close()");
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothPan getService() {
+ return mProfileConnector.getService();
+ }
+
+ /** @hide */
+ protected void finalize() {
+ close();
+ }
+
+ /**
+ * Initiate connection to a profile of the remote bluetooth device.
+ *
+ * <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
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")");
+ final IBluetoothPan service = getService();
+ 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.connect(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
+ */
+ @UnsupportedAppUsage
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothPan service = getService();
+ 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.disconnect(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 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothPan service = getService();
+ 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;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) log("getConnectedDevices()");
+ final IBluetoothPan service = getService();
+ 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.getConnectedDevices(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}
+ * @hide
+ */
+ @Override
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (VDBG) log("getDevicesMatchingStates()");
+ final IBluetoothPan service = getService();
+ 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}
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public int getConnectionState(@NonNull BluetoothDevice device) {
+ if (VDBG) log("getState(" + device + ")");
+ final IBluetoothPan service = getService();
+ 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.getConnectionState(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;
+ }
+
+ /**
+ * Turns on/off bluetooth tethering
+ *
+ * @param value is whether to enable or disable bluetooth tethering
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ android.Manifest.permission.TETHER_PRIVILEGED,
+ })
+ public void setBluetoothTethering(boolean value) {
+ String pkgName = mContext.getOpPackageName();
+ if (DBG) log("setBluetoothTethering(" + value + "), calling package:" + pkgName);
+ final IBluetoothPan service = getService();
+ 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.setBluetoothTethering(value, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * Determines whether tethering is enabled
+ *
+ * @return true if tethering is on, false if not or some error occurred
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isTetheringOn() {
+ if (VDBG) log("isTetheringOn()");
+ final IBluetoothPan service = getService();
+ 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.isTetheringOn(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ @UnsupportedAppUsage
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ @UnsupportedAppUsage
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ @UnsupportedAppUsage
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothPbap.java b/framework/java/android/bluetooth/BluetoothPbap.java
new file mode 100644
index 0000000000..de2db9c2ca
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothPbap.java
@@ -0,0 +1,317 @@
+/*
+ * 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 android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Public API for controlling the Bluetooth Pbap Service. This includes
+ * Bluetooth Phone book Access profile.
+ * BluetoothPbap is a proxy object for controlling the Bluetooth Pbap
+ * Service via IPC.
+ *
+ * Creating a BluetoothPbap object will create a binding with the
+ * BluetoothPbap service. Users of this object should call close() when they
+ * are finished with the BluetoothPbap, so that this proxy object can unbind
+ * from the service.
+ *
+ * This BluetoothPbap object is not immediately bound to the
+ * BluetoothPbap service. Use the ServiceListener interface to obtain a
+ * notification when it is bound, this is especially important if you wish to
+ * immediately call methods on BluetoothPbap after construction.
+ *
+ * To get an instance of the BluetoothPbap class, you can call
+ * {@link BluetoothAdapter#getProfileProxy(Context, ServiceListener, int)} with the final param
+ * being {@link BluetoothProfile#PBAP}. The ServiceListener should be able to get the instance of
+ * BluetoothPbap in {@link android.bluetooth.BluetoothProfile.ServiceListener#onServiceConnected}.
+ *
+ * Android only supports one connected Bluetooth Pce at a time.
+ *
+ * @hide
+ */
+@SystemApi
+public class BluetoothPbap implements BluetoothProfile {
+
+ private static final String TAG = "BluetoothPbap";
+ private static final boolean DBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the PBAP
+ * profile.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link BluetoothProfile#EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link BluetoothProfile#EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ * <p>{@link BluetoothProfile#EXTRA_STATE} or {@link BluetoothProfile#EXTRA_PREVIOUS_STATE}
+ * can be any of {@link BluetoothProfile#STATE_DISCONNECTED},
+ * {@link BluetoothProfile#STATE_CONNECTING}, {@link BluetoothProfile#STATE_CONNECTED},
+ * {@link BluetoothProfile#STATE_DISCONNECTING}.
+ *
+ * @hide
+ */
+ @SuppressLint("ActionValue")
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED";
+
+ private final AttributionSource mAttributionSource;
+
+ /** @hide */
+ public static final int RESULT_FAILURE = 0;
+ /** @hide */
+ public static final int RESULT_SUCCESS = 1;
+ /**
+ * Connection canceled before completion.
+ *
+ * @hide
+ */
+ public static final int RESULT_CANCELED = 2;
+
+ private BluetoothAdapter mAdapter;
+ private final BluetoothProfileConnector<IBluetoothPbap> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.PBAP, "BluetoothPbap",
+ IBluetoothPbap.class.getName()) {
+ @Override
+ public IBluetoothPbap getServiceInterface(IBinder service) {
+ return IBluetoothPbap.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothPbap proxy object.
+ *
+ * @hide
+ */
+ public BluetoothPbap(Context context, ServiceListener listener, BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ /** @hide */
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothPbap will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ *
+ * @hide
+ */
+ public synchronized void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothPbap getService() {
+ return (IBluetoothPbap) mProfileConnector.getService();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ log("getConnectedDevices()");
+ final IBluetoothPbap service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ return new ArrayList<BluetoothDevice>();
+ }
+ try {
+ return Attributable.setAttributionSource(
+ service.getConnectedDevices(mAttributionSource), mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return new ArrayList<BluetoothDevice>();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @SystemApi
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @BtProfileState int getConnectionState(@NonNull BluetoothDevice device) {
+ log("getConnectionState: device=" + device);
+ try {
+ final IBluetoothPbap service = getService();
+ if (service != null && isEnabled() && isValidDevice(device)) {
+ return service.getConnectionState(device, mAttributionSource);
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return BluetoothProfile.STATE_DISCONNECTED;
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @hide
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ log("getDevicesMatchingConnectionStates: states=" + Arrays.toString(states));
+ final IBluetoothPbap service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ return new ArrayList<BluetoothDevice>();
+ }
+ try {
+ return Attributable.setAttributionSource(
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource),
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return new ArrayList<BluetoothDevice>();
+ }
+
+ /**
+ * Set connection policy of the profile and tries to disconnect it if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of:
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
+ * {@link BluetoothProfile#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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ try {
+ final IBluetoothPbap service = getService();
+ if (service != null && isEnabled()
+ && isValidDevice(device)) {
+ if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ return false;
+ }
+ return service.setConnectionPolicy(device, connectionPolicy, mAttributionSource);
+ }
+ if (service == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+
+ /**
+ * Disconnects the current Pbap client (PCE). Currently this call blocks,
+ * it may soon be made asynchronous. Returns false if this proxy object is
+ * not currently connected to the Pbap service.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ log("disconnect()");
+ final IBluetoothPbap service = getService();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ return false;
+ }
+ try {
+ service.disconnect(device, mAttributionSource);
+ return true;
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return false;
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ if (DBG) {
+ Log.d(TAG, msg);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothPbapClient.java b/framework/java/android/bluetooth/BluetoothPbapClient.java
new file mode 100644
index 0000000000..e096de8cb8
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothPbapClient.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2016 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.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the APIs to control the Bluetooth PBAP Client Profile.
+ *
+ * @hide
+ */
+public final class BluetoothPbapClient implements BluetoothProfile {
+
+ private static final String TAG = "BluetoothPbapClient";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.pbapclient.profile.action.CONNECTION_STATE_CHANGED";
+
+ /** There was an error trying to obtain the state */
+ public static final int STATE_ERROR = -1;
+
+ public static final int RESULT_FAILURE = 0;
+ public static final int RESULT_SUCCESS = 1;
+ /** Connection canceled before completion. */
+ public static final int RESULT_CANCELED = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothPbapClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.PBAP_CLIENT,
+ "BluetoothPbapClient", IBluetoothPbapClient.class.getName()) {
+ @Override
+ public IBluetoothPbapClient getServiceInterface(IBinder service) {
+ return IBluetoothPbapClient.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothPbapClient proxy object.
+ */
+ BluetoothPbapClient(Context context, ServiceListener listener, BluetoothAdapter adapter) {
+ if (DBG) {
+ Log.d(TAG, "Create BluetoothPbapClient proxy object");
+ }
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothPbapClient will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ */
+ public synchronized void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothPbapClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Initiate connection.
+ * Upon successful connection to remote PBAP server the Client will
+ * attempt to automatically download the users phonebook and call log.
+ *
+ * @param device a remote device we want connect to
+ * @return <code>true</code> if command has been issued successfully; <code>false</code>
+ * otherwise;
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) {
+ log("connect(" + device + ") for PBAP Client.");
+ }
+ final IBluetoothPbapClient service = getService();
+ 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.connect(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 disconnect.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on error, true otherwise
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) {
+ log("disconnect(" + device + ")" + new Exception());
+ }
+ final IBluetoothPbapClient service = getService();
+ final boolean defaultValue = true;
+ 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.disconnect(device, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ return true;
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the list of connected devices.
+ * Currently at most one.
+ *
+ * @return list of connected devices
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (DBG) {
+ log("getConnectedDevices()");
+ }
+ final IBluetoothPbapClient service = getService();
+ 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.getConnectedDevices(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;
+ }
+
+ /**
+ * Get the list of devices matching specified states. Currently at most one.
+ *
+ * @return list of matching devices
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) {
+ log("getDevicesMatchingStates()");
+ }
+ final IBluetoothPbapClient service = getService();
+ 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;
+ }
+
+ /**
+ * Get connection state of device
+ *
+ * @return device connection state
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (DBG) {
+ log("getConnectionState(" + device + ")");
+ }
+ final IBluetoothPbapClient service = getService();
+ 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.getConnectionState(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;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.isEnabled();
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) {
+ log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ }
+ final IBluetoothPbapClient service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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
+ */
+ @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 IBluetoothPbapClient service = getService();
+ 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;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothProfile.java b/framework/java/android/bluetooth/BluetoothProfile.java
new file mode 100644
index 0000000000..d0f74e9857
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothProfile.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2010-2016 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 android.annotation.IntDef;
+import android.annotation.RequiresNoPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Public APIs for the Bluetooth Profiles.
+ *
+ * <p> Clients should call {@link BluetoothAdapter#getProfileProxy},
+ * to get the Profile Proxy. Each public profile implements this
+ * interface.
+ */
+public interface BluetoothProfile {
+
+ /**
+ * Extra for the connection state intents of the individual profiles.
+ *
+ * This extra represents the current connection state of the profile of the
+ * Bluetooth device.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_STATE = "android.bluetooth.profile.extra.STATE";
+
+ /**
+ * Extra for the connection state intents of the individual profiles.
+ *
+ * This extra represents the previous connection state of the profile of the
+ * Bluetooth device.
+ */
+ @SuppressLint("ActionValue")
+ String EXTRA_PREVIOUS_STATE =
+ "android.bluetooth.profile.extra.PREVIOUS_STATE";
+
+ /** The profile is in disconnected state */
+ int STATE_DISCONNECTED = 0;
+ /** The profile is in connecting state */
+ int STATE_CONNECTING = 1;
+ /** The profile is in connected state */
+ int STATE_CONNECTED = 2;
+ /** The profile is in disconnecting state */
+ int STATE_DISCONNECTING = 3;
+
+ /** @hide */
+ @IntDef({
+ STATE_DISCONNECTED,
+ STATE_CONNECTING,
+ STATE_CONNECTED,
+ STATE_DISCONNECTING,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BtProfileState {}
+
+ /**
+ * Headset and Handsfree profile
+ */
+ int HEADSET = 1;
+
+ /**
+ * A2DP profile.
+ */
+ int A2DP = 2;
+
+ /**
+ * Health Profile
+ *
+ * @deprecated Health Device Profile (HDP) and MCAP protocol are no longer used. New
+ * apps should use Bluetooth Low Energy based solutions such as {@link BluetoothGatt},
+ * {@link BluetoothAdapter#listenUsingL2capChannel()}, or
+ * {@link BluetoothDevice#createL2capChannel(int)}
+ */
+ @Deprecated
+ int HEALTH = 3;
+
+ /**
+ * HID Host
+ *
+ * @hide
+ */
+ int HID_HOST = 4;
+
+ /**
+ * PAN Profile
+ *
+ * @hide
+ */
+ @SystemApi
+ int PAN = 5;
+
+ /**
+ * PBAP
+ *
+ * @hide
+ */
+ int PBAP = 6;
+
+ /**
+ * GATT
+ */
+ int GATT = 7;
+
+ /**
+ * GATT_SERVER
+ */
+ int GATT_SERVER = 8;
+
+ /**
+ * MAP Profile
+ *
+ * @hide
+ */
+ int MAP = 9;
+
+ /*
+ * SAP Profile
+ * @hide
+ */
+ int SAP = 10;
+
+ /**
+ * A2DP Sink Profile
+ *
+ * @hide
+ */
+ @SystemApi
+ int A2DP_SINK = 11;
+
+ /**
+ * AVRCP Controller Profile
+ *
+ * @hide
+ */
+ @SystemApi
+ int AVRCP_CONTROLLER = 12;
+
+ /**
+ * AVRCP Target Profile
+ *
+ * @hide
+ */
+ int AVRCP = 13;
+
+ /**
+ * Headset Client - HFP HF Role
+ *
+ * @hide
+ */
+ @SystemApi
+ int HEADSET_CLIENT = 16;
+
+ /**
+ * PBAP Client
+ *
+ * @hide
+ */
+ @SystemApi
+ int PBAP_CLIENT = 17;
+
+ /**
+ * MAP Messaging Client Equipment (MCE)
+ *
+ * @hide
+ */
+ @SystemApi
+ int MAP_CLIENT = 18;
+
+ /**
+ * HID Device
+ */
+ int HID_DEVICE = 19;
+
+ /**
+ * Object Push Profile (OPP)
+ *
+ * @hide
+ */
+ int OPP = 20;
+
+ /**
+ * Hearing Aid Device
+ *
+ */
+ int HEARING_AID = 21;
+
+ /**
+ * LE Audio Device
+ *
+ */
+ int LE_AUDIO = 22;
+
+ /**
+ * Volume Control profile
+ *
+ * @hide
+ */
+ @SystemApi
+ int VOLUME_CONTROL = 23;
+
+ /**
+ * @hide
+ * Media Control Profile server
+ *
+ */
+ int MCP_SERVER = 24;
+
+ /**
+ * Coordinated Set Identification Profile set coordinator
+ *
+ */
+ int CSIP_SET_COORDINATOR = 25;
+
+ /**
+ * LE Audio Broadcast Source
+ *
+ * @hide
+ */
+ int LE_AUDIO_BROADCAST = 26;
+
+ /**
+ * @hide
+ * Telephone Bearer Service from Call Control Profile
+ *
+ */
+ int LE_CALL_CONTROL = 27;
+
+ /**
+ * Max profile ID. This value should be updated whenever a new profile is added to match
+ * the largest value assigned to a profile.
+ *
+ * @hide
+ */
+ int MAX_PROFILE_ID = 27;
+
+ /**
+ * Default priority for devices that we try to auto-connect to and
+ * and allow incoming connections for the profile
+ *
+ * @hide
+ **/
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ int PRIORITY_AUTO_CONNECT = 1000;
+
+ /**
+ * Default priority for devices that allow incoming
+ * and outgoing connections for the profile
+ *
+ * @hide
+ * @deprecated Replaced with {@link #CONNECTION_POLICY_ALLOWED}
+ **/
+ @Deprecated
+ @SystemApi
+ int PRIORITY_ON = 100;
+
+ /**
+ * Default priority for devices that does not allow incoming
+ * connections and outgoing connections for the profile.
+ *
+ * @hide
+ * @deprecated Replaced with {@link #CONNECTION_POLICY_FORBIDDEN}
+ **/
+ @Deprecated
+ @SystemApi
+ int PRIORITY_OFF = 0;
+
+ /**
+ * Default priority when not set or when the device is unpaired
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ int PRIORITY_UNDEFINED = -1;
+
+ /** @hide */
+ @IntDef(prefix = "CONNECTION_POLICY_", value = {CONNECTION_POLICY_ALLOWED,
+ CONNECTION_POLICY_FORBIDDEN, CONNECTION_POLICY_UNKNOWN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConnectionPolicy{}
+
+ /**
+ * Default connection policy for devices that allow incoming and outgoing connections
+ * for the profile
+ *
+ * @hide
+ **/
+ @SystemApi
+ int CONNECTION_POLICY_ALLOWED = 100;
+
+ /**
+ * Default connection policy for devices that do not allow incoming or outgoing connections
+ * for the profile.
+ *
+ * @hide
+ **/
+ @SystemApi
+ int CONNECTION_POLICY_FORBIDDEN = 0;
+
+ /**
+ * Default connection policy when not set or when the device is unpaired
+ *
+ * @hide
+ */
+ @SystemApi
+ int CONNECTION_POLICY_UNKNOWN = -1;
+
+ /**
+ * Get connected devices for this specific profile.
+ *
+ * <p> Return the set of devices which are in state {@link #STATE_CONNECTED}
+ *
+ * @return List of devices. The list will be empty on error.
+ */
+ public List<BluetoothDevice> getConnectedDevices();
+
+ /**
+ * Get a list of devices that match any of the given connection
+ * states.
+ *
+ * <p> If none of the devices match any of the given states,
+ * an empty list will be returned.
+ *
+ * @param states Array of states. States can be one of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING},
+ * @return List of devices. The list will be empty on error.
+ */
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states);
+
+ /**
+ * Get the current connection state of the profile
+ *
+ * @param device Remote bluetooth device.
+ * @return State of the profile connection. One of {@link #STATE_CONNECTED}, {@link
+ * #STATE_CONNECTING}, {@link #STATE_DISCONNECTED}, {@link #STATE_DISCONNECTING}
+ */
+ @BtProfileState int getConnectionState(BluetoothDevice device);
+
+ /**
+ * An interface for notifying BluetoothProfile IPC clients when they have
+ * been connected or disconnected to the service.
+ */
+ public interface ServiceListener {
+ /**
+ * Called to notify the client when the proxy object has been
+ * connected to the service.
+ *
+ * @param profile - One of {@link #HEADSET} or {@link #A2DP}
+ * @param proxy - One of {@link BluetoothHeadset} or {@link BluetoothA2dp}
+ */
+ @RequiresNoPermission
+ public void onServiceConnected(int profile, BluetoothProfile proxy);
+
+ /**
+ * Called to notify the client that this proxy object has been
+ * disconnected from the service.
+ *
+ * @param profile - One of {@link #HEADSET} or {@link #A2DP}
+ */
+ @RequiresNoPermission
+ public void onServiceDisconnected(int profile);
+ }
+
+ /**
+ * Convert an integer value of connection state into human readable string
+ *
+ * @param connectionState - One of {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, or {@link #STATE_DISCONNECTED}
+ * @return a string representation of the connection state, STATE_UNKNOWN if the state
+ * is not defined
+ * @hide
+ */
+ static String getConnectionStateName(int connectionState) {
+ switch (connectionState) {
+ case STATE_DISCONNECTED:
+ return "STATE_DISCONNECTED";
+ case STATE_CONNECTING:
+ return "STATE_CONNECTING";
+ case STATE_CONNECTED:
+ return "STATE_CONNECTED";
+ case STATE_DISCONNECTING:
+ return "STATE_DISCONNECTING";
+ default:
+ return "STATE_UNKNOWN";
+ }
+ }
+
+ /**
+ * Convert an integer value of profile ID into human readable string
+ *
+ * @param profile profile ID
+ * @return profile name as String, UNKOWN_PROFILE if the profile ID is not defined.
+ * @hide
+ */
+ static String getProfileName(int profile) {
+ switch(profile) {
+ case HEADSET:
+ return "HEADSET";
+ case A2DP:
+ return "A2DP";
+ case HID_HOST:
+ return "HID_HOST";
+ case PAN:
+ return "PAN";
+ case PBAP:
+ return "PBAP";
+ case GATT:
+ return "GATT";
+ case GATT_SERVER:
+ return "GATT_SERVER";
+ case MAP:
+ return "MAP";
+ case SAP:
+ return "SAP";
+ case A2DP_SINK:
+ return "A2DP_SINK";
+ case AVRCP_CONTROLLER:
+ return "AVRCP_CONTROLLER";
+ case AVRCP:
+ return "AVRCP";
+ case HEADSET_CLIENT:
+ return "HEADSET_CLIENT";
+ case PBAP_CLIENT:
+ return "PBAP_CLIENT";
+ case MAP_CLIENT:
+ return "MAP_CLIENT";
+ case HID_DEVICE:
+ return "HID_DEVICE";
+ case OPP:
+ return "OPP";
+ case HEARING_AID:
+ return "HEARING_AID";
+ case LE_AUDIO:
+ return "LE_AUDIO";
+ default:
+ return "UNKNOWN_PROFILE";
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothProfileConnector.java b/framework/java/android/bluetooth/BluetoothProfileConnector.java
new file mode 100644
index 0000000000..79373f1a32
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothProfileConnector.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2019 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import java.util.List;
+/**
+ * Connector for Bluetooth profile proxies to bind manager service and
+ * profile services
+ * @param <T> The Bluetooth profile interface for this connection.
+ * @hide
+ */
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+public abstract class BluetoothProfileConnector<T> {
+ private final CloseGuard mCloseGuard = new CloseGuard();
+ private final int mProfileId;
+ private BluetoothProfile.ServiceListener mServiceListener;
+ private final BluetoothProfile mProfileProxy;
+ private Context mContext;
+ private final String mProfileName;
+ private final String mServiceName;
+ private volatile T mService;
+
+ private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback =
+ new IBluetoothStateChangeCallback.Stub() {
+ public void onBluetoothStateChange(boolean up) {
+ if (up) {
+ doBind();
+ } else {
+ doUnbind();
+ }
+ }
+ };
+
+ private @Nullable ComponentName resolveSystemService(@NonNull Intent intent,
+ @NonNull PackageManager pm, @PackageManager.ComponentInfoFlags int flags) {
+ List<ResolveInfo> results = pm.queryIntentServices(intent, flags);
+ if (results == null) {
+ return null;
+ }
+ ComponentName comp = null;
+ for (int i = 0; i < results.size(); i++) {
+ ResolveInfo ri = results.get(i);
+ if ((ri.serviceInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ continue;
+ }
+ ComponentName foundComp = new ComponentName(ri.serviceInfo.applicationInfo.packageName,
+ ri.serviceInfo.name);
+ if (comp != null) {
+ throw new IllegalStateException("Multiple system services handle " + intent
+ + ": " + comp + ", " + foundComp);
+ }
+ comp = foundComp;
+ }
+ return comp;
+ }
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ logDebug("Proxy object connected");
+ mService = getServiceInterface(service);
+
+ if (mServiceListener != null) {
+ mServiceListener.onServiceConnected(mProfileId, mProfileProxy);
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ logDebug("Proxy object disconnected");
+ doUnbind();
+ if (mServiceListener != null) {
+ mServiceListener.onServiceDisconnected(mProfileId);
+ }
+ }
+ };
+
+ BluetoothProfileConnector(BluetoothProfile profile, int profileId, String profileName,
+ String serviceName) {
+ mProfileId = profileId;
+ mProfileProxy = profile;
+ mProfileName = profileName;
+ mServiceName = serviceName;
+ }
+
+ /** {@hide} */
+ @Override
+ public void finalize() {
+ mCloseGuard.warnIfOpen();
+ doUnbind();
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private boolean doBind() {
+ synchronized (mConnection) {
+ if (mService == null) {
+ logDebug("Binding service...");
+ mCloseGuard.open("doUnbind");
+ try {
+ Intent intent = new Intent(mServiceName);
+ ComponentName comp = resolveSystemService(intent, mContext.getPackageManager(),
+ 0);
+ intent.setComponent(comp);
+ if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0,
+ UserHandle.CURRENT)) {
+ logError("Could not bind to Bluetooth Service with " + intent);
+ return false;
+ }
+ } catch (SecurityException se) {
+ logError("Failed to bind service. " + se);
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private void doUnbind() {
+ synchronized (mConnection) {
+ if (mService != null) {
+ logDebug("Unbinding service...");
+ mCloseGuard.close();
+ try {
+ mContext.unbindService(mConnection);
+ } catch (IllegalArgumentException ie) {
+ logError("Unable to unbind service: " + ie);
+ } finally {
+ mService = null;
+ }
+ }
+ }
+ }
+
+ void connect(Context context, BluetoothProfile.ServiceListener listener) {
+ mContext = context;
+ mServiceListener = listener;
+ IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+
+ // 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");
+ }
+
+ if (mgr != null) {
+ try {
+ mgr.registerStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException re) {
+ logError("Failed to register state change callback. " + re);
+ }
+ }
+ doBind();
+ }
+
+ void disconnect() {
+ mServiceListener = null;
+ IBluetoothManager mgr = BluetoothAdapter.getDefaultAdapter().getBluetoothManager();
+ if (mgr != null) {
+ try {
+ mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback);
+ } catch (RemoteException re) {
+ logError("Failed to unregister state change callback" + re);
+ }
+ }
+ doUnbind();
+ }
+
+ T getService() {
+ return mService;
+ }
+
+ /**
+ * This abstract function is used to implement method to get the
+ * connected Bluetooth service interface.
+ * @param service the connected binder service.
+ * @return T the binder interface of {@code service}.
+ * @hide
+ */
+ public abstract T getServiceInterface(IBinder service);
+
+ private void logDebug(String log) {
+ Log.d(mProfileName, log);
+ }
+
+ private void logError(String log) {
+ Log.e(mProfileName, log);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothSap.java b/framework/java/android/bluetooth/BluetoothSap.java
new file mode 100644
index 0000000000..808fa39133
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothSap.java
@@ -0,0 +1,491 @@
+/*
+ * 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.Manifest;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This class provides the APIs to control the Bluetooth SIM
+ * Access Profile (SAP).
+ *
+ * <p>BluetoothSap is a proxy object for controlling the Bluetooth
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothSap proxy object.
+ *
+ * <p>Each method is protected with its appropriate permission.
+ *
+ * @hide
+ */
+public final class BluetoothSap implements BluetoothProfile {
+
+ private static final String TAG = "BluetoothSap";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Intent used to broadcast the change in connection state of the profile.
+ *
+ * <p>This intent will have 4 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}.
+ *
+ * @hide
+ */
+ @RequiresLegacyBluetoothPermission
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.sap.profile.action.CONNECTION_STATE_CHANGED";
+
+ /**
+ * There was an error trying to obtain the state.
+ *
+ * @hide
+ */
+ public static final int STATE_ERROR = -1;
+
+ /**
+ * Connection state change succceeded.
+ *
+ * @hide
+ */
+ public static final int RESULT_SUCCESS = 1;
+
+ /**
+ * Connection canceled before completion.
+ *
+ * @hide
+ */
+ public static final int RESULT_CANCELED = 2;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothSap> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.SAP,
+ "BluetoothSap", IBluetoothSap.class.getName()) {
+ @Override
+ public IBluetoothSap getServiceInterface(IBinder service) {
+ return IBluetoothSap.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothSap proxy object.
+ */
+ /* package */ BluetoothSap(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ if (DBG) Log.d(TAG, "Create BluetoothSap proxy object");
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ }
+
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Close the connection to the backing service.
+ * Other public functions of BluetoothSap will return default error
+ * results once close() has been called. Multiple invocations of close()
+ * are ok.
+ *
+ * @hide
+ */
+ public synchronized void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothSap getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Get the current state of the BluetoothSap service.
+ *
+ * @return One of the STATE_ return codes, or STATE_ERROR if this proxy object is currently not
+ * connected to the Sap service.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getState() {
+ if (VDBG) log("getState()");
+ final IBluetoothSap service = getService();
+ final int defaultValue = BluetoothSap.STATE_ERROR;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getState(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 currently connected remote Bluetooth device (PCE).
+ *
+ * @return The remote Bluetooth device, or null if not in connected or connecting state, or if
+ * this proxy object is not connected to the Sap service.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public BluetoothDevice getClient() {
+ if (VDBG) log("getClient()");
+ final IBluetoothSap service = getService();
+ 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.getClient(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;
+ }
+
+ /**
+ * Returns true if the specified Bluetooth device is connected.
+ * Returns false if not connected, or if this proxy object is not
+ * currently connected to the Sap service.
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean isConnected(BluetoothDevice device) {
+ if (VDBG) log("isConnected(" + device + ")");
+ final IBluetoothSap service = getService();
+ 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.isConnected(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 connection. Initiation of outgoing connections is not
+ * supported for SAP server.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public boolean connect(BluetoothDevice device) {
+ if (DBG) log("connect(" + device + ")" + "not supported for SAPS");
+ return false;
+ }
+
+ /**
+ * Initiate disconnect.
+ *
+ * @param device Remote Bluetooth Device
+ * @return false on error, true otherwise
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean disconnect(BluetoothDevice device) {
+ if (DBG) log("disconnect(" + device + ")");
+ final IBluetoothSap service = getService();
+ 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.disconnect(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 list of connected devices. Currently at most one.
+ *
+ * @return list of connected devices
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (DBG) log("getConnectedDevices()");
+ final IBluetoothSap service = getService();
+ 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.getConnectedDevices(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;
+ }
+
+ /**
+ * Get the list of devices matching specified states. Currently at most one.
+ *
+ * @return list of matching devices
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) log("getDevicesMatchingStates()");
+ final IBluetoothSap service = getService();
+ 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;
+ }
+
+ /**
+ * Get connection state of device
+ *
+ * @return device connection state
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (DBG) log("getConnectionState(" + device + ")");
+ final IBluetoothSap service = getService();
+ 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.getConnectionState(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 #PRIORITY_ON} or {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothSap service = getService();
+ 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_OFF}, {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ 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
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @ConnectionPolicy int getConnectionPolicy(BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothSap service = getService();
+ 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;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.isEnabled();
+ }
+
+ private static boolean isValidDevice(BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothServerSocket.java b/framework/java/android/bluetooth/BluetoothServerSocket.java
new file mode 100644
index 0000000000..bb4e35483f
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothServerSocket.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.SuppressLint;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Handler;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * A listening Bluetooth socket.
+ *
+ * <p>The interface for Bluetooth Sockets is similar to that of TCP sockets:
+ * {@link java.net.Socket} and {@link java.net.ServerSocket}. On the server
+ * side, use a {@link BluetoothServerSocket} to create a listening server
+ * socket. When a connection is accepted by the {@link BluetoothServerSocket},
+ * it will return a new {@link BluetoothSocket} to manage the connection.
+ * On the client side, use a single {@link BluetoothSocket} to both initiate
+ * an outgoing connection and to manage the connection.
+ *
+ * <p>For Bluetooth BR/EDR, the most common type of socket is RFCOMM, which is the type supported by
+ * the Android APIs. RFCOMM is a connection-oriented, streaming transport over Bluetooth BR/EDR. It
+ * is also known as the Serial Port Profile (SPP). To create a listening
+ * {@link BluetoothServerSocket} that's ready for incoming Bluetooth BR/EDR connections, use {@link
+ * BluetoothAdapter#listenUsingRfcommWithServiceRecord
+ * BluetoothAdapter.listenUsingRfcommWithServiceRecord()}.
+ *
+ * <p>For Bluetooth LE, the socket uses LE Connection-oriented Channel (CoC). LE CoC is a
+ * connection-oriented, streaming transport over Bluetooth LE and has a credit-based flow control.
+ * Correspondingly, use {@link BluetoothAdapter#listenUsingL2capChannel
+ * BluetoothAdapter.listenUsingL2capChannel()} to create a listening {@link BluetoothServerSocket}
+ * that's ready for incoming Bluetooth LE CoC connections. For LE CoC, you can use {@link #getPsm()}
+ * to get the protocol/service multiplexer (PSM) value that the peer needs to use to connect to your
+ * socket.
+ *
+ * <p> After the listening {@link BluetoothServerSocket} is created, call {@link #accept()} to
+ * listen for incoming connection requests. This call will block until a connection is established,
+ * at which point, it will return a {@link BluetoothSocket} to manage the connection. Once the
+ * {@link BluetoothSocket} is acquired, it's a good idea to call {@link #close()} on the {@link
+ * BluetoothServerSocket} when it's no longer needed for accepting
+ * connections. Closing the {@link BluetoothServerSocket} will <em>not</em> close the returned
+ * {@link BluetoothSocket}.
+ *
+ * <p>{@link BluetoothServerSocket} is thread
+ * safe. In particular, {@link #close} will always immediately abort ongoing
+ * operations and close the server socket.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using Bluetooth, read the
+ * <a href="{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer guide.</p>
+ * </div>
+ *
+ * {@see BluetoothSocket}
+ */
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+public final class BluetoothServerSocket implements Closeable {
+
+ private static final String TAG = "BluetoothServerSocket";
+ private static final boolean DBG = false;
+ @UnsupportedAppUsage(publicAlternatives = "Use public {@link BluetoothServerSocket} API "
+ + "instead.")
+ /*package*/ final BluetoothSocket mSocket;
+ private Handler mHandler;
+ private int mMessage;
+ private int mChannel;
+
+ /**
+ * Construct a socket for incoming connections.
+ *
+ * @param type type of socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param port remote port
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, int port)
+ throws IOException {
+ mChannel = port;
+ mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null);
+ if (port == BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ mSocket.setExcludeSdp(true);
+ }
+ }
+
+ /**
+ * Construct a socket for incoming connections.
+ *
+ * @param type type of socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param port remote port
+ * @param mitm enforce person-in-the-middle protection for authentication.
+ * @param min16DigitPin enforce a minimum length of 16 digits for a sec mode 2 connection
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, int port,
+ boolean mitm, boolean min16DigitPin)
+ throws IOException {
+ mChannel = port;
+ mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port, null, mitm,
+ min16DigitPin);
+ if (port == BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ mSocket.setExcludeSdp(true);
+ }
+ }
+
+ /**
+ * Construct a socket for incoming connections.
+ *
+ * @param type type of socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param uuid uuid
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ /*package*/ BluetoothServerSocket(int type, boolean auth, boolean encrypt, ParcelUuid uuid)
+ throws IOException {
+ mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, -1, uuid);
+ // TODO: This is the same as mChannel = -1 - is this intentional?
+ mChannel = mSocket.getPort();
+ }
+
+
+ /**
+ * Block until a connection is established.
+ * <p>Returns a connected {@link BluetoothSocket} on successful connection.
+ * <p>Once this call returns, it can be called again to accept subsequent
+ * incoming connections.
+ * <p>{@link #close} can be used to abort this call from another thread.
+ *
+ * @return a connected {@link BluetoothSocket}
+ * @throws IOException on error, for example this call was aborted, or timeout
+ */
+ public BluetoothSocket accept() throws IOException {
+ return accept(-1);
+ }
+
+ /**
+ * Block until a connection is established, with timeout.
+ * <p>Returns a connected {@link BluetoothSocket} on successful connection.
+ * <p>Once this call returns, it can be called again to accept subsequent
+ * incoming connections.
+ * <p>{@link #close} can be used to abort this call from another thread.
+ *
+ * @return a connected {@link BluetoothSocket}
+ * @throws IOException on error, for example this call was aborted, or timeout
+ */
+ public BluetoothSocket accept(int timeout) throws IOException {
+ return mSocket.accept(timeout);
+ }
+
+ /**
+ * Immediately close this socket, and release all associated resources.
+ * <p>Causes blocked calls on this socket in other threads to immediately
+ * throw an IOException.
+ * <p>Closing the {@link BluetoothServerSocket} will <em>not</em>
+ * close any {@link BluetoothSocket} received from {@link #accept()}.
+ */
+ public void close() throws IOException {
+ if (DBG) Log.d(TAG, "BluetoothServerSocket:close() called. mChannel=" + mChannel);
+ synchronized (this) {
+ if (mHandler != null) {
+ mHandler.obtainMessage(mMessage).sendToTarget();
+ }
+ }
+ mSocket.close();
+ }
+
+ /*package*/
+ synchronized void setCloseHandler(Handler handler, int message) {
+ mHandler = handler;
+ mMessage = message;
+ }
+
+ /*package*/ void setServiceName(String serviceName) {
+ mSocket.setServiceName(serviceName);
+ }
+
+ /**
+ * Returns the channel on which this socket is bound.
+ *
+ * @hide
+ */
+ public int getChannel() {
+ return mChannel;
+ }
+
+ /**
+ * Returns the assigned dynamic protocol/service multiplexer (PSM) value for the listening L2CAP
+ * Connection-oriented Channel (CoC) server socket. This server socket must be returned by the
+ * {@link BluetoothAdapter#listenUsingL2capChannel()} or {@link
+ * BluetoothAdapter#listenUsingInsecureL2capChannel()}. The returned value is undefined if this
+ * method is called on non-L2CAP server sockets.
+ *
+ * @return the assigned PSM or LE_PSM value depending on transport
+ */
+ public int getPsm() {
+ return mChannel;
+ }
+
+ /**
+ * Sets the channel on which future sockets are bound.
+ * Currently used only when a channel is auto generated.
+ */
+ /*package*/ void setChannel(int newChannel) {
+ /* TODO: From a design/architecture perspective this is wrong.
+ * The bind operation should be conducted through this class
+ * and the resulting port should be kept in mChannel, and
+ * not set from BluetoothAdapter. */
+ if (mSocket != null) {
+ if (mSocket.getPort() != newChannel) {
+ Log.w(TAG, "The port set is different that the underlying port. mSocket.getPort(): "
+ + mSocket.getPort() + " requested newChannel: " + newChannel);
+ }
+ }
+ mChannel = newChannel;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("ServerSocket: Type: ");
+ switch (mSocket.getConnectionType()) {
+ case BluetoothSocket.TYPE_RFCOMM: {
+ sb.append("TYPE_RFCOMM");
+ break;
+ }
+ case BluetoothSocket.TYPE_L2CAP: {
+ sb.append("TYPE_L2CAP");
+ break;
+ }
+ case BluetoothSocket.TYPE_L2CAP_LE: {
+ sb.append("TYPE_L2CAP_LE");
+ break;
+ }
+ case BluetoothSocket.TYPE_SCO: {
+ sb.append("TYPE_SCO");
+ break;
+ }
+ }
+ sb.append(" Channel: ").append(mChannel);
+ return sb.toString();
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothSocket.java b/framework/java/android/bluetooth/BluetoothSocket.java
new file mode 100644
index 0000000000..db5b75148e
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothSocket.java
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2012 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 android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.net.LocalSocket;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * A connected or connecting Bluetooth socket.
+ *
+ * <p>The interface for Bluetooth Sockets is similar to that of TCP sockets:
+ * {@link java.net.Socket} and {@link java.net.ServerSocket}. On the server
+ * side, use a {@link BluetoothServerSocket} to create a listening server
+ * socket. When a connection is accepted by the {@link BluetoothServerSocket},
+ * it will return a new {@link BluetoothSocket} to manage the connection.
+ * On the client side, use a single {@link BluetoothSocket} to both initiate
+ * an outgoing connection and to manage the connection.
+ *
+ * <p>The most common type of Bluetooth socket is RFCOMM, which is the type
+ * supported by the Android APIs. RFCOMM is a connection-oriented, streaming
+ * transport over Bluetooth. It is also known as the Serial Port Profile (SPP).
+ *
+ * <p>To create a {@link BluetoothSocket} for connecting to a known device, use
+ * {@link BluetoothDevice#createRfcommSocketToServiceRecord
+ * BluetoothDevice.createRfcommSocketToServiceRecord()}.
+ * Then call {@link #connect()} to attempt a connection to the remote device.
+ * This call will block until a connection is established or the connection
+ * fails.
+ *
+ * <p>To create a {@link BluetoothSocket} as a server (or "host"), see the
+ * {@link BluetoothServerSocket} documentation.
+ *
+ * <p>Once the socket is connected, whether initiated as a client or accepted
+ * as a server, open the IO streams by calling {@link #getInputStream} and
+ * {@link #getOutputStream} in order to retrieve {@link java.io.InputStream}
+ * and {@link java.io.OutputStream} objects, respectively, which are
+ * automatically connected to the socket.
+ *
+ * <p>{@link BluetoothSocket} is thread
+ * safe. In particular, {@link #close} will always immediately abort ongoing
+ * operations and close the socket.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using Bluetooth, read the
+ * <a href="{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> developer guide.</p>
+ * </div>
+ *
+ * {@see BluetoothServerSocket}
+ * {@see java.io.InputStream}
+ * {@see java.io.OutputStream}
+ */
+public final class BluetoothSocket implements Closeable {
+ private static final String TAG = "BluetoothSocket";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE);
+
+ /** @hide */
+ public static final int MAX_RFCOMM_CHANNEL = 30;
+ /*package*/ static final int MAX_L2CAP_PACKAGE_SIZE = 0xFFFF;
+
+ /** RFCOMM socket */
+ public static final int TYPE_RFCOMM = 1;
+
+ /** SCO socket */
+ public static final int TYPE_SCO = 2;
+
+ /** L2CAP socket */
+ public static final int TYPE_L2CAP = 3;
+
+ /** L2CAP socket on BR/EDR transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_BREDR = TYPE_L2CAP;
+
+ /** L2CAP socket on LE transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_LE = 4;
+
+ /*package*/ static final int EBADFD = 77;
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ /*package*/ static final int EADDRINUSE = 98;
+
+ /*package*/ static final int SEC_FLAG_ENCRYPT = 1;
+ /*package*/ static final int SEC_FLAG_AUTH = 1 << 1;
+ /*package*/ static final int BTSOCK_FLAG_NO_SDP = 1 << 2;
+ /*package*/ static final int SEC_FLAG_AUTH_MITM = 1 << 3;
+ /*package*/ static final int SEC_FLAG_AUTH_16_DIGIT = 1 << 4;
+
+ private final int mType; /* one of TYPE_RFCOMM etc */
+ private BluetoothDevice mDevice; /* remote device */
+ private String mAddress; /* remote address */
+ private final boolean mAuth;
+ private final boolean mEncrypt;
+ private final BluetoothInputStream mInputStream;
+ private final BluetoothOutputStream mOutputStream;
+ private final ParcelUuid mUuid;
+ /** when true no SPP SDP record will be created */
+ private boolean mExcludeSdp = false;
+ /** when true Person-in-the-middle protection will be enabled */
+ private boolean mAuthMitm = false;
+ /** Minimum 16 digit pin for sec mode 2 connections */
+ private boolean mMin16DigitPin = false;
+ @UnsupportedAppUsage(publicAlternatives = "Use {@link BluetoothSocket} public API instead.")
+ private ParcelFileDescriptor mPfd;
+ @UnsupportedAppUsage
+ private LocalSocket mSocket;
+ private InputStream mSocketIS;
+ private OutputStream mSocketOS;
+ @UnsupportedAppUsage
+ private int mPort; /* RFCOMM channel or L2CAP psm */
+ private int mFd;
+ private String mServiceName;
+ private static final int PROXY_CONNECTION_TIMEOUT = 5000;
+
+ private static final int SOCK_SIGNAL_SIZE = 20;
+
+ private ByteBuffer mL2capBuffer = null;
+ private int mMaxTxPacketSize = 0; // The l2cap maximum packet size supported by the peer.
+ private int mMaxRxPacketSize = 0; // The l2cap maximum packet size that can be received.
+
+ private enum SocketState {
+ INIT,
+ CONNECTED,
+ LISTENING,
+ CLOSED,
+ }
+
+ /** prevents all native calls after destroyNative() */
+ private volatile SocketState mSocketState;
+
+ /** protects mSocketState */
+ //private final ReentrantReadWriteLock mLock;
+
+ /**
+ * Construct a BluetoothSocket.
+ *
+ * @param type type of socket
+ * @param fd fd to use for connected socket, or -1 for a new socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param device remote device that this socket can connect to
+ * @param port remote port
+ * @param uuid SDP uuid
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt,
+ BluetoothDevice device, int port, ParcelUuid uuid) throws IOException {
+ this(type, fd, auth, encrypt, device, port, uuid, false, false);
+ }
+
+ /**
+ * Construct a BluetoothSocket.
+ *
+ * @param type type of socket
+ * @param fd fd to use for connected socket, or -1 for a new socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param device remote device that this socket can connect to
+ * @param port remote port
+ * @param uuid SDP uuid
+ * @param mitm enforce person-in-the-middle protection.
+ * @param min16DigitPin enforce a minimum length of 16 digits for a sec mode 2 connection
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt,
+ BluetoothDevice device, int port, ParcelUuid uuid, boolean mitm, boolean min16DigitPin)
+ throws IOException {
+ if (VDBG) Log.d(TAG, "Creating new BluetoothSocket of type: " + type);
+ if (type == BluetoothSocket.TYPE_RFCOMM && uuid == null && fd == -1
+ && port != BluetoothAdapter.SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
+ if (port < 1 || port > MAX_RFCOMM_CHANNEL) {
+ throw new IOException("Invalid RFCOMM channel: " + port);
+ }
+ }
+ if (uuid != null) {
+ mUuid = uuid;
+ } else {
+ mUuid = new ParcelUuid(new UUID(0, 0));
+ }
+ mType = type;
+ mAuth = auth;
+ mAuthMitm = mitm;
+ mMin16DigitPin = min16DigitPin;
+ mEncrypt = encrypt;
+ mDevice = device;
+ mPort = port;
+ mFd = fd;
+
+ mSocketState = SocketState.INIT;
+
+ if (device == null) {
+ // Server socket
+ mAddress = BluetoothAdapter.getDefaultAdapter().getAddress();
+ } else {
+ // Remote socket
+ mAddress = device.getAddress();
+ }
+ mInputStream = new BluetoothInputStream(this);
+ mOutputStream = new BluetoothOutputStream(this);
+ }
+
+ private BluetoothSocket(BluetoothSocket s) {
+ if (VDBG) Log.d(TAG, "Creating new Private BluetoothSocket of type: " + s.mType);
+ mUuid = s.mUuid;
+ mType = s.mType;
+ mAuth = s.mAuth;
+ mEncrypt = s.mEncrypt;
+ mPort = s.mPort;
+ mInputStream = new BluetoothInputStream(this);
+ mOutputStream = new BluetoothOutputStream(this);
+ mMaxRxPacketSize = s.mMaxRxPacketSize;
+ mMaxTxPacketSize = s.mMaxTxPacketSize;
+
+ mServiceName = s.mServiceName;
+ mExcludeSdp = s.mExcludeSdp;
+ mAuthMitm = s.mAuthMitm;
+ mMin16DigitPin = s.mMin16DigitPin;
+ }
+
+ private BluetoothSocket acceptSocket(String remoteAddr) throws IOException {
+ BluetoothSocket as = new BluetoothSocket(this);
+ as.mSocketState = SocketState.CONNECTED;
+ FileDescriptor[] fds = mSocket.getAncillaryFileDescriptors();
+ if (DBG) Log.d(TAG, "socket fd passed by stack fds: " + Arrays.toString(fds));
+ if (fds == null || fds.length != 1) {
+ Log.e(TAG, "socket fd passed from stack failed, fds: " + Arrays.toString(fds));
+ as.close();
+ throw new IOException("bt socket acept failed");
+ }
+
+ as.mPfd = ParcelFileDescriptor.dup(fds[0]);
+ as.mSocket = LocalSocket.createConnectedLocalSocket(fds[0]);
+ as.mSocketIS = as.mSocket.getInputStream();
+ as.mSocketOS = as.mSocket.getOutputStream();
+ as.mAddress = remoteAddr;
+ as.mDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(remoteAddr);
+ return as;
+ }
+
+ /**
+ * Construct a BluetoothSocket from address. Used by native code.
+ *
+ * @param type type of socket
+ * @param fd fd to use for connected socket, or -1 for a new socket
+ * @param auth require the remote device to be authenticated
+ * @param encrypt require the connection to be encrypted
+ * @param address remote device that this socket can connect to
+ * @param port remote port
+ * @throws IOException On error, for example Bluetooth not available, or insufficient
+ * privileges
+ */
+ private BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address,
+ int port) throws IOException {
+ this(type, fd, auth, encrypt, new BluetoothDevice(address), port, null, false, false);
+ }
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private int getSecurityFlags() {
+ int flags = 0;
+ if (mAuth) {
+ flags |= SEC_FLAG_AUTH;
+ }
+ if (mEncrypt) {
+ flags |= SEC_FLAG_ENCRYPT;
+ }
+ if (mExcludeSdp) {
+ flags |= BTSOCK_FLAG_NO_SDP;
+ }
+ if (mAuthMitm) {
+ flags |= SEC_FLAG_AUTH_MITM;
+ }
+ if (mMin16DigitPin) {
+ flags |= SEC_FLAG_AUTH_16_DIGIT;
+ }
+ return flags;
+ }
+
+ /**
+ * Get the remote device this socket is connecting, or connected, to.
+ *
+ * @return remote device
+ */
+ @RequiresNoPermission
+ public BluetoothDevice getRemoteDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Get the input stream associated with this socket.
+ * <p>The input stream will be returned even if the socket is not yet
+ * connected, but operations on that stream will throw IOException until
+ * the associated socket is connected.
+ *
+ * @return InputStream
+ */
+ @RequiresNoPermission
+ public InputStream getInputStream() throws IOException {
+ return mInputStream;
+ }
+
+ /**
+ * Get the output stream associated with this socket.
+ * <p>The output stream will be returned even if the socket is not yet
+ * connected, but operations on that stream will throw IOException until
+ * the associated socket is connected.
+ *
+ * @return OutputStream
+ */
+ @RequiresNoPermission
+ public OutputStream getOutputStream() throws IOException {
+ return mOutputStream;
+ }
+
+ /**
+ * Get the connection status of this socket, ie, whether there is an active connection with
+ * remote device.
+ *
+ * @return true if connected false if not connected
+ */
+ @RequiresNoPermission
+ public boolean isConnected() {
+ return mSocketState == SocketState.CONNECTED;
+ }
+
+ /*package*/ void setServiceName(String name) {
+ mServiceName = name;
+ }
+
+ /**
+ * Attempt to connect to a remote device.
+ * <p>This method will block until a connection is made or the connection
+ * fails. If this method returns without an exception then this socket
+ * is now connected.
+ * <p>Creating new connections to
+ * remote Bluetooth devices should not be attempted while device discovery
+ * is in progress. Device discovery is a heavyweight procedure on the
+ * Bluetooth adapter and will significantly slow a device connection.
+ * Use {@link BluetoothAdapter#cancelDiscovery()} to cancel an ongoing
+ * discovery. Discovery is not managed by the Activity,
+ * but is run as a system service, so an application should always call
+ * {@link BluetoothAdapter#cancelDiscovery()} even if it
+ * did not directly request a discovery, just to be sure.
+ * <p>{@link #close} can be used to abort this call from another thread.
+ *
+ * @throws IOException on error, for example connection failure
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void connect() throws IOException {
+ if (mDevice == null) throw new IOException("Connect is called on null device");
+
+ try {
+ if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed");
+ IBluetooth bluetoothProxy =
+ BluetoothAdapter.getDefaultAdapter().getBluetoothService();
+ if (bluetoothProxy == null) throw new IOException("Bluetooth is off");
+ mPfd = bluetoothProxy.getSocketManager().connectSocket(mDevice, mType,
+ mUuid, mPort, getSecurityFlags());
+ synchronized (this) {
+ if (DBG) Log.d(TAG, "connect(), SocketState: " + mSocketState + ", mPfd: " + mPfd);
+ if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed");
+ if (mPfd == null) throw new IOException("bt socket connect failed");
+ FileDescriptor fd = mPfd.getFileDescriptor();
+ mSocket = LocalSocket.createConnectedLocalSocket(fd);
+ mSocketIS = mSocket.getInputStream();
+ mSocketOS = mSocket.getOutputStream();
+ }
+ int channel = readInt(mSocketIS);
+ if (channel <= 0) {
+ throw new IOException("bt socket connect failed");
+ }
+ mPort = channel;
+ waitSocketSignal(mSocketIS);
+ synchronized (this) {
+ if (mSocketState == SocketState.CLOSED) {
+ throw new IOException("bt socket closed");
+ }
+ mSocketState = SocketState.CONNECTED;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ throw new IOException("unable to send RPC: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Currently returns unix errno instead of throwing IOException,
+ * so that BluetoothAdapter can check the error code for EADDRINUSE
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ /*package*/ int bindListen() {
+ int ret;
+ if (mSocketState == SocketState.CLOSED) return EBADFD;
+ IBluetooth bluetoothProxy = BluetoothAdapter.getDefaultAdapter().getBluetoothService();
+ if (bluetoothProxy == null) {
+ Log.e(TAG, "bindListen fail, reason: bluetooth is off");
+ return -1;
+ }
+ try {
+ if (DBG) Log.d(TAG, "bindListen(): mPort=" + mPort + ", mType=" + mType);
+ mPfd = bluetoothProxy.getSocketManager().createSocketChannel(mType, mServiceName,
+ mUuid, mPort, getSecurityFlags());
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ return -1;
+ }
+
+ // read out port number
+ try {
+ synchronized (this) {
+ if (DBG) {
+ Log.d(TAG, "bindListen(), SocketState: " + mSocketState + ", mPfd: " + mPfd);
+ }
+ if (mSocketState != SocketState.INIT) return EBADFD;
+ if (mPfd == null) return -1;
+ FileDescriptor fd = mPfd.getFileDescriptor();
+ if (fd == null) {
+ Log.e(TAG, "bindListen(), null file descriptor");
+ return -1;
+ }
+
+ if (DBG) Log.d(TAG, "bindListen(), Create LocalSocket");
+ mSocket = LocalSocket.createConnectedLocalSocket(fd);
+ if (DBG) Log.d(TAG, "bindListen(), new LocalSocket.getInputStream()");
+ mSocketIS = mSocket.getInputStream();
+ mSocketOS = mSocket.getOutputStream();
+ }
+ if (DBG) Log.d(TAG, "bindListen(), readInt mSocketIS: " + mSocketIS);
+ int channel = readInt(mSocketIS);
+ synchronized (this) {
+ if (mSocketState == SocketState.INIT) {
+ mSocketState = SocketState.LISTENING;
+ }
+ }
+ if (DBG) Log.d(TAG, "bindListen(): channel=" + channel + ", mPort=" + mPort);
+ if (mPort <= -1) {
+ mPort = channel;
+ } // else ASSERT(mPort == channel)
+ ret = 0;
+ } catch (IOException e) {
+ if (mPfd != null) {
+ try {
+ mPfd.close();
+ } catch (IOException e1) {
+ Log.e(TAG, "bindListen, close mPfd: " + e1);
+ }
+ mPfd = null;
+ }
+ Log.e(TAG, "bindListen, fail to get port number, exception: " + e);
+ return -1;
+ }
+ return ret;
+ }
+
+ /*package*/ BluetoothSocket accept(int timeout) throws IOException {
+ BluetoothSocket acceptedSocket;
+ if (mSocketState != SocketState.LISTENING) {
+ throw new IOException("bt socket is not in listen state");
+ }
+ if (timeout > 0) {
+ Log.d(TAG, "accept() set timeout (ms):" + timeout);
+ mSocket.setSoTimeout(timeout);
+ }
+ String RemoteAddr = waitSocketSignal(mSocketIS);
+ if (timeout > 0) {
+ mSocket.setSoTimeout(0);
+ }
+ synchronized (this) {
+ if (mSocketState != SocketState.LISTENING) {
+ throw new IOException("bt socket is not in listen state");
+ }
+ acceptedSocket = acceptSocket(RemoteAddr);
+ //quick drop the reference of the file handle
+ }
+ return acceptedSocket;
+ }
+
+ /*package*/ int available() throws IOException {
+ if (VDBG) Log.d(TAG, "available: " + mSocketIS);
+ return mSocketIS.available();
+ }
+
+ /*package*/ int read(byte[] b, int offset, int length) throws IOException {
+ int ret = 0;
+ if (VDBG) Log.d(TAG, "read in: " + mSocketIS + " len: " + length);
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
+ int bytesToRead = length;
+ if (VDBG) {
+ Log.v(TAG, "l2cap: read(): offset: " + offset + " length:" + length
+ + "mL2capBuffer= " + mL2capBuffer);
+ }
+ if (mL2capBuffer == null) {
+ createL2capRxBuffer();
+ }
+ if (mL2capBuffer.remaining() == 0) {
+ if (VDBG) Log.v(TAG, "l2cap buffer empty, refilling...");
+ if (fillL2capRxBuffer() == -1) {
+ return -1;
+ }
+ }
+ if (bytesToRead > mL2capBuffer.remaining()) {
+ bytesToRead = mL2capBuffer.remaining();
+ }
+ if (VDBG) {
+ Log.v(TAG, "get(): offset: " + offset
+ + " bytesToRead: " + bytesToRead);
+ }
+ mL2capBuffer.get(b, offset, bytesToRead);
+ ret = bytesToRead;
+ } else {
+ if (VDBG) Log.v(TAG, "default: read(): offset: " + offset + " length:" + length);
+ ret = mSocketIS.read(b, offset, length);
+ }
+ if (ret < 0) {
+ throw new IOException("bt socket closed, read return: " + ret);
+ }
+ if (VDBG) Log.d(TAG, "read out: " + mSocketIS + " ret: " + ret);
+ return ret;
+ }
+
+ /*package*/ int write(byte[] b, int offset, int length) throws IOException {
+
+ //TODO: Since bindings can exist between the SDU size and the
+ // protocol, we might need to throw an exception instead of just
+ // splitting the write into multiple smaller writes.
+ // Rfcomm uses dynamic allocation, and should not have any bindings
+ // to the actual message length.
+ if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length);
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
+ if (length <= mMaxTxPacketSize) {
+ mSocketOS.write(b, offset, length);
+ } else {
+ if (DBG) {
+ Log.w(TAG, "WARNING: Write buffer larger than L2CAP packet size!\n"
+ + "Packet will be divided into SDU packets of size "
+ + mMaxTxPacketSize);
+ }
+ int tmpOffset = offset;
+ int bytesToWrite = length;
+ while (bytesToWrite > 0) {
+ int tmpLength = (bytesToWrite > mMaxTxPacketSize)
+ ? mMaxTxPacketSize
+ : bytesToWrite;
+ mSocketOS.write(b, tmpOffset, tmpLength);
+ tmpOffset += tmpLength;
+ bytesToWrite -= tmpLength;
+ }
+ }
+ } else {
+ mSocketOS.write(b, offset, length);
+ }
+ // There is no good way to confirm since the entire process is asynchronous anyway
+ if (VDBG) Log.d(TAG, "write out: " + mSocketOS + " length: " + length);
+ return length;
+ }
+
+ @Override
+ public void close() throws IOException {
+ Log.d(TAG, "close() this: " + this + ", channel: " + mPort + ", mSocketIS: " + mSocketIS
+ + ", mSocketOS: " + mSocketOS + "mSocket: " + mSocket + ", mSocketState: "
+ + mSocketState);
+ if (mSocketState == SocketState.CLOSED) {
+ return;
+ } else {
+ synchronized (this) {
+ if (mSocketState == SocketState.CLOSED) {
+ return;
+ }
+ mSocketState = SocketState.CLOSED;
+ if (mSocket != null) {
+ if (DBG) Log.d(TAG, "Closing mSocket: " + mSocket);
+ mSocket.shutdownInput();
+ mSocket.shutdownOutput();
+ mSocket.close();
+ mSocket = null;
+ }
+ if (mPfd != null) {
+ mPfd.close();
+ mPfd = null;
+ }
+ }
+ }
+ }
+
+ /*package */ void removeChannel() {
+ }
+
+ /*package */ int getPort() {
+ return mPort;
+ }
+
+ /**
+ * Get the maximum supported Transmit packet size for the underlying transport.
+ * Use this to optimize the writes done to the output socket, to avoid sending
+ * half full packets.
+ *
+ * @return the maximum supported Transmit packet size for the underlying transport.
+ */
+ @RequiresNoPermission
+ public int getMaxTransmitPacketSize() {
+ return mMaxTxPacketSize;
+ }
+
+ /**
+ * Get the maximum supported Receive packet size for the underlying transport.
+ * Use this to optimize the reads done on the input stream, as any call to read
+ * will return a maximum of this amount of bytes - or for some transports a
+ * multiple of this value.
+ *
+ * @return the maximum supported Receive packet size for the underlying transport.
+ */
+ @RequiresNoPermission
+ public int getMaxReceivePacketSize() {
+ return mMaxRxPacketSize;
+ }
+
+ /**
+ * Get the type of the underlying connection.
+ *
+ * @return one of {@link #TYPE_RFCOMM}, {@link #TYPE_SCO} or {@link #TYPE_L2CAP}
+ */
+ @RequiresNoPermission
+ public int getConnectionType() {
+ if (mType == TYPE_L2CAP_LE) {
+ // Treat the LE CoC to be the same type as L2CAP.
+ return TYPE_L2CAP;
+ }
+ return mType;
+ }
+
+ /**
+ * Change if a SDP entry should be automatically created.
+ * Must be called before calling .bind, for the call to have any effect.
+ *
+ * @param excludeSdp <li>TRUE - do not auto generate SDP record. <li>FALSE - default - auto
+ * generate SPP SDP record.
+ * @hide
+ */
+ @RequiresNoPermission
+ public void setExcludeSdp(boolean excludeSdp) {
+ mExcludeSdp = excludeSdp;
+ }
+
+ /**
+ * Set the LE Transmit Data Length to be the maximum that the BT Controller is capable of. This
+ * parameter is used by the BT Controller to set the maximum transmission packet size on this
+ * connection. This function is currently used for testing only.
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void requestMaximumTxDataLength() throws IOException {
+ if (mDevice == null) {
+ throw new IOException("requestMaximumTxDataLength is called on null device");
+ }
+
+ try {
+ if (mSocketState == SocketState.CLOSED) {
+ throw new IOException("socket closed");
+ }
+ IBluetooth bluetoothProxy =
+ BluetoothAdapter.getDefaultAdapter().getBluetoothService();
+ if (bluetoothProxy == null) {
+ throw new IOException("Bluetooth is off");
+ }
+
+ if (DBG) Log.d(TAG, "requestMaximumTxDataLength");
+ bluetoothProxy.getSocketManager().requestMaximumTxDataLength(mDevice);
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ throw new IOException("unable to send RPC: " + e.getMessage());
+ }
+ }
+
+ private String convertAddr(final byte[] addr) {
+ return String.format(Locale.US, "%02X:%02X:%02X:%02X:%02X:%02X",
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+ }
+
+ private String waitSocketSignal(InputStream is) throws IOException {
+ byte[] sig = new byte[SOCK_SIGNAL_SIZE];
+ int ret = readAll(is, sig);
+ if (VDBG) {
+ Log.d(TAG, "waitSocketSignal read " + SOCK_SIGNAL_SIZE + " bytes signal ret: " + ret);
+ }
+ ByteBuffer bb = ByteBuffer.wrap(sig);
+ /* the struct in native is decorated with __attribute__((packed)), hence this is possible */
+ bb.order(ByteOrder.nativeOrder());
+ int size = bb.getShort();
+ if (size != SOCK_SIGNAL_SIZE) {
+ throw new IOException("Connection failure, wrong signal size: " + size);
+ }
+ byte[] addr = new byte[6];
+ bb.get(addr);
+ int channel = bb.getInt();
+ int status = bb.getInt();
+ mMaxTxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value
+ mMaxRxPacketSize = (bb.getShort() & 0xffff); // Convert to unsigned value
+ String RemoteAddr = convertAddr(addr);
+ if (VDBG) {
+ Log.d(TAG, "waitSocketSignal: sig size: " + size + ", remote addr: "
+ + RemoteAddr + ", channel: " + channel + ", status: " + status
+ + " MaxRxPktSize: " + mMaxRxPacketSize + " MaxTxPktSize: " + mMaxTxPacketSize);
+ }
+ if (status != 0) {
+ throw new IOException("Connection failure, status: " + status);
+ }
+ return RemoteAddr;
+ }
+
+ private void createL2capRxBuffer() {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
+ // Allocate the buffer to use for reads.
+ if (VDBG) Log.v(TAG, " Creating mL2capBuffer: mMaxPacketSize: " + mMaxRxPacketSize);
+ mL2capBuffer = ByteBuffer.wrap(new byte[mMaxRxPacketSize]);
+ if (VDBG) Log.v(TAG, "mL2capBuffer.remaining()" + mL2capBuffer.remaining());
+ mL2capBuffer.limit(0); // Ensure we do a real read at the first read-request
+ if (VDBG) {
+ Log.v(TAG, "mL2capBuffer.remaining() after limit(0):" + mL2capBuffer.remaining());
+ }
+ }
+ }
+
+ private int readAll(InputStream is, byte[] b) throws IOException {
+ int left = b.length;
+ while (left > 0) {
+ int ret = is.read(b, b.length - left, left);
+ if (ret <= 0) {
+ throw new IOException("read failed, socket might closed or timeout, read ret: "
+ + ret);
+ }
+ left -= ret;
+ if (left != 0) {
+ Log.w(TAG, "readAll() looping, read partial size: " + (b.length - left)
+ + ", expect size: " + b.length);
+ }
+ }
+ return b.length;
+ }
+
+ private int readInt(InputStream is) throws IOException {
+ byte[] ibytes = new byte[4];
+ int ret = readAll(is, ibytes);
+ if (VDBG) Log.d(TAG, "inputStream.read ret: " + ret);
+ ByteBuffer bb = ByteBuffer.wrap(ibytes);
+ bb.order(ByteOrder.nativeOrder());
+ return bb.getInt();
+ }
+
+ private int fillL2capRxBuffer() throws IOException {
+ mL2capBuffer.rewind();
+ int ret = mSocketIS.read(mL2capBuffer.array());
+ if (ret == -1) {
+ // reached end of stream - return -1
+ mL2capBuffer.limit(0);
+ return -1;
+ }
+ mL2capBuffer.limit(ret);
+ return ret;
+ }
+
+
+}
diff --git a/framework/java/android/bluetooth/BluetoothStatusCodes.java b/framework/java/android/bluetooth/BluetoothStatusCodes.java
new file mode 100644
index 0000000000..9dafa073ab
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothStatusCodes.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2021 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 android.annotation.SystemApi;
+
+/**
+ * A class with constants representing possible return values for Bluetooth APIs. General return
+ * values occupy the range 0 to 99. Profile-specific return values occupy the range 100-999.
+ * API-specific return values start at 1000. The exception to this is the "UNKNOWN" error code which
+ * occupies the max integer value.
+ */
+public final class BluetoothStatusCodes {
+
+ private BluetoothStatusCodes() {}
+
+ /**
+ * Indicates that the API call was successful
+ */
+ public static final int SUCCESS = 0;
+
+ /**
+ * Error code indicating that Bluetooth is not enabled
+ */
+ public static final int ERROR_BLUETOOTH_NOT_ENABLED = 1;
+
+ /**
+ * Error code indicating that the API call was initiated by neither the system nor the active
+ * Zuser
+ */
+ public static final int ERROR_BLUETOOTH_NOT_ALLOWED = 2;
+
+ /**
+ * Error code indicating that the Bluetooth Device specified is not bonded
+ */
+ public static final int ERROR_DEVICE_NOT_BONDED = 3;
+
+ /**
+ * Error code indicating that the Bluetooth Device specified is not connected, but is bonded
+ *
+ * @hide
+ */
+ public static final int ERROR_DEVICE_NOT_CONNECTED = 4;
+
+ /**
+ * Error code indicating that the caller does not have the
+ * {@link android.Manifest.permission#BLUETOOTH_ADVERTISE} permission
+ *
+ * @hide
+ */
+ public static final int ERROR_MISSING_BLUETOOTH_ADVERTISE_PERMISSION = 5;
+
+ /**
+ * Error code indicating that the caller does not have the
+ * {@link android.Manifest.permission#BLUETOOTH_CONNECT} permission
+ */
+ public static final int ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION = 6;
+
+ /**
+ * Error code indicating that the caller does not have the
+ * {@link android.Manifest.permission#BLUETOOTH_SCAN} permission
+ *
+ * @hide
+ */
+ public static final int ERROR_MISSING_BLUETOOTH_SCAN_PERMISSION = 7;
+
+ /**
+ * Error code indicating that the caller does not have the
+ * {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} permission
+ */
+ public static final int ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION = 8;
+
+ /**
+ * Error code indicating that the profile service is not bound. You can bind a profile service
+ * by calling {@link BluetoothAdapter#getProfileProxy}
+ */
+ public static final int ERROR_PROFILE_SERVICE_NOT_BOUND = 9;
+
+ /**
+ * Error code indicating that the feature is not supported.
+ */
+ public static final int ERROR_FEATURE_NOT_SUPPORTED = 10;
+
+ /**
+ * A GATT writeCharacteristic request is not permitted on the remote device.
+ */
+ public static final int ERROR_GATT_WRITE_NOT_ALLOWED = 101;
+
+ /**
+ * A GATT writeCharacteristic request is issued to a busy remote device.
+ */
+ public static final int ERROR_GATT_WRITE_REQUEST_BUSY = 102;
+
+ /**
+ * If another application has already requested {@link OobData} then another fetch will be
+ * disallowed until the callback is removed.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ERROR_ANOTHER_ACTIVE_OOB_REQUEST = 1000;
+
+ /**
+ * Indicates that the ACL disconnected due to an explicit request from the local device.
+ * <p>
+ * Example cause: This is a normal disconnect reason, e.g., user/app initiates
+ * disconnection.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_LOCAL_REQUEST = 1100;
+
+ /**
+ * Indicates that the ACL disconnected due to an explicit request from the remote device.
+ * <p>
+ * Example cause: This is a normal disconnect reason, e.g., user/app initiates
+ * disconnection.
+ * <p>
+ * Example solution: The app can also prompt the user to check their remote device.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_REMOTE_REQUEST = 1101;
+
+ /**
+ * Generic disconnect reason indicating the ACL disconnected due to an error on the local
+ * device.
+ * <p>
+ * Example solution: Prompt the user to check their local device (e.g., phone, car
+ * headunit).
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_LOCAL = 1102;
+
+ /**
+ * Generic disconnect reason indicating the ACL disconnected due to an error on the remote
+ * device.
+ * <p>
+ * Example solution: Prompt the user to check their remote device (e.g., headset, car
+ * headunit, watch).
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_REMOTE = 1103;
+
+ /**
+ * Indicates that the ACL disconnected due to a timeout.
+ * <p>
+ * Example cause: remote device might be out of range.
+ * <p>
+ * Example solution: Prompt user to verify their remote device is on or in
+ * connection/pairing mode.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_TIMEOUT = 1104;
+
+ /**
+ * Indicates that the ACL disconnected due to link key issues.
+ * <p>
+ * Example cause: Devices are either unpaired or remote device is refusing our pairing
+ * request.
+ * <p>
+ * Example solution: Prompt user to unpair and pair again.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_SECURITY = 1105;
+
+ /**
+ * Indicates that the ACL disconnected due to the local device's system policy.
+ * <p>
+ * Example cause: privacy policy, power management policy, permissions, etc.
+ * <p>
+ * Example solution: Prompt the user to check settings, or check with their system
+ * administrator (e.g. some corp-managed devices do not allow OPP connection).
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_SYSTEM_POLICY = 1106;
+
+ /**
+ * Indicates that the ACL disconnected due to resource constraints, either on the local
+ * device or the remote device.
+ * <p>
+ * Example cause: controller is busy, memory limit reached, maximum number of connections
+ * reached.
+ * <p>
+ * Example solution: The app should wait and try again. If still failing, prompt the user
+ * to disconnect some devices, or toggle Bluetooth on the local and/or the remote device.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_RESOURCE_LIMIT_REACHED = 1107;
+
+ /**
+ * Indicates that the ACL disconnected because another ACL connection already exists.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_CONNECTION_ALREADY_EXISTS = 1108;
+
+ /**
+ * Indicates that the ACL disconnected due to incorrect parameters passed in from the app.
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_DISCONNECT_REASON_BAD_PARAMETERS = 1109;
+
+ /**
+ * Indicates that setting the LE Audio Broadcast mode failed.
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_SOURCE_SET_BROADCAST_MODE_FAILED = 1110;
+
+ /**
+ * Indicates that setting a new encryption key for Bluetooth LE Audio Broadcast Source failed.
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_SOURCE_SET_ENCRYPTION_KEY_FAILED = 1111;
+
+ /**
+ * Indicates that connecting to a remote Broadcast Audio Scan Service failed.
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_AUDIO_SCAN_SERVICE_CONNECT_FAILED = 1112;
+
+ /**
+ * Indicates that disconnecting from a remote Broadcast Audio Scan Service failed.
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_AUDIO_SCAN_SERVICE_DISCONNECT_FAILED = 1113;
+
+ /**
+ * Indicates that enabling LE Audio Broadcast encryption failed
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_SOURCE_ENABLE_ENCRYPTION_FAILED = 1114;
+
+ /**
+ * Indicates that disabling LE Audio Broadcast encryption failed
+ * <p>
+ * Example solution: Change parameters and try again. If error persists, the app can report
+ * telemetry and/or log the error in a bugreport.
+ *
+ * @hide
+ */
+ public static final int ERROR_LE_AUDIO_BROADCAST_SOURCE_DISABLE_ENCRYPTION_FAILED = 1115;
+
+ /**
+ * Indicates that an unknown error has occurred has occurred.
+ */
+ public static final int ERROR_UNKNOWN = Integer.MAX_VALUE;
+}
diff --git a/framework/java/android/bluetooth/BluetoothUtils.java b/framework/java/android/bluetooth/BluetoothUtils.java
new file mode 100644
index 0000000000..867469241f
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothUtils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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 java.time.Duration;
+
+/**
+ * {@hide}
+ */
+public final class BluetoothUtils {
+ /**
+ * This utility class cannot be instantiated
+ */
+ private BluetoothUtils() {}
+
+ /**
+ * Timeout value for synchronous binder call
+ */
+ private static final Duration SYNC_CALLS_TIMEOUT = Duration.ofSeconds(5);
+
+ /**
+ * @return timeout value for synchronous binder call
+ */
+ static Duration getSyncTimeout() {
+ return SYNC_CALLS_TIMEOUT;
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothUuid.java b/framework/java/android/bluetooth/BluetoothUuid.java
new file mode 100644
index 0000000000..2a8ff51850
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothUuid.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2009 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.ParcelUuid;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.UUID;
+
+/**
+ * Static helper methods and constants to decode the ParcelUuid of remote devices.
+ *
+ * @hide
+ */
+@SystemApi
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+public final class BluetoothUuid {
+
+ /* See Bluetooth Assigned Numbers document - SDP section, to get the values of UUIDs
+ * for the various services.
+ *
+ * The following 128 bit values are calculated as:
+ * uuid * 2^96 + BASE_UUID
+ */
+
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid A2DP_SINK =
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid A2DP_SOURCE =
+ ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid ADV_AUDIO_DIST =
+ ParcelUuid.fromString("0000110D-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HSP =
+ ParcelUuid.fromString("00001108-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HSP_AG =
+ ParcelUuid.fromString("00001112-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HFP =
+ ParcelUuid.fromString("0000111E-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HFP_AG =
+ ParcelUuid.fromString("0000111F-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid AVRCP_CONTROLLER =
+ ParcelUuid.fromString("0000110E-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid AVRCP_TARGET =
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid OBEX_OBJECT_PUSH =
+ ParcelUuid.fromString("00001105-0000-1000-8000-00805f9b34fb");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HID =
+ ParcelUuid.fromString("00001124-0000-1000-8000-00805f9b34fb");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HOGP =
+ ParcelUuid.fromString("00001812-0000-1000-8000-00805f9b34fb");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid PANU =
+ ParcelUuid.fromString("00001115-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid NAP =
+ ParcelUuid.fromString("00001116-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid BNEP =
+ ParcelUuid.fromString("0000000f-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid PBAP_PCE =
+ ParcelUuid.fromString("0000112e-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid PBAP_PSE =
+ ParcelUuid.fromString("0000112f-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid MAP =
+ ParcelUuid.fromString("00001134-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid MNS =
+ ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid MAS =
+ ParcelUuid.fromString("00001132-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid SAP =
+ ParcelUuid.fromString("0000112D-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid HEARING_AID =
+ ParcelUuid.fromString("0000FDF0-0000-1000-8000-00805f9b34fb");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid LE_AUDIO =
+ ParcelUuid.fromString("0000184E-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid DIP =
+ ParcelUuid.fromString("00001200-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid VOLUME_CONTROL =
+ ParcelUuid.fromString("00001844-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid GENERIC_MEDIA_CONTROL =
+ ParcelUuid.fromString("00001849-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid MEDIA_CONTROL =
+ ParcelUuid.fromString("00001848-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid COORDINATED_SET =
+ ParcelUuid.fromString("00001846-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid CAP =
+ ParcelUuid.fromString("00001853-0000-1000-8000-00805F9B34FB");
+ /** @hide */
+ @NonNull
+ @SystemApi
+ public static final ParcelUuid BASE_UUID =
+ ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
+
+ /**
+ * Length of bytes for 16 bit UUID
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UUID_BYTES_16_BIT = 2;
+ /**
+ * Length of bytes for 32 bit UUID
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UUID_BYTES_32_BIT = 4;
+ /**
+ * Length of bytes for 128 bit UUID
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int UUID_BYTES_128_BIT = 16;
+
+ /**
+ * Returns true if there any common ParcelUuids in uuidA and uuidB.
+ *
+ * @param uuidA - List of ParcelUuids
+ * @param uuidB - List of ParcelUuids
+ *
+ * @hide
+ */
+ @SystemApi
+ public static boolean containsAnyUuid(@Nullable ParcelUuid[] uuidA,
+ @Nullable ParcelUuid[] uuidB) {
+ if (uuidA == null && uuidB == null) return true;
+
+ if (uuidA == null) {
+ return uuidB.length == 0;
+ }
+
+ if (uuidB == null) {
+ return uuidA.length == 0;
+ }
+
+ HashSet<ParcelUuid> uuidSet = new HashSet<ParcelUuid>(Arrays.asList(uuidA));
+ for (ParcelUuid uuid : uuidB) {
+ if (uuidSet.contains(uuid)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Extract the Service Identifier or the actual uuid from the Parcel Uuid.
+ * For example, if 0000110B-0000-1000-8000-00805F9B34FB is the parcel Uuid,
+ * this function will return 110B
+ *
+ * @param parcelUuid
+ * @return the service identifier.
+ */
+ private static int getServiceIdentifierFromParcelUuid(ParcelUuid parcelUuid) {
+ UUID uuid = parcelUuid.getUuid();
+ long value = (uuid.getMostSignificantBits() & 0xFFFFFFFF00000000L) >>> 32;
+ return (int) value;
+ }
+
+ /**
+ * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
+ * but the returned UUID is always in 128-bit format.
+ * Note UUID is little endian in Bluetooth.
+ *
+ * @param uuidBytes Byte representation of uuid.
+ * @return {@link ParcelUuid} parsed from bytes.
+ * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public static ParcelUuid parseUuidFrom(@Nullable byte[] uuidBytes) {
+ if (uuidBytes == null) {
+ throw new IllegalArgumentException("uuidBytes cannot be null");
+ }
+ int length = uuidBytes.length;
+ if (length != UUID_BYTES_16_BIT && length != UUID_BYTES_32_BIT
+ && length != UUID_BYTES_128_BIT) {
+ throw new IllegalArgumentException("uuidBytes length invalid - " + length);
+ }
+
+ // Construct a 128 bit UUID.
+ if (length == UUID_BYTES_128_BIT) {
+ ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
+ long msb = buf.getLong(8);
+ long lsb = buf.getLong(0);
+ return new ParcelUuid(new UUID(msb, lsb));
+ }
+
+ // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
+ // 128_bit_value = uuid * 2^96 + BASE_UUID
+ long shortUuid;
+ if (length == UUID_BYTES_16_BIT) {
+ shortUuid = uuidBytes[0] & 0xFF;
+ shortUuid += (uuidBytes[1] & 0xFF) << 8;
+ } else {
+ shortUuid = uuidBytes[0] & 0xFF;
+ shortUuid += (uuidBytes[1] & 0xFF) << 8;
+ shortUuid += (uuidBytes[2] & 0xFF) << 16;
+ shortUuid += (uuidBytes[3] & 0xFF) << 24;
+ }
+ long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
+ long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
+ return new ParcelUuid(new UUID(msb, lsb));
+ }
+
+ /**
+ * Parse UUID to bytes. The returned value is shortest representation, a 16-bit, 32-bit or
+ * 128-bit UUID, Note returned value is little endian (Bluetooth).
+ *
+ * @param uuid uuid to parse.
+ * @return shortest representation of {@code uuid} as bytes.
+ * @throws IllegalArgumentException If the {@code uuid} is null.
+ *
+ * @hide
+ */
+ public static byte[] uuidToBytes(ParcelUuid uuid) {
+ if (uuid == null) {
+ throw new IllegalArgumentException("uuid cannot be null");
+ }
+
+ if (is16BitUuid(uuid)) {
+ byte[] uuidBytes = new byte[UUID_BYTES_16_BIT];
+ int uuidVal = getServiceIdentifierFromParcelUuid(uuid);
+ uuidBytes[0] = (byte) (uuidVal & 0xFF);
+ uuidBytes[1] = (byte) ((uuidVal & 0xFF00) >> 8);
+ return uuidBytes;
+ }
+
+ if (is32BitUuid(uuid)) {
+ byte[] uuidBytes = new byte[UUID_BYTES_32_BIT];
+ int uuidVal = getServiceIdentifierFromParcelUuid(uuid);
+ uuidBytes[0] = (byte) (uuidVal & 0xFF);
+ uuidBytes[1] = (byte) ((uuidVal & 0xFF00) >> 8);
+ uuidBytes[2] = (byte) ((uuidVal & 0xFF0000) >> 16);
+ uuidBytes[3] = (byte) ((uuidVal & 0xFF000000) >> 24);
+ return uuidBytes;
+ }
+
+ // Construct a 128 bit UUID.
+ long msb = uuid.getUuid().getMostSignificantBits();
+ long lsb = uuid.getUuid().getLeastSignificantBits();
+
+ byte[] uuidBytes = new byte[UUID_BYTES_128_BIT];
+ ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
+ buf.putLong(8, msb);
+ buf.putLong(0, lsb);
+ return uuidBytes;
+ }
+
+ /**
+ * Check whether the given parcelUuid can be converted to 16 bit bluetooth uuid.
+ *
+ * @param parcelUuid
+ * @return true if the parcelUuid can be converted to 16 bit uuid, false otherwise.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static boolean is16BitUuid(ParcelUuid parcelUuid) {
+ UUID uuid = parcelUuid.getUuid();
+ if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
+ return false;
+ }
+ return ((uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL) == 0x1000L);
+ }
+
+
+ /**
+ * Check whether the given parcelUuid can be converted to 32 bit bluetooth uuid.
+ *
+ * @param parcelUuid
+ * @return true if the parcelUuid can be converted to 32 bit uuid, false otherwise.
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static boolean is32BitUuid(ParcelUuid parcelUuid) {
+ UUID uuid = parcelUuid.getUuid();
+ if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) {
+ return false;
+ }
+ if (is16BitUuid(parcelUuid)) {
+ return false;
+ }
+ return ((uuid.getMostSignificantBits() & 0xFFFFFFFFL) == 0x1000L);
+ }
+
+ private BluetoothUuid() {}
+}
diff --git a/framework/java/android/bluetooth/BluetoothVolumeControl.java b/framework/java/android/bluetooth/BluetoothVolumeControl.java
new file mode 100644
index 0000000000..27532aabc3
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothVolumeControl.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * 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.Manifest;
+import android.annotation.IntRange;
+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.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+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;
+
+/**
+ * This class provides the public APIs to control the Bluetooth Volume Control service.
+ *
+ * <p>BluetoothVolumeControl is a proxy object for controlling the Bluetooth VC
+ * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get
+ * the BluetoothVolumeControl proxy object.
+ * @hide
+ */
+@SystemApi
+public final class BluetoothVolumeControl implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothVolumeControl";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Volume Control
+ * 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}.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("ActionValue")
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.volume-control.profile.action.CONNECTION_STATE_CHANGED";
+
+ private BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothVolumeControl> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.VOLUME_CONTROL, TAG,
+ IBluetoothVolumeControl.class.getName()) {
+ @Override
+ public IBluetoothVolumeControl getServiceInterface(IBinder service) {
+ return IBluetoothVolumeControl.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothVolumeControl proxy object for interacting with the local
+ * Bluetooth Volume Control service.
+ */
+ /*package*/ BluetoothVolumeControl(Context context, ServiceListener listener,
+ BluetoothAdapter adapter) {
+ mAdapter = adapter;
+ mAttributionSource = adapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ @RequiresPermission(Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothVolumeControl getService() { return mProfileConnector.getService(); }
+
+ /**
+ * Get the list of connected devices. Currently at most one.
+ *
+ * @return list of connected devices
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (DBG) log("getConnectedDevices()");
+ final IBluetoothVolumeControl service = getService();
+ 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.getConnectedDevices(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;
+ }
+
+ /**
+ * Get the list of devices matching specified states. Currently at most one.
+ *
+ * @return list of matching devices
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ if (DBG) log("getDevicesMatchingStates()");
+ final IBluetoothVolumeControl service = getService();
+ 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;
+ }
+
+ /**
+ * Get connection state of device
+ *
+ * @return device connection state
+ *
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public int getConnectionState(BluetoothDevice device) {
+ if (DBG) log("getConnectionState(" + device + ")");
+ final IBluetoothVolumeControl service = getService();
+ 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.getConnectionState(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;
+ }
+
+ /**
+ * Tells remote device to set an absolute volume.
+ *
+ * @param volume Absolute volume to be set on remote device.
+ * Minimum value is 0 and maximum value is 255
+ * @hide
+ */
+ @SystemApi
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void setVolume(@Nullable BluetoothDevice device,
+ @IntRange(from = 0, to = 255) int volume) {
+ if (DBG) log("setVolume(" + volume + ")");
+ final IBluetoothVolumeControl service = getService();
+ 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.setVolume(device, volume, mAttributionSource, recv);
+ recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ /**
+ * 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,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothVolumeControl service = getService();
+ 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 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 IBluetoothVolumeControl service = getService();
+ 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;
+ }
+
+ private boolean isEnabled() {
+ return mAdapter.getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ private static boolean isValidDevice(@Nullable BluetoothDevice device) {
+ return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress());
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BufferConstraint.java b/framework/java/android/bluetooth/BufferConstraint.java
new file mode 100644
index 0000000000..cbffc788c3
--- /dev/null
+++ b/framework/java/android/bluetooth/BufferConstraint.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 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 android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Stores a codec's constraints on buffering length in milliseconds.
+ *
+ * {@hide}
+ */
+@SystemApi
+public final class BufferConstraint implements Parcelable {
+
+ private static final String TAG = "BufferConstraint";
+ private int mDefaultMillis;
+ private int mMaxMillis;
+ private int mMinMillis;
+
+ public BufferConstraint(int defaultMillis, int maxMillis,
+ int minMillis) {
+ mDefaultMillis = defaultMillis;
+ mMaxMillis = maxMillis;
+ mMinMillis = minMillis;
+ }
+
+ BufferConstraint(Parcel in) {
+ mDefaultMillis = in.readInt();
+ mMaxMillis = in.readInt();
+ mMinMillis = in.readInt();
+ }
+
+ public static final @NonNull Parcelable.Creator<BufferConstraint> CREATOR =
+ new Parcelable.Creator<BufferConstraint>() {
+ public BufferConstraint createFromParcel(Parcel in) {
+ return new BufferConstraint(in);
+ }
+
+ public BufferConstraint[] newArray(int size) {
+ return new BufferConstraint[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeInt(mDefaultMillis);
+ out.writeInt(mMaxMillis);
+ out.writeInt(mMinMillis);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Get the default buffer millis
+ *
+ * @return default buffer millis
+ * @hide
+ */
+ @SystemApi
+ public int getDefaultMillis() {
+ return mDefaultMillis;
+ }
+
+ /**
+ * Get the maximum buffer millis
+ *
+ * @return maximum buffer millis
+ * @hide
+ */
+ @SystemApi
+ public int getMaxMillis() {
+ return mMaxMillis;
+ }
+
+ /**
+ * Get the minimum buffer millis
+ *
+ * @return minimum buffer millis
+ * @hide
+ */
+ @SystemApi
+ public int getMinMillis() {
+ return mMinMillis;
+ }
+}
diff --git a/framework/java/android/bluetooth/BufferConstraints.java b/framework/java/android/bluetooth/BufferConstraints.java
new file mode 100644
index 0000000000..97d97232b7
--- /dev/null
+++ b/framework/java/android/bluetooth/BufferConstraints.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * A parcelable collection of buffer constraints by codec type.
+ *
+ * {@hide}
+ */
+@SystemApi
+public final class BufferConstraints implements Parcelable {
+ public static final int BUFFER_CODEC_MAX_NUM = 32;
+
+ private static final String TAG = "BufferConstraints";
+
+ private Map<Integer, BufferConstraint> mBufferConstraints;
+ private List<BufferConstraint> mBufferConstraintList;
+
+ public BufferConstraints(@NonNull List<BufferConstraint>
+ bufferConstraintList) {
+
+ mBufferConstraintList = new ArrayList<BufferConstraint>(bufferConstraintList);
+ mBufferConstraints = new HashMap<Integer, BufferConstraint>();
+ for (int i = 0; i < BUFFER_CODEC_MAX_NUM; i++) {
+ mBufferConstraints.put(i, bufferConstraintList.get(i));
+ }
+ }
+
+ BufferConstraints(Parcel in) {
+ mBufferConstraintList = new ArrayList<BufferConstraint>();
+ mBufferConstraints = new HashMap<Integer, BufferConstraint>();
+ in.readList(mBufferConstraintList, BufferConstraint.class.getClassLoader());
+ for (int i = 0; i < mBufferConstraintList.size(); i++) {
+ mBufferConstraints.put(i, mBufferConstraintList.get(i));
+ }
+ }
+
+ public static final @NonNull Parcelable.Creator<BufferConstraints> CREATOR =
+ new Parcelable.Creator<BufferConstraints>() {
+ public BufferConstraints createFromParcel(Parcel in) {
+ return new BufferConstraints(in);
+ }
+
+ public BufferConstraints[] newArray(int size) {
+ return new BufferConstraints[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeList(mBufferConstraintList);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Get the buffer constraints by codec type.
+ *
+ * @param codec Audio codec
+ * @return buffer constraints by codec type.
+ * @hide
+ */
+ @SystemApi
+ public @Nullable BufferConstraint forCodec(@BluetoothCodecConfig.SourceCodecType int codec) {
+ return mBufferConstraints.get(codec);
+ }
+}
diff --git a/framework/java/android/bluetooth/OWNERS b/framework/java/android/bluetooth/OWNERS
new file mode 100644
index 0000000000..fd60bed31a
--- /dev/null
+++ b/framework/java/android/bluetooth/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 27441
+
+rahulsabnis@google.com
+sattiraju@google.com
+siyuanh@google.com
+zachoverflow@google.com
+
diff --git a/framework/java/android/bluetooth/OobData.java b/framework/java/android/bluetooth/OobData.java
new file mode 100644
index 0000000000..bb0b95649b
--- /dev/null
+++ b/framework/java/android/bluetooth/OobData.java
@@ -0,0 +1,958 @@
+/**
+ * Copyright (C) 2016 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 java.util.Objects.requireNonNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Out Of Band Data for Bluetooth device pairing.
+ *
+ * <p>This object represents optional data obtained from a remote device through
+ * an out-of-band channel (eg. NFC, QR).
+ *
+ * <p>References:
+ * NFC AD Forum SSP 1.1 (AD)
+ * {@link https://members.nfc-forum.org//apps/group_public/download.php/24620/NFCForum-AD-BTSSP_1_1.pdf}
+ * Core Specification Supplement (CSS) V9
+ *
+ * <p>There are several BR/EDR Examples
+ *
+ * <p>Negotiated Handover:
+ * Bluetooth Carrier Configuration Record:
+ * - OOB Data Length
+ * - Device Address
+ * - Class of Device
+ * - Simple Pairing Hash C
+ * - Simple Pairing Randomizer R
+ * - Service Class UUID
+ * - Bluetooth Local Name
+ *
+ * <p>Static Handover:
+ * Bluetooth Carrier Configuration Record:
+ * - OOB Data Length
+ * - Device Address
+ * - Class of Device
+ * - Service Class UUID
+ * - Bluetooth Local Name
+ *
+ * <p>Simplified Tag Format for Single BT Carrier:
+ * Bluetooth OOB Data Record:
+ * - OOB Data Length
+ * - Device Address
+ * - Class of Device
+ * - Service Class UUID
+ * - Bluetooth Local Name
+ *
+ * @hide
+ */
+@SystemApi
+public final class OobData implements Parcelable {
+
+ private static final String TAG = "OobData";
+ /** The {@link OobData#mClassicLength} may be. (AD 3.1.1) (CSS 1.6.2) @hide */
+ @SystemApi
+ public static final int OOB_LENGTH_OCTETS = 2;
+ /**
+ * The length for the {@link OobData#mDeviceAddressWithType}(6) and Address Type(1).
+ * (AD 3.1.2) (CSS 1.6.2)
+ * @hide
+ */
+ @SystemApi
+ public static final int DEVICE_ADDRESS_OCTETS = 7;
+ /** The Class of Device is 3 octets. (AD 3.1.3) (CSS 1.6.2) @hide */
+ @SystemApi
+ public static final int CLASS_OF_DEVICE_OCTETS = 3;
+ /** The Confirmation data must be 16 octets. (AD 3.2.2) (CSS 1.6.2) @hide */
+ @SystemApi
+ public static final int CONFIRMATION_OCTETS = 16;
+ /** The Randomizer data must be 16 octets. (AD 3.2.3) (CSS 1.6.2) @hide */
+ @SystemApi
+ public static final int RANDOMIZER_OCTETS = 16;
+ /** The LE Device Role length is 1 octet. (AD 3.3.2) (CSS 1.17) @hide */
+ @SystemApi
+ public static final int LE_DEVICE_ROLE_OCTETS = 1;
+ /** The {@link OobData#mLeTemporaryKey} length. (3.4.1) @hide */
+ @SystemApi
+ public static final int LE_TK_OCTETS = 16;
+ /** The {@link OobData#mLeAppearance} length. (3.4.1) @hide */
+ @SystemApi
+ public static final int LE_APPEARANCE_OCTETS = 2;
+ /** The {@link OobData#mLeFlags} length. (3.4.1) @hide */
+ @SystemApi
+ public static final int LE_DEVICE_FLAG_OCTETS = 1; // 1 octet to hold the 0-4 value.
+
+ // Le Roles
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "LE_DEVICE_ROLE_" },
+ value = {
+ LE_DEVICE_ROLE_PERIPHERAL_ONLY,
+ LE_DEVICE_ROLE_CENTRAL_ONLY,
+ LE_DEVICE_ROLE_BOTH_PREFER_PERIPHERAL,
+ LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL
+ }
+ )
+ public @interface LeRole {}
+
+ /** @hide */
+ @SystemApi
+ public static final int LE_DEVICE_ROLE_PERIPHERAL_ONLY = 0x00;
+ /** @hide */
+ @SystemApi
+ public static final int LE_DEVICE_ROLE_CENTRAL_ONLY = 0x01;
+ /** @hide */
+ @SystemApi
+ public static final int LE_DEVICE_ROLE_BOTH_PREFER_PERIPHERAL = 0x02;
+ /** @hide */
+ @SystemApi
+ public static final int LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL = 0x03;
+
+ // Le Flags
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ prefix = { "LE_FLAG_" },
+ value = {
+ LE_FLAG_LIMITED_DISCOVERY_MODE,
+ LE_FLAG_GENERAL_DISCOVERY_MODE,
+ LE_FLAG_BREDR_NOT_SUPPORTED,
+ LE_FLAG_SIMULTANEOUS_CONTROLLER,
+ LE_FLAG_SIMULTANEOUS_HOST
+ }
+ )
+ public @interface LeFlag {}
+
+ /** @hide */
+ @SystemApi
+ public static final int LE_FLAG_LIMITED_DISCOVERY_MODE = 0x00;
+ /** @hide */
+ @SystemApi
+ public static final int LE_FLAG_GENERAL_DISCOVERY_MODE = 0x01;
+ /** @hide */
+ @SystemApi
+ public static final int LE_FLAG_BREDR_NOT_SUPPORTED = 0x02;
+ /** @hide */
+ @SystemApi
+ public static final int LE_FLAG_SIMULTANEOUS_CONTROLLER = 0x03;
+ /** @hide */
+ @SystemApi
+ public static final int LE_FLAG_SIMULTANEOUS_HOST = 0x04;
+
+ /**
+ * Builds an {@link OobData} object and validates that the required combination
+ * of values are present to create the LE specific OobData type.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class LeBuilder {
+
+ /**
+ * It is recommended that this Hash C is generated anew for each
+ * pairing.
+ *
+ * <p>It should be noted that on passive NFC this isn't possible as the data is static
+ * and immutable.
+ */
+ private byte[] mConfirmationHash = null;
+
+ /**
+ * Optional, but adds more validity to the pairing.
+ *
+ * <p>If not present a value of 0 is assumed.
+ */
+ private byte[] mRandomizerHash = new byte[] {
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ };
+
+ /**
+ * The Bluetooth Device user-friendly name presented over Bluetooth Technology.
+ *
+ * <p>This is the name that may be displayed to the device user as part of the UI.
+ */
+ private byte[] mDeviceName = null;
+
+ /**
+ * Sets the Bluetooth Device name to be used for UI purposes.
+ *
+ * <p>Optional attribute.
+ *
+ * @param deviceName byte array representing the name, may be 0 in length, not null.
+ *
+ * @return {@link OobData#ClassicBuilder}
+ *
+ * @throws NullPointerException if deviceName is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public LeBuilder setDeviceName(@NonNull byte[] deviceName) {
+ requireNonNull(deviceName);
+ this.mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * The Bluetooth Device Address is the address to which the OOB data belongs.
+ *
+ * <p>The length MUST be {@link OobData#DEVICE_ADDRESS_OCTETS} octets.
+ *
+ * <p> Address is encoded in Little Endian order.
+ *
+ * <p>e.g. 00:01:02:03:04:05 would be x05x04x03x02x01x00
+ */
+ private final byte[] mDeviceAddressWithType;
+
+ /**
+ * During an LE connection establishment, one must be in the Peripheral mode and the other
+ * in the Central role.
+ *
+ * <p>Possible Values:
+ * {@link LE_DEVICE_ROLE_PERIPHERAL_ONLY} Only Peripheral supported
+ * {@link LE_DEVICE_ROLE_CENTRAL_ONLY} Only Central supported
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_PERIPHERAL} Central & Peripheral supported;
+ * Peripheral Preferred
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL} Only peripheral supported; Central Preferred
+ * 0x04 - 0xFF Reserved
+ */
+ private final @LeRole int mLeDeviceRole;
+
+ /**
+ * Temporary key value from the Security Manager.
+ *
+ * <p> Must be {@link LE_TK_OCTETS} in size
+ */
+ private byte[] mLeTemporaryKey = null;
+
+ /**
+ * Defines the representation of the external appearance of the device.
+ *
+ * <p>For example, a mouse, remote control, or keyboard.
+ *
+ * <p>Used for visual on discovering device to represent icon/string/etc...
+ */
+ private byte[] mLeAppearance = null;
+
+ /**
+ * Contains which discoverable mode to use, BR/EDR support and capability.
+ *
+ * <p>Possible LE Flags:
+ * {@link LE_FLAG_LIMITED_DISCOVERY_MODE} LE Limited Discoverable Mode.
+ * {@link LE_FLAG_GENERAL_DISCOVERY_MODE} LE General Discoverable Mode.
+ * {@link LE_FLAG_BREDR_NOT_SUPPORTED} BR/EDR Not Supported. Bit 37 of
+ * LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_CONTROLLER} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Controller).
+ * Bit 49 of LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_HOST} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Host).
+ * Bit 55 of LMP Feature Mask Definitions.
+ * <b>0x05- 0x07 Reserved</b>
+ */
+ private @LeFlag int mLeFlags = LE_FLAG_GENERAL_DISCOVERY_MODE; // Invalid default
+
+ /**
+ * Main creation method for creating a LE version of {@link OobData}.
+ *
+ * <p>This object will allow the caller to call {@link LeBuilder#build()}
+ * to build the data object or add any option information to the builder.
+ *
+ * @param deviceAddressWithType the LE device address plus the address type (7 octets);
+ * not null.
+ * @param leDeviceRole whether the device supports Peripheral, Central,
+ * Both including preference; not null. (1 octet)
+ * @param confirmationHash Array consisting of {@link OobData#CONFIRMATION_OCTETS} octets
+ * of data. Data is derived from controller/host stack and is
+ * required for pairing OOB.
+ *
+ * <p>Possible Values:
+ * {@link LE_DEVICE_ROLE_PERIPHERAL_ONLY} Only Peripheral supported
+ * {@link LE_DEVICE_ROLE_CENTRAL_ONLY} Only Central supported
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_PERIPHERAL} Central & Peripheral supported;
+ * Peripheral Preferred
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL} Only peripheral supported; Central Preferred
+ * 0x04 - 0xFF Reserved
+ *
+ * @throws IllegalArgumentException if any of the values fail to be set.
+ * @throws NullPointerException if any argument is null.
+ *
+ * @hide
+ */
+ @SystemApi
+ public LeBuilder(@NonNull byte[] confirmationHash, @NonNull byte[] deviceAddressWithType,
+ @LeRole int leDeviceRole) {
+ requireNonNull(confirmationHash);
+ requireNonNull(deviceAddressWithType);
+ if (confirmationHash.length != OobData.CONFIRMATION_OCTETS) {
+ throw new IllegalArgumentException("confirmationHash must be "
+ + OobData.CONFIRMATION_OCTETS + " octets in length.");
+ }
+ this.mConfirmationHash = confirmationHash;
+ if (deviceAddressWithType.length != OobData.DEVICE_ADDRESS_OCTETS) {
+ throw new IllegalArgumentException("confirmationHash must be "
+ + OobData.DEVICE_ADDRESS_OCTETS+ " octets in length.");
+ }
+ this.mDeviceAddressWithType = deviceAddressWithType;
+ if (leDeviceRole < LE_DEVICE_ROLE_PERIPHERAL_ONLY
+ || leDeviceRole > LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL) {
+ throw new IllegalArgumentException("leDeviceRole must be a valid value.");
+ }
+ this.mLeDeviceRole = leDeviceRole;
+ }
+
+ /**
+ * Sets the Temporary Key value to be used by the LE Security Manager during
+ * LE pairing.
+ *
+ * @param leTemporaryKey byte array that shall be 16 bytes. Please see Bluetooth CSSv6,
+ * Part A 1.8 for a detailed description.
+ *
+ * @return {@link OobData#Builder}
+ *
+ * @throws IllegalArgumentException if the leTemporaryKey is an invalid format.
+ * @throws NullinterException if leTemporaryKey is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public LeBuilder setLeTemporaryKey(@NonNull byte[] leTemporaryKey) {
+ requireNonNull(leTemporaryKey);
+ if (leTemporaryKey.length != LE_TK_OCTETS) {
+ throw new IllegalArgumentException("leTemporaryKey must be "
+ + LE_TK_OCTETS + " octets in length.");
+ }
+ this.mLeTemporaryKey = leTemporaryKey;
+ return this;
+ }
+
+ /**
+ * @param randomizerHash byte array consisting of {@link OobData#RANDOMIZER_OCTETS} octets
+ * of data. Data is derived from controller/host stack and is required for pairing OOB.
+ * Also, randomizerHash may be all 0s or null in which case it becomes all 0s.
+ *
+ * @throws IllegalArgumentException if null or incorrect length randomizerHash was passed.
+ * @throws NullPointerException if randomizerHash is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public LeBuilder setRandomizerHash(@NonNull byte[] randomizerHash) {
+ requireNonNull(randomizerHash);
+ if (randomizerHash.length != OobData.RANDOMIZER_OCTETS) {
+ throw new IllegalArgumentException("randomizerHash must be "
+ + OobData.RANDOMIZER_OCTETS + " octets in length.");
+ }
+ this.mRandomizerHash = randomizerHash;
+ return this;
+ }
+
+ /**
+ * Sets the LE Flags necessary for the pairing scenario or discovery mode.
+ *
+ * @param leFlags enum value representing the 1 octet of data about discovery modes.
+ *
+ * <p>Possible LE Flags:
+ * {@link LE_FLAG_LIMITED_DISCOVERY_MODE} LE Limited Discoverable Mode.
+ * {@link LE_FLAG_GENERAL_DISCOVERY_MODE} LE General Discoverable Mode.
+ * {@link LE_FLAG_BREDR_NOT_SUPPORTED} BR/EDR Not Supported. Bit 37 of
+ * LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_CONTROLLER} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Controller) Bit 49 of LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_HOST} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Host).
+ * Bit 55 of LMP Feature Mask Definitions.
+ * 0x05- 0x07 Reserved
+ *
+ * @throws IllegalArgumentException for invalid flag
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public LeBuilder setLeFlags(@LeFlag int leFlags) {
+ if (leFlags < LE_FLAG_LIMITED_DISCOVERY_MODE || leFlags > LE_FLAG_SIMULTANEOUS_HOST) {
+ throw new IllegalArgumentException("leFlags must be a valid value.");
+ }
+ this.mLeFlags = leFlags;
+ return this;
+ }
+
+ /**
+ * Validates and builds the {@link OobData} object for LE Security.
+ *
+ * @return {@link OobData} with given builder values
+ *
+ * @throws IllegalStateException if either of the 2 required fields were not set.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public OobData build() {
+ final OobData oob =
+ new OobData(this.mDeviceAddressWithType, this.mLeDeviceRole,
+ this.mConfirmationHash);
+
+ // If we have values, set them, otherwise use default
+ oob.mLeTemporaryKey =
+ (this.mLeTemporaryKey != null) ? this.mLeTemporaryKey : oob.mLeTemporaryKey;
+ oob.mLeAppearance = (this.mLeAppearance != null)
+ ? this.mLeAppearance : oob.mLeAppearance;
+ oob.mLeFlags = (this.mLeFlags != 0xF) ? this.mLeFlags : oob.mLeFlags;
+ oob.mDeviceName = (this.mDeviceName != null) ? this.mDeviceName : oob.mDeviceName;
+ oob.mRandomizerHash = this.mRandomizerHash;
+ return oob;
+ }
+ }
+
+ /**
+ * Builds an {@link OobData} object and validates that the required combination
+ * of values are present to create the Classic specific OobData type.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final class ClassicBuilder {
+ // Used by both Classic and LE
+ /**
+ * It is recommended that this Hash C is generated anew for each
+ * pairing.
+ *
+ * <p>It should be noted that on passive NFC this isn't possible as the data is static
+ * and immutable.
+ *
+ * @hide
+ */
+ private byte[] mConfirmationHash = null;
+
+ /**
+ * Optional, but adds more validity to the pairing.
+ *
+ * <p>If not present a value of 0 is assumed.
+ *
+ * @hide
+ */
+ private byte[] mRandomizerHash = new byte[] {
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ };
+
+ /**
+ * The Bluetooth Device user-friendly name presented over Bluetooth Technology.
+ *
+ * <p>This is the name that may be displayed to the device user as part of the UI.
+ *
+ * @hide
+ */
+ private byte[] mDeviceName = null;
+
+ /**
+ * This length value provides the absolute length of total OOB data block used for
+ * Bluetooth BR/EDR
+ *
+ * <p>OOB communication, which includes the length field itself and the Bluetooth
+ * Device Address.
+ *
+ * <p>The minimum length that may be represented in this field is 8.
+ *
+ * @hide
+ */
+ private final byte[] mClassicLength;
+
+ /**
+ * The Bluetooth Device Address is the address to which the OOB data belongs.
+ *
+ * <p>The length MUST be {@link OobData#DEVICE_ADDRESS_OCTETS} octets.
+ *
+ * <p> Address is encoded in Little Endian order.
+ *
+ * <p>e.g. 00:01:02:03:04:05 would be x05x04x03x02x01x00
+ *
+ * @hide
+ */
+ private final byte[] mDeviceAddressWithType;
+
+ /**
+ * Class of Device information is to be used to provide a graphical representation
+ * to the user as part of UI involving operations.
+ *
+ * <p>This is not to be used to determine a particular service can be used.
+ *
+ * <p>The length MUST be {@link OobData#CLASS_OF_DEVICE_OCTETS} octets.
+ *
+ * @hide
+ */
+ private byte[] mClassOfDevice = null;
+
+ /**
+ * Main creation method for creating a Classic version of {@link OobData}.
+ *
+ * <p>This object will allow the caller to call {@link ClassicBuilder#build()}
+ * to build the data object or add any option information to the builder.
+ *
+ * @param confirmationHash byte array consisting of {@link OobData#CONFIRMATION_OCTETS}
+ * octets of data. Data is derived from controller/host stack and is required for pairing
+ * OOB.
+ * @param classicLength byte array representing the length of data from 8-65535 across 2
+ * octets (0xXXXX).
+ * @param deviceAddressWithType byte array representing the Bluetooth Address of the device
+ * that owns the OOB data. (i.e. the originator) [6 octets]
+ *
+ * @throws IllegalArgumentException if any of the values fail to be set.
+ * @throws NullPointerException if any argument is null.
+ *
+ * @hide
+ */
+ @SystemApi
+ public ClassicBuilder(@NonNull byte[] confirmationHash, @NonNull byte[] classicLength,
+ @NonNull byte[] deviceAddressWithType) {
+ requireNonNull(confirmationHash);
+ requireNonNull(classicLength);
+ requireNonNull(deviceAddressWithType);
+ if (confirmationHash.length != OobData.CONFIRMATION_OCTETS) {
+ throw new IllegalArgumentException("confirmationHash must be "
+ + OobData.CONFIRMATION_OCTETS + " octets in length.");
+ }
+ this.mConfirmationHash = confirmationHash;
+ if (classicLength.length != OOB_LENGTH_OCTETS) {
+ throw new IllegalArgumentException("classicLength must be "
+ + OOB_LENGTH_OCTETS + " octets in length.");
+ }
+ this.mClassicLength = classicLength;
+ if (deviceAddressWithType.length != DEVICE_ADDRESS_OCTETS) {
+ throw new IllegalArgumentException("deviceAddressWithType must be "
+ + DEVICE_ADDRESS_OCTETS + " octets in length.");
+ }
+ this.mDeviceAddressWithType = deviceAddressWithType;
+ }
+
+ /**
+ * @param randomizerHash byte array consisting of {@link OobData#RANDOMIZER_OCTETS} octets
+ * of data. Data is derived from controller/host stack and is required for pairing OOB.
+ * Also, randomizerHash may be all 0s or null in which case it becomes all 0s.
+ *
+ * @throws IllegalArgumentException if null or incorrect length randomizerHash was passed.
+ * @throws NullPointerException if randomizerHash is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public ClassicBuilder setRandomizerHash(@NonNull byte[] randomizerHash) {
+ requireNonNull(randomizerHash);
+ if (randomizerHash.length != OobData.RANDOMIZER_OCTETS) {
+ throw new IllegalArgumentException("randomizerHash must be "
+ + OobData.RANDOMIZER_OCTETS + " octets in length.");
+ }
+ this.mRandomizerHash = randomizerHash;
+ return this;
+ }
+
+ /**
+ * Sets the Bluetooth Device name to be used for UI purposes.
+ *
+ * <p>Optional attribute.
+ *
+ * @param deviceName byte array representing the name, may be 0 in length, not null.
+ *
+ * @return {@link OobData#ClassicBuilder}
+ *
+ * @throws NullPointerException if deviceName is null
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public ClassicBuilder setDeviceName(@NonNull byte[] deviceName) {
+ requireNonNull(deviceName);
+ this.mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Sets the Bluetooth Class of Device; used for UI purposes only.
+ *
+ * <p>Not an indicator of available services!
+ *
+ * <p>Optional attribute.
+ *
+ * @param classOfDevice byte array of {@link OobData#CLASS_OF_DEVICE_OCTETS} octets.
+ *
+ * @return {@link OobData#ClassicBuilder}
+ *
+ * @throws IllegalArgumentException if length is not equal to
+ * {@link OobData#CLASS_OF_DEVICE_OCTETS} octets.
+ * @throws NullPointerException if classOfDevice is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public ClassicBuilder setClassOfDevice(@NonNull byte[] classOfDevice) {
+ requireNonNull(classOfDevice);
+ if (classOfDevice.length != OobData.CLASS_OF_DEVICE_OCTETS) {
+ throw new IllegalArgumentException("classOfDevice must be "
+ + OobData.CLASS_OF_DEVICE_OCTETS + " octets in length.");
+ }
+ this.mClassOfDevice = classOfDevice;
+ return this;
+ }
+
+ /**
+ * Validates and builds the {@link OobDat object for Classic Security.
+ *
+ * @return {@link OobData} with previously given builder values.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public OobData build() {
+ final OobData oob =
+ new OobData(this.mClassicLength, this.mDeviceAddressWithType,
+ this.mConfirmationHash);
+ // If we have values, set them, otherwise use default
+ oob.mDeviceName = (this.mDeviceName != null) ? this.mDeviceName : oob.mDeviceName;
+ oob.mClassOfDevice = (this.mClassOfDevice != null)
+ ? this.mClassOfDevice : oob.mClassOfDevice;
+ oob.mRandomizerHash = this.mRandomizerHash;
+ return oob;
+ }
+ }
+
+ // Members (Defaults for Optionals must be set or Parceling fails on NPE)
+ // Both
+ private final byte[] mDeviceAddressWithType;
+ private final byte[] mConfirmationHash;
+ private byte[] mRandomizerHash = new byte[] {
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+ };
+ // Default the name to "Bluetooth Device"
+ private byte[] mDeviceName = new byte[] {
+ // Bluetooth
+ 0x42, 0x6c, 0x75, 0x65, 0x74, 0x6f, 0x6f, 0x74, 0x68,
+ // <space>Device
+ 0x20, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65
+ };
+
+ // Classic
+ private final byte[] mClassicLength;
+ private byte[] mClassOfDevice = new byte[CLASS_OF_DEVICE_OCTETS];
+
+ // LE
+ private final @LeRole int mLeDeviceRole;
+ private byte[] mLeTemporaryKey = new byte[LE_TK_OCTETS];
+ private byte[] mLeAppearance = new byte[LE_APPEARANCE_OCTETS];
+ private @LeFlag int mLeFlags = LE_FLAG_LIMITED_DISCOVERY_MODE;
+
+ /**
+ * @return byte array representing the MAC address of a bluetooth device.
+ * The Address is 6 octets long with a 1 octet address type associated with the address.
+ *
+ * <p>For classic this will be 6 byte address plus the default of PUBLIC_ADDRESS Address Type.
+ * For LE there are more choices for Address Type.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public byte[] getDeviceAddressWithType() {
+ return mDeviceAddressWithType;
+ }
+
+ /**
+ * @return byte array representing the confirmationHash value
+ * which is used to confirm the identity to the controller.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public byte[] getConfirmationHash() {
+ return mConfirmationHash;
+ }
+
+ /**
+ * @return byte array representing the randomizerHash value
+ * which is used to verify the identity of the controller.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public byte[] getRandomizerHash() {
+ return mRandomizerHash;
+ }
+
+ /**
+ * @return Device Name used for displaying name in UI.
+ *
+ * <p>Also, this will be populated with the LE Local Name if the data is for LE.
+ *
+ * @hide
+ */
+ @Nullable
+ @SystemApi
+ public byte[] getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * @return byte array representing the oob data length which is the length
+ * of all of the data including these octets.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public byte[] getClassicLength() {
+ return mClassicLength;
+ }
+
+ /**
+ * @return byte array representing the class of device for UI display.
+ *
+ * <p>Does not indicate services available; for display only.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public byte[] getClassOfDevice() {
+ return mClassOfDevice;
+ }
+
+ /**
+ * @return Temporary Key used for LE pairing.
+ *
+ * @hide
+ */
+ @Nullable
+ @SystemApi
+ public byte[] getLeTemporaryKey() {
+ return mLeTemporaryKey;
+ }
+
+ /**
+ * @return Appearance used for LE pairing. For use in UI situations
+ * when determining what sort of icons or text to display regarding
+ * the device.
+ *
+ * @hide
+ */
+ @Nullable
+ @SystemApi
+ public byte[] getLeAppearance() {
+ return mLeAppearance;
+ }
+
+ /**
+ * @return Flags used to determing discoverable mode to use, BR/EDR Support, and Capability.
+ *
+ * <p>Possible LE Flags:
+ * {@link LE_FLAG_LIMITED_DISCOVERY_MODE} LE Limited Discoverable Mode.
+ * {@link LE_FLAG_GENERAL_DISCOVERY_MODE} LE General Discoverable Mode.
+ * {@link LE_FLAG_BREDR_NOT_SUPPORTED} BR/EDR Not Supported. Bit 37 of
+ * LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_CONTROLLER} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Controller).
+ * Bit 49 of LMP Feature Mask Definitions.
+ * {@link LE_FLAG_SIMULTANEOUS_HOST} Simultaneous LE and BR/EDR to
+ * Same Device Capable (Host).
+ * Bit 55 of LMP Feature Mask Definitions.
+ * <b>0x05- 0x07 Reserved</b>
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @LeFlag
+ public int getLeFlags() {
+ return mLeFlags;
+ }
+
+ /**
+ * @return the supported and preferred roles of the LE device.
+ *
+ * <p>Possible Values:
+ * {@link LE_DEVICE_ROLE_PERIPHERAL_ONLY} Only Peripheral supported
+ * {@link LE_DEVICE_ROLE_CENTRAL_ONLY} Only Central supported
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_PERIPHERAL} Central & Peripheral supported;
+ * Peripheral Preferred
+ * {@link LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL} Only peripheral supported; Central Preferred
+ * 0x04 - 0xFF Reserved
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @LeRole
+ public int getLeDeviceRole() {
+ return mLeDeviceRole;
+ }
+
+ /**
+ * Classic Security Constructor
+ */
+ private OobData(@NonNull byte[] classicLength, @NonNull byte[] deviceAddressWithType,
+ @NonNull byte[] confirmationHash) {
+ mClassicLength = classicLength;
+ mDeviceAddressWithType = deviceAddressWithType;
+ mConfirmationHash = confirmationHash;
+ mLeDeviceRole = -1; // Satisfy final
+ }
+
+ /**
+ * LE Security Constructor
+ */
+ private OobData(@NonNull byte[] deviceAddressWithType, @LeRole int leDeviceRole,
+ @NonNull byte[] confirmationHash) {
+ mDeviceAddressWithType = deviceAddressWithType;
+ mLeDeviceRole = leDeviceRole;
+ mConfirmationHash = confirmationHash;
+ mClassicLength = new byte[OOB_LENGTH_OCTETS]; // Satisfy final
+ }
+
+ private OobData(Parcel in) {
+ // Both
+ mDeviceAddressWithType = in.createByteArray();
+ mConfirmationHash = in.createByteArray();
+ mRandomizerHash = in.createByteArray();
+ mDeviceName = in.createByteArray();
+
+ // Classic
+ mClassicLength = in.createByteArray();
+ mClassOfDevice = in.createByteArray();
+
+ // LE
+ mLeDeviceRole = in.readInt();
+ mLeTemporaryKey = in.createByteArray();
+ mLeAppearance = in.createByteArray();
+ mLeFlags = in.readInt();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ // Both
+ // Required
+ out.writeByteArray(mDeviceAddressWithType);
+ // Required
+ out.writeByteArray(mConfirmationHash);
+ // Optional
+ out.writeByteArray(mRandomizerHash);
+ // Optional
+ out.writeByteArray(mDeviceName);
+
+ // Classic
+ // Required
+ out.writeByteArray(mClassicLength);
+ // Optional
+ out.writeByteArray(mClassOfDevice);
+
+ // LE
+ // Required
+ out.writeInt(mLeDeviceRole);
+ // Required
+ out.writeByteArray(mLeTemporaryKey);
+ // Optional
+ out.writeByteArray(mLeAppearance);
+ // Optional
+ out.writeInt(mLeFlags);
+ }
+
+ // For Parcelable
+ public static final @android.annotation.NonNull Parcelable.Creator<OobData> CREATOR =
+ new Parcelable.Creator<OobData>() {
+ public OobData createFromParcel(Parcel in) {
+ return new OobData(in);
+ }
+
+ public OobData[] newArray(int size) {
+ return new OobData[size];
+ }
+ };
+
+ /**
+ * @return a {@link String} representation of the OobData object.
+ *
+ * @hide
+ */
+ @Override
+ @NonNull
+ public String toString() {
+ return "OobData: \n\t"
+ // Both
+ + "Device Address With Type: " + toHexString(mDeviceAddressWithType) + "\n\t"
+ + "Confirmation: " + toHexString(mConfirmationHash) + "\n\t"
+ + "Randomizer: " + toHexString(mRandomizerHash) + "\n\t"
+ + "Device Name: " + toHexString(mDeviceName) + "\n\t"
+ // Classic
+ + "OobData Length: " + toHexString(mClassicLength) + "\n\t"
+ + "Class of Device: " + toHexString(mClassOfDevice) + "\n\t"
+ // LE
+ + "LE Device Role: " + toHexString(mLeDeviceRole) + "\n\t"
+ + "LE Temporary Key: " + toHexString(mLeTemporaryKey) + "\n\t"
+ + "LE Appearance: " + toHexString(mLeAppearance) + "\n\t"
+ + "LE Flags: " + toHexString(mLeFlags) + "\n\t";
+ }
+
+ @NonNull
+ private String toHexString(int b) {
+ return toHexString(new byte[] {(byte) b});
+ }
+
+ @NonNull
+ private String toHexString(byte b) {
+ return toHexString(new byte[] {b});
+ }
+
+ @NonNull
+ private String toHexString(byte[] array) {
+ if (array == null) return "null";
+ StringBuilder builder = new StringBuilder(array.length * 2);
+ for (byte b: array) {
+ builder.append(String.format("%02x", b));
+ }
+ return builder.toString();
+ }
+}
diff --git a/framework/java/android/bluetooth/SdpDipRecord.java b/framework/java/android/bluetooth/SdpDipRecord.java
new file mode 100644
index 0000000000..84b0eef059
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpDipRecord.java
@@ -0,0 +1,104 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 java.util.Arrays;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data representation of a Object Push Profile Server side SDP record.
+ */
+/** @hide */
+public class SdpDipRecord implements Parcelable {
+ private final int mSpecificationId;
+ private final int mVendorId;
+ private final int mVendorIdSource;
+ private final int mProductId;
+ private final int mVersion;
+ private final boolean mPrimaryRecord;
+
+ public SdpDipRecord(int specificationId,
+ int vendorId, int vendorIdSource,
+ int productId, int version,
+ boolean primaryRecord) {
+ super();
+ this.mSpecificationId = specificationId;
+ this.mVendorId = vendorId;
+ this.mVendorIdSource = vendorIdSource;
+ this.mProductId = productId;
+ this.mVersion = version;
+ this.mPrimaryRecord = primaryRecord;
+ }
+
+ public SdpDipRecord(Parcel in) {
+ this.mSpecificationId = in.readInt();
+ this.mVendorId = in.readInt();
+ this.mVendorIdSource = in.readInt();
+ this.mProductId = in.readInt();
+ this.mVersion = in.readInt();
+ this.mPrimaryRecord = in.readBoolean();
+ }
+
+ public int getSpecificationId() {
+ return mSpecificationId;
+ }
+
+ public int getVendorId() {
+ return mVendorId;
+ }
+
+ public int getVendorIdSource() {
+ return mVendorIdSource;
+ }
+
+ public int getProductId() {
+ return mProductId;
+ }
+
+ public int getVersion() {
+ return mVersion;
+ }
+
+ public boolean getPrimaryRecord() {
+ return mPrimaryRecord;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSpecificationId);
+ dest.writeInt(mVendorId);
+ dest.writeInt(mVendorIdSource);
+ dest.writeInt(mProductId);
+ dest.writeInt(mVersion);
+ dest.writeBoolean(mPrimaryRecord);
+ }
+
+ @Override
+ public int describeContents() {
+ /* No special objects */
+ return 0;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpDipRecord createFromParcel(Parcel in) {
+ return new SdpDipRecord(in);
+ }
+ public SdpDipRecord[] newArray(int size) {
+ return new SdpDipRecord[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/SdpMasRecord.java b/framework/java/android/bluetooth/SdpMasRecord.java
new file mode 100644
index 0000000000..72d49380b7
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpMasRecord.java
@@ -0,0 +1,150 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public class SdpMasRecord implements Parcelable {
+ private final int mMasInstanceId;
+ private final int mL2capPsm;
+ private final int mRfcommChannelNumber;
+ private final int mProfileVersion;
+ private final int mSupportedFeatures;
+ private final int mSupportedMessageTypes;
+ private final String mServiceName;
+
+ /** Message type */
+ public static final class MessageType {
+ public static final int EMAIL = 0x01;
+ public static final int SMS_GSM = 0x02;
+ public static final int SMS_CDMA = 0x04;
+ public static final int MMS = 0x08;
+ }
+
+ public SdpMasRecord(int masInstanceId,
+ int l2capPsm,
+ int rfcommChannelNumber,
+ int profileVersion,
+ int supportedFeatures,
+ int supportedMessageTypes,
+ String serviceName) {
+ mMasInstanceId = masInstanceId;
+ mL2capPsm = l2capPsm;
+ mRfcommChannelNumber = rfcommChannelNumber;
+ mProfileVersion = profileVersion;
+ mSupportedFeatures = supportedFeatures;
+ mSupportedMessageTypes = supportedMessageTypes;
+ mServiceName = serviceName;
+ }
+
+ public SdpMasRecord(Parcel in) {
+ mMasInstanceId = in.readInt();
+ mL2capPsm = in.readInt();
+ mRfcommChannelNumber = in.readInt();
+ mProfileVersion = in.readInt();
+ mSupportedFeatures = in.readInt();
+ mSupportedMessageTypes = in.readInt();
+ mServiceName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ public int getMasInstanceId() {
+ return mMasInstanceId;
+ }
+
+ public int getL2capPsm() {
+ return mL2capPsm;
+ }
+
+ public int getRfcommCannelNumber() {
+ return mRfcommChannelNumber;
+ }
+
+ public int getProfileVersion() {
+ return mProfileVersion;
+ }
+
+ public int getSupportedFeatures() {
+ return mSupportedFeatures;
+ }
+
+ public int getSupportedMessageTypes() {
+ return mSupportedMessageTypes;
+ }
+
+ public boolean msgSupported(int msg) {
+ return (mSupportedMessageTypes & msg) != 0;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mMasInstanceId);
+ dest.writeInt(mL2capPsm);
+ dest.writeInt(mRfcommChannelNumber);
+ dest.writeInt(mProfileVersion);
+ dest.writeInt(mSupportedFeatures);
+ dest.writeInt(mSupportedMessageTypes);
+ dest.writeString(mServiceName);
+ }
+
+ @Override
+ public String toString() {
+ String ret = "Bluetooth MAS SDP Record:\n";
+
+ if (mMasInstanceId != -1) {
+ ret += "Mas Instance Id: " + mMasInstanceId + "\n";
+ }
+ if (mRfcommChannelNumber != -1) {
+ ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n";
+ }
+ if (mL2capPsm != -1) {
+ ret += "L2CAP PSM: " + mL2capPsm + "\n";
+ }
+ if (mServiceName != null) {
+ ret += "Service Name: " + mServiceName + "\n";
+ }
+ if (mProfileVersion != -1) {
+ ret += "Profile version: " + mProfileVersion + "\n";
+ }
+ if (mSupportedMessageTypes != -1) {
+ ret += "Supported msg types: " + mSupportedMessageTypes + "\n";
+ }
+ if (mSupportedFeatures != -1) {
+ ret += "Supported features: " + mSupportedFeatures + "\n";
+ }
+ return ret;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpMasRecord createFromParcel(Parcel in) {
+ return new SdpMasRecord(in);
+ }
+
+ public SdpRecord[] newArray(int size) {
+ return new SdpRecord[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/SdpMnsRecord.java b/framework/java/android/bluetooth/SdpMnsRecord.java
new file mode 100644
index 0000000000..a781d5df7d
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpMnsRecord.java
@@ -0,0 +1,114 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public class SdpMnsRecord implements Parcelable {
+ private final int mL2capPsm;
+ private final int mRfcommChannelNumber;
+ private final int mSupportedFeatures;
+ private final int mProfileVersion;
+ private final String mServiceName;
+
+ public SdpMnsRecord(int l2capPsm,
+ int rfcommChannelNumber,
+ int profileVersion,
+ int supportedFeatures,
+ String serviceName) {
+ mL2capPsm = l2capPsm;
+ mRfcommChannelNumber = rfcommChannelNumber;
+ mSupportedFeatures = supportedFeatures;
+ mServiceName = serviceName;
+ mProfileVersion = profileVersion;
+ }
+
+ public SdpMnsRecord(Parcel in) {
+ mRfcommChannelNumber = in.readInt();
+ mL2capPsm = in.readInt();
+ mServiceName = in.readString();
+ mSupportedFeatures = in.readInt();
+ mProfileVersion = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+
+ public int getL2capPsm() {
+ return mL2capPsm;
+ }
+
+ public int getRfcommChannelNumber() {
+ return mRfcommChannelNumber;
+ }
+
+ public int getSupportedFeatures() {
+ return mSupportedFeatures;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ public int getProfileVersion() {
+ return mProfileVersion;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRfcommChannelNumber);
+ dest.writeInt(mL2capPsm);
+ dest.writeString(mServiceName);
+ dest.writeInt(mSupportedFeatures);
+ dest.writeInt(mProfileVersion);
+ }
+
+ public String toString() {
+ String ret = "Bluetooth MNS SDP Record:\n";
+
+ if (mRfcommChannelNumber != -1) {
+ ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n";
+ }
+ if (mL2capPsm != -1) {
+ ret += "L2CAP PSM: " + mL2capPsm + "\n";
+ }
+ if (mServiceName != null) {
+ ret += "Service Name: " + mServiceName + "\n";
+ }
+ if (mSupportedFeatures != -1) {
+ ret += "Supported features: " + mSupportedFeatures + "\n";
+ }
+ if (mProfileVersion != -1) {
+ ret += "Profile_version: " + mProfileVersion + "\n";
+ }
+ return ret;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpMnsRecord createFromParcel(Parcel in) {
+ return new SdpMnsRecord(in);
+ }
+
+ public SdpMnsRecord[] newArray(int size) {
+ return new SdpMnsRecord[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/SdpOppOpsRecord.java b/framework/java/android/bluetooth/SdpOppOpsRecord.java
new file mode 100644
index 0000000000..e30745b898
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpOppOpsRecord.java
@@ -0,0 +1,121 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * Data representation of a Object Push Profile Server side SDP record.
+ */
+
+/** @hide */
+public class SdpOppOpsRecord implements Parcelable {
+
+ private final String mServiceName;
+ private final int mRfcommChannel;
+ private final int mL2capPsm;
+ private final int mProfileVersion;
+ private final byte[] mFormatsList;
+
+ public SdpOppOpsRecord(String serviceName, int rfcommChannel,
+ int l2capPsm, int version, byte[] formatsList) {
+ super();
+ mServiceName = serviceName;
+ mRfcommChannel = rfcommChannel;
+ mL2capPsm = l2capPsm;
+ mProfileVersion = version;
+ mFormatsList = formatsList;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ public int getRfcommChannel() {
+ return mRfcommChannel;
+ }
+
+ public int getL2capPsm() {
+ return mL2capPsm;
+ }
+
+ public int getProfileVersion() {
+ return mProfileVersion;
+ }
+
+ public byte[] getFormatsList() {
+ return mFormatsList;
+ }
+
+ @Override
+ public int describeContents() {
+ /* No special objects */
+ return 0;
+ }
+
+ public SdpOppOpsRecord(Parcel in) {
+ mRfcommChannel = in.readInt();
+ mL2capPsm = in.readInt();
+ mProfileVersion = in.readInt();
+ mServiceName = in.readString();
+ int arrayLength = in.readInt();
+ if (arrayLength > 0) {
+ byte[] bytes = new byte[arrayLength];
+ in.readByteArray(bytes);
+ mFormatsList = bytes;
+ } else {
+ mFormatsList = null;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRfcommChannel);
+ dest.writeInt(mL2capPsm);
+ dest.writeInt(mProfileVersion);
+ dest.writeString(mServiceName);
+ if (mFormatsList != null && mFormatsList.length > 0) {
+ dest.writeInt(mFormatsList.length);
+ dest.writeByteArray(mFormatsList);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Bluetooth OPP Server SDP Record:\n");
+ sb.append(" RFCOMM Chan Number: ").append(mRfcommChannel);
+ sb.append("\n L2CAP PSM: ").append(mL2capPsm);
+ sb.append("\n Profile version: ").append(mProfileVersion);
+ sb.append("\n Service Name: ").append(mServiceName);
+ sb.append("\n Formats List: ").append(Arrays.toString(mFormatsList));
+ return sb.toString();
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpOppOpsRecord createFromParcel(Parcel in) {
+ return new SdpOppOpsRecord(in);
+ }
+
+ public SdpOppOpsRecord[] newArray(int size) {
+ return new SdpOppOpsRecord[size];
+ }
+ };
+
+}
diff --git a/framework/java/android/bluetooth/SdpPseRecord.java b/framework/java/android/bluetooth/SdpPseRecord.java
new file mode 100644
index 0000000000..72249d0585
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpPseRecord.java
@@ -0,0 +1,129 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public class SdpPseRecord implements Parcelable {
+ private final int mL2capPsm;
+ private final int mRfcommChannelNumber;
+ private final int mProfileVersion;
+ private final int mSupportedFeatures;
+ private final int mSupportedRepositories;
+ private final String mServiceName;
+
+ public SdpPseRecord(int l2capPsm,
+ int rfcommChannelNumber,
+ int profileVersion,
+ int supportedFeatures,
+ int supportedRepositories,
+ String serviceName) {
+ mL2capPsm = l2capPsm;
+ mRfcommChannelNumber = rfcommChannelNumber;
+ mProfileVersion = profileVersion;
+ mSupportedFeatures = supportedFeatures;
+ mSupportedRepositories = supportedRepositories;
+ mServiceName = serviceName;
+ }
+
+ public SdpPseRecord(Parcel in) {
+ mRfcommChannelNumber = in.readInt();
+ mL2capPsm = in.readInt();
+ mProfileVersion = in.readInt();
+ mSupportedFeatures = in.readInt();
+ mSupportedRepositories = in.readInt();
+ mServiceName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ // TODO Auto-generated method stub
+ return 0;
+ }
+
+ public int getL2capPsm() {
+ return mL2capPsm;
+ }
+
+ public int getRfcommChannelNumber() {
+ return mRfcommChannelNumber;
+ }
+
+ public int getSupportedFeatures() {
+ return mSupportedFeatures;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ public int getProfileVersion() {
+ return mProfileVersion;
+ }
+
+ public int getSupportedRepositories() {
+ return mSupportedRepositories;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRfcommChannelNumber);
+ dest.writeInt(mL2capPsm);
+ dest.writeInt(mProfileVersion);
+ dest.writeInt(mSupportedFeatures);
+ dest.writeInt(mSupportedRepositories);
+ dest.writeString(mServiceName);
+
+ }
+
+ @Override
+ public String toString() {
+ String ret = "Bluetooth MNS SDP Record:\n";
+
+ if (mRfcommChannelNumber != -1) {
+ ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n";
+ }
+ if (mL2capPsm != -1) {
+ ret += "L2CAP PSM: " + mL2capPsm + "\n";
+ }
+ if (mProfileVersion != -1) {
+ ret += "profile version: " + mProfileVersion + "\n";
+ }
+ if (mServiceName != null) {
+ ret += "Service Name: " + mServiceName + "\n";
+ }
+ if (mSupportedFeatures != -1) {
+ ret += "Supported features: " + mSupportedFeatures + "\n";
+ }
+ if (mSupportedRepositories != -1) {
+ ret += "Supported repositories: " + mSupportedRepositories + "\n";
+ }
+
+ return ret;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpPseRecord createFromParcel(Parcel in) {
+ return new SdpPseRecord(in);
+ }
+
+ public SdpPseRecord[] newArray(int size) {
+ return new SdpPseRecord[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/SdpRecord.java b/framework/java/android/bluetooth/SdpRecord.java
new file mode 100644
index 0000000000..730862ec6f
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpRecord.java
@@ -0,0 +1,77 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/** @hide */
+public class SdpRecord implements Parcelable {
+
+ private final byte[] mRawData;
+ private final int mRawSize;
+
+ @Override
+ public String toString() {
+ return "BluetoothSdpRecord [rawData=" + Arrays.toString(mRawData)
+ + ", rawSize=" + mRawSize + "]";
+ }
+
+ public SdpRecord(int sizeRecord, byte[] record) {
+ mRawData = record;
+ mRawSize = sizeRecord;
+ }
+
+ public SdpRecord(Parcel in) {
+ mRawSize = in.readInt();
+ mRawData = new byte[mRawSize];
+ in.readByteArray(mRawData);
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRawSize);
+ dest.writeByteArray(mRawData);
+
+
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpRecord createFromParcel(Parcel in) {
+ return new SdpRecord(in);
+ }
+
+ public SdpRecord[] newArray(int size) {
+ return new SdpRecord[size];
+ }
+ };
+
+ public byte[] getRawData() {
+ return mRawData;
+ }
+
+ public int getRawSize() {
+ return mRawSize;
+ }
+}
diff --git a/framework/java/android/bluetooth/SdpSapsRecord.java b/framework/java/android/bluetooth/SdpSapsRecord.java
new file mode 100644
index 0000000000..a1e2f7b51f
--- /dev/null
+++ b/framework/java/android/bluetooth/SdpSapsRecord.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 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 android.os.Parcel;
+import android.os.Parcelable;
+
+/** @hide */
+public class SdpSapsRecord implements Parcelable {
+ private final int mRfcommChannelNumber;
+ private final int mProfileVersion;
+ private final String mServiceName;
+
+ public SdpSapsRecord(int rfcommChannelNumber, int profileVersion, String serviceName) {
+ mRfcommChannelNumber = rfcommChannelNumber;
+ mProfileVersion = profileVersion;
+ mServiceName = serviceName;
+ }
+
+ public SdpSapsRecord(Parcel in) {
+ mRfcommChannelNumber = in.readInt();
+ mProfileVersion = in.readInt();
+ mServiceName = in.readString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public int getRfcommCannelNumber() {
+ return mRfcommChannelNumber;
+ }
+
+ public int getProfileVersion() {
+ return mProfileVersion;
+ }
+
+ public String getServiceName() {
+ return mServiceName;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRfcommChannelNumber);
+ dest.writeInt(mProfileVersion);
+ dest.writeString(mServiceName);
+
+ }
+
+ @Override
+ public String toString() {
+ String ret = "Bluetooth MAS SDP Record:\n";
+
+ if (mRfcommChannelNumber != -1) {
+ ret += "RFCOMM Chan Number: " + mRfcommChannelNumber + "\n";
+ }
+ if (mServiceName != null) {
+ ret += "Service Name: " + mServiceName + "\n";
+ }
+ if (mProfileVersion != -1) {
+ ret += "Profile version: " + mProfileVersion + "\n";
+ }
+ return ret;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ public SdpSapsRecord createFromParcel(Parcel in) {
+ return new SdpSapsRecord(in);
+ }
+
+ public SdpRecord[] newArray(int size) {
+ return new SdpRecord[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/UidTraffic.java b/framework/java/android/bluetooth/UidTraffic.java
new file mode 100644
index 0000000000..9982fa6121
--- /dev/null
+++ b/framework/java/android/bluetooth/UidTraffic.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 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 android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Record of data traffic (in bytes) by an application identified by its UID.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS)
+public final class UidTraffic implements Cloneable, Parcelable {
+ private final int mAppUid;
+ private long mRxBytes;
+ private long mTxBytes;
+
+ /** @hide */
+ public UidTraffic(int appUid, long rx, long tx) {
+ mAppUid = appUid;
+ mRxBytes = rx;
+ mTxBytes = tx;
+ }
+
+ /** @hide */
+ private UidTraffic(Parcel in) {
+ mAppUid = in.readInt();
+ mRxBytes = in.readLong();
+ mTxBytes = in.readLong();
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAppUid);
+ dest.writeLong(mRxBytes);
+ dest.writeLong(mTxBytes);
+ }
+
+ /** @hide */
+ public void setRxBytes(long bytes) {
+ mRxBytes = bytes;
+ }
+
+ /** @hide */
+ public void setTxBytes(long bytes) {
+ mTxBytes = bytes;
+ }
+
+ /** @hide */
+ public void addRxBytes(long bytes) {
+ mRxBytes += bytes;
+ }
+
+ /** @hide */
+ public void addTxBytes(long bytes) {
+ mTxBytes += bytes;
+ }
+
+ /**
+ * @return corresponding app Uid
+ */
+ public int getUid() {
+ return mAppUid;
+ }
+
+ /**
+ * @return rx bytes count
+ */
+ public long getRxBytes() {
+ return mRxBytes;
+ }
+
+ /**
+ * @return tx bytes count
+ */
+ public long getTxBytes() {
+ return mTxBytes;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public UidTraffic clone() {
+ return new UidTraffic(mAppUid, mRxBytes, mTxBytes);
+ }
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return "UidTraffic{mAppUid=" + mAppUid + ", mRxBytes=" + mRxBytes + ", mTxBytes="
+ + mTxBytes + '}';
+ }
+
+ public static final @android.annotation.NonNull Creator<UidTraffic> CREATOR = new Creator<UidTraffic>() {
+ @Override
+ public UidTraffic createFromParcel(Parcel source) {
+ return new UidTraffic(source);
+ }
+
+ @Override
+ public UidTraffic[] newArray(int size) {
+ return new UidTraffic[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresBluetoothAdvertisePermission.java b/framework/java/android/bluetooth/annotations/RequiresBluetoothAdvertisePermission.java
new file mode 100644
index 0000000000..c508c2c9ca
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresBluetoothAdvertisePermission.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc For apps targeting {@link Build.VERSION_CODES#S} or or higher,
+ * this requires the {@link Manifest.permission#BLUETOOTH_ADVERTISE}
+ * permission which can be gained with
+ * {@link android.app.Activity#requestPermissions(String[], int)}.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresBluetoothAdvertisePermission {
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresBluetoothConnectPermission.java b/framework/java/android/bluetooth/annotations/RequiresBluetoothConnectPermission.java
new file mode 100644
index 0000000000..e159eaafe2
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresBluetoothConnectPermission.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc For apps targeting {@link Build.VERSION_CODES#S} or or higher,
+ * this requires the {@link Manifest.permission#BLUETOOTH_CONNECT}
+ * permission which can be gained with
+ * {@link android.app.Activity#requestPermissions(String[], int)}.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresBluetoothConnectPermission {
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresBluetoothLocationPermission.java b/framework/java/android/bluetooth/annotations/RequiresBluetoothLocationPermission.java
new file mode 100644
index 0000000000..2bb3204139
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresBluetoothLocationPermission.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc In addition, this requires either the
+ * {@link Manifest.permission#ACCESS_FINE_LOCATION}
+ * permission or a strong assertion that you will never derive the
+ * physical location of the device. You can make this assertion by
+ * declaring {@code usesPermissionFlags="neverForLocation"} on the
+ * relevant {@code <uses-permission>} manifest tag, but it may
+ * restrict the types of Bluetooth devices you can interact with.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresBluetoothLocationPermission {
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresBluetoothScanPermission.java b/framework/java/android/bluetooth/annotations/RequiresBluetoothScanPermission.java
new file mode 100644
index 0000000000..800ff39933
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresBluetoothScanPermission.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc For apps targeting {@link Build.VERSION_CODES#S} or or higher,
+ * this requires the {@link Manifest.permission#BLUETOOTH_SCAN}
+ * permission which can be gained with
+ * {@link android.app.Activity#requestPermissions(String[], int)}.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresBluetoothScanPermission {
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothAdminPermission.java b/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothAdminPermission.java
new file mode 100644
index 0000000000..9adf695cde
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothAdminPermission.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc For apps targeting {@link Build.VERSION_CODES#R} or lower, this
+ * requires the {@link Manifest.permission#BLUETOOTH_ADMIN}
+ * permission which can be gained with a simple
+ * {@code <uses-permission>} manifest tag.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresLegacyBluetoothAdminPermission {
+}
diff --git a/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothPermission.java b/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothPermission.java
new file mode 100644
index 0000000000..79621c366f
--- /dev/null
+++ b/framework/java/android/bluetooth/annotations/RequiresLegacyBluetoothPermission.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.annotations;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.Manifest;
+import android.os.Build;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * @memberDoc For apps targeting {@link Build.VERSION_CODES#R} or lower, this
+ * requires the {@link Manifest.permission#BLUETOOTH} permission
+ * which can be gained with a simple {@code <uses-permission>}
+ * manifest tag.
+ * @hide
+ */
+@Retention(SOURCE)
+@Target({METHOD, FIELD})
+public @interface RequiresLegacyBluetoothPermission {
+}
diff --git a/framework/java/android/bluetooth/le/AdvertiseCallback.java b/framework/java/android/bluetooth/le/AdvertiseCallback.java
new file mode 100644
index 0000000000..4fa8c4f2f5
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertiseCallback.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+/**
+ * Bluetooth LE advertising callbacks, used to deliver advertising operation status.
+ */
+public abstract class AdvertiseCallback {
+
+ /**
+ * The requested operation was successful.
+ *
+ * @hide
+ */
+ public static final int ADVERTISE_SUCCESS = 0;
+
+ /**
+ * Failed to start advertising as the advertise data to be broadcasted is larger than 31 bytes.
+ */
+ public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 1;
+
+ /**
+ * Failed to start advertising because no advertising instance is available.
+ */
+ public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 2;
+
+ /**
+ * Failed to start advertising as the advertising is already started.
+ */
+ public static final int ADVERTISE_FAILED_ALREADY_STARTED = 3;
+
+ /**
+ * Operation failed due to an internal error.
+ */
+ public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 4;
+
+ /**
+ * This feature is not supported on this platform.
+ */
+ public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 5;
+
+ /**
+ * Callback triggered in response to {@link BluetoothLeAdvertiser#startAdvertising} indicating
+ * that the advertising has been started successfully.
+ *
+ * @param settingsInEffect The actual settings used for advertising, which may be different from
+ * what has been requested.
+ */
+ public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+ }
+
+ /**
+ * Callback when advertising could not be started.
+ *
+ * @param errorCode Error code (see ADVERTISE_FAILED_* constants) for advertising start
+ * failures.
+ */
+ public void onStartFailure(int errorCode) {
+ }
+}
diff --git a/framework/java/android/bluetooth/le/AdvertiseData.java b/framework/java/android/bluetooth/le/AdvertiseData.java
new file mode 100644
index 0000000000..fdf62ec3a6
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertiseData.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Advertise data packet container for Bluetooth LE advertising. This represents the data to be
+ * advertised as well as the scan response data for active scans.
+ * <p>
+ * Use {@link AdvertiseData.Builder} to create an instance of {@link AdvertiseData} to be
+ * advertised.
+ *
+ * @see BluetoothLeAdvertiser
+ * @see ScanRecord
+ */
+public final class AdvertiseData implements Parcelable {
+
+ @Nullable
+ private final List<ParcelUuid> mServiceUuids;
+
+ @NonNull
+ private final List<ParcelUuid> mServiceSolicitationUuids;
+
+ @Nullable
+ private final List<TransportDiscoveryData> mTransportDiscoveryData;
+
+ private final SparseArray<byte[]> mManufacturerSpecificData;
+ private final Map<ParcelUuid, byte[]> mServiceData;
+ private final boolean mIncludeTxPowerLevel;
+ private final boolean mIncludeDeviceName;
+
+ private AdvertiseData(List<ParcelUuid> serviceUuids,
+ List<ParcelUuid> serviceSolicitationUuids,
+ List<TransportDiscoveryData> transportDiscoveryData,
+ SparseArray<byte[]> manufacturerData,
+ Map<ParcelUuid, byte[]> serviceData,
+ boolean includeTxPowerLevel,
+ boolean includeDeviceName) {
+ mServiceUuids = serviceUuids;
+ mServiceSolicitationUuids = serviceSolicitationUuids;
+ mTransportDiscoveryData = transportDiscoveryData;
+ mManufacturerSpecificData = manufacturerData;
+ mServiceData = serviceData;
+ mIncludeTxPowerLevel = includeTxPowerLevel;
+ mIncludeDeviceName = includeDeviceName;
+ }
+
+ /**
+ * Returns a list of service UUIDs within the advertisement that are used to identify the
+ * Bluetooth GATT services.
+ */
+ public List<ParcelUuid> getServiceUuids() {
+ return mServiceUuids;
+ }
+
+ /**
+ * Returns a list of service solicitation UUIDs within the advertisement that we invite to connect.
+ */
+ @NonNull
+ public List<ParcelUuid> getServiceSolicitationUuids() {
+ return mServiceSolicitationUuids;
+ }
+
+ /**
+ * Returns a list of {@link TransportDiscoveryData} within the advertisement.
+ */
+ @NonNull
+ public List<TransportDiscoveryData> getTransportDiscoveryData() {
+ if (mTransportDiscoveryData == null) {
+ return Collections.emptyList();
+ }
+ return mTransportDiscoveryData;
+ }
+
+ /**
+ * Returns an array of manufacturer Id and the corresponding manufacturer specific data. The
+ * manufacturer id is a non-negative number assigned by Bluetooth SIG.
+ */
+ public SparseArray<byte[]> getManufacturerSpecificData() {
+ return mManufacturerSpecificData;
+ }
+
+ /**
+ * Returns a map of 16-bit UUID and its corresponding service data.
+ */
+ public Map<ParcelUuid, byte[]> getServiceData() {
+ return mServiceData;
+ }
+
+ /**
+ * Whether the transmission power level will be included in the advertisement packet.
+ */
+ public boolean getIncludeTxPowerLevel() {
+ return mIncludeTxPowerLevel;
+ }
+
+ /**
+ * Whether the device name will be included in the advertisement packet.
+ */
+ public boolean getIncludeDeviceName() {
+ return mIncludeDeviceName;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceUuids, mServiceSolicitationUuids, mTransportDiscoveryData,
+ mManufacturerSpecificData, mServiceData, mIncludeDeviceName, mIncludeTxPowerLevel);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ AdvertiseData other = (AdvertiseData) obj;
+ return Objects.equals(mServiceUuids, other.mServiceUuids)
+ && Objects.equals(mServiceSolicitationUuids, other.mServiceSolicitationUuids)
+ && Objects.equals(mTransportDiscoveryData, other.mTransportDiscoveryData)
+ && BluetoothLeUtils.equals(mManufacturerSpecificData,
+ other.mManufacturerSpecificData)
+ && BluetoothLeUtils.equals(mServiceData, other.mServiceData)
+ && mIncludeDeviceName == other.mIncludeDeviceName
+ && mIncludeTxPowerLevel == other.mIncludeTxPowerLevel;
+ }
+
+ @Override
+ public String toString() {
+ return "AdvertiseData [mServiceUuids=" + mServiceUuids + ", mServiceSolicitationUuids="
+ + mServiceSolicitationUuids + ", mTransportDiscoveryData="
+ + mTransportDiscoveryData + ", mManufacturerSpecificData="
+ + BluetoothLeUtils.toString(mManufacturerSpecificData) + ", mServiceData="
+ + BluetoothLeUtils.toString(mServiceData)
+ + ", mIncludeTxPowerLevel=" + mIncludeTxPowerLevel + ", mIncludeDeviceName="
+ + mIncludeDeviceName + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedArray(mServiceUuids.toArray(new ParcelUuid[mServiceUuids.size()]), flags);
+ dest.writeTypedArray(mServiceSolicitationUuids.toArray(
+ new ParcelUuid[mServiceSolicitationUuids.size()]), flags);
+
+ dest.writeTypedList(mTransportDiscoveryData);
+
+ // mManufacturerSpecificData could not be null.
+ dest.writeInt(mManufacturerSpecificData.size());
+ for (int i = 0; i < mManufacturerSpecificData.size(); ++i) {
+ dest.writeInt(mManufacturerSpecificData.keyAt(i));
+ dest.writeByteArray(mManufacturerSpecificData.valueAt(i));
+ }
+ dest.writeInt(mServiceData.size());
+ for (ParcelUuid uuid : mServiceData.keySet()) {
+ dest.writeTypedObject(uuid, flags);
+ dest.writeByteArray(mServiceData.get(uuid));
+ }
+ dest.writeByte((byte) (getIncludeTxPowerLevel() ? 1 : 0));
+ dest.writeByte((byte) (getIncludeDeviceName() ? 1 : 0));
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AdvertiseData> CREATOR =
+ new Creator<AdvertiseData>() {
+ @Override
+ public AdvertiseData[] newArray(int size) {
+ return new AdvertiseData[size];
+ }
+
+ @Override
+ public AdvertiseData createFromParcel(Parcel in) {
+ Builder builder = new Builder();
+ ArrayList<ParcelUuid> uuids = in.createTypedArrayList(ParcelUuid.CREATOR);
+ for (ParcelUuid uuid : uuids) {
+ builder.addServiceUuid(uuid);
+ }
+
+ ArrayList<ParcelUuid> solicitationUuids = in.createTypedArrayList(ParcelUuid.CREATOR);
+ for (ParcelUuid uuid : solicitationUuids) {
+ builder.addServiceSolicitationUuid(uuid);
+ }
+
+ List<TransportDiscoveryData> transportDiscoveryData =
+ in.createTypedArrayList(TransportDiscoveryData.CREATOR);
+ for (TransportDiscoveryData tdd : transportDiscoveryData) {
+ builder.addTransportDiscoveryData(tdd);
+ }
+
+ int manufacturerSize = in.readInt();
+ for (int i = 0; i < manufacturerSize; ++i) {
+ int manufacturerId = in.readInt();
+ byte[] manufacturerData = in.createByteArray();
+ builder.addManufacturerData(manufacturerId, manufacturerData);
+ }
+ int serviceDataSize = in.readInt();
+ for (int i = 0; i < serviceDataSize; ++i) {
+ ParcelUuid serviceDataUuid = in.readTypedObject(ParcelUuid.CREATOR);
+ byte[] serviceData = in.createByteArray();
+ builder.addServiceData(serviceDataUuid, serviceData);
+ }
+ builder.setIncludeTxPowerLevel(in.readByte() == 1);
+ builder.setIncludeDeviceName(in.readByte() == 1);
+ return builder.build();
+ }
+ };
+
+ /**
+ * Builder for {@link AdvertiseData}.
+ */
+ public static final class Builder {
+ @Nullable
+ private List<ParcelUuid> mServiceUuids = new ArrayList<ParcelUuid>();
+ @NonNull
+ private List<ParcelUuid> mServiceSolicitationUuids = new ArrayList<ParcelUuid>();
+ @Nullable
+ private List<TransportDiscoveryData> mTransportDiscoveryData =
+ new ArrayList<TransportDiscoveryData>();
+ private SparseArray<byte[]> mManufacturerSpecificData = new SparseArray<byte[]>();
+ private Map<ParcelUuid, byte[]> mServiceData = new ArrayMap<ParcelUuid, byte[]>();
+ private boolean mIncludeTxPowerLevel;
+ private boolean mIncludeDeviceName;
+
+ /**
+ * Add a service UUID to advertise data.
+ *
+ * @param serviceUuid A service UUID to be advertised.
+ * @throws IllegalArgumentException If the {@code serviceUuid} is null.
+ */
+ public Builder addServiceUuid(ParcelUuid serviceUuid) {
+ if (serviceUuid == null) {
+ throw new IllegalArgumentException("serviceUuid is null");
+ }
+ mServiceUuids.add(serviceUuid);
+ return this;
+ }
+
+ /**
+ * Add a service solicitation UUID to advertise data.
+ *
+ * @param serviceSolicitationUuid A service solicitation UUID to be advertised.
+ * @throws IllegalArgumentException If the {@code serviceSolicitationUuid} is null.
+ */
+ @NonNull
+ public Builder addServiceSolicitationUuid(@NonNull ParcelUuid serviceSolicitationUuid) {
+ if (serviceSolicitationUuid == null) {
+ throw new IllegalArgumentException("serviceSolicitationUuid is null");
+ }
+ mServiceSolicitationUuids.add(serviceSolicitationUuid);
+ return this;
+ }
+
+ /**
+ * Add service data to advertise data.
+ *
+ * @param serviceDataUuid 16-bit UUID of the service the data is associated with
+ * @param serviceData Service data
+ * @throws IllegalArgumentException If the {@code serviceDataUuid} or {@code serviceData} is
+ * empty.
+ */
+ public Builder addServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) {
+ if (serviceDataUuid == null || serviceData == null) {
+ throw new IllegalArgumentException(
+ "serviceDataUuid or serviceDataUuid is null");
+ }
+ mServiceData.put(serviceDataUuid, serviceData);
+ return this;
+ }
+
+ /**
+ * Add Transport Discovery Data to advertise data.
+ *
+ * @param transportDiscoveryData Transport Discovery Data, consisting of one or more
+ * Transport Blocks. Transport Discovery Data AD Type Code is already included.
+ * @throws IllegalArgumentException If the {@code transportDiscoveryData} is empty
+ */
+ @NonNull
+ public Builder addTransportDiscoveryData(
+ @NonNull TransportDiscoveryData transportDiscoveryData) {
+ if (transportDiscoveryData == null) {
+ throw new IllegalArgumentException("transportDiscoveryData is null");
+ }
+ mTransportDiscoveryData.add(transportDiscoveryData);
+ return this;
+ }
+
+ /**
+ * Add manufacturer specific data.
+ * <p>
+ * Please refer to the Bluetooth Assigned Numbers document provided by the <a
+ * href="https://www.bluetooth.org">Bluetooth SIG</a> for a list of existing company
+ * identifiers.
+ *
+ * @param manufacturerId Manufacturer ID assigned by Bluetooth SIG.
+ * @param manufacturerSpecificData Manufacturer specific data
+ * @throws IllegalArgumentException If the {@code manufacturerId} is negative or {@code
+ * manufacturerSpecificData} is null.
+ */
+ public Builder addManufacturerData(int manufacturerId, byte[] manufacturerSpecificData) {
+ if (manufacturerId < 0) {
+ throw new IllegalArgumentException(
+ "invalid manufacturerId - " + manufacturerId);
+ }
+ if (manufacturerSpecificData == null) {
+ throw new IllegalArgumentException("manufacturerSpecificData is null");
+ }
+ mManufacturerSpecificData.put(manufacturerId, manufacturerSpecificData);
+ return this;
+ }
+
+ /**
+ * Whether the transmission power level should be included in the advertise packet. Tx power
+ * level field takes 3 bytes in advertise packet.
+ */
+ public Builder setIncludeTxPowerLevel(boolean includeTxPowerLevel) {
+ mIncludeTxPowerLevel = includeTxPowerLevel;
+ return this;
+ }
+
+ /**
+ * Set whether the device name should be included in advertise packet.
+ */
+ public Builder setIncludeDeviceName(boolean includeDeviceName) {
+ mIncludeDeviceName = includeDeviceName;
+ return this;
+ }
+
+ /**
+ * Build the {@link AdvertiseData}.
+ */
+ public AdvertiseData build() {
+ return new AdvertiseData(mServiceUuids, mServiceSolicitationUuids,
+ mTransportDiscoveryData, mManufacturerSpecificData, mServiceData,
+ mIncludeTxPowerLevel, mIncludeDeviceName);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/AdvertiseSettings.java b/framework/java/android/bluetooth/le/AdvertiseSettings.java
new file mode 100644
index 0000000000..c52a6ee339
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertiseSettings.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.bluetooth.le.AdvertisingSetParameters.AddressTypeStatus;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The {@link AdvertiseSettings} provide a way to adjust advertising preferences for each
+ * Bluetooth LE advertisement instance. Use {@link AdvertiseSettings.Builder} to create an
+ * instance of this class.
+ */
+public final class AdvertiseSettings implements Parcelable {
+ /**
+ * Perform Bluetooth LE advertising in low power mode. This is the default and preferred
+ * advertising mode as it consumes the least power.
+ */
+ public static final int ADVERTISE_MODE_LOW_POWER = 0;
+
+ /**
+ * Perform Bluetooth LE advertising in balanced power mode. This is balanced between advertising
+ * frequency and power consumption.
+ */
+ public static final int ADVERTISE_MODE_BALANCED = 1;
+
+ /**
+ * Perform Bluetooth LE advertising in low latency, high power mode. This has the highest power
+ * consumption and should not be used for continuous background advertising.
+ */
+ public static final int ADVERTISE_MODE_LOW_LATENCY = 2;
+
+ /**
+ * Advertise using the lowest transmission (TX) power level. Low transmission power can be used
+ * to restrict the visibility range of advertising packets.
+ */
+ public static final int ADVERTISE_TX_POWER_ULTRA_LOW = 0;
+
+ /**
+ * Advertise using low TX power level.
+ */
+ public static final int ADVERTISE_TX_POWER_LOW = 1;
+
+ /**
+ * Advertise using medium TX power level.
+ */
+ public static final int ADVERTISE_TX_POWER_MEDIUM = 2;
+
+ /**
+ * Advertise using high TX power level. This corresponds to largest visibility range of the
+ * advertising packet.
+ */
+ public static final int ADVERTISE_TX_POWER_HIGH = 3;
+
+ /**
+ * The maximum limited advertisement duration as specified by the Bluetooth SIG
+ */
+ private static final int LIMITED_ADVERTISING_MAX_MILLIS = 180 * 1000;
+
+
+ private final int mAdvertiseMode;
+ private final int mAdvertiseTxPowerLevel;
+ private final int mAdvertiseTimeoutMillis;
+ private final boolean mAdvertiseConnectable;
+ private final int mOwnAddressType;
+
+ private AdvertiseSettings(int advertiseMode, int advertiseTxPowerLevel,
+ boolean advertiseConnectable, int advertiseTimeout,
+ @AddressTypeStatus int ownAddressType) {
+ mAdvertiseMode = advertiseMode;
+ mAdvertiseTxPowerLevel = advertiseTxPowerLevel;
+ mAdvertiseConnectable = advertiseConnectable;
+ mAdvertiseTimeoutMillis = advertiseTimeout;
+ mOwnAddressType = ownAddressType;
+ }
+
+ private AdvertiseSettings(Parcel in) {
+ mAdvertiseMode = in.readInt();
+ mAdvertiseTxPowerLevel = in.readInt();
+ mAdvertiseConnectable = in.readInt() != 0;
+ mAdvertiseTimeoutMillis = in.readInt();
+ mOwnAddressType = in.readInt();
+ }
+
+ /**
+ * Returns the advertise mode.
+ */
+ public int getMode() {
+ return mAdvertiseMode;
+ }
+
+ /**
+ * Returns the TX power level for advertising.
+ */
+ public int getTxPowerLevel() {
+ return mAdvertiseTxPowerLevel;
+ }
+
+ /**
+ * Returns whether the advertisement will indicate connectable.
+ */
+ public boolean isConnectable() {
+ return mAdvertiseConnectable;
+ }
+
+ /**
+ * Returns the advertising time limit in milliseconds.
+ */
+ public int getTimeout() {
+ return mAdvertiseTimeoutMillis;
+ }
+
+ /**
+ * @return the own address type for advertising
+ *
+ * @hide
+ */
+ @SystemApi
+ public @AddressTypeStatus int getOwnAddressType() {
+ return mOwnAddressType;
+ }
+
+ @Override
+ public String toString() {
+ return "Settings [mAdvertiseMode=" + mAdvertiseMode
+ + ", mAdvertiseTxPowerLevel=" + mAdvertiseTxPowerLevel
+ + ", mAdvertiseConnectable=" + mAdvertiseConnectable
+ + ", mAdvertiseTimeoutMillis=" + mAdvertiseTimeoutMillis
+ + ", mOwnAddressType=" + mOwnAddressType + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mAdvertiseMode);
+ dest.writeInt(mAdvertiseTxPowerLevel);
+ dest.writeInt(mAdvertiseConnectable ? 1 : 0);
+ dest.writeInt(mAdvertiseTimeoutMillis);
+ dest.writeInt(mOwnAddressType);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AdvertiseSettings> CREATOR =
+ new Creator<AdvertiseSettings>() {
+ @Override
+ public AdvertiseSettings[] newArray(int size) {
+ return new AdvertiseSettings[size];
+ }
+
+ @Override
+ public AdvertiseSettings createFromParcel(Parcel in) {
+ return new AdvertiseSettings(in);
+ }
+ };
+
+ /**
+ * Builder class for {@link AdvertiseSettings}.
+ */
+ public static final class Builder {
+ private int mMode = ADVERTISE_MODE_LOW_POWER;
+ private int mTxPowerLevel = ADVERTISE_TX_POWER_MEDIUM;
+ private int mTimeoutMillis = 0;
+ private boolean mConnectable = true;
+ private int mOwnAddressType = AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT;
+
+ /**
+ * Set advertise mode to control the advertising power and latency.
+ *
+ * @param advertiseMode Bluetooth LE Advertising mode, can only be one of {@link
+ * AdvertiseSettings#ADVERTISE_MODE_LOW_POWER},
+ * {@link AdvertiseSettings#ADVERTISE_MODE_BALANCED},
+ * or {@link AdvertiseSettings#ADVERTISE_MODE_LOW_LATENCY}.
+ * @throws IllegalArgumentException If the advertiseMode is invalid.
+ */
+ public Builder setAdvertiseMode(int advertiseMode) {
+ if (advertiseMode < ADVERTISE_MODE_LOW_POWER
+ || advertiseMode > ADVERTISE_MODE_LOW_LATENCY) {
+ throw new IllegalArgumentException("unknown mode " + advertiseMode);
+ }
+ mMode = advertiseMode;
+ return this;
+ }
+
+ /**
+ * Set advertise TX power level to control the transmission power level for the advertising.
+ *
+ * @param txPowerLevel Transmission power of Bluetooth LE Advertising, can only be one of
+ * {@link AdvertiseSettings#ADVERTISE_TX_POWER_ULTRA_LOW}, {@link
+ * AdvertiseSettings#ADVERTISE_TX_POWER_LOW},
+ * {@link AdvertiseSettings#ADVERTISE_TX_POWER_MEDIUM}
+ * or {@link AdvertiseSettings#ADVERTISE_TX_POWER_HIGH}.
+ * @throws IllegalArgumentException If the {@code txPowerLevel} is invalid.
+ */
+ public Builder setTxPowerLevel(int txPowerLevel) {
+ if (txPowerLevel < ADVERTISE_TX_POWER_ULTRA_LOW
+ || txPowerLevel > ADVERTISE_TX_POWER_HIGH) {
+ throw new IllegalArgumentException("unknown tx power level " + txPowerLevel);
+ }
+ mTxPowerLevel = txPowerLevel;
+ return this;
+ }
+
+ /**
+ * Set whether the advertisement type should be connectable or non-connectable.
+ *
+ * @param connectable Controls whether the advertisment type will be connectable (true) or
+ * non-connectable (false).
+ */
+ public Builder setConnectable(boolean connectable) {
+ mConnectable = connectable;
+ return this;
+ }
+
+ /**
+ * Limit advertising to a given amount of time.
+ *
+ * @param timeoutMillis Advertising time limit. May not exceed 180000 milliseconds. A value
+ * of 0 will disable the time limit.
+ * @throws IllegalArgumentException If the provided timeout is over 180000 ms.
+ */
+ public Builder setTimeout(int timeoutMillis) {
+ if (timeoutMillis < 0 || timeoutMillis > LIMITED_ADVERTISING_MAX_MILLIS) {
+ throw new IllegalArgumentException("timeoutMillis invalid (must be 0-"
+ + LIMITED_ADVERTISING_MAX_MILLIS + " milliseconds)");
+ }
+ mTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Set own address type for advertising to control public or privacy mode. If used to set
+ * address type anything other than {@link AdvertisingSetParameters#ADDRESS_TYPE_DEFAULT},
+ * then it will require BLUETOOTH_PRIVILEGED permission and will be checked at the
+ * time of starting advertising.
+ *
+ * @throws IllegalArgumentException If the {@code ownAddressType} is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull Builder setOwnAddressType(@AddressTypeStatus int ownAddressType) {
+ if (ownAddressType < AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT
+ || ownAddressType > AdvertisingSetParameters.ADDRESS_TYPE_RANDOM) {
+ throw new IllegalArgumentException("unknown address type " + ownAddressType);
+ }
+ mOwnAddressType = ownAddressType;
+ return this;
+ }
+
+ /**
+ * Build the {@link AdvertiseSettings} object.
+ */
+ public AdvertiseSettings build() {
+ return new AdvertiseSettings(mMode, mTxPowerLevel, mConnectable, mTimeoutMillis,
+ mOwnAddressType);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/AdvertisingSet.java b/framework/java/android/bluetooth/le/AdvertisingSet.java
new file mode 100644
index 0000000000..bbdb6953af
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertisingSet.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.annotations.RequiresBluetoothAdvertisePermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.content.AttributionSource;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * This class provides a way to control single Bluetooth LE advertising instance.
+ * <p>
+ * To get an instance of {@link AdvertisingSet}, call the
+ * {@link BluetoothLeAdvertiser#startAdvertisingSet} method.
+ *
+ * @see AdvertiseData
+ */
+public final class AdvertisingSet {
+ private static final String TAG = "AdvertisingSet";
+
+ private final IBluetoothGatt mGatt;
+ private int mAdvertiserId;
+ private AttributionSource mAttributionSource;
+
+ /* package */ AdvertisingSet(int advertiserId, IBluetoothManager bluetoothManager,
+ AttributionSource attributionSource) {
+ mAdvertiserId = advertiserId;
+ mAttributionSource = attributionSource;
+ try {
+ mGatt = bluetoothManager.getBluetoothGatt();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get Bluetooth gatt - ", e);
+ throw new IllegalStateException("Failed to get Bluetooth");
+ }
+ }
+
+ /* package */ void setAdvertiserId(int advertiserId) {
+ mAdvertiserId = advertiserId;
+ }
+
+ /**
+ * Enables Advertising. This method returns immediately, the operation status is
+ * delivered through {@code callback.onAdvertisingEnabled()}.
+ *
+ * @param enable whether the advertising should be enabled (true), or disabled (false)
+ * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535
+ * (655,350 ms)
+ * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the
+ * controller shall attempt to send prior to terminating the extended advertising, even if the
+ * duration has not expired. Valid range is from 1 to 255.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void enableAdvertising(boolean enable, int duration,
+ int maxExtendedAdvertisingEvents) {
+ try {
+ mGatt.enableAdvertisingSet(mAdvertiserId, enable, duration,
+ maxExtendedAdvertisingEvents, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Set/update data being Advertised. Make sure that data doesn't exceed the size limit for
+ * specified AdvertisingSetParameters. This method returns immediately, the operation status is
+ * delivered through {@code callback.onAdvertisingDataSet()}.
+ * <p>
+ * Advertising data must be empty if non-legacy scannable advertising is used.
+ *
+ * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable,
+ * three bytes will be added for flags. If the update takes place when the advertising set is
+ * enabled, the data can be maximum 251 bytes long.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setAdvertisingData(AdvertiseData advertiseData) {
+ try {
+ mGatt.setAdvertisingData(mAdvertiserId, advertiseData, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Set/update scan response data. Make sure that data doesn't exceed the size limit for
+ * specified AdvertisingSetParameters. This method returns immediately, the operation status
+ * is delivered through {@code callback.onScanResponseDataSet()}.
+ *
+ * @param scanResponse Scan response associated with the advertisement data. Size must not
+ * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the update takes place
+ * when the advertising set is enabled, the data can be maximum 251 bytes long.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setScanResponseData(AdvertiseData scanResponse) {
+ try {
+ mGatt.setScanResponseData(mAdvertiserId, scanResponse, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Update advertising parameters associated with this AdvertisingSet. Must be called when
+ * advertising is not active. This method returns immediately, the operation status is delivered
+ * through {@code callback.onAdvertisingParametersUpdated}.
+ *
+ * @param parameters advertising set parameters.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setAdvertisingParameters(AdvertisingSetParameters parameters) {
+ try {
+ mGatt.setAdvertisingParameters(mAdvertiserId, parameters, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Update periodic advertising parameters associated with this set. Must be called when
+ * periodic advertising is not enabled. This method returns immediately, the operation
+ * status is delivered through {@code callback.onPeriodicAdvertisingParametersUpdated()}.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setPeriodicAdvertisingParameters(PeriodicAdvertisingParameters parameters) {
+ try {
+ mGatt.setPeriodicAdvertisingParameters(mAdvertiserId, parameters, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Used to set periodic advertising data, must be called after setPeriodicAdvertisingParameters,
+ * or after advertising was started with periodic advertising data set. This method returns
+ * immediately, the operation status is delivered through
+ * {@code callback.onPeriodicAdvertisingDataSet()}.
+ *
+ * @param periodicData Periodic advertising data. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the update takes place when the
+ * periodic advertising is enabled for this set, the data can be maximum 251 bytes long.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setPeriodicAdvertisingData(AdvertiseData periodicData) {
+ try {
+ mGatt.setPeriodicAdvertisingData(mAdvertiserId, periodicData, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Used to enable/disable periodic advertising. This method returns immediately, the operation
+ * status is delivered through {@code callback.onPeriodicAdvertisingEnable()}.
+ *
+ * @param enable whether the periodic advertising should be enabled (true), or disabled
+ * (false).
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void setPeriodicAdvertisingEnabled(boolean enable) {
+ try {
+ mGatt.setPeriodicAdvertisingEnable(mAdvertiserId, enable, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Returns address associated with this advertising set.
+ * This method is exposed only for Bluetooth PTS tests, no app or system service
+ * should ever use it.
+ *
+ * @hide
+ */
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_ADVERTISE,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public void getOwnAddress() {
+ try {
+ mGatt.getOwnAddress(mAdvertiserId, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote exception - ", e);
+ }
+ }
+
+ /**
+ * Returns advertiserId associated with this advertising set.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public int getAdvertiserId() {
+ return mAdvertiserId;
+ }
+}
diff --git a/framework/java/android/bluetooth/le/AdvertisingSetCallback.java b/framework/java/android/bluetooth/le/AdvertisingSetCallback.java
new file mode 100644
index 0000000000..51324fdb01
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertisingSetCallback.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+/**
+ * Bluetooth LE advertising set callbacks, used to deliver advertising operation
+ * status.
+ */
+public abstract class AdvertisingSetCallback {
+
+ /**
+ * The requested operation was successful.
+ */
+ public static final int ADVERTISE_SUCCESS = 0;
+
+ /**
+ * Failed to start advertising as the advertise data to be broadcasted is too
+ * large.
+ */
+ public static final int ADVERTISE_FAILED_DATA_TOO_LARGE = 1;
+
+ /**
+ * Failed to start advertising because no advertising instance is available.
+ */
+ public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 2;
+
+ /**
+ * Failed to start advertising as the advertising is already started.
+ */
+ public static final int ADVERTISE_FAILED_ALREADY_STARTED = 3;
+
+ /**
+ * Operation failed due to an internal error.
+ */
+ public static final int ADVERTISE_FAILED_INTERNAL_ERROR = 4;
+
+ /**
+ * This feature is not supported on this platform.
+ */
+ public static final int ADVERTISE_FAILED_FEATURE_UNSUPPORTED = 5;
+
+ /**
+ * Callback triggered in response to {@link BluetoothLeAdvertiser#startAdvertisingSet}
+ * indicating result of the operation. If status is ADVERTISE_SUCCESS, then advertisingSet
+ * contains the started set and it is advertising. If error occurred, advertisingSet is
+ * null, and status will be set to proper error code.
+ *
+ * @param advertisingSet The advertising set that was started or null if error.
+ * @param txPower tx power that will be used for this set.
+ * @param status Status of the operation.
+ */
+ public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link BluetoothLeAdvertiser#stopAdvertisingSet}
+ * indicating advertising set is stopped.
+ *
+ * @param advertisingSet The advertising set.
+ */
+ public void onAdvertisingSetStopped(AdvertisingSet advertisingSet) {
+ }
+
+ /**
+ * Callback triggered in response to {@link BluetoothLeAdvertiser#startAdvertisingSet}
+ * indicating result of the operation. If status is ADVERTISE_SUCCESS, then advertising set is
+ * advertising.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setAdvertisingData} indicating
+ * result of the operation. If status is ADVERTISE_SUCCESS, then data was changed.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onAdvertisingDataSet(AdvertisingSet advertisingSet, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setAdvertisingData} indicating
+ * result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onScanResponseDataSet(AdvertisingSet advertisingSet, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setAdvertisingParameters}
+ * indicating result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param txPower tx power that will be used for this set.
+ * @param status Status of the operation.
+ */
+ public void onAdvertisingParametersUpdated(AdvertisingSet advertisingSet,
+ int txPower, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setPeriodicAdvertisingParameters}
+ * indicating result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onPeriodicAdvertisingParametersUpdated(AdvertisingSet advertisingSet, int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setPeriodicAdvertisingData}
+ * indicating result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onPeriodicAdvertisingDataSet(AdvertisingSet advertisingSet,
+ int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#setPeriodicAdvertisingEnabled}
+ * indicating result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param status Status of the operation.
+ */
+ public void onPeriodicAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enable,
+ int status) {
+ }
+
+ /**
+ * Callback triggered in response to {@link AdvertisingSet#getOwnAddress()}
+ * indicating result of the operation.
+ *
+ * @param advertisingSet The advertising set.
+ * @param addressType type of address.
+ * @param address advertising set bluetooth address.
+ * @hide
+ */
+ public void onOwnAddressRead(AdvertisingSet advertisingSet, int addressType, String address) {
+ }
+}
diff --git a/framework/java/android/bluetooth/le/AdvertisingSetParameters.java b/framework/java/android/bluetooth/le/AdvertisingSetParameters.java
new file mode 100644
index 0000000000..5c8fae6519
--- /dev/null
+++ b/framework/java/android/bluetooth/le/AdvertisingSetParameters.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The {@link AdvertisingSetParameters} provide a way to adjust advertising
+ * preferences for each
+ * Bluetooth LE advertising set. Use {@link AdvertisingSetParameters.Builder} to
+ * create an
+ * instance of this class.
+ */
+public final class AdvertisingSetParameters implements Parcelable {
+
+ /**
+ * Advertise on low frequency, around every 1000ms. This is the default and
+ * preferred advertising mode as it consumes the least power.
+ */
+ public static final int INTERVAL_HIGH = 1600;
+
+ /**
+ * Advertise on medium frequency, around every 250ms. This is balanced
+ * between advertising frequency and power consumption.
+ */
+ public static final int INTERVAL_MEDIUM = 400;
+
+ /**
+ * Perform high frequency, low latency advertising, around every 100ms. This
+ * has the highest power consumption and should not be used for continuous
+ * background advertising.
+ */
+ public static final int INTERVAL_LOW = 160;
+
+ /**
+ * Minimum value for advertising interval.
+ */
+ public static final int INTERVAL_MIN = 160;
+
+ /**
+ * Maximum value for advertising interval.
+ */
+ public static final int INTERVAL_MAX = 16777215;
+
+ /**
+ * Advertise using the lowest transmission (TX) power level. Low transmission
+ * power can be used to restrict the visibility range of advertising packets.
+ */
+ public static final int TX_POWER_ULTRA_LOW = -21;
+
+ /**
+ * Advertise using low TX power level.
+ */
+ public static final int TX_POWER_LOW = -15;
+
+ /**
+ * Advertise using medium TX power level.
+ */
+ public static final int TX_POWER_MEDIUM = -7;
+
+ /**
+ * Advertise using high TX power level. This corresponds to largest visibility
+ * range of the advertising packet.
+ */
+ public static final int TX_POWER_HIGH = 1;
+
+ /**
+ * Minimum value for TX power.
+ */
+ public static final int TX_POWER_MIN = -127;
+
+ /**
+ * Maximum value for TX power.
+ */
+ public static final int TX_POWER_MAX = 1;
+
+ /**
+ * The maximum limited advertisement duration as specified by the Bluetooth
+ * SIG
+ */
+ private static final int LIMITED_ADVERTISING_MAX_MILLIS = 180 * 1000;
+
+ /** @hide */
+ @IntDef(prefix = "ADDRESS_TYPE_", value = {
+ ADDRESS_TYPE_DEFAULT,
+ ADDRESS_TYPE_PUBLIC,
+ ADDRESS_TYPE_RANDOM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AddressTypeStatus {}
+
+ /**
+ * Advertise own address type that corresponds privacy settings of the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ADDRESS_TYPE_DEFAULT = -1;
+
+ /**
+ * Advertise own public address type.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ADDRESS_TYPE_PUBLIC = 0;
+
+ /**
+ * Generate and adverise own resolvable private address.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int ADDRESS_TYPE_RANDOM = 1;
+
+ private final boolean mIsLegacy;
+ private final boolean mIsAnonymous;
+ private final boolean mIncludeTxPower;
+ private final int mPrimaryPhy;
+ private final int mSecondaryPhy;
+ private final boolean mConnectable;
+ private final boolean mScannable;
+ private final int mInterval;
+ private final int mTxPowerLevel;
+ private final int mOwnAddressType;
+
+ private AdvertisingSetParameters(boolean connectable, boolean scannable, boolean isLegacy,
+ boolean isAnonymous, boolean includeTxPower,
+ int primaryPhy, int secondaryPhy,
+ int interval, int txPowerLevel, @AddressTypeStatus int ownAddressType) {
+ mConnectable = connectable;
+ mScannable = scannable;
+ mIsLegacy = isLegacy;
+ mIsAnonymous = isAnonymous;
+ mIncludeTxPower = includeTxPower;
+ mPrimaryPhy = primaryPhy;
+ mSecondaryPhy = secondaryPhy;
+ mInterval = interval;
+ mTxPowerLevel = txPowerLevel;
+ mOwnAddressType = ownAddressType;
+ }
+
+ private AdvertisingSetParameters(Parcel in) {
+ mConnectable = in.readInt() != 0;
+ mScannable = in.readInt() != 0;
+ mIsLegacy = in.readInt() != 0;
+ mIsAnonymous = in.readInt() != 0;
+ mIncludeTxPower = in.readInt() != 0;
+ mPrimaryPhy = in.readInt();
+ mSecondaryPhy = in.readInt();
+ mInterval = in.readInt();
+ mTxPowerLevel = in.readInt();
+ mOwnAddressType = in.readInt();
+ }
+
+ /**
+ * Returns whether the advertisement will be connectable.
+ */
+ public boolean isConnectable() {
+ return mConnectable;
+ }
+
+ /**
+ * Returns whether the advertisement will be scannable.
+ */
+ public boolean isScannable() {
+ return mScannable;
+ }
+
+ /**
+ * Returns whether the legacy advertisement will be used.
+ */
+ public boolean isLegacy() {
+ return mIsLegacy;
+ }
+
+ /**
+ * Returns whether the advertisement will be anonymous.
+ */
+ public boolean isAnonymous() {
+ return mIsAnonymous;
+ }
+
+ /**
+ * Returns whether the TX Power will be included.
+ */
+ public boolean includeTxPower() {
+ return mIncludeTxPower;
+ }
+
+ /**
+ * Returns the primary advertising phy.
+ */
+ public int getPrimaryPhy() {
+ return mPrimaryPhy;
+ }
+
+ /**
+ * Returns the secondary advertising phy.
+ */
+ public int getSecondaryPhy() {
+ return mSecondaryPhy;
+ }
+
+ /**
+ * Returns the advertising interval.
+ */
+ public int getInterval() {
+ return mInterval;
+ }
+
+ /**
+ * Returns the TX power level for advertising.
+ */
+ public int getTxPowerLevel() {
+ return mTxPowerLevel;
+ }
+
+ /**
+ * @return the own address type for advertising
+ *
+ * @hide
+ */
+ @SystemApi
+ public @AddressTypeStatus int getOwnAddressType() {
+ return mOwnAddressType;
+ }
+
+ @Override
+ public String toString() {
+ return "AdvertisingSetParameters [connectable=" + mConnectable
+ + ", isLegacy=" + mIsLegacy
+ + ", isAnonymous=" + mIsAnonymous
+ + ", includeTxPower=" + mIncludeTxPower
+ + ", primaryPhy=" + mPrimaryPhy
+ + ", secondaryPhy=" + mSecondaryPhy
+ + ", interval=" + mInterval
+ + ", txPowerLevel=" + mTxPowerLevel
+ + ", ownAddressType=" + mOwnAddressType + "]";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mConnectable ? 1 : 0);
+ dest.writeInt(mScannable ? 1 : 0);
+ dest.writeInt(mIsLegacy ? 1 : 0);
+ dest.writeInt(mIsAnonymous ? 1 : 0);
+ dest.writeInt(mIncludeTxPower ? 1 : 0);
+ dest.writeInt(mPrimaryPhy);
+ dest.writeInt(mSecondaryPhy);
+ dest.writeInt(mInterval);
+ dest.writeInt(mTxPowerLevel);
+ dest.writeInt(mOwnAddressType);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<AdvertisingSetParameters> CREATOR =
+ new Creator<AdvertisingSetParameters>() {
+ @Override
+ public AdvertisingSetParameters[] newArray(int size) {
+ return new AdvertisingSetParameters[size];
+ }
+
+ @Override
+ public AdvertisingSetParameters createFromParcel(Parcel in) {
+ return new AdvertisingSetParameters(in);
+ }
+ };
+
+ /**
+ * Builder class for {@link AdvertisingSetParameters}.
+ */
+ public static final class Builder {
+ private boolean mConnectable = false;
+ private boolean mScannable = false;
+ private boolean mIsLegacy = false;
+ private boolean mIsAnonymous = false;
+ private boolean mIncludeTxPower = false;
+ private int mPrimaryPhy = BluetoothDevice.PHY_LE_1M;
+ private int mSecondaryPhy = BluetoothDevice.PHY_LE_1M;
+ private int mInterval = INTERVAL_LOW;
+ private int mTxPowerLevel = TX_POWER_MEDIUM;
+ private int mOwnAddressType = ADDRESS_TYPE_DEFAULT;
+
+ /**
+ * Set whether the advertisement type should be connectable or
+ * non-connectable.
+ * Legacy advertisements can be both connectable and scannable. Non-legacy
+ * advertisements can be only scannable or only connectable.
+ *
+ * @param connectable Controls whether the advertisement type will be connectable (true) or
+ * non-connectable (false).
+ */
+ public Builder setConnectable(boolean connectable) {
+ mConnectable = connectable;
+ return this;
+ }
+
+ /**
+ * Set whether the advertisement type should be scannable.
+ * Legacy advertisements can be both connectable and scannable. Non-legacy
+ * advertisements can be only scannable or only connectable.
+ *
+ * @param scannable Controls whether the advertisement type will be scannable (true) or
+ * non-scannable (false).
+ */
+ public Builder setScannable(boolean scannable) {
+ mScannable = scannable;
+ return this;
+ }
+
+ /**
+ * When set to true, advertising set will advertise 4.x Spec compliant
+ * advertisements.
+ *
+ * @param isLegacy whether legacy advertising mode should be used.
+ */
+ public Builder setLegacyMode(boolean isLegacy) {
+ mIsLegacy = isLegacy;
+ return this;
+ }
+
+ /**
+ * Set whether advertiser address should be ommited from all packets. If this
+ * mode is used, periodic advertising can't be enabled for this set.
+ *
+ * This is used only if legacy mode is not used.
+ *
+ * @param isAnonymous whether anonymous advertising should be used.
+ */
+ public Builder setAnonymous(boolean isAnonymous) {
+ mIsAnonymous = isAnonymous;
+ return this;
+ }
+
+ /**
+ * Set whether TX power should be included in the extended header.
+ *
+ * This is used only if legacy mode is not used.
+ *
+ * @param includeTxPower whether TX power should be included in extended header
+ */
+ public Builder setIncludeTxPower(boolean includeTxPower) {
+ mIncludeTxPower = includeTxPower;
+ return this;
+ }
+
+ /**
+ * Set the primary physical channel used for this advertising set.
+ *
+ * This is used only if legacy mode is not used.
+ *
+ * Use {@link BluetoothAdapter#isLeCodedPhySupported} to determine if LE Coded PHY is
+ * supported on this device.
+ *
+ * @param primaryPhy Primary advertising physical channel, can only be {@link
+ * BluetoothDevice#PHY_LE_1M} or {@link BluetoothDevice#PHY_LE_CODED}.
+ * @throws IllegalArgumentException If the primaryPhy is invalid.
+ */
+ public Builder setPrimaryPhy(int primaryPhy) {
+ if (primaryPhy != BluetoothDevice.PHY_LE_1M
+ && primaryPhy != BluetoothDevice.PHY_LE_CODED) {
+ throw new IllegalArgumentException("bad primaryPhy " + primaryPhy);
+ }
+ mPrimaryPhy = primaryPhy;
+ return this;
+ }
+
+ /**
+ * Set the secondary physical channel used for this advertising set.
+ *
+ * This is used only if legacy mode is not used.
+ *
+ * Use {@link BluetoothAdapter#isLeCodedPhySupported} and
+ * {@link BluetoothAdapter#isLe2MPhySupported} to determine if LE Coded PHY or 2M PHY is
+ * supported on this device.
+ *
+ * @param secondaryPhy Secondary advertising physical channel, can only be one of {@link
+ * BluetoothDevice#PHY_LE_1M}, {@link BluetoothDevice#PHY_LE_2M} or {@link
+ * BluetoothDevice#PHY_LE_CODED}.
+ * @throws IllegalArgumentException If the secondaryPhy is invalid.
+ */
+ public Builder setSecondaryPhy(int secondaryPhy) {
+ if (secondaryPhy != BluetoothDevice.PHY_LE_1M
+ && secondaryPhy != BluetoothDevice.PHY_LE_2M
+ && secondaryPhy != BluetoothDevice.PHY_LE_CODED) {
+ throw new IllegalArgumentException("bad secondaryPhy " + secondaryPhy);
+ }
+ mSecondaryPhy = secondaryPhy;
+ return this;
+ }
+
+ /**
+ * Set advertising interval.
+ *
+ * @param interval Bluetooth LE Advertising interval, in 0.625ms unit. Valid range is from
+ * 160 (100ms) to 16777215 (10,485.759375 s). Recommended values are: {@link
+ * AdvertisingSetParameters#INTERVAL_LOW}, {@link AdvertisingSetParameters#INTERVAL_MEDIUM},
+ * or {@link AdvertisingSetParameters#INTERVAL_HIGH}.
+ * @throws IllegalArgumentException If the interval is invalid.
+ */
+ public Builder setInterval(int interval) {
+ if (interval < INTERVAL_MIN || interval > INTERVAL_MAX) {
+ throw new IllegalArgumentException("unknown interval " + interval);
+ }
+ mInterval = interval;
+ return this;
+ }
+
+ /**
+ * Set the transmission power level for the advertising.
+ *
+ * @param txPowerLevel Transmission power of Bluetooth LE Advertising, in dBm. The valid
+ * range is [-127, 1] Recommended values are:
+ * {@link AdvertisingSetParameters#TX_POWER_ULTRA_LOW},
+ * {@link AdvertisingSetParameters#TX_POWER_LOW},
+ * {@link AdvertisingSetParameters#TX_POWER_MEDIUM},
+ * or {@link AdvertisingSetParameters#TX_POWER_HIGH}.
+ * @throws IllegalArgumentException If the {@code txPowerLevel} is invalid.
+ */
+ public Builder setTxPowerLevel(int txPowerLevel) {
+ if (txPowerLevel < TX_POWER_MIN || txPowerLevel > TX_POWER_MAX) {
+ throw new IllegalArgumentException("unknown txPowerLevel " + txPowerLevel);
+ }
+ mTxPowerLevel = txPowerLevel;
+ return this;
+ }
+
+ /**
+ * Set own address type for advertising to control public or privacy mode. If used to set
+ * address type anything other than {@link AdvertisingSetParameters#ADDRESS_TYPE_DEFAULT},
+ * then it will require BLUETOOTH_PRIVILEGED permission and will be checked at the
+ * time of starting advertising.
+ *
+ * @throws IllegalArgumentException If the {@code ownAddressType} is invalid
+ *
+ * @hide
+ */
+ @SystemApi
+ public @NonNull Builder setOwnAddressType(@AddressTypeStatus int ownAddressType) {
+ if (ownAddressType < AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT
+ || ownAddressType > AdvertisingSetParameters.ADDRESS_TYPE_RANDOM) {
+ throw new IllegalArgumentException("unknown address type " + ownAddressType);
+ }
+ mOwnAddressType = ownAddressType;
+ return this;
+ }
+
+ /**
+ * Build the {@link AdvertisingSetParameters} object.
+ *
+ * @throws IllegalStateException if invalid combination of parameters is used.
+ */
+ public AdvertisingSetParameters build() {
+ if (mIsLegacy) {
+ if (mIsAnonymous) {
+ throw new IllegalArgumentException("Legacy advertising can't be anonymous");
+ }
+
+ if (mConnectable && !mScannable) {
+ throw new IllegalStateException(
+ "Legacy advertisement can't be connectable and non-scannable");
+ }
+
+ if (mIncludeTxPower) {
+ throw new IllegalStateException(
+ "Legacy advertising can't include TX power level in header");
+ }
+ } else {
+ if (mConnectable && mScannable) {
+ throw new IllegalStateException(
+ "Advertising can't be both connectable and scannable");
+ }
+
+ if (mIsAnonymous && mConnectable) {
+ throw new IllegalStateException(
+ "Advertising can't be both connectable and anonymous");
+ }
+ }
+
+ return new AdvertisingSetParameters(mConnectable, mScannable, mIsLegacy, mIsAnonymous,
+ mIncludeTxPower, mPrimaryPhy, mSecondaryPhy, mInterval, mTxPowerLevel,
+ mOwnAddressType);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java b/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java
new file mode 100644
index 0000000000..879dceedaa
--- /dev/null
+++ b/framework/java/android/bluetooth/le/BluetoothLeAdvertiser.java
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothUuid;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.annotations.RequiresBluetoothAdvertisePermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.content.AttributionSource;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelUuid;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class provides a way to perform Bluetooth LE advertise operations, such as starting and
+ * stopping advertising. An advertiser can broadcast up to 31 bytes of advertisement data
+ * represented by {@link AdvertiseData}.
+ * <p>
+ * To get an instance of {@link BluetoothLeAdvertiser}, call the
+ * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method.
+ *
+ * @see AdvertiseData
+ */
+public final class BluetoothLeAdvertiser {
+
+ private static final String TAG = "BluetoothLeAdvertiser";
+
+ private static final int MAX_ADVERTISING_DATA_BYTES = 1650;
+ private static final int MAX_LEGACY_ADVERTISING_DATA_BYTES = 31;
+ // Each fields need one byte for field length and another byte for field type.
+ private static final int OVERHEAD_BYTES_PER_FIELD = 2;
+ // Flags field will be set by system.
+ private static final int FLAGS_FIELD_BYTES = 3;
+ private static final int MANUFACTURER_SPECIFIC_DATA_LENGTH = 2;
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final IBluetoothManager mBluetoothManager;
+ private final AttributionSource mAttributionSource;
+
+ private final Handler mHandler;
+ private final Map<AdvertiseCallback, AdvertisingSetCallback>
+ mLegacyAdvertisers = new HashMap<>();
+ private final Map<AdvertisingSetCallback, IAdvertisingSetCallback>
+ mCallbackWrappers = Collections.synchronizedMap(new HashMap<>());
+ private final Map<Integer, AdvertisingSet>
+ mAdvertisingSets = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Use BluetoothAdapter.getLeAdvertiser() instead.
+ *
+ * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management
+ * @hide
+ */
+ public BluetoothLeAdvertiser(BluetoothAdapter bluetoothAdapter) {
+ mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
+ mBluetoothManager = mBluetoothAdapter.getBluetoothManager();
+ mAttributionSource = mBluetoothAdapter.getAttributionSource();
+ mHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Start Bluetooth LE Advertising. On success, the {@code advertiseData} will be broadcasted.
+ * Returns immediately, the operation status is delivered through {@code callback}.
+ *
+ * @param settings Settings for Bluetooth LE advertising.
+ * @param advertiseData Advertisement data to be broadcasted.
+ * @param callback Callback for advertising status.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertising(AdvertiseSettings settings,
+ AdvertiseData advertiseData, final AdvertiseCallback callback) {
+ startAdvertising(settings, advertiseData, null, callback);
+ }
+
+ /**
+ * Start Bluetooth LE Advertising. The {@code advertiseData} will be broadcasted if the
+ * operation succeeds. The {@code scanResponse} is returned when a scanning device sends an
+ * active scan request. This method returns immediately, the operation status is delivered
+ * through {@code callback}.
+ *
+ * @param settings Settings for Bluetooth LE advertising.
+ * @param advertiseData Advertisement data to be advertised in advertisement packet.
+ * @param scanResponse Scan response associated with the advertisement data.
+ * @param callback Callback for advertising status.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertising(AdvertiseSettings settings,
+ AdvertiseData advertiseData, AdvertiseData scanResponse,
+ final AdvertiseCallback callback) {
+ synchronized (mLegacyAdvertisers) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ boolean isConnectable = settings.isConnectable();
+ if (totalBytes(advertiseData, isConnectable) > MAX_LEGACY_ADVERTISING_DATA_BYTES
+ || totalBytes(scanResponse, false) > MAX_LEGACY_ADVERTISING_DATA_BYTES) {
+ postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE);
+ return;
+ }
+ if (mLegacyAdvertisers.containsKey(callback)) {
+ postStartFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED);
+ return;
+ }
+
+ AdvertisingSetParameters.Builder parameters = new AdvertisingSetParameters.Builder();
+ parameters.setLegacyMode(true);
+ parameters.setConnectable(isConnectable);
+ parameters.setScannable(true); // legacy advertisements we support are always scannable
+ parameters.setOwnAddressType(settings.getOwnAddressType());
+ if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) {
+ parameters.setInterval(1600); // 1s
+ } else if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_BALANCED) {
+ parameters.setInterval(400); // 250ms
+ } else if (settings.getMode() == AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) {
+ parameters.setInterval(160); // 100ms
+ }
+
+ if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW) {
+ parameters.setTxPowerLevel(-21);
+ } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_LOW) {
+ parameters.setTxPowerLevel(-15);
+ } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) {
+ parameters.setTxPowerLevel(-7);
+ } else if (settings.getTxPowerLevel() == AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) {
+ parameters.setTxPowerLevel(1);
+ }
+
+ int duration = 0;
+ int timeoutMillis = settings.getTimeout();
+ if (timeoutMillis > 0) {
+ duration = (timeoutMillis < 10) ? 1 : timeoutMillis / 10;
+ }
+
+ AdvertisingSetCallback wrapped = wrapOldCallback(callback, settings);
+ mLegacyAdvertisers.put(callback, wrapped);
+ startAdvertisingSet(parameters.build(), advertiseData, scanResponse, null, null,
+ duration, 0, wrapped);
+ }
+ }
+
+ @SuppressLint({
+ "AndroidFrameworkBluetoothPermission",
+ "AndroidFrameworkRequiresPermission",
+ })
+ AdvertisingSetCallback wrapOldCallback(AdvertiseCallback callback, AdvertiseSettings settings) {
+ return new AdvertisingSetCallback() {
+ @Override
+ public void onAdvertisingSetStarted(AdvertisingSet advertisingSet, int txPower,
+ int status) {
+ if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ postStartFailure(callback, status);
+ return;
+ }
+
+ postStartSuccess(callback, settings);
+ }
+
+ /* Legacy advertiser is disabled on timeout */
+ @Override
+ public void onAdvertisingEnabled(AdvertisingSet advertisingSet, boolean enabled,
+ int status) {
+ if (enabled) {
+ Log.e(TAG, "Legacy advertiser should be only disabled on timeout,"
+ + " but was enabled!");
+ return;
+ }
+
+ stopAdvertising(callback);
+ }
+
+ };
+ }
+
+ /**
+ * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in
+ * {@link BluetoothLeAdvertiser#startAdvertising}.
+ *
+ * @param callback {@link AdvertiseCallback} identifies the advertising instance to stop.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void stopAdvertising(final AdvertiseCallback callback) {
+ synchronized (mLegacyAdvertisers) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ AdvertisingSetCallback wrapper = mLegacyAdvertisers.get(callback);
+ if (wrapper == null) return;
+
+ stopAdvertisingSet(wrapper);
+
+ mLegacyAdvertisers.remove(callback);
+ }
+ }
+
+ /**
+ * Creates a new advertising set. If operation succeed, device will start advertising. This
+ * method returns immediately, the operation status is delivered through
+ * {@code callback.onAdvertisingSetStarted()}.
+ * <p>
+ *
+ * @param parameters advertising set parameters.
+ * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable,
+ * three bytes will be added for flags.
+ * @param scanResponse Scan response associated with the advertisement data. Size must not
+ * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will
+ * not be started.
+ * @param periodicData Periodic advertising data. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param callback Callback for advertising set.
+ * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable
+ * size, or unsupported advertising PHY is selected, or when attempt to use Periodic Advertising
+ * feature is made when it's not supported by the controller.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertisingSet(AdvertisingSetParameters parameters,
+ AdvertiseData advertiseData, AdvertiseData scanResponse,
+ PeriodicAdvertisingParameters periodicParameters,
+ AdvertiseData periodicData, AdvertisingSetCallback callback) {
+ startAdvertisingSet(parameters, advertiseData, scanResponse, periodicParameters,
+ periodicData, 0, 0, callback, new Handler(Looper.getMainLooper()));
+ }
+
+ /**
+ * Creates a new advertising set. If operation succeed, device will start advertising. This
+ * method returns immediately, the operation status is delivered through
+ * {@code callback.onAdvertisingSetStarted()}.
+ * <p>
+ *
+ * @param parameters advertising set parameters.
+ * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable,
+ * three bytes will be added for flags.
+ * @param scanResponse Scan response associated with the advertisement data. Size must not
+ * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will
+ * not be started.
+ * @param periodicData Periodic advertising data. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param callback Callback for advertising set.
+ * @param handler thread upon which the callbacks will be invoked.
+ * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable
+ * size, or unsupported advertising PHY is selected, or when attempt to use Periodic Advertising
+ * feature is made when it's not supported by the controller.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertisingSet(AdvertisingSetParameters parameters,
+ AdvertiseData advertiseData, AdvertiseData scanResponse,
+ PeriodicAdvertisingParameters periodicParameters,
+ AdvertiseData periodicData, AdvertisingSetCallback callback,
+ Handler handler) {
+ startAdvertisingSet(parameters, advertiseData, scanResponse, periodicParameters,
+ periodicData, 0, 0, callback, handler);
+ }
+
+ /**
+ * Creates a new advertising set. If operation succeed, device will start advertising. This
+ * method returns immediately, the operation status is delivered through
+ * {@code callback.onAdvertisingSetStarted()}.
+ * <p>
+ *
+ * @param parameters advertising set parameters.
+ * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable,
+ * three bytes will be added for flags.
+ * @param scanResponse Scan response associated with the advertisement data. Size must not
+ * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param periodicParameters periodic advertisng parameters. If null, periodic advertising will
+ * not be started.
+ * @param periodicData Periodic advertising data. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}.
+ * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535
+ * (655,350 ms). 0 means advertising should continue until stopped.
+ * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the
+ * controller shall attempt to send prior to terminating the extended advertising, even if the
+ * duration has not expired. Valid range is from 1 to 255. 0 means no maximum.
+ * @param callback Callback for advertising set.
+ * @throws IllegalArgumentException when any of the data parameter exceed the maximum allowable
+ * size, or unsupported advertising PHY is selected, or when attempt to use Periodic Advertising
+ * feature is made when it's not supported by the controller.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertisingSet(AdvertisingSetParameters parameters,
+ AdvertiseData advertiseData, AdvertiseData scanResponse,
+ PeriodicAdvertisingParameters periodicParameters,
+ AdvertiseData periodicData, int duration,
+ int maxExtendedAdvertisingEvents,
+ AdvertisingSetCallback callback) {
+ startAdvertisingSet(parameters, advertiseData, scanResponse, periodicParameters,
+ periodicData, duration, maxExtendedAdvertisingEvents, callback,
+ new Handler(Looper.getMainLooper()));
+ }
+
+ /**
+ * Creates a new advertising set. If operation succeed, device will start advertising. This
+ * method returns immediately, the operation status is delivered through
+ * {@code callback.onAdvertisingSetStarted()}.
+ * <p>
+ *
+ * @param parameters Advertising set parameters.
+ * @param advertiseData Advertisement data to be broadcasted. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}. If the advertisement is connectable,
+ * three bytes will be added for flags.
+ * @param scanResponse Scan response associated with the advertisement data. Size must not
+ * exceed {@link BluetoothAdapter#getLeMaximumAdvertisingDataLength}
+ * @param periodicParameters Periodic advertisng parameters. If null, periodic advertising will
+ * not be started.
+ * @param periodicData Periodic advertising data. Size must not exceed {@link
+ * BluetoothAdapter#getLeMaximumAdvertisingDataLength}
+ * @param duration advertising duration, in 10ms unit. Valid range is from 1 (10ms) to 65535
+ * (655,350 ms). 0 means advertising should continue until stopped.
+ * @param maxExtendedAdvertisingEvents maximum number of extended advertising events the
+ * controller shall attempt to send prior to terminating the extended advertising, even if the
+ * duration has not expired. Valid range is from 1 to 255. 0 means no maximum.
+ * @param callback Callback for advertising set.
+ * @param handler Thread upon which the callbacks will be invoked.
+ * @throws IllegalArgumentException When any of the data parameter exceed the maximum allowable
+ * size, or unsupported advertising PHY is selected, or when attempt to use Periodic Advertising
+ * feature is made when it's not supported by the controller, or when
+ * maxExtendedAdvertisingEvents is used on a controller that doesn't support the LE Extended
+ * Advertising
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void startAdvertisingSet(AdvertisingSetParameters parameters,
+ AdvertiseData advertiseData, AdvertiseData scanResponse,
+ PeriodicAdvertisingParameters periodicParameters,
+ AdvertiseData periodicData, int duration,
+ int maxExtendedAdvertisingEvents, AdvertisingSetCallback callback,
+ Handler handler) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+
+ boolean isConnectable = parameters.isConnectable();
+ if (parameters.isLegacy()) {
+ if (totalBytes(advertiseData, isConnectable) > MAX_LEGACY_ADVERTISING_DATA_BYTES) {
+ throw new IllegalArgumentException("Legacy advertising data too big");
+ }
+
+ if (totalBytes(scanResponse, false) > MAX_LEGACY_ADVERTISING_DATA_BYTES) {
+ throw new IllegalArgumentException("Legacy scan response data too big");
+ }
+ } else {
+ boolean supportCodedPhy = mBluetoothAdapter.isLeCodedPhySupported();
+ boolean support2MPhy = mBluetoothAdapter.isLe2MPhySupported();
+ int pphy = parameters.getPrimaryPhy();
+ int sphy = parameters.getSecondaryPhy();
+ if (pphy == BluetoothDevice.PHY_LE_CODED && !supportCodedPhy) {
+ throw new IllegalArgumentException("Unsupported primary PHY selected");
+ }
+
+ if ((sphy == BluetoothDevice.PHY_LE_CODED && !supportCodedPhy)
+ || (sphy == BluetoothDevice.PHY_LE_2M && !support2MPhy)) {
+ throw new IllegalArgumentException("Unsupported secondary PHY selected");
+ }
+
+ int maxData = mBluetoothAdapter.getLeMaximumAdvertisingDataLength();
+ if (totalBytes(advertiseData, isConnectable) > maxData) {
+ throw new IllegalArgumentException("Advertising data too big");
+ }
+
+ if (totalBytes(scanResponse, false) > maxData) {
+ throw new IllegalArgumentException("Scan response data too big");
+ }
+
+ if (totalBytes(periodicData, false) > maxData) {
+ throw new IllegalArgumentException("Periodic advertising data too big");
+ }
+
+ boolean supportPeriodic = mBluetoothAdapter.isLePeriodicAdvertisingSupported();
+ if (periodicParameters != null && !supportPeriodic) {
+ throw new IllegalArgumentException(
+ "Controller does not support LE Periodic Advertising");
+ }
+ }
+
+ if (maxExtendedAdvertisingEvents < 0 || maxExtendedAdvertisingEvents > 255) {
+ throw new IllegalArgumentException(
+ "maxExtendedAdvertisingEvents out of range: " + maxExtendedAdvertisingEvents);
+ }
+
+ if (maxExtendedAdvertisingEvents != 0
+ && !mBluetoothAdapter.isLePeriodicAdvertisingSupported()) {
+ throw new IllegalArgumentException(
+ "Can't use maxExtendedAdvertisingEvents with controller that don't support "
+ + "LE Extended Advertising");
+ }
+
+ if (duration < 0 || duration > 65535) {
+ throw new IllegalArgumentException("duration out of range: " + duration);
+ }
+
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get Bluetooth GATT - ", e);
+ postStartSetFailure(handler, callback,
+ AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ return;
+ }
+
+ if (gatt == null) {
+ Log.e(TAG, "Bluetooth GATT is null");
+ postStartSetFailure(handler, callback,
+ AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ return;
+ }
+
+ IAdvertisingSetCallback wrapped = wrap(callback, handler);
+ if (mCallbackWrappers.putIfAbsent(callback, wrapped) != null) {
+ throw new IllegalArgumentException(
+ "callback instance already associated with advertising");
+ }
+
+ try {
+ gatt.startAdvertisingSet(parameters, advertiseData, scanResponse, periodicParameters,
+ periodicData, duration, maxExtendedAdvertisingEvents, wrapped,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to start advertising set - ", e);
+ postStartSetFailure(handler, callback,
+ AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ return;
+ }
+ }
+
+ /**
+ * Used to dispose of a {@link AdvertisingSet} object, obtained with {@link
+ * BluetoothLeAdvertiser#startAdvertisingSet}.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ public void stopAdvertisingSet(AdvertisingSetCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+
+ IAdvertisingSetCallback wrapped = mCallbackWrappers.remove(callback);
+ if (wrapped == null) {
+ return;
+ }
+
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ gatt.stopAdvertisingSet(wrapped, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to stop advertising - ", e);
+ }
+ }
+
+ /**
+ * Cleans up advertisers. Should be called when bluetooth is down.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public void cleanup() {
+ mLegacyAdvertisers.clear();
+ mCallbackWrappers.clear();
+ mAdvertisingSets.clear();
+ }
+
+ // Compute the size of advertisement data or scan resp
+ @RequiresBluetoothAdvertisePermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADVERTISE)
+ private int totalBytes(AdvertiseData data, boolean isFlagsIncluded) {
+ if (data == null) return 0;
+ // Flags field is omitted if the advertising is not connectable.
+ int size = (isFlagsIncluded) ? FLAGS_FIELD_BYTES : 0;
+ if (data.getServiceUuids() != null) {
+ int num16BitUuids = 0;
+ int num32BitUuids = 0;
+ int num128BitUuids = 0;
+ for (ParcelUuid uuid : data.getServiceUuids()) {
+ if (BluetoothUuid.is16BitUuid(uuid)) {
+ ++num16BitUuids;
+ } else if (BluetoothUuid.is32BitUuid(uuid)) {
+ ++num32BitUuids;
+ } else {
+ ++num128BitUuids;
+ }
+ }
+ // 16 bit service uuids are grouped into one field when doing advertising.
+ if (num16BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD + num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT;
+ }
+ // 32 bit service uuids are grouped into one field when doing advertising.
+ if (num32BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD + num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT;
+ }
+ // 128 bit service uuids are grouped into one field when doing advertising.
+ if (num128BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD
+ + num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT;
+ }
+ }
+ if (data.getServiceSolicitationUuids() != null) {
+ int num16BitUuids = 0;
+ int num32BitUuids = 0;
+ int num128BitUuids = 0;
+ for (ParcelUuid uuid : data.getServiceSolicitationUuids()) {
+ if (BluetoothUuid.is16BitUuid(uuid)) {
+ ++num16BitUuids;
+ } else if (BluetoothUuid.is32BitUuid(uuid)) {
+ ++num32BitUuids;
+ } else {
+ ++num128BitUuids;
+ }
+ }
+ // 16 bit service uuids are grouped into one field when doing advertising.
+ if (num16BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD + num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT;
+ }
+ // 32 bit service uuids are grouped into one field when doing advertising.
+ if (num32BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD + num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT;
+ }
+ // 128 bit service uuids are grouped into one field when doing advertising.
+ if (num128BitUuids != 0) {
+ size += OVERHEAD_BYTES_PER_FIELD
+ + num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT;
+ }
+ }
+ for (TransportDiscoveryData transportDiscoveryData : data.getTransportDiscoveryData()) {
+ size += OVERHEAD_BYTES_PER_FIELD + transportDiscoveryData.totalBytes();
+ }
+ for (ParcelUuid uuid : data.getServiceData().keySet()) {
+ int uuidLen = BluetoothUuid.uuidToBytes(uuid).length;
+ size += OVERHEAD_BYTES_PER_FIELD + uuidLen
+ + byteLength(data.getServiceData().get(uuid));
+ }
+ for (int i = 0; i < data.getManufacturerSpecificData().size(); ++i) {
+ size += OVERHEAD_BYTES_PER_FIELD + MANUFACTURER_SPECIFIC_DATA_LENGTH
+ + byteLength(data.getManufacturerSpecificData().valueAt(i));
+ }
+ if (data.getIncludeTxPowerLevel()) {
+ size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte.
+ }
+ if (data.getIncludeDeviceName()) {
+ final int length = mBluetoothAdapter.getNameLengthForAdvertise();
+ if (length >= 0) {
+ size += OVERHEAD_BYTES_PER_FIELD + length;
+ }
+ }
+ return size;
+ }
+
+ private int byteLength(byte[] array) {
+ return array == null ? 0 : array.length;
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ IAdvertisingSetCallback wrap(AdvertisingSetCallback callback, Handler handler) {
+ return new IAdvertisingSetCallback.Stub() {
+ @Override
+ public void onAdvertisingSetStarted(int advertiserId, int txPower, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ callback.onAdvertisingSetStarted(null, 0, status);
+ mCallbackWrappers.remove(callback);
+ return;
+ }
+
+ AdvertisingSet advertisingSet = new AdvertisingSet(
+ advertiserId, mBluetoothManager, mAttributionSource);
+ mAdvertisingSets.put(advertiserId, advertisingSet);
+ callback.onAdvertisingSetStarted(advertisingSet, txPower, status);
+ }
+ });
+ }
+
+ @Override
+ public void onOwnAddressRead(int advertiserId, int addressType, String address) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onOwnAddressRead(advertisingSet, addressType, address);
+ }
+ });
+ }
+
+ @Override
+ public void onAdvertisingSetStopped(int advertiserId) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onAdvertisingSetStopped(advertisingSet);
+ mAdvertisingSets.remove(advertiserId);
+ mCallbackWrappers.remove(callback);
+ }
+ });
+ }
+
+ @Override
+ public void onAdvertisingEnabled(int advertiserId, boolean enabled, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onAdvertisingEnabled(advertisingSet, enabled, status);
+ }
+ });
+ }
+
+ @Override
+ public void onAdvertisingDataSet(int advertiserId, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onAdvertisingDataSet(advertisingSet, status);
+ }
+ });
+ }
+
+ @Override
+ public void onScanResponseDataSet(int advertiserId, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onScanResponseDataSet(advertisingSet, status);
+ }
+ });
+ }
+
+ @Override
+ public void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onAdvertisingParametersUpdated(advertisingSet, txPower, status);
+ }
+ });
+ }
+
+ @Override
+ public void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onPeriodicAdvertisingParametersUpdated(advertisingSet, status);
+ }
+ });
+ }
+
+ @Override
+ public void onPeriodicAdvertisingDataSet(int advertiserId, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onPeriodicAdvertisingDataSet(advertisingSet, status);
+ }
+ });
+ }
+
+ @Override
+ public void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ AdvertisingSet advertisingSet = mAdvertisingSets.get(advertiserId);
+ callback.onPeriodicAdvertisingEnabled(advertisingSet, enable, status);
+ }
+ });
+ }
+ };
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private void postStartSetFailure(Handler handler, final AdvertisingSetCallback callback,
+ final int error) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onAdvertisingSetStarted(null, 0, error);
+ }
+ });
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private void postStartFailure(final AdvertiseCallback callback, final int error) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onStartFailure(error);
+ }
+ });
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private void postStartSuccess(final AdvertiseCallback callback,
+ final AdvertiseSettings settings) {
+ mHandler.post(new Runnable() {
+
+ @Override
+ public void run() {
+ callback.onStartSuccess(settings);
+ }
+ });
+ }
+}
diff --git a/framework/java/android/bluetooth/le/BluetoothLeScanner.java b/framework/java/android/bluetooth/le/BluetoothLeScanner.java
new file mode 100644
index 0000000000..540e5a778c
--- /dev/null
+++ b/framework/java/android/bluetooth/le/BluetoothLeScanner.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresNoPermission;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.bluetooth.Attributable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.content.AttributionSource;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class provides methods to perform scan related operations for Bluetooth LE devices. An
+ * application can scan for a particular type of Bluetooth LE devices using {@link ScanFilter}. It
+ * can also request different types of callbacks for delivering the result.
+ * <p>
+ * Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of
+ * {@link BluetoothLeScanner}.
+ *
+ * @see ScanFilter
+ */
+public final class BluetoothLeScanner {
+
+ private static final String TAG = "BluetoothLeScanner";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = false;
+
+ /**
+ * Extra containing a list of ScanResults. It can have one or more results if there was no
+ * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this
+ * extra will not be available.
+ */
+ public static final String EXTRA_LIST_SCAN_RESULT =
+ "android.bluetooth.le.extra.LIST_SCAN_RESULT";
+
+ /**
+ * Optional extra indicating the error code, if any. The error code will be one of the
+ * SCAN_FAILED_* codes in {@link ScanCallback}.
+ */
+ public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE";
+
+ /**
+ * Optional extra indicating the callback type, which will be one of
+ * CALLBACK_TYPE_* constants in {@link ScanSettings}.
+ *
+ * @see ScanCallback#onScanResult(int, ScanResult)
+ */
+ public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE";
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final IBluetoothManager mBluetoothManager;
+ private final AttributionSource mAttributionSource;
+
+ private final Handler mHandler;
+ private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients;
+
+ /**
+ * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead.
+ *
+ * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management.
+ * @param opPackageName The opPackageName of the context this object was created from
+ * @param featureId The featureId of the context this object was created from
+ * @hide
+ */
+ public BluetoothLeScanner(BluetoothAdapter bluetoothAdapter) {
+ mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
+ mBluetoothManager = mBluetoothAdapter.getBluetoothManager();
+ mAttributionSource = mBluetoothAdapter.getAttributionSource();
+ mHandler = new Handler(Looper.getMainLooper());
+ mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>();
+ }
+
+ /**
+ * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
+ * delivered through {@code callback}. For unfiltered scans, scanning is stopped on screen
+ * off to save power. Scanning is resumed when screen is turned on again. To avoid this, use
+ * {@link #startScan(List, ScanSettings, ScanCallback)} with desired {@link ScanFilter}.
+ * <p>
+ * An app must have
+ * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} permission
+ * in order to get results. An App targeting Android Q or later must have
+ * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
+ * in order to get results.
+ *
+ * @param callback Callback used to deliver scan results.
+ * @throws IllegalArgumentException If {@code callback} is null.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void startScan(final ScanCallback callback) {
+ startScan(null, new ScanSettings.Builder().build(), callback);
+ }
+
+ /**
+ * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
+ * For unfiltered scans, scanning is stopped on screen off to save power. Scanning is
+ * resumed when screen is turned on again. To avoid this, do filetered scanning by
+ * using proper {@link ScanFilter}.
+ * <p>
+ * An app must have
+ * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} permission
+ * in order to get results. An App targeting Android Q or later must have
+ * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
+ * in order to get results.
+ *
+ * @param filters {@link ScanFilter}s for finding exact BLE devices.
+ * @param settings Settings for the scan.
+ * @param callback Callback used to deliver scan results.
+ * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void startScan(List<ScanFilter> filters, ScanSettings settings,
+ final ScanCallback callback) {
+ startScan(filters, settings, null, callback, /*callbackIntent=*/ null);
+ }
+
+ /**
+ * Start Bluetooth LE scan using a {@link PendingIntent}. The scan results will be delivered via
+ * the PendingIntent. Use this method of scanning if your process is not always running and it
+ * should be started when scan results are available.
+ * <p>
+ * An app must have
+ * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} permission
+ * in order to get results. An App targeting Android Q or later must have
+ * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
+ * in order to get results.
+ * <p>
+ * When the PendingIntent is delivered, the Intent passed to the receiver or activity
+ * will contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE},
+ * {@link #EXTRA_ERROR_CODE} and {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of
+ * the scan.
+ *
+ * @param filters Optional list of ScanFilters for finding exact BLE devices.
+ * @param settings Optional settings for the scan.
+ * @param callbackIntent The PendingIntent to deliver the result to.
+ * @return Returns 0 for success or an error code from {@link ScanCallback} if the scan request
+ * could not be sent.
+ * @see #stopScan(PendingIntent)
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public int startScan(@Nullable List<ScanFilter> filters, @Nullable ScanSettings settings,
+ @NonNull PendingIntent callbackIntent) {
+ return startScan(filters,
+ settings != null ? settings : new ScanSettings.Builder().build(),
+ null, null, callbackIntent);
+ }
+
+ /**
+ * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to
+ * specify on behalf of which application(s) the work is being done.
+ *
+ * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
+ * the scan.
+ * @param callback Callback used to deliver scan results.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.UPDATE_DEVICE_STATS
+ })
+ public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) {
+ startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback);
+ }
+
+ /**
+ * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but
+ * allows the caller to specify on behalf of which application(s) the work is being done.
+ *
+ * @param filters {@link ScanFilter}s for finding exact BLE devices.
+ * @param settings Settings for the scan.
+ * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
+ * the scan.
+ * @param callback Callback used to deliver scan results.
+ * @hide
+ */
+ @SystemApi
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_SCAN,
+ android.Manifest.permission.UPDATE_DEVICE_STATS
+ })
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ public void startScanFromSource(List<ScanFilter> filters, ScanSettings settings,
+ final WorkSource workSource, final ScanCallback callback) {
+ startScan(filters, settings, workSource, callback, null);
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ private int startScan(List<ScanFilter> filters, ScanSettings settings,
+ final WorkSource workSource, final ScanCallback callback,
+ final PendingIntent callbackIntent) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ if (callback == null && callbackIntent == null) {
+ throw new IllegalArgumentException("callback is null");
+ }
+ if (settings == null) {
+ throw new IllegalArgumentException("settings is null");
+ }
+ synchronized (mLeScanClients) {
+ if (callback != null && mLeScanClients.containsKey(callback)) {
+ return postCallbackErrorOrReturn(callback,
+ ScanCallback.SCAN_FAILED_ALREADY_STARTED);
+ }
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ } catch (RemoteException e) {
+ gatt = null;
+ }
+ if (gatt == null) {
+ return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+ }
+ if (!isSettingsConfigAllowedForScan(settings)) {
+ return postCallbackErrorOrReturn(callback,
+ ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
+ }
+ if (!isHardwareResourcesAvailableForScan(settings)) {
+ return postCallbackErrorOrReturn(callback,
+ ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
+ }
+ if (!isSettingsAndFilterComboAllowed(settings, filters)) {
+ return postCallbackErrorOrReturn(callback,
+ ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
+ }
+ if (callback != null) {
+ BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
+ settings, workSource, callback);
+ wrapper.startRegistration();
+ } else {
+ try {
+ gatt.startScanForIntent(callbackIntent, settings, filters,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ return ScanCallback.SCAN_FAILED_INTERNAL_ERROR;
+ }
+ }
+ }
+ return ScanCallback.NO_ERROR;
+ }
+
+ /**
+ * Stops an ongoing Bluetooth LE scan.
+ *
+ * @param callback
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopScan(ScanCallback callback) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ synchronized (mLeScanClients) {
+ BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback);
+ if (wrapper == null) {
+ if (DBG) Log.d(TAG, "could not find callback wrapper");
+ return;
+ }
+ wrapper.stopLeScan();
+ }
+ }
+
+ /**
+ * Stops an ongoing Bluetooth LE scan started using a PendingIntent. When creating the
+ * PendingIntent parameter, please do not use the FLAG_CANCEL_CURRENT flag. Otherwise, the stop
+ * scan may have no effect.
+ *
+ * @param callbackIntent The PendingIntent that was used to start the scan.
+ * @see #startScan(List, ScanSettings, PendingIntent)
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopScan(PendingIntent callbackIntent) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ gatt.stopScanForIntent(callbackIntent, mAttributionSource);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
+ * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
+ * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data
+ * will be delivered through the {@code callback}.
+ *
+ * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
+ * used to start scan.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void flushPendingScanResults(ScanCallback callback) {
+ BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null!");
+ }
+ synchronized (mLeScanClients) {
+ BleScanCallbackWrapper wrapper = mLeScanClients.get(callback);
+ if (wrapper == null) {
+ return;
+ }
+ wrapper.flushPendingBatchResults();
+ }
+ }
+
+ /**
+ * Start truncated scan.
+ *
+ * @deprecated this is not used anywhere
+ *
+ * @hide
+ */
+ @Deprecated
+ @SystemApi
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings,
+ final ScanCallback callback) {
+ int filterSize = truncatedFilters.size();
+ List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize);
+ for (TruncatedFilter filter : truncatedFilters) {
+ scanFilters.add(filter.getFilter());
+ }
+ startScan(scanFilters, settings, null, callback, null);
+ }
+
+ /**
+ * Cleans up scan clients. Should be called when bluetooth is down.
+ *
+ * @hide
+ */
+ @RequiresNoPermission
+ public void cleanup() {
+ mLeScanClients.clear();
+ }
+
+ /**
+ * Bluetooth GATT interface callbacks
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private class BleScanCallbackWrapper extends IScannerCallback.Stub {
+ private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
+
+ private final ScanCallback mScanCallback;
+ private final List<ScanFilter> mFilters;
+ private final WorkSource mWorkSource;
+ private ScanSettings mSettings;
+ private IBluetoothGatt mBluetoothGatt;
+
+ // mLeHandle 0: not registered
+ // -2: registration failed because app is scanning to frequently
+ // -1: scan stopped or registration failed
+ // > 0: registered and scan started
+ private int mScannerId;
+
+ public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
+ List<ScanFilter> filters, ScanSettings settings,
+ WorkSource workSource, ScanCallback scanCallback) {
+ mBluetoothGatt = bluetoothGatt;
+ mFilters = filters;
+ mSettings = settings;
+ mWorkSource = workSource;
+ mScanCallback = scanCallback;
+ mScannerId = 0;
+ }
+
+ public void startRegistration() {
+ synchronized (this) {
+ // Scan stopped.
+ if (mScannerId == -1 || mScannerId == -2) return;
+ try {
+ mBluetoothGatt.registerScanner(this, mWorkSource, mAttributionSource);
+ wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
+ } catch (InterruptedException | RemoteException e) {
+ Log.e(TAG, "application registeration exception", e);
+ postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
+ }
+ if (mScannerId > 0) {
+ mLeScanClients.put(mScanCallback, this);
+ } else {
+ // Registration timed out or got exception, reset RscannerId to -1 so no
+ // subsequent operations can proceed.
+ if (mScannerId == 0) mScannerId = -1;
+
+ // If scanning too frequently, don't report anything to the app.
+ if (mScannerId == -2) return;
+
+ postCallbackError(mScanCallback,
+ ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
+ }
+ }
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void stopLeScan() {
+ synchronized (this) {
+ if (mScannerId <= 0) {
+ Log.e(TAG, "Error state, mLeHandle: " + mScannerId);
+ return;
+ }
+ try {
+ mBluetoothGatt.stopScan(mScannerId, mAttributionSource);
+ mBluetoothGatt.unregisterScanner(mScannerId, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to stop scan and unregister", e);
+ }
+ mScannerId = -1;
+ }
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ void flushPendingBatchResults() {
+ synchronized (this) {
+ if (mScannerId <= 0) {
+ Log.e(TAG, "Error state, mLeHandle: " + mScannerId);
+ return;
+ }
+ try {
+ mBluetoothGatt.flushPendingBatchResults(mScannerId, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get pending scan results", e);
+ }
+ }
+ }
+
+ /**
+ * Application interface registered - app is ready to go
+ */
+ @Override
+ public void onScannerRegistered(int status, int scannerId) {
+ Log.d(TAG, "onScannerRegistered() - status=" + status
+ + " scannerId=" + scannerId + " mScannerId=" + mScannerId);
+ synchronized (this) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ try {
+ if (mScannerId == -1) {
+ // Registration succeeds after timeout, unregister scanner.
+ mBluetoothGatt.unregisterScanner(scannerId, mAttributionSource);
+ } else {
+ mScannerId = scannerId;
+ mBluetoothGatt.startScan(mScannerId, mSettings, mFilters,
+ mAttributionSource);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "fail to start le scan: " + e);
+ mScannerId = -1;
+ }
+ } else if (status == ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY) {
+ // applicaiton was scanning too frequently
+ mScannerId = -2;
+ } else {
+ // registration failed
+ mScannerId = -1;
+ }
+ notifyAll();
+ }
+ }
+
+ /**
+ * Callback reporting an LE scan result.
+ *
+ * @hide
+ */
+ @Override
+ public void onScanResult(final ScanResult scanResult) {
+ Attributable.setAttributionSource(scanResult, mAttributionSource);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onScanResult() - mScannerId=" + mScannerId);
+ }
+ if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString());
+
+ // Check null in case the scan has been stopped
+ synchronized (this) {
+ if (mScannerId <= 0) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Ignoring result as scan stopped.");
+ }
+ return;
+ };
+ }
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onScanResult() - handler run");
+ }
+ mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult);
+ }
+ });
+ }
+
+ @Override
+ public void onBatchScanResults(final List<ScanResult> results) {
+ Attributable.setAttributionSource(results, mAttributionSource);
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mScanCallback.onBatchScanResults(results);
+ }
+ });
+ }
+
+ @Override
+ public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) {
+ Attributable.setAttributionSource(scanResult, mAttributionSource);
+ if (VDBG) {
+ Log.d(TAG, "onFoundOrLost() - onFound = " + onFound + " " + scanResult.toString());
+ }
+
+ // Check null in case the scan has been stopped
+ synchronized (this) {
+ if (mScannerId <= 0) {
+ return;
+ }
+ }
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (onFound) {
+ mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
+ scanResult);
+ } else {
+ mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST,
+ scanResult);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onScanManagerErrorCallback(final int errorCode) {
+ if (VDBG) {
+ Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode);
+ }
+ synchronized (this) {
+ if (mScannerId <= 0) {
+ return;
+ }
+ }
+ postCallbackError(mScanCallback, errorCode);
+ }
+ }
+
+ private int postCallbackErrorOrReturn(final ScanCallback callback, final int errorCode) {
+ if (callback == null) {
+ return errorCode;
+ } else {
+ postCallbackError(callback, errorCode);
+ return ScanCallback.NO_ERROR;
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private void postCallbackError(final ScanCallback callback, final int errorCode) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onScanFailed(errorCode);
+ }
+ });
+ }
+
+ private boolean isSettingsConfigAllowedForScan(ScanSettings settings) {
+ if (mBluetoothAdapter.isOffloadedFilteringSupported()) {
+ return true;
+ }
+ final int callbackType = settings.getCallbackType();
+ // Only support regular scan if no offloaded filter support.
+ if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
+ && settings.getReportDelayMillis() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isSettingsAndFilterComboAllowed(ScanSettings settings,
+ List<ScanFilter> filterList) {
+ final int callbackType = settings.getCallbackType();
+ // If onlost/onfound is requested, a non-empty filter is expected
+ if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
+ | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) {
+ if (filterList == null) {
+ return false;
+ }
+ for (ScanFilter filter : filterList) {
+ if (filter.isAllFieldsEmpty()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) {
+ final int callbackType = settings.getCallbackType();
+ if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0
+ || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) {
+ // For onlost/onfound, we required hw support be available
+ return (mBluetoothAdapter.isOffloadedFilteringSupported()
+ && mBluetoothAdapter.isHardwareTrackingFiltersAvailable());
+ }
+ return true;
+ }
+}
diff --git a/framework/java/android/bluetooth/le/BluetoothLeUtils.java b/framework/java/android/bluetooth/le/BluetoothLeUtils.java
new file mode 100644
index 0000000000..ed50b09597
--- /dev/null
+++ b/framework/java/android/bluetooth/le/BluetoothLeUtils.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.bluetooth.BluetoothAdapter;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Helper class for Bluetooth LE utils.
+ *
+ * @hide
+ */
+public class BluetoothLeUtils {
+
+ /**
+ * Returns a string composed from a {@link SparseArray}.
+ */
+ static String toString(SparseArray<byte[]> array) {
+ if (array == null) {
+ return "null";
+ }
+ if (array.size() == 0) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder();
+ buffer.append('{');
+ for (int i = 0; i < array.size(); ++i) {
+ buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i)));
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ /**
+ * Returns a string composed from a {@link Map}.
+ */
+ static <T> String toString(Map<T, byte[]> map) {
+ if (map == null) {
+ return "null";
+ }
+ if (map.isEmpty()) {
+ return "{}";
+ }
+ StringBuilder buffer = new StringBuilder();
+ buffer.append('{');
+ Iterator<Map.Entry<T, byte[]>> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<T, byte[]> entry = it.next();
+ Object key = entry.getKey();
+ buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
+ if (it.hasNext()) {
+ buffer.append(", ");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ /**
+ * Check whether two {@link SparseArray} equal.
+ */
+ static boolean equals(SparseArray<byte[]> array, SparseArray<byte[]> otherArray) {
+ if (array == otherArray) {
+ return true;
+ }
+ if (array == null || otherArray == null) {
+ return false;
+ }
+ if (array.size() != otherArray.size()) {
+ return false;
+ }
+
+ // Keys are guaranteed in ascending order when indices are in ascending order.
+ for (int i = 0; i < array.size(); ++i) {
+ if (array.keyAt(i) != otherArray.keyAt(i)
+ || !Arrays.equals(array.valueAt(i), otherArray.valueAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check whether two {@link Map} equal.
+ */
+ static <T> boolean equals(Map<T, byte[]> map, Map<T, byte[]> otherMap) {
+ if (map == otherMap) {
+ return true;
+ }
+ if (map == null || otherMap == null) {
+ return false;
+ }
+ if (map.size() != otherMap.size()) {
+ return false;
+ }
+ Set<T> keys = map.keySet();
+ if (!keys.equals(otherMap.keySet())) {
+ return false;
+ }
+ for (T key : keys) {
+ if (!Objects.deepEquals(map.get(key), otherMap.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Ensure Bluetooth is turned on.
+ *
+ * @throws IllegalStateException If {@code adapter} is null or Bluetooth state is not {@link
+ * BluetoothAdapter#STATE_ON}.
+ */
+ static void checkAdapterStateOn(BluetoothAdapter adapter) {
+ if (adapter == null || !adapter.isLeEnabled()) {
+ throw new IllegalStateException("BT Adapter is not turned ON");
+ }
+ }
+
+ /**
+ * Compares two UUIDs with a UUID mask.
+ *
+ * @param data first {@link #UUID} to compare.
+ * @param uuid second {@link #UUID} to compare.
+ * @param mask mask {@link #UUID}.
+ * @return true if both UUIDs are equals when masked, false otherwise.
+ */
+ static boolean maskedEquals(UUID data, UUID uuid, UUID mask) {
+ if (mask == null) {
+ return Objects.equals(data, uuid);
+ }
+ return (data.getLeastSignificantBits() & mask.getLeastSignificantBits())
+ == (uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
+ && (data.getMostSignificantBits() & mask.getMostSignificantBits())
+ == (uuid.getMostSignificantBits() & mask.getMostSignificantBits());
+ }
+}
diff --git a/framework/java/android/bluetooth/le/OWNERS b/framework/java/android/bluetooth/le/OWNERS
new file mode 100644
index 0000000000..3523ee0640
--- /dev/null
+++ b/framework/java/android/bluetooth/le/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 27441
+
+zachoverflow@google.com
+siyuanh@google.com
diff --git a/framework/java/android/bluetooth/le/PeriodicAdvertisingCallback.java b/framework/java/android/bluetooth/le/PeriodicAdvertisingCallback.java
new file mode 100644
index 0000000000..14ac911fcb
--- /dev/null
+++ b/framework/java/android/bluetooth/le/PeriodicAdvertisingCallback.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.bluetooth.BluetoothDevice;
+
+/**
+ * Bluetooth LE periodic advertising callbacks, used to deliver periodic
+ * advertising operation status.
+ *
+ * @hide
+ * @see PeriodicAdvertisingManager#createSync
+ */
+public abstract class PeriodicAdvertisingCallback {
+
+ /**
+ * The requested operation was successful.
+ *
+ * @hide
+ */
+ public static final int SYNC_SUCCESS = 0;
+
+ /**
+ * Sync failed to be established because remote device did not respond.
+ */
+ public static final int SYNC_NO_RESPONSE = 1;
+
+ /**
+ * Sync failed to be established because controller can't support more syncs.
+ */
+ public static final int SYNC_NO_RESOURCES = 2;
+
+
+ /**
+ * Callback when synchronization was established.
+ *
+ * @param syncHandle handle used to identify this synchronization.
+ * @param device remote device.
+ * @param advertisingSid synchronized advertising set id.
+ * @param skip The number of periodic advertising packets that can be skipped after a successful
+ * receive in force. @see PeriodicAdvertisingManager#createSync
+ * @param timeout Synchronization timeout for the periodic advertising in force. One unit is
+ * 10ms. @see PeriodicAdvertisingManager#createSync
+ * @param timeout
+ * @param status operation status.
+ */
+ public void onSyncEstablished(int syncHandle, BluetoothDevice device,
+ int advertisingSid, int skip, int timeout,
+ int status) {
+ }
+
+ /**
+ * Callback when periodic advertising report is received.
+ *
+ * @param report periodic advertising report.
+ */
+ public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) {
+ }
+
+ /**
+ * Callback when periodic advertising synchronization was lost.
+ *
+ * @param syncHandle handle used to identify this synchronization.
+ */
+ public void onSyncLost(int syncHandle) {
+ }
+}
diff --git a/framework/java/android/bluetooth/le/PeriodicAdvertisingManager.java b/framework/java/android/bluetooth/le/PeriodicAdvertisingManager.java
new file mode 100644
index 0000000000..bbd31170bb
--- /dev/null
+++ b/framework/java/android/bluetooth/le/PeriodicAdvertisingManager.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.bluetooth.Attributable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.annotations.RequiresBluetoothLocationPermission;
+import android.bluetooth.annotations.RequiresBluetoothScanPermission;
+import android.bluetooth.annotations.RequiresLegacyBluetoothAdminPermission;
+import android.content.AttributionSource;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class provides methods to perform periodic advertising related
+ * operations. An application can register for periodic advertisements using
+ * {@link PeriodicAdvertisingManager#registerSync}.
+ * <p>
+ * Use {@link BluetoothAdapter#getPeriodicAdvertisingManager()} to get an
+ * instance of {@link PeriodicAdvertisingManager}.
+ *
+ * @hide
+ */
+public final class PeriodicAdvertisingManager {
+
+ private static final String TAG = "PeriodicAdvertisingManager";
+
+ private static final int SKIP_MIN = 0;
+ private static final int SKIP_MAX = 499;
+ private static final int TIMEOUT_MIN = 10;
+ private static final int TIMEOUT_MAX = 16384;
+
+ private static final int SYNC_STARTING = -1;
+
+ private final BluetoothAdapter mBluetoothAdapter;
+ private final IBluetoothManager mBluetoothManager;
+ private final AttributionSource mAttributionSource;
+
+ /* maps callback, to callback wrapper and sync handle */
+ Map<PeriodicAdvertisingCallback,
+ IPeriodicAdvertisingCallback /* callbackWrapper */> mCallbackWrappers;
+
+ /**
+ * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead.
+ *
+ * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management.
+ * @hide
+ */
+ public PeriodicAdvertisingManager(BluetoothAdapter bluetoothAdapter) {
+ mBluetoothAdapter = Objects.requireNonNull(bluetoothAdapter);
+ mBluetoothManager = mBluetoothAdapter.getBluetoothManager();
+ mAttributionSource = mBluetoothAdapter.getAttributionSource();
+ mCallbackWrappers = new IdentityHashMap<>();
+ }
+
+ /**
+ * Synchronize with periodic advertising pointed to by the {@code scanResult}.
+ * The {@code scanResult} used must contain a valid advertisingSid. First
+ * call to registerSync will use the {@code skip} and {@code timeout} provided.
+ * Subsequent calls from other apps, trying to sync with same set will reuse
+ * existing sync, thus {@code skip} and {@code timeout} values will not take
+ * effect. The values in effect will be returned in
+ * {@link PeriodicAdvertisingCallback#onSyncEstablished}.
+ *
+ * @param scanResult Scan result containing advertisingSid.
+ * @param skip The number of periodic advertising packets that can be skipped after a successful
+ * receive. Must be between 0 and 499.
+ * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must
+ * be between 10 (100ms) and 16384 (163.84s).
+ * @param callback Callback used to deliver all operations status.
+ * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or
+ * {@code timeout} is invalid or {@code callback} is null.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void registerSync(ScanResult scanResult, int skip, int timeout,
+ PeriodicAdvertisingCallback callback) {
+ registerSync(scanResult, skip, timeout, callback, null);
+ }
+
+ /**
+ * Synchronize with periodic advertising pointed to by the {@code scanResult}.
+ * The {@code scanResult} used must contain a valid advertisingSid. First
+ * call to registerSync will use the {@code skip} and {@code timeout} provided.
+ * Subsequent calls from other apps, trying to sync with same set will reuse
+ * existing sync, thus {@code skip} and {@code timeout} values will not take
+ * effect. The values in effect will be returned in
+ * {@link PeriodicAdvertisingCallback#onSyncEstablished}.
+ *
+ * @param scanResult Scan result containing advertisingSid.
+ * @param skip The number of periodic advertising packets that can be skipped after a successful
+ * receive. Must be between 0 and 499.
+ * @param timeout Synchronization timeout for the periodic advertising. One unit is 10ms. Must
+ * be between 10 (100ms) and 16384 (163.84s).
+ * @param callback Callback used to deliver all operations status.
+ * @param handler thread upon which the callbacks will be invoked.
+ * @throws IllegalArgumentException if {@code scanResult} is null or {@code skip} is invalid or
+ * {@code timeout} is invalid or {@code callback} is null.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresBluetoothLocationPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void registerSync(ScanResult scanResult, int skip, int timeout,
+ PeriodicAdvertisingCallback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback can't be null");
+ }
+
+ if (scanResult == null) {
+ throw new IllegalArgumentException("scanResult can't be null");
+ }
+
+ if (scanResult.getAdvertisingSid() == ScanResult.SID_NOT_PRESENT) {
+ throw new IllegalArgumentException("scanResult must contain a valid sid");
+ }
+
+ if (skip < SKIP_MIN || skip > SKIP_MAX) {
+ throw new IllegalArgumentException(
+ "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX);
+ }
+
+ if (timeout < TIMEOUT_MIN || timeout > TIMEOUT_MAX) {
+ throw new IllegalArgumentException(
+ "timeout must be between " + TIMEOUT_MIN + " and " + TIMEOUT_MAX);
+ }
+
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get Bluetooth gatt - ", e);
+ callback.onSyncEstablished(0, scanResult.getDevice(), scanResult.getAdvertisingSid(),
+ skip, timeout,
+ PeriodicAdvertisingCallback.SYNC_NO_RESOURCES);
+ return;
+ }
+
+ if (handler == null) {
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ IPeriodicAdvertisingCallback wrapped = wrap(callback, handler);
+ mCallbackWrappers.put(callback, wrapped);
+
+ try {
+ gatt.registerSync(
+ scanResult, skip, timeout, wrapped, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register sync - ", e);
+ return;
+ }
+ }
+
+ /**
+ * Cancel pending attempt to create sync, or terminate existing sync.
+ *
+ * @param callback Callback used to deliver all operations status.
+ * @throws IllegalArgumentException if {@code callback} is null, or not a properly registered
+ * callback.
+ */
+ @RequiresLegacyBluetoothAdminPermission
+ @RequiresBluetoothScanPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_SCAN)
+ public void unregisterSync(PeriodicAdvertisingCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback can't be null");
+ }
+
+ IBluetoothGatt gatt;
+ try {
+ gatt = mBluetoothManager.getBluetoothGatt();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to get Bluetooth gatt - ", e);
+ return;
+ }
+
+ IPeriodicAdvertisingCallback wrapper = mCallbackWrappers.remove(callback);
+ if (wrapper == null) {
+ throw new IllegalArgumentException("callback was not properly registered");
+ }
+
+ try {
+ gatt.unregisterSync(wrapper, mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to cancel sync creation - ", e);
+ return;
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkBluetoothPermission")
+ private IPeriodicAdvertisingCallback wrap(PeriodicAdvertisingCallback callback,
+ Handler handler) {
+ return new IPeriodicAdvertisingCallback.Stub() {
+ public void onSyncEstablished(int syncHandle, BluetoothDevice device,
+ int advertisingSid, int skip, int timeout, int status) {
+ Attributable.setAttributionSource(device, mAttributionSource);
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onSyncEstablished(syncHandle, device, advertisingSid, skip,
+ timeout,
+ status);
+
+ if (status != PeriodicAdvertisingCallback.SYNC_SUCCESS) {
+ // App can still unregister the sync until notified it failed. Remove
+ // callback
+ // after app was notifed.
+ mCallbackWrappers.remove(callback);
+ }
+ }
+ });
+ }
+
+ public void onPeriodicAdvertisingReport(PeriodicAdvertisingReport report) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onPeriodicAdvertisingReport(report);
+ }
+ });
+ }
+
+ public void onSyncLost(int syncHandle) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onSyncLost(syncHandle);
+ // App can still unregister the sync until notified it's lost.
+ // Remove callback after app was notifed.
+ mCallbackWrappers.remove(callback);
+ }
+ });
+ }
+ };
+ }
+}
diff --git a/framework/java/android/bluetooth/le/PeriodicAdvertisingParameters.java b/framework/java/android/bluetooth/le/PeriodicAdvertisingParameters.java
new file mode 100644
index 0000000000..4e64dbed70
--- /dev/null
+++ b/framework/java/android/bluetooth/le/PeriodicAdvertisingParameters.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The {@link PeriodicAdvertisingParameters} provide a way to adjust periodic
+ * advertising preferences for each Bluetooth LE advertising set. Use {@link
+ * PeriodicAdvertisingParameters.Builder} to create an instance of this class.
+ */
+public final class PeriodicAdvertisingParameters implements Parcelable {
+
+ private static final int INTERVAL_MIN = 80;
+ private static final int INTERVAL_MAX = 65519;
+
+ private final boolean mIncludeTxPower;
+ private final int mInterval;
+
+ private PeriodicAdvertisingParameters(boolean includeTxPower, int interval) {
+ mIncludeTxPower = includeTxPower;
+ mInterval = interval;
+ }
+
+ private PeriodicAdvertisingParameters(Parcel in) {
+ mIncludeTxPower = in.readInt() != 0;
+ mInterval = in.readInt();
+ }
+
+ /**
+ * Returns whether the TX Power will be included.
+ */
+ public boolean getIncludeTxPower() {
+ return mIncludeTxPower;
+ }
+
+ /**
+ * Returns the periodic advertising interval, in 1.25ms unit.
+ * Valid values are from 80 (100ms) to 65519 (81.89875s).
+ */
+ public int getInterval() {
+ return mInterval;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mIncludeTxPower ? 1 : 0);
+ dest.writeInt(mInterval);
+ }
+
+ public static final Parcelable
+ .Creator<PeriodicAdvertisingParameters> CREATOR =
+ new Creator<PeriodicAdvertisingParameters>() {
+ @Override
+ public PeriodicAdvertisingParameters[] newArray(int size) {
+ return new PeriodicAdvertisingParameters[size];
+ }
+
+ @Override
+ public PeriodicAdvertisingParameters createFromParcel(Parcel in) {
+ return new PeriodicAdvertisingParameters(in);
+ }
+ };
+
+ public static final class Builder {
+ private boolean mIncludeTxPower = false;
+ private int mInterval = INTERVAL_MAX;
+
+ /**
+ * Whether the transmission power level should be included in the periodic
+ * packet.
+ */
+ public Builder setIncludeTxPower(boolean includeTxPower) {
+ mIncludeTxPower = includeTxPower;
+ return this;
+ }
+
+ /**
+ * Set advertising interval for periodic advertising, in 1.25ms unit.
+ * Valid values are from 80 (100ms) to 65519 (81.89875s).
+ * Value from range [interval, interval+20ms] will be picked as the actual value.
+ *
+ * @throws IllegalArgumentException If the interval is invalid.
+ */
+ public Builder setInterval(int interval) {
+ if (interval < INTERVAL_MIN || interval > INTERVAL_MAX) {
+ throw new IllegalArgumentException("Invalid interval (must be " + INTERVAL_MIN
+ + "-" + INTERVAL_MAX + ")");
+ }
+ mInterval = interval;
+ return this;
+ }
+
+ /**
+ * Build the {@link AdvertisingSetParameters} object.
+ */
+ public PeriodicAdvertisingParameters build() {
+ return new PeriodicAdvertisingParameters(mIncludeTxPower, mInterval);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/PeriodicAdvertisingReport.java b/framework/java/android/bluetooth/le/PeriodicAdvertisingReport.java
new file mode 100644
index 0000000000..54b953c25c
--- /dev/null
+++ b/framework/java/android/bluetooth/le/PeriodicAdvertisingReport.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2017 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.le;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * PeriodicAdvertisingReport for Bluetooth LE synchronized advertising.
+ *
+ * @hide
+ */
+public final class PeriodicAdvertisingReport implements Parcelable {
+
+ /**
+ * The data returned is complete
+ */
+ public static final int DATA_COMPLETE = 0;
+
+ /**
+ * The data returned is incomplete. The controller was unsuccessfull to
+ * receive all chained packets, returning only partial data.
+ */
+ public static final int DATA_INCOMPLETE_TRUNCATED = 2;
+
+ private int mSyncHandle;
+ private int mTxPower;
+ private int mRssi;
+ private int mDataStatus;
+
+ // periodic advertising data.
+ @Nullable
+ private ScanRecord mData;
+
+ // Device timestamp when the result was last seen.
+ private long mTimestampNanos;
+
+ /**
+ * Constructor of periodic advertising result.
+ */
+ public PeriodicAdvertisingReport(int syncHandle, int txPower, int rssi,
+ int dataStatus, ScanRecord data) {
+ mSyncHandle = syncHandle;
+ mTxPower = txPower;
+ mRssi = rssi;
+ mDataStatus = dataStatus;
+ mData = data;
+ }
+
+ private PeriodicAdvertisingReport(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSyncHandle);
+ dest.writeInt(mTxPower);
+ dest.writeInt(mRssi);
+ dest.writeInt(mDataStatus);
+ if (mData != null) {
+ dest.writeInt(1);
+ dest.writeByteArray(mData.getBytes());
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ private void readFromParcel(Parcel in) {
+ mSyncHandle = in.readInt();
+ mTxPower = in.readInt();
+ mRssi = in.readInt();
+ mDataStatus = in.readInt();
+ if (in.readInt() == 1) {
+ mData = ScanRecord.parseFromBytes(in.createByteArray());
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Returns the synchronization handle.
+ */
+ public int getSyncHandle() {
+ return mSyncHandle;
+ }
+
+ /**
+ * Returns the transmit power in dBm. The valid range is [-127, 126]. Value
+ * of 127 means information was not available.
+ */
+ public int getTxPower() {
+ return mTxPower;
+ }
+
+ /**
+ * Returns the received signal strength in dBm. The valid range is [-127, 20].
+ */
+ public int getRssi() {
+ return mRssi;
+ }
+
+ /**
+ * Returns the data status. Can be one of {@link PeriodicAdvertisingReport#DATA_COMPLETE}
+ * or {@link PeriodicAdvertisingReport#DATA_INCOMPLETE_TRUNCATED}.
+ */
+ public int getDataStatus() {
+ return mDataStatus;
+ }
+
+ /**
+ * Returns the data contained in this periodic advertising report.
+ */
+ @Nullable
+ public ScanRecord getData() {
+ return mData;
+ }
+
+ /**
+ * Returns timestamp since boot when the scan record was observed.
+ */
+ public long getTimestampNanos() {
+ return mTimestampNanos;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSyncHandle, mTxPower, mRssi, mDataStatus, mData, mTimestampNanos);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ PeriodicAdvertisingReport other = (PeriodicAdvertisingReport) obj;
+ return (mSyncHandle == other.mSyncHandle)
+ && (mTxPower == other.mTxPower)
+ && (mRssi == other.mRssi)
+ && (mDataStatus == other.mDataStatus)
+ && Objects.equals(mData, other.mData)
+ && (mTimestampNanos == other.mTimestampNanos);
+ }
+
+ @Override
+ public String toString() {
+ return "PeriodicAdvertisingReport{syncHandle=" + mSyncHandle
+ + ", txPower=" + mTxPower + ", rssi=" + mRssi + ", dataStatus=" + mDataStatus
+ + ", data=" + Objects.toString(mData) + ", timestampNanos=" + mTimestampNanos + '}';
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<PeriodicAdvertisingReport> CREATOR =
+ new Creator<PeriodicAdvertisingReport>() {
+ @Override
+ public PeriodicAdvertisingReport createFromParcel(Parcel source) {
+ return new PeriodicAdvertisingReport(source);
+ }
+
+ @Override
+ public PeriodicAdvertisingReport[] newArray(int size) {
+ return new PeriodicAdvertisingReport[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/le/ResultStorageDescriptor.java b/framework/java/android/bluetooth/le/ResultStorageDescriptor.java
new file mode 100644
index 0000000000..f65048975d
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ResultStorageDescriptor.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Describes the way to store scan result.
+ *
+ * @deprecated this is not used anywhere
+ *
+ * @hide
+ */
+@Deprecated
+@SystemApi
+public final class ResultStorageDescriptor implements Parcelable {
+ private int mType;
+ private int mOffset;
+ private int mLength;
+
+ public int getType() {
+ return mType;
+ }
+
+ public int getOffset() {
+ return mOffset;
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ /**
+ * Constructor of {@link ResultStorageDescriptor}
+ *
+ * @param type Type of the data.
+ * @param offset Offset from start of the advertise packet payload.
+ * @param length Byte length of the data
+ */
+ public ResultStorageDescriptor(int type, int offset, int length) {
+ mType = type;
+ mOffset = offset;
+ mLength = length;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mOffset);
+ dest.writeInt(mLength);
+ }
+
+ private ResultStorageDescriptor(Parcel in) {
+ ReadFromParcel(in);
+ }
+
+ private void ReadFromParcel(Parcel in) {
+ mType = in.readInt();
+ mOffset = in.readInt();
+ mLength = in.readInt();
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ResultStorageDescriptor> CREATOR =
+ new Creator<ResultStorageDescriptor>() {
+ @Override
+ public ResultStorageDescriptor createFromParcel(Parcel source) {
+ return new ResultStorageDescriptor(source);
+ }
+
+ @Override
+ public ResultStorageDescriptor[] newArray(int size) {
+ return new ResultStorageDescriptor[size];
+ }
+ };
+}
diff --git a/framework/java/android/bluetooth/le/ScanCallback.java b/framework/java/android/bluetooth/le/ScanCallback.java
new file mode 100644
index 0000000000..53d9310a12
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ScanCallback.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import java.util.List;
+
+/**
+ * Bluetooth LE scan callbacks. Scan results are reported using these callbacks.
+ *
+ * @see BluetoothLeScanner#startScan
+ */
+public abstract class ScanCallback {
+ /**
+ * Fails to start scan as BLE scan with the same settings is already started by the app.
+ */
+ public static final int SCAN_FAILED_ALREADY_STARTED = 1;
+
+ /**
+ * Fails to start scan as app cannot be registered.
+ */
+ public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2;
+
+ /**
+ * Fails to start scan due an internal error
+ */
+ public static final int SCAN_FAILED_INTERNAL_ERROR = 3;
+
+ /**
+ * Fails to start power optimized scan as this feature is not supported.
+ */
+ public static final int SCAN_FAILED_FEATURE_UNSUPPORTED = 4;
+
+ /**
+ * Fails to start scan as it is out of hardware resources.
+ *
+ * @hide
+ */
+ public static final int SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES = 5;
+
+ /**
+ * Fails to start scan as application tries to scan too frequently.
+ * @hide
+ */
+ public static final int SCAN_FAILED_SCANNING_TOO_FREQUENTLY = 6;
+
+ static final int NO_ERROR = 0;
+
+ /**
+ * Callback when a BLE advertisement has been found.
+ *
+ * @param callbackType Determines how this callback was triggered. Could be one of {@link
+ * ScanSettings#CALLBACK_TYPE_ALL_MATCHES}, {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or
+ * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST}
+ * @param result A Bluetooth LE scan result.
+ */
+ public void onScanResult(int callbackType, ScanResult result) {
+ }
+
+ /**
+ * Callback when batch results are delivered.
+ *
+ * @param results List of scan results that are previously scanned.
+ */
+ public void onBatchScanResults(List<ScanResult> results) {
+ }
+
+ /**
+ * Callback when scan could not be started.
+ *
+ * @param errorCode Error code (one of SCAN_FAILED_*) for scan failure.
+ */
+ public void onScanFailed(int errorCode) {
+ }
+}
diff --git a/framework/java/android/bluetooth/le/ScanFilter.java b/framework/java/android/bluetooth/le/ScanFilter.java
new file mode 100644
index 0000000000..b059193ae0
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ScanFilter.java
@@ -0,0 +1,910 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothDevice.AddressType;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to
+ * restrict scan results to only those that are of interest to them.
+ * <p>
+ * Current filtering on the following fields are supported:
+ * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
+ * <li>Name of remote Bluetooth LE device.
+ * <li>Mac address of the remote device.
+ * <li>Service data which is the data associated with a service.
+ * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
+ *
+ * @see ScanResult
+ * @see BluetoothLeScanner
+ */
+public final class ScanFilter implements Parcelable {
+
+ @Nullable
+ private final String mDeviceName;
+
+ @Nullable
+ private final String mDeviceAddress;
+
+ private final @AddressType int mAddressType;
+
+ @Nullable
+ private final byte[] mIrk;
+
+ @Nullable
+ private final ParcelUuid mServiceUuid;
+ @Nullable
+ private final ParcelUuid mServiceUuidMask;
+
+ @Nullable
+ private final ParcelUuid mServiceSolicitationUuid;
+ @Nullable
+ private final ParcelUuid mServiceSolicitationUuidMask;
+
+ @Nullable
+ private final ParcelUuid mServiceDataUuid;
+ @Nullable
+ private final byte[] mServiceData;
+ @Nullable
+ private final byte[] mServiceDataMask;
+
+ private final int mManufacturerId;
+ @Nullable
+ private final byte[] mManufacturerData;
+ @Nullable
+ private final byte[] mManufacturerDataMask;
+
+ /** @hide */
+ public static final ScanFilter EMPTY = new ScanFilter.Builder().build();
+
+ private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
+ ParcelUuid uuidMask, ParcelUuid solicitationUuid,
+ ParcelUuid solicitationUuidMask, ParcelUuid serviceDataUuid,
+ byte[] serviceData, byte[] serviceDataMask,
+ int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask,
+ @AddressType int addressType, @Nullable byte[] irk) {
+ mDeviceName = name;
+ mServiceUuid = uuid;
+ mServiceUuidMask = uuidMask;
+ mServiceSolicitationUuid = solicitationUuid;
+ mServiceSolicitationUuidMask = solicitationUuidMask;
+ mDeviceAddress = deviceAddress;
+ mServiceDataUuid = serviceDataUuid;
+ mServiceData = serviceData;
+ mServiceDataMask = serviceDataMask;
+ mManufacturerId = manufacturerId;
+ mManufacturerData = manufacturerData;
+ mManufacturerDataMask = manufacturerDataMask;
+ mAddressType = addressType;
+ mIrk = irk;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDeviceName == null ? 0 : 1);
+ if (mDeviceName != null) {
+ dest.writeString(mDeviceName);
+ }
+ dest.writeInt(mDeviceAddress == null ? 0 : 1);
+ if (mDeviceAddress != null) {
+ dest.writeString(mDeviceAddress);
+ }
+ dest.writeInt(mServiceUuid == null ? 0 : 1);
+ if (mServiceUuid != null) {
+ dest.writeParcelable(mServiceUuid, flags);
+ dest.writeInt(mServiceUuidMask == null ? 0 : 1);
+ if (mServiceUuidMask != null) {
+ dest.writeParcelable(mServiceUuidMask, flags);
+ }
+ }
+ dest.writeInt(mServiceSolicitationUuid == null ? 0 : 1);
+ if (mServiceSolicitationUuid != null) {
+ dest.writeParcelable(mServiceSolicitationUuid, flags);
+ dest.writeInt(mServiceSolicitationUuidMask == null ? 0 : 1);
+ if (mServiceSolicitationUuidMask != null) {
+ dest.writeParcelable(mServiceSolicitationUuidMask, flags);
+ }
+ }
+ dest.writeInt(mServiceDataUuid == null ? 0 : 1);
+ if (mServiceDataUuid != null) {
+ dest.writeParcelable(mServiceDataUuid, flags);
+ dest.writeInt(mServiceData == null ? 0 : 1);
+ if (mServiceData != null) {
+ dest.writeInt(mServiceData.length);
+ dest.writeByteArray(mServiceData);
+
+ dest.writeInt(mServiceDataMask == null ? 0 : 1);
+ if (mServiceDataMask != null) {
+ dest.writeInt(mServiceDataMask.length);
+ dest.writeByteArray(mServiceDataMask);
+ }
+ }
+ }
+ dest.writeInt(mManufacturerId);
+ dest.writeInt(mManufacturerData == null ? 0 : 1);
+ if (mManufacturerData != null) {
+ dest.writeInt(mManufacturerData.length);
+ dest.writeByteArray(mManufacturerData);
+
+ dest.writeInt(mManufacturerDataMask == null ? 0 : 1);
+ if (mManufacturerDataMask != null) {
+ dest.writeInt(mManufacturerDataMask.length);
+ dest.writeByteArray(mManufacturerDataMask);
+ }
+ }
+
+ // IRK
+ if (mDeviceAddress != null) {
+ dest.writeInt(mAddressType);
+ dest.writeInt(mIrk == null ? 0 : 1);
+ if (mIrk != null) {
+ dest.writeByteArray(mIrk);
+ }
+ }
+ }
+
+ /**
+ * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel.
+ */
+ public static final @android.annotation.NonNull Creator<ScanFilter> CREATOR =
+ new Creator<ScanFilter>() {
+
+ @Override
+ public ScanFilter[] newArray(int size) {
+ return new ScanFilter[size];
+ }
+
+ @Override
+ public ScanFilter createFromParcel(Parcel in) {
+ Builder builder = new Builder();
+ if (in.readInt() == 1) {
+ builder.setDeviceName(in.readString());
+ }
+ String address = null;
+ // If we have a non-null address
+ if (in.readInt() == 1) {
+ address = in.readString();
+ }
+ if (in.readInt() == 1) {
+ ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader());
+ builder.setServiceUuid(uuid);
+ if (in.readInt() == 1) {
+ ParcelUuid uuidMask = in.readParcelable(
+ ParcelUuid.class.getClassLoader());
+ builder.setServiceUuid(uuid, uuidMask);
+ }
+ }
+ if (in.readInt() == 1) {
+ ParcelUuid solicitationUuid = in.readParcelable(
+ ParcelUuid.class.getClassLoader());
+ builder.setServiceSolicitationUuid(solicitationUuid);
+ if (in.readInt() == 1) {
+ ParcelUuid solicitationUuidMask = in.readParcelable(
+ ParcelUuid.class.getClassLoader());
+ builder.setServiceSolicitationUuid(solicitationUuid,
+ solicitationUuidMask);
+ }
+ }
+ if (in.readInt() == 1) {
+ ParcelUuid servcieDataUuid =
+ in.readParcelable(ParcelUuid.class.getClassLoader());
+ if (in.readInt() == 1) {
+ int serviceDataLength = in.readInt();
+ byte[] serviceData = new byte[serviceDataLength];
+ in.readByteArray(serviceData);
+ if (in.readInt() == 0) {
+ builder.setServiceData(servcieDataUuid, serviceData);
+ } else {
+ int serviceDataMaskLength = in.readInt();
+ byte[] serviceDataMask = new byte[serviceDataMaskLength];
+ in.readByteArray(serviceDataMask);
+ builder.setServiceData(
+ servcieDataUuid, serviceData, serviceDataMask);
+ }
+ }
+ }
+
+ int manufacturerId = in.readInt();
+ if (in.readInt() == 1) {
+ int manufacturerDataLength = in.readInt();
+ byte[] manufacturerData = new byte[manufacturerDataLength];
+ in.readByteArray(manufacturerData);
+ if (in.readInt() == 0) {
+ builder.setManufacturerData(manufacturerId, manufacturerData);
+ } else {
+ int manufacturerDataMaskLength = in.readInt();
+ byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
+ in.readByteArray(manufacturerDataMask);
+ builder.setManufacturerData(manufacturerId, manufacturerData,
+ manufacturerDataMask);
+ }
+ }
+
+ // IRK
+ if (address != null) {
+ final int addressType = in.readInt();
+ if (in.readInt() == 1) {
+ final byte[] irk = new byte[16];
+ in.readByteArray(irk);
+ builder.setDeviceAddress(address, addressType, irk);
+ } else {
+ builder.setDeviceAddress(address, addressType);
+ }
+ }
+ return builder.build();
+ }
+ };
+
+ /**
+ * Returns the filter set the device name field of Bluetooth advertisement data.
+ */
+ @Nullable
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * Returns the filter set on the service uuid.
+ */
+ @Nullable
+ public ParcelUuid getServiceUuid() {
+ return mServiceUuid;
+ }
+
+ @Nullable
+ public ParcelUuid getServiceUuidMask() {
+ return mServiceUuidMask;
+ }
+
+ /**
+ * Returns the filter set on the service Solicitation uuid.
+ */
+ @Nullable
+ public ParcelUuid getServiceSolicitationUuid() {
+ return mServiceSolicitationUuid;
+ }
+
+ /**
+ * Returns the filter set on the service Solicitation uuid mask.
+ */
+ @Nullable
+ public ParcelUuid getServiceSolicitationUuidMask() {
+ return mServiceSolicitationUuidMask;
+ }
+
+ @Nullable
+ public String getDeviceAddress() {
+ return mDeviceAddress;
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public @AddressType int getAddressType() {
+ return mAddressType;
+ }
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ @Nullable
+ public byte[] getIrk() {
+ return mIrk;
+ }
+
+ @Nullable
+ public byte[] getServiceData() {
+ return mServiceData;
+ }
+
+ @Nullable
+ public byte[] getServiceDataMask() {
+ return mServiceDataMask;
+ }
+
+ @Nullable
+ public ParcelUuid getServiceDataUuid() {
+ return mServiceDataUuid;
+ }
+
+ /**
+ * Returns the manufacturer id. -1 if the manufacturer filter is not set.
+ */
+ public int getManufacturerId() {
+ return mManufacturerId;
+ }
+
+ @Nullable
+ public byte[] getManufacturerData() {
+ return mManufacturerData;
+ }
+
+ @Nullable
+ public byte[] getManufacturerDataMask() {
+ return mManufacturerDataMask;
+ }
+
+ /**
+ * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match
+ * if it matches all the field filters.
+ */
+ public boolean matches(ScanResult scanResult) {
+ if (scanResult == null) {
+ return false;
+ }
+ BluetoothDevice device = scanResult.getDevice();
+ // Device match.
+ if (mDeviceAddress != null
+ && (device == null || !mDeviceAddress.equals(device.getAddress()))) {
+ return false;
+ }
+
+ ScanRecord scanRecord = scanResult.getScanRecord();
+
+ // Scan record is null but there exist filters on it.
+ if (scanRecord == null
+ && (mDeviceName != null || mServiceUuid != null || mManufacturerData != null
+ || mServiceData != null || mServiceSolicitationUuid != null)) {
+ return false;
+ }
+
+ // Local name match.
+ if (mDeviceName != null && !mDeviceName.equals(scanRecord.getDeviceName())) {
+ return false;
+ }
+
+ // UUID match.
+ if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
+ scanRecord.getServiceUuids())) {
+ return false;
+ }
+
+ // solicitation UUID match.
+ if (mServiceSolicitationUuid != null && !matchesServiceSolicitationUuids(
+ mServiceSolicitationUuid, mServiceSolicitationUuidMask,
+ scanRecord.getServiceSolicitationUuids())) {
+ return false;
+ }
+
+ // Service data match
+ if (mServiceDataUuid != null) {
+ if (!matchesPartialData(mServiceData, mServiceDataMask,
+ scanRecord.getServiceData(mServiceDataUuid))) {
+ return false;
+ }
+ }
+
+ // Manufacturer data match.
+ if (mManufacturerId >= 0) {
+ if (!matchesPartialData(mManufacturerData, mManufacturerDataMask,
+ scanRecord.getManufacturerSpecificData(mManufacturerId))) {
+ return false;
+ }
+ }
+ // All filters match.
+ return true;
+ }
+
+ /**
+ * Check if the uuid pattern is contained in a list of parcel uuids.
+ *
+ * @hide
+ */
+ public static boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask,
+ List<ParcelUuid> uuids) {
+ if (uuid == null) {
+ return true;
+ }
+ if (uuids == null) {
+ return false;
+ }
+
+ for (ParcelUuid parcelUuid : uuids) {
+ UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
+ if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if the uuid pattern matches the particular service uuid.
+ private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
+ return BluetoothLeUtils.maskedEquals(data, uuid, mask);
+ }
+
+ /**
+ * Check if the solicitation uuid pattern is contained in a list of parcel uuids.
+ *
+ */
+ private static boolean matchesServiceSolicitationUuids(ParcelUuid solicitationUuid,
+ ParcelUuid parcelSolicitationUuidMask, List<ParcelUuid> solicitationUuids) {
+ if (solicitationUuid == null) {
+ return true;
+ }
+ if (solicitationUuids == null) {
+ return false;
+ }
+
+ for (ParcelUuid parcelSolicitationUuid : solicitationUuids) {
+ UUID solicitationUuidMask = parcelSolicitationUuidMask == null
+ ? null : parcelSolicitationUuidMask.getUuid();
+ if (matchesServiceUuid(solicitationUuid.getUuid(), solicitationUuidMask,
+ parcelSolicitationUuid.getUuid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if the solicitation uuid pattern matches the particular service solicitation uuid.
+ private static boolean matchesServiceSolicitationUuid(UUID solicitationUuid,
+ UUID solicitationUuidMask, UUID data) {
+ return BluetoothLeUtils.maskedEquals(data, solicitationUuid, solicitationUuidMask);
+ }
+
+ // Check whether the data pattern matches the parsed data.
+ private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) {
+ if (parsedData == null || parsedData.length < data.length) {
+ return false;
+ }
+ if (dataMask == null) {
+ for (int i = 0; i < data.length; ++i) {
+ if (parsedData[i] != data[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ for (int i = 0; i < data.length; ++i) {
+ if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "BluetoothLeScanFilter [mDeviceName=" + mDeviceName + ", mDeviceAddress="
+ + mDeviceAddress
+ + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask
+ + ", mServiceSolicitationUuid=" + mServiceSolicitationUuid
+ + ", mServiceSolicitationUuidMask=" + mServiceSolicitationUuidMask
+ + ", mServiceDataUuid=" + Objects.toString(mServiceDataUuid) + ", mServiceData="
+ + Arrays.toString(mServiceData) + ", mServiceDataMask="
+ + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId
+ + ", mManufacturerData=" + Arrays.toString(mManufacturerData)
+ + ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + "]";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceName, mDeviceAddress, mManufacturerId,
+ Arrays.hashCode(mManufacturerData),
+ Arrays.hashCode(mManufacturerDataMask),
+ mServiceDataUuid,
+ Arrays.hashCode(mServiceData),
+ Arrays.hashCode(mServiceDataMask),
+ mServiceUuid, mServiceUuidMask,
+ mServiceSolicitationUuid, mServiceSolicitationUuidMask);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ScanFilter other = (ScanFilter) obj;
+ return Objects.equals(mDeviceName, other.mDeviceName)
+ && Objects.equals(mDeviceAddress, other.mDeviceAddress)
+ && mManufacturerId == other.mManufacturerId
+ && Objects.deepEquals(mManufacturerData, other.mManufacturerData)
+ && Objects.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask)
+ && Objects.equals(mServiceDataUuid, other.mServiceDataUuid)
+ && Objects.deepEquals(mServiceData, other.mServiceData)
+ && Objects.deepEquals(mServiceDataMask, other.mServiceDataMask)
+ && Objects.equals(mServiceUuid, other.mServiceUuid)
+ && Objects.equals(mServiceUuidMask, other.mServiceUuidMask)
+ && Objects.equals(mServiceSolicitationUuid, other.mServiceSolicitationUuid)
+ && Objects.equals(mServiceSolicitationUuidMask,
+ other.mServiceSolicitationUuidMask);
+ }
+
+ /**
+ * Checks if the scanfilter is empty
+ *
+ * @hide
+ */
+ public boolean isAllFieldsEmpty() {
+ return EMPTY.equals(this);
+ }
+
+ /**
+ * Builder class for {@link ScanFilter}.
+ */
+ public static final class Builder {
+
+ /**
+ * @hide
+ */
+ @SystemApi
+ public static final int LEN_IRK_OCTETS = 16;
+
+ private String mDeviceName;
+ private String mDeviceAddress;
+ private @AddressType int mAddressType = BluetoothDevice.ADDRESS_TYPE_PUBLIC;
+ private byte[] mIrk;
+
+ private ParcelUuid mServiceUuid;
+ private ParcelUuid mUuidMask;
+
+ private ParcelUuid mServiceSolicitationUuid;
+ private ParcelUuid mServiceSolicitationUuidMask;
+
+ private ParcelUuid mServiceDataUuid;
+ private byte[] mServiceData;
+ private byte[] mServiceDataMask;
+
+ private int mManufacturerId = -1;
+ private byte[] mManufacturerData;
+ private byte[] mManufacturerDataMask;
+
+ /**
+ * Set filter on device name.
+ */
+ public Builder setDeviceName(String deviceName) {
+ mDeviceName = deviceName;
+ return this;
+ }
+
+ /**
+ * Set filter on device address.
+ *
+ * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+ * format of "01:02:03:AB:CD:EF". The device address can be validated using {@link
+ * BluetoothAdapter#checkBluetoothAddress}. The @AddressType is defaulted to {@link
+ * BluetoothDevice#ADDRESS_TYPE_PUBLIC}
+ * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+ */
+ public Builder setDeviceAddress(String deviceAddress) {
+ if (deviceAddress == null) {
+ mDeviceAddress = deviceAddress;
+ return this;
+ }
+ return setDeviceAddress(deviceAddress, BluetoothDevice.ADDRESS_TYPE_PUBLIC);
+ }
+
+ /**
+ * Set filter on Address with AddressType
+ *
+ * <p>This key is used to resolve a private address from a public address.
+ *
+ * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+ * format of "01:02:03:AB:CD:EF". The device address can be validated using {@link
+ * BluetoothAdapter#checkBluetoothAddress}. May be any type of address.
+ * @param addressType indication of the type of address
+ * e.g. {@link BluetoothDevice#ADDRESS_TYPE_PUBLIC}
+ * or {@link BluetoothDevice#ADDRESS_TYPE_RANDOM}
+ *
+ * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+ * @throws IllegalArgumentException If the {@code addressType} is invalid length
+ * @throws NullPointerException if {@code deviceAddress} is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public Builder setDeviceAddress(@NonNull String deviceAddress,
+ @AddressType int addressType) {
+ return setDeviceAddressInternal(deviceAddress, addressType, null);
+ }
+
+ /**
+ * Set filter on Address with AddressType and the Identity Resolving Key (IRK).
+ *
+ * <p>The IRK is used to resolve a {@link BluetoothDevice#ADDRESS_TYPE_PUBLIC} from
+ * a PRIVATE_ADDRESS type.
+ *
+ * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+ * format of "01:02:03:AB:CD:EF". The device address can be validated using {@link
+ * BluetoothAdapter#checkBluetoothAddress}. This Address type must only be PUBLIC OR RANDOM
+ * STATIC.
+ * @param addressType indication of the type of address
+ * e.g. {@link BluetoothDevice#ADDRESS_TYPE_PUBLIC}
+ * or {@link BluetoothDevice#ADDRESS_TYPE_RANDOM}
+ * @param irk non-null byte array representing the Identity Resolving Key
+ *
+ * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+ * @throws IllegalArgumentException if the {@code irk} is invalid length.
+ * @throws IllegalArgumentException If the {@code addressType} is invalid length or is not
+ * PUBLIC or RANDOM STATIC when an IRK is present.
+ * @throws NullPointerException if {@code deviceAddress} or {@code irk} is null.
+ *
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ public Builder setDeviceAddress(@NonNull String deviceAddress,
+ @AddressType int addressType,
+ @NonNull byte[] irk) {
+ requireNonNull(irk);
+ if (irk.length != LEN_IRK_OCTETS) {
+ throw new IllegalArgumentException("'irk' is invalid length!");
+ }
+ return setDeviceAddressInternal(deviceAddress, addressType, irk);
+ }
+
+ /**
+ * Set filter on Address with AddressType and the Identity Resolving Key (IRK).
+ *
+ * <p>Internal setter for the device address
+ *
+ * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+ * format of "01:02:03:AB:CD:EF". The device address can be validated using {@link
+ * BluetoothAdapter#checkBluetoothAddress}.
+ * @param addressType indication of the type of address
+ * e.g. {@link BluetoothDevice#ADDRESS_TYPE_PUBLIC}
+ * @param irk non-null byte array representing the Identity Resolving Address; nullable
+ * internally.
+ *
+ * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+ * @throws IllegalArgumentException If the {@code addressType} is invalid length.
+ * @throws NullPointerException if {@code deviceAddress} is null.
+ *
+ * @hide
+ */
+ @NonNull
+ private Builder setDeviceAddressInternal(@NonNull String deviceAddress,
+ @AddressType int addressType,
+ @Nullable byte[] irk) {
+
+ // Make sure our deviceAddress is valid!
+ requireNonNull(deviceAddress);
+ if (!BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
+ throw new IllegalArgumentException("invalid device address " + deviceAddress);
+ }
+
+ // Verify type range
+ if (addressType < BluetoothDevice.ADDRESS_TYPE_PUBLIC
+ || addressType > BluetoothDevice.ADDRESS_TYPE_RANDOM) {
+ throw new IllegalArgumentException("'addressType' is invalid!");
+ }
+
+ // IRK can only be used for a PUBLIC or RANDOM (STATIC) Address.
+ if (addressType == BluetoothDevice.ADDRESS_TYPE_RANDOM) {
+ // Don't want a bad combination of address and irk!
+ if (irk != null) {
+ // Since there are 3 possible RANDOM subtypes we must check to make sure
+ // the correct type of address is used.
+ if (!BluetoothAdapter.isAddressRandomStatic(deviceAddress)) {
+ throw new IllegalArgumentException(
+ "Invalid combination: IRK requires either a PUBLIC or "
+ + "RANDOM (STATIC) Address");
+ }
+ }
+ }
+
+ // PUBLIC doesn't require extra work
+ // Without an IRK any address may be accepted
+
+ mDeviceAddress = deviceAddress;
+ mAddressType = addressType;
+ mIrk = irk;
+ return this;
+ }
+
+ /**
+ * Set filter on service uuid.
+ */
+ public Builder setServiceUuid(ParcelUuid serviceUuid) {
+ mServiceUuid = serviceUuid;
+ mUuidMask = null; // clear uuid mask
+ return this;
+ }
+
+ /**
+ * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the
+ * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the
+ * bit in {@code serviceUuid}, and 0 to ignore that bit.
+ *
+ * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but {@code
+ * uuidMask} is not {@code null}.
+ */
+ public Builder setServiceUuid(ParcelUuid serviceUuid, ParcelUuid uuidMask) {
+ if (mUuidMask != null && mServiceUuid == null) {
+ throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
+ }
+ mServiceUuid = serviceUuid;
+ mUuidMask = uuidMask;
+ return this;
+ }
+
+
+ /**
+ * Set filter on service solicitation uuid.
+ */
+ public @NonNull Builder setServiceSolicitationUuid(
+ @Nullable ParcelUuid serviceSolicitationUuid) {
+ mServiceSolicitationUuid = serviceSolicitationUuid;
+ if (serviceSolicitationUuid == null) {
+ mServiceSolicitationUuidMask = null;
+ }
+ return this;
+ }
+
+
+ /**
+ * Set filter on partial service Solicitation uuid. The {@code SolicitationUuidMask} is the
+ * bit mask for the {@code serviceSolicitationUuid}. Set any bit in the mask to 1 to
+ * indicate a match is needed for the bit in {@code serviceSolicitationUuid}, and 0 to
+ * ignore that bit.
+ *
+ * @param serviceSolicitationUuid can only be null if solicitationUuidMask is null.
+ * @param solicitationUuidMask can be null or a mask with no restriction.
+ *
+ * @throws IllegalArgumentException If {@code serviceSolicitationUuid} is {@code null} but
+ * {@code serviceSolicitationUuidMask} is not {@code null}.
+ */
+ public @NonNull Builder setServiceSolicitationUuid(
+ @Nullable ParcelUuid serviceSolicitationUuid,
+ @Nullable ParcelUuid solicitationUuidMask) {
+ if (solicitationUuidMask != null && serviceSolicitationUuid == null) {
+ throw new IllegalArgumentException(
+ "SolicitationUuid is null while SolicitationUuidMask is not null!");
+ }
+ mServiceSolicitationUuid = serviceSolicitationUuid;
+ mServiceSolicitationUuidMask = solicitationUuidMask;
+ return this;
+ }
+
+ /**
+ * Set filtering on service data.
+ *
+ * @throws IllegalArgumentException If {@code serviceDataUuid} is null.
+ */
+ public Builder setServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) {
+ if (serviceDataUuid == null) {
+ throw new IllegalArgumentException("serviceDataUuid is null");
+ }
+ mServiceDataUuid = serviceDataUuid;
+ mServiceData = serviceData;
+ mServiceDataMask = null; // clear service data mask
+ return this;
+ }
+
+ /**
+ * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
+ * match the one in service data, otherwise set it to 0 to ignore that bit.
+ * <p>
+ * The {@code serviceDataMask} must have the same length of the {@code serviceData}.
+ *
+ * @throws IllegalArgumentException If {@code serviceDataUuid} is null or {@code
+ * serviceDataMask} is {@code null} while {@code serviceData} is not or {@code
+ * serviceDataMask} and {@code serviceData} has different length.
+ */
+ public Builder setServiceData(ParcelUuid serviceDataUuid,
+ byte[] serviceData, byte[] serviceDataMask) {
+ if (serviceDataUuid == null) {
+ throw new IllegalArgumentException("serviceDataUuid is null");
+ }
+ if (mServiceDataMask != null) {
+ if (mServiceData == null) {
+ throw new IllegalArgumentException(
+ "serviceData is null while serviceDataMask is not null");
+ }
+ // Since the mServiceDataMask is a bit mask for mServiceData, the lengths of the two
+ // byte array need to be the same.
+ if (mServiceData.length != mServiceDataMask.length) {
+ throw new IllegalArgumentException(
+ "size mismatch for service data and service data mask");
+ }
+ }
+ mServiceDataUuid = serviceDataUuid;
+ mServiceData = serviceData;
+ mServiceDataMask = serviceDataMask;
+ return this;
+ }
+
+ /**
+ * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
+ *
+ * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
+ */
+ public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData) {
+ if (manufacturerData != null && manufacturerId < 0) {
+ throw new IllegalArgumentException("invalid manufacture id");
+ }
+ mManufacturerId = manufacturerId;
+ mManufacturerData = manufacturerData;
+ mManufacturerDataMask = null; // clear manufacturer data mask
+ return this;
+ }
+
+ /**
+ * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs
+ * to match the one in manufacturer data, otherwise set it to 0.
+ * <p>
+ * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}.
+ *
+ * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or {@code
+ * manufacturerData} is null while {@code manufacturerDataMask} is not, or {@code
+ * manufacturerData} and {@code manufacturerDataMask} have different length.
+ */
+ public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData,
+ byte[] manufacturerDataMask) {
+ if (manufacturerData != null && manufacturerId < 0) {
+ throw new IllegalArgumentException("invalid manufacture id");
+ }
+ if (mManufacturerDataMask != null) {
+ if (mManufacturerData == null) {
+ throw new IllegalArgumentException(
+ "manufacturerData is null while manufacturerDataMask is not null");
+ }
+ // Since the mManufacturerDataMask is a bit mask for mManufacturerData, the lengths
+ // of the two byte array need to be the same.
+ if (mManufacturerData.length != mManufacturerDataMask.length) {
+ throw new IllegalArgumentException(
+ "size mismatch for manufacturerData and manufacturerDataMask");
+ }
+ }
+ mManufacturerId = manufacturerId;
+ mManufacturerData = manufacturerData;
+ mManufacturerDataMask = manufacturerDataMask;
+ return this;
+ }
+
+ /**
+ * Build {@link ScanFilter}.
+ *
+ * @throws IllegalArgumentException If the filter cannot be built.
+ */
+ public ScanFilter build() {
+ return new ScanFilter(mDeviceName, mDeviceAddress,
+ mServiceUuid, mUuidMask, mServiceSolicitationUuid,
+ mServiceSolicitationUuidMask,
+ mServiceDataUuid, mServiceData, mServiceDataMask,
+ mManufacturerId, mManufacturerData, mManufacturerDataMask,
+ mAddressType, mIrk);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/ScanRecord.java b/framework/java/android/bluetooth/le/ScanRecord.java
new file mode 100644
index 0000000000..9b8c2eaf4d
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ScanRecord.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothUuid;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.ParcelUuid;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * Represents a scan record from Bluetooth LE scan.
+ */
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+public final class ScanRecord {
+
+ private static final String TAG = "ScanRecord";
+
+ // The following data type values are assigned by Bluetooth SIG.
+ // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18.
+ private static final int DATA_TYPE_FLAGS = 0x01;
+ private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
+ private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
+ private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
+ private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
+ private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
+ private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
+ private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
+ private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
+ private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
+ private static final int DATA_TYPE_SERVICE_DATA_16_BIT = 0x16;
+ private static final int DATA_TYPE_SERVICE_DATA_32_BIT = 0x20;
+ private static final int DATA_TYPE_SERVICE_DATA_128_BIT = 0x21;
+ private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT = 0x14;
+ private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT = 0x1F;
+ private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT = 0x15;
+ private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
+
+ // Flags of the advertising data.
+ private final int mAdvertiseFlags;
+
+ @Nullable
+ private final List<ParcelUuid> mServiceUuids;
+ @Nullable
+ private final List<ParcelUuid> mServiceSolicitationUuids;
+
+ private final SparseArray<byte[]> mManufacturerSpecificData;
+
+ private final Map<ParcelUuid, byte[]> mServiceData;
+
+ // Transmission power level(in dB).
+ private final int mTxPowerLevel;
+
+ // Local name of the Bluetooth LE device.
+ private final String mDeviceName;
+
+ // Raw bytes of scan record.
+ private final byte[] mBytes;
+
+ /**
+ * Returns the advertising flags indicating the discoverable mode and capability of the device.
+ * Returns -1 if the flag field is not set.
+ */
+ public int getAdvertiseFlags() {
+ return mAdvertiseFlags;
+ }
+
+ /**
+ * Returns a list of service UUIDs within the advertisement that are used to identify the
+ * bluetooth GATT services.
+ */
+ public List<ParcelUuid> getServiceUuids() {
+ return mServiceUuids;
+ }
+
+ /**
+ * Returns a list of service solicitation UUIDs within the advertisement that are used to
+ * identify the Bluetooth GATT services.
+ */
+ @NonNull
+ public List<ParcelUuid> getServiceSolicitationUuids() {
+ return mServiceSolicitationUuids;
+ }
+
+ /**
+ * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
+ * data.
+ */
+ public SparseArray<byte[]> getManufacturerSpecificData() {
+ return mManufacturerSpecificData;
+ }
+
+ /**
+ * Returns the manufacturer specific data associated with the manufacturer id. Returns
+ * {@code null} if the {@code manufacturerId} is not found.
+ */
+ @Nullable
+ public byte[] getManufacturerSpecificData(int manufacturerId) {
+ if (mManufacturerSpecificData == null) {
+ return null;
+ }
+ return mManufacturerSpecificData.get(manufacturerId);
+ }
+
+ /**
+ * Returns a map of service UUID and its corresponding service data.
+ */
+ public Map<ParcelUuid, byte[]> getServiceData() {
+ return mServiceData;
+ }
+
+ /**
+ * Returns the service data byte array associated with the {@code serviceUuid}. Returns
+ * {@code null} if the {@code serviceDataUuid} is not found.
+ */
+ @Nullable
+ public byte[] getServiceData(ParcelUuid serviceDataUuid) {
+ if (serviceDataUuid == null || mServiceData == null) {
+ return null;
+ }
+ return mServiceData.get(serviceDataUuid);
+ }
+
+ /**
+ * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE}
+ * if the field is not set. This value can be used to calculate the path loss of a received
+ * packet using the following equation:
+ * <p>
+ * <code>pathloss = txPowerLevel - rssi</code>
+ */
+ public int getTxPowerLevel() {
+ return mTxPowerLevel;
+ }
+
+ /**
+ * Returns the local name of the BLE device. This is a UTF-8 encoded string.
+ */
+ @Nullable
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ /**
+ * Returns raw bytes of scan record.
+ */
+ public byte[] getBytes() {
+ return mBytes;
+ }
+
+ /**
+ * Test if any fields contained inside this scan record are matched by the
+ * given matcher.
+ *
+ * @hide
+ */
+ public boolean matchesAnyField(@NonNull Predicate<byte[]> matcher) {
+ int pos = 0;
+ while (pos < mBytes.length) {
+ final int length = mBytes[pos] & 0xFF;
+ if (length == 0) {
+ break;
+ }
+ if (matcher.test(Arrays.copyOfRange(mBytes, pos, pos + length + 1))) {
+ return true;
+ }
+ pos += length + 1;
+ }
+ return false;
+ }
+
+ private ScanRecord(List<ParcelUuid> serviceUuids,
+ List<ParcelUuid> serviceSolicitationUuids,
+ SparseArray<byte[]> manufacturerData,
+ Map<ParcelUuid, byte[]> serviceData,
+ int advertiseFlags, int txPowerLevel,
+ String localName, byte[] bytes) {
+ mServiceSolicitationUuids = serviceSolicitationUuids;
+ mServiceUuids = serviceUuids;
+ mManufacturerSpecificData = manufacturerData;
+ mServiceData = serviceData;
+ mDeviceName = localName;
+ mAdvertiseFlags = advertiseFlags;
+ mTxPowerLevel = txPowerLevel;
+ mBytes = bytes;
+ }
+
+ /**
+ * Parse scan record bytes to {@link ScanRecord}.
+ * <p>
+ * The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18.
+ * <p>
+ * All numerical multi-byte entities and values shall use little-endian <strong>byte</strong>
+ * order.
+ *
+ * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static ScanRecord parseFromBytes(byte[] scanRecord) {
+ if (scanRecord == null) {
+ return null;
+ }
+
+ int currentPos = 0;
+ int advertiseFlag = -1;
+ List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>();
+ List<ParcelUuid> serviceSolicitationUuids = new ArrayList<ParcelUuid>();
+ String localName = null;
+ int txPowerLevel = Integer.MIN_VALUE;
+
+ SparseArray<byte[]> manufacturerData = new SparseArray<byte[]>();
+ Map<ParcelUuid, byte[]> serviceData = new ArrayMap<ParcelUuid, byte[]>();
+
+ try {
+ while (currentPos < scanRecord.length) {
+ // length is unsigned int.
+ int length = scanRecord[currentPos++] & 0xFF;
+ if (length == 0) {
+ break;
+ }
+ // Note the length includes the length of the field type itself.
+ int dataLength = length - 1;
+ // fieldType is unsigned int.
+ int fieldType = scanRecord[currentPos++] & 0xFF;
+ switch (fieldType) {
+ case DATA_TYPE_FLAGS:
+ advertiseFlag = scanRecord[currentPos] & 0xFF;
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos,
+ dataLength, BluetoothUuid.UUID_BYTES_16_BIT, serviceUuids);
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos, dataLength,
+ BluetoothUuid.UUID_BYTES_32_BIT, serviceUuids);
+ break;
+ case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
+ case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
+ parseServiceUuid(scanRecord, currentPos, dataLength,
+ BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids);
+ break;
+ case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT:
+ parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+ BluetoothUuid.UUID_BYTES_16_BIT, serviceSolicitationUuids);
+ break;
+ case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT:
+ parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+ BluetoothUuid.UUID_BYTES_32_BIT, serviceSolicitationUuids);
+ break;
+ case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT:
+ parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+ BluetoothUuid.UUID_BYTES_128_BIT, serviceSolicitationUuids);
+ break;
+ case DATA_TYPE_LOCAL_NAME_SHORT:
+ case DATA_TYPE_LOCAL_NAME_COMPLETE:
+ localName = new String(
+ extractBytes(scanRecord, currentPos, dataLength));
+ break;
+ case DATA_TYPE_TX_POWER_LEVEL:
+ txPowerLevel = scanRecord[currentPos];
+ break;
+ case DATA_TYPE_SERVICE_DATA_16_BIT:
+ case DATA_TYPE_SERVICE_DATA_32_BIT:
+ case DATA_TYPE_SERVICE_DATA_128_BIT:
+ int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT;
+ if (fieldType == DATA_TYPE_SERVICE_DATA_32_BIT) {
+ serviceUuidLength = BluetoothUuid.UUID_BYTES_32_BIT;
+ } else if (fieldType == DATA_TYPE_SERVICE_DATA_128_BIT) {
+ serviceUuidLength = BluetoothUuid.UUID_BYTES_128_BIT;
+ }
+
+ byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
+ serviceUuidLength);
+ ParcelUuid serviceDataUuid = BluetoothUuid.parseUuidFrom(
+ serviceDataUuidBytes);
+ byte[] serviceDataArray = extractBytes(scanRecord,
+ currentPos + serviceUuidLength, dataLength - serviceUuidLength);
+ serviceData.put(serviceDataUuid, serviceDataArray);
+ break;
+ case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
+ // The first two bytes of the manufacturer specific data are
+ // manufacturer ids in little endian.
+ int manufacturerId = ((scanRecord[currentPos + 1] & 0xFF) << 8)
+ + (scanRecord[currentPos] & 0xFF);
+ byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
+ dataLength - 2);
+ manufacturerData.put(manufacturerId, manufacturerDataBytes);
+ break;
+ default:
+ // Just ignore, we don't handle such data type.
+ break;
+ }
+ currentPos += dataLength;
+ }
+
+ if (serviceUuids.isEmpty()) {
+ serviceUuids = null;
+ }
+ return new ScanRecord(serviceUuids, serviceSolicitationUuids, manufacturerData,
+ serviceData, advertiseFlag, txPowerLevel, localName, scanRecord);
+ } catch (Exception e) {
+ Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord));
+ // As the record is invalid, ignore all the parsed results for this packet
+ // and return an empty record with raw scanRecord bytes in results
+ return new ScanRecord(null, null, null, null, -1, Integer.MIN_VALUE, null, scanRecord);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids
+ + ", mServiceSolicitationUuids=" + mServiceSolicitationUuids
+ + ", mManufacturerSpecificData=" + BluetoothLeUtils.toString(
+ mManufacturerSpecificData)
+ + ", mServiceData=" + BluetoothLeUtils.toString(mServiceData)
+ + ", mTxPowerLevel=" + mTxPowerLevel + ", mDeviceName=" + mDeviceName + "]";
+ }
+
+ // Parse service UUIDs.
+ private static int parseServiceUuid(byte[] scanRecord, int currentPos, int dataLength,
+ int uuidLength, List<ParcelUuid> serviceUuids) {
+ while (dataLength > 0) {
+ byte[] uuidBytes = extractBytes(scanRecord, currentPos,
+ uuidLength);
+ serviceUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes));
+ dataLength -= uuidLength;
+ currentPos += uuidLength;
+ }
+ return currentPos;
+ }
+
+ /**
+ * Parse service Solicitation UUIDs.
+ */
+ private static int parseServiceSolicitationUuid(byte[] scanRecord, int currentPos,
+ int dataLength, int uuidLength, List<ParcelUuid> serviceSolicitationUuids) {
+ while (dataLength > 0) {
+ byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength);
+ serviceSolicitationUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes));
+ dataLength -= uuidLength;
+ currentPos += uuidLength;
+ }
+ return currentPos;
+ }
+
+ // Helper method to extract bytes from byte array.
+ private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
+ byte[] bytes = new byte[length];
+ System.arraycopy(scanRecord, start, bytes, 0, length);
+ return bytes;
+ }
+}
diff --git a/framework/java/android/bluetooth/le/ScanResult.java b/framework/java/android/bluetooth/le/ScanResult.java
new file mode 100644
index 0000000000..f437d867ea
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ScanResult.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.Attributable;
+import android.bluetooth.BluetoothDevice;
+import android.content.AttributionSource;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * ScanResult for Bluetooth LE scan.
+ */
+public final class ScanResult implements Parcelable, Attributable {
+
+ /**
+ * For chained advertisements, inidcates tha the data contained in this
+ * scan result is complete.
+ */
+ public static final int DATA_COMPLETE = 0x00;
+
+ /**
+ * For chained advertisements, indicates that the controller was
+ * unable to receive all chained packets and the scan result contains
+ * incomplete truncated data.
+ */
+ public static final int DATA_TRUNCATED = 0x02;
+
+ /**
+ * Indicates that the secondary physical layer was not used.
+ */
+ public static final int PHY_UNUSED = 0x00;
+
+ /**
+ * Advertising Set ID is not present in the packet.
+ */
+ public static final int SID_NOT_PRESENT = 0xFF;
+
+ /**
+ * TX power is not present in the packet.
+ */
+ public static final int TX_POWER_NOT_PRESENT = 0x7F;
+
+ /**
+ * Periodic advertising interval is not present in the packet.
+ */
+ public static final int PERIODIC_INTERVAL_NOT_PRESENT = 0x00;
+
+ /**
+ * Mask for checking whether event type represents legacy advertisement.
+ */
+ private static final int ET_LEGACY_MASK = 0x10;
+
+ /**
+ * Mask for checking whether event type represents connectable advertisement.
+ */
+ private static final int ET_CONNECTABLE_MASK = 0x01;
+
+ // Remote Bluetooth device.
+ private BluetoothDevice mDevice;
+
+ // Scan record, including advertising data and scan response data.
+ @Nullable
+ private ScanRecord mScanRecord;
+
+ // Received signal strength.
+ private int mRssi;
+
+ // Device timestamp when the result was last seen.
+ private long mTimestampNanos;
+
+ private int mEventType;
+ private int mPrimaryPhy;
+ private int mSecondaryPhy;
+ private int mAdvertisingSid;
+ private int mTxPower;
+ private int mPeriodicAdvertisingInterval;
+
+ /**
+ * Constructs a new ScanResult.
+ *
+ * @param device Remote Bluetooth device found.
+ * @param scanRecord Scan record including both advertising data and scan response data.
+ * @param rssi Received signal strength.
+ * @param timestampNanos Timestamp at which the scan result was observed.
+ * @deprecated use {@link #ScanResult(BluetoothDevice, int, int, int, int, int, int, int,
+ * ScanRecord, long)}
+ */
+ @Deprecated
+ public ScanResult(BluetoothDevice device, ScanRecord scanRecord, int rssi,
+ long timestampNanos) {
+ mDevice = device;
+ mScanRecord = scanRecord;
+ mRssi = rssi;
+ mTimestampNanos = timestampNanos;
+ mEventType = (DATA_COMPLETE << 5) | ET_LEGACY_MASK | ET_CONNECTABLE_MASK;
+ mPrimaryPhy = BluetoothDevice.PHY_LE_1M;
+ mSecondaryPhy = PHY_UNUSED;
+ mAdvertisingSid = SID_NOT_PRESENT;
+ mTxPower = 127;
+ mPeriodicAdvertisingInterval = 0;
+ }
+
+ /**
+ * Constructs a new ScanResult.
+ *
+ * @param device Remote Bluetooth device found.
+ * @param eventType Event type.
+ * @param primaryPhy Primary advertising phy.
+ * @param secondaryPhy Secondary advertising phy.
+ * @param advertisingSid Advertising set ID.
+ * @param txPower Transmit power.
+ * @param rssi Received signal strength.
+ * @param periodicAdvertisingInterval Periodic advertising interval.
+ * @param scanRecord Scan record including both advertising data and scan response data.
+ * @param timestampNanos Timestamp at which the scan result was observed.
+ */
+ public ScanResult(BluetoothDevice device, int eventType, int primaryPhy, int secondaryPhy,
+ int advertisingSid, int txPower, int rssi, int periodicAdvertisingInterval,
+ ScanRecord scanRecord, long timestampNanos) {
+ mDevice = device;
+ mEventType = eventType;
+ mPrimaryPhy = primaryPhy;
+ mSecondaryPhy = secondaryPhy;
+ mAdvertisingSid = advertisingSid;
+ mTxPower = txPower;
+ mRssi = rssi;
+ mPeriodicAdvertisingInterval = periodicAdvertisingInterval;
+ mScanRecord = scanRecord;
+ mTimestampNanos = timestampNanos;
+ }
+
+ private ScanResult(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (mDevice != null) {
+ dest.writeInt(1);
+ mDevice.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mScanRecord != null) {
+ dest.writeInt(1);
+ dest.writeByteArray(mScanRecord.getBytes());
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeInt(mRssi);
+ dest.writeLong(mTimestampNanos);
+ dest.writeInt(mEventType);
+ dest.writeInt(mPrimaryPhy);
+ dest.writeInt(mSecondaryPhy);
+ dest.writeInt(mAdvertisingSid);
+ dest.writeInt(mTxPower);
+ dest.writeInt(mPeriodicAdvertisingInterval);
+ }
+
+ private void readFromParcel(Parcel in) {
+ if (in.readInt() == 1) {
+ mDevice = BluetoothDevice.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() == 1) {
+ mScanRecord = ScanRecord.parseFromBytes(in.createByteArray());
+ }
+ mRssi = in.readInt();
+ mTimestampNanos = in.readLong();
+ mEventType = in.readInt();
+ mPrimaryPhy = in.readInt();
+ mSecondaryPhy = in.readInt();
+ mAdvertisingSid = in.readInt();
+ mTxPower = in.readInt();
+ mPeriodicAdvertisingInterval = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** {@hide} */
+ public void setAttributionSource(@NonNull AttributionSource attributionSource) {
+ Attributable.setAttributionSource(mDevice, attributionSource);
+ }
+
+ /**
+ * Returns the remote Bluetooth device identified by the Bluetooth device address.
+ */
+ public BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Returns the scan record, which is a combination of advertisement and scan response.
+ */
+ @Nullable
+ public ScanRecord getScanRecord() {
+ return mScanRecord;
+ }
+
+ /**
+ * Returns the received signal strength in dBm. The valid range is [-127, 126].
+ */
+ public int getRssi() {
+ return mRssi;
+ }
+
+ /**
+ * Returns timestamp since boot when the scan record was observed.
+ */
+ public long getTimestampNanos() {
+ return mTimestampNanos;
+ }
+
+ /**
+ * Returns true if this object represents legacy scan result.
+ * Legacy scan results do not contain advanced advertising information
+ * as specified in the Bluetooth Core Specification v5.
+ */
+ public boolean isLegacy() {
+ return (mEventType & ET_LEGACY_MASK) != 0;
+ }
+
+ /**
+ * Returns true if this object represents connectable scan result.
+ */
+ public boolean isConnectable() {
+ return (mEventType & ET_CONNECTABLE_MASK) != 0;
+ }
+
+ /**
+ * Returns the data status.
+ * Can be one of {@link ScanResult#DATA_COMPLETE} or
+ * {@link ScanResult#DATA_TRUNCATED}.
+ */
+ public int getDataStatus() {
+ // return bit 5 and 6
+ return (mEventType >> 5) & 0x03;
+ }
+
+ /**
+ * Returns the primary Physical Layer
+ * on which this advertisment was received.
+ * Can be one of {@link BluetoothDevice#PHY_LE_1M} or
+ * {@link BluetoothDevice#PHY_LE_CODED}.
+ */
+ public int getPrimaryPhy() {
+ return mPrimaryPhy;
+ }
+
+ /**
+ * Returns the secondary Physical Layer
+ * on which this advertisment was received.
+ * Can be one of {@link BluetoothDevice#PHY_LE_1M},
+ * {@link BluetoothDevice#PHY_LE_2M}, {@link BluetoothDevice#PHY_LE_CODED}
+ * or {@link ScanResult#PHY_UNUSED} - if the advertisement
+ * was not received on a secondary physical channel.
+ */
+ public int getSecondaryPhy() {
+ return mSecondaryPhy;
+ }
+
+ /**
+ * Returns the advertising set id.
+ * May return {@link ScanResult#SID_NOT_PRESENT} if
+ * no set id was is present.
+ */
+ public int getAdvertisingSid() {
+ return mAdvertisingSid;
+ }
+
+ /**
+ * Returns the transmit power in dBm.
+ * Valid range is [-127, 126]. A value of {@link ScanResult#TX_POWER_NOT_PRESENT}
+ * indicates that the TX power is not present.
+ */
+ public int getTxPower() {
+ return mTxPower;
+ }
+
+ /**
+ * Returns the periodic advertising interval in units of 1.25ms.
+ * Valid range is 6 (7.5ms) to 65536 (81918.75ms). A value of
+ * {@link ScanResult#PERIODIC_INTERVAL_NOT_PRESENT} means periodic
+ * advertising interval is not present.
+ */
+ public int getPeriodicAdvertisingInterval() {
+ return mPeriodicAdvertisingInterval;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDevice, mRssi, mScanRecord, mTimestampNanos,
+ mEventType, mPrimaryPhy, mSecondaryPhy,
+ mAdvertisingSid, mTxPower,
+ mPeriodicAdvertisingInterval);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ScanResult other = (ScanResult) obj;
+ return Objects.equals(mDevice, other.mDevice) && (mRssi == other.mRssi)
+ && Objects.equals(mScanRecord, other.mScanRecord)
+ && (mTimestampNanos == other.mTimestampNanos)
+ && mEventType == other.mEventType
+ && mPrimaryPhy == other.mPrimaryPhy
+ && mSecondaryPhy == other.mSecondaryPhy
+ && mAdvertisingSid == other.mAdvertisingSid
+ && mTxPower == other.mTxPower
+ && mPeriodicAdvertisingInterval == other.mPeriodicAdvertisingInterval;
+ }
+
+ @Override
+ public String toString() {
+ return "ScanResult{" + "device=" + mDevice + ", scanRecord="
+ + Objects.toString(mScanRecord) + ", rssi=" + mRssi
+ + ", timestampNanos=" + mTimestampNanos + ", eventType=" + mEventType
+ + ", primaryPhy=" + mPrimaryPhy + ", secondaryPhy=" + mSecondaryPhy
+ + ", advertisingSid=" + mAdvertisingSid + ", txPower=" + mTxPower
+ + ", periodicAdvertisingInterval=" + mPeriodicAdvertisingInterval + '}';
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ScanResult> CREATOR = new Creator<ScanResult>() {
+ @Override
+ public ScanResult createFromParcel(Parcel source) {
+ return new ScanResult(source);
+ }
+
+ @Override
+ public ScanResult[] newArray(int size) {
+ return new ScanResult[size];
+ }
+ };
+
+}
diff --git a/framework/java/android/bluetooth/le/ScanSettings.java b/framework/java/android/bluetooth/le/ScanSettings.java
new file mode 100644
index 0000000000..1aa7cb5111
--- /dev/null
+++ b/framework/java/android/bluetooth/le/ScanSettings.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.SystemApi;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Bluetooth LE scan settings are passed to {@link BluetoothLeScanner#startScan} to define the
+ * parameters for the scan.
+ */
+public final class ScanSettings implements Parcelable {
+
+ /**
+ * A special Bluetooth LE scan mode. Applications using this scan mode will passively listen for
+ * other scan results without starting BLE scans themselves.
+ */
+ public static final int SCAN_MODE_OPPORTUNISTIC = -1;
+
+ /**
+ * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the
+ * least power. This mode is enforced if the scanning application is not in foreground.
+ */
+ public static final int SCAN_MODE_LOW_POWER = 0;
+
+ /**
+ * Perform Bluetooth LE scan in balanced power mode. Scan results are returned at a rate that
+ * provides a good trade-off between scan frequency and power consumption.
+ */
+ public static final int SCAN_MODE_BALANCED = 1;
+
+ /**
+ * Scan using highest duty cycle. It's recommended to only use this mode when the application is
+ * running in the foreground.
+ */
+ public static final int SCAN_MODE_LOW_LATENCY = 2;
+
+ /**
+ * Perform Bluetooth LE scan in ambient discovery mode. This mode has lower duty cycle and more
+ * aggressive scan interval than balanced mode that provides a good trade-off between scan
+ * latency and power consumption.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SCAN_MODE_AMBIENT_DISCOVERY = 3;
+
+ /**
+ * Trigger a callback for every Bluetooth advertisement found that matches the filter criteria.
+ * If no filter is active, all advertisement packets are reported.
+ */
+ public static final int CALLBACK_TYPE_ALL_MATCHES = 1;
+
+ /**
+ * A result callback is only triggered for the first advertisement packet received that matches
+ * the filter criteria.
+ */
+ public static final int CALLBACK_TYPE_FIRST_MATCH = 2;
+
+ /**
+ * Receive a callback when advertisements are no longer received from a device that has been
+ * previously reported by a first match callback.
+ */
+ public static final int CALLBACK_TYPE_MATCH_LOST = 4;
+
+
+ /**
+ * Determines how many advertisements to match per filter, as this is scarce hw resource
+ */
+ /**
+ * Match one advertisement per filter
+ */
+ public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1;
+
+ /**
+ * Match few advertisement per filter, depends on current capability and availibility of
+ * the resources in hw
+ */
+ public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2;
+
+ /**
+ * Match as many advertisement per filter as hw could allow, depends on current
+ * capability and availibility of the resources in hw
+ */
+ public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3;
+
+
+ /**
+ * In Aggressive mode, hw will determine a match sooner even with feeble signal strength
+ * and few number of sightings/match in a duration.
+ */
+ public static final int MATCH_MODE_AGGRESSIVE = 1;
+
+ /**
+ * For sticky mode, higher threshold of signal strength and sightings is required
+ * before reporting by hw
+ */
+ public static final int MATCH_MODE_STICKY = 2;
+
+ /**
+ * Request full scan results which contain the device, rssi, advertising data, scan response
+ * as well as the scan timestamp.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SCAN_RESULT_TYPE_FULL = 0;
+
+ /**
+ * Request abbreviated scan results which contain the device, rssi and scan timestamp.
+ * <p>
+ * <b>Note:</b> It is possible for an application to get more scan results than it asked for, if
+ * there are multiple apps using this type.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final int SCAN_RESULT_TYPE_ABBREVIATED = 1;
+
+ /**
+ * Use all supported PHYs for scanning.
+ * This will check the controller capabilities, and start
+ * the scan on 1Mbit and LE Coded PHYs if supported, or on
+ * the 1Mbit PHY only.
+ */
+ public static final int PHY_LE_ALL_SUPPORTED = 255;
+
+ // Bluetooth LE scan mode.
+ private int mScanMode;
+
+ // Bluetooth LE scan callback type
+ private int mCallbackType;
+
+ // Bluetooth LE scan result type
+ private int mScanResultType;
+
+ // Time of delay for reporting the scan result
+ private long mReportDelayMillis;
+
+ private int mMatchMode;
+
+ private int mNumOfMatchesPerFilter;
+
+ // Include only legacy advertising results
+ private boolean mLegacy;
+
+ private int mPhy;
+
+ public int getScanMode() {
+ return mScanMode;
+ }
+
+ public int getCallbackType() {
+ return mCallbackType;
+ }
+
+ public int getScanResultType() {
+ return mScanResultType;
+ }
+
+ /**
+ * @hide
+ */
+ public int getMatchMode() {
+ return mMatchMode;
+ }
+
+ /**
+ * @hide
+ */
+ public int getNumOfMatches() {
+ return mNumOfMatchesPerFilter;
+ }
+
+ /**
+ * Returns whether only legacy advertisements will be returned.
+ * Legacy advertisements include advertisements as specified
+ * by the Bluetooth core specification 4.2 and below.
+ */
+ public boolean getLegacy() {
+ return mLegacy;
+ }
+
+ /**
+ * Returns the physical layer used during a scan.
+ */
+ public int getPhy() {
+ return mPhy;
+ }
+
+ /**
+ * Returns report delay timestamp based on the device clock.
+ */
+ public long getReportDelayMillis() {
+ return mReportDelayMillis;
+ }
+
+ private ScanSettings(int scanMode, int callbackType, int scanResultType,
+ long reportDelayMillis, int matchMode,
+ int numOfMatchesPerFilter, boolean legacy, int phy) {
+ mScanMode = scanMode;
+ mCallbackType = callbackType;
+ mScanResultType = scanResultType;
+ mReportDelayMillis = reportDelayMillis;
+ mNumOfMatchesPerFilter = numOfMatchesPerFilter;
+ mMatchMode = matchMode;
+ mLegacy = legacy;
+ mPhy = phy;
+ }
+
+ private ScanSettings(Parcel in) {
+ mScanMode = in.readInt();
+ mCallbackType = in.readInt();
+ mScanResultType = in.readInt();
+ mReportDelayMillis = in.readLong();
+ mMatchMode = in.readInt();
+ mNumOfMatchesPerFilter = in.readInt();
+ mLegacy = in.readInt() != 0;
+ mPhy = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mScanMode);
+ dest.writeInt(mCallbackType);
+ dest.writeInt(mScanResultType);
+ dest.writeLong(mReportDelayMillis);
+ dest.writeInt(mMatchMode);
+ dest.writeInt(mNumOfMatchesPerFilter);
+ dest.writeInt(mLegacy ? 1 : 0);
+ dest.writeInt(mPhy);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<ScanSettings> CREATOR =
+ new Creator<ScanSettings>() {
+ @Override
+ public ScanSettings[] newArray(int size) {
+ return new ScanSettings[size];
+ }
+
+ @Override
+ public ScanSettings createFromParcel(Parcel in) {
+ return new ScanSettings(in);
+ }
+ };
+
+ /**
+ * Builder for {@link ScanSettings}.
+ */
+ public static final class Builder {
+ private int mScanMode = SCAN_MODE_LOW_POWER;
+ private int mCallbackType = CALLBACK_TYPE_ALL_MATCHES;
+ private int mScanResultType = SCAN_RESULT_TYPE_FULL;
+ private long mReportDelayMillis = 0;
+ private int mMatchMode = MATCH_MODE_AGGRESSIVE;
+ private int mNumOfMatchesPerFilter = MATCH_NUM_MAX_ADVERTISEMENT;
+ private boolean mLegacy = true;
+ private int mPhy = PHY_LE_ALL_SUPPORTED;
+
+ /**
+ * Set scan mode for Bluetooth LE scan.
+ *
+ * @param scanMode The scan mode can be one of {@link ScanSettings#SCAN_MODE_LOW_POWER},
+ * {@link ScanSettings#SCAN_MODE_BALANCED} or {@link ScanSettings#SCAN_MODE_LOW_LATENCY}.
+ * @throws IllegalArgumentException If the {@code scanMode} is invalid.
+ */
+ public Builder setScanMode(int scanMode) {
+ switch (scanMode) {
+ case SCAN_MODE_OPPORTUNISTIC:
+ case SCAN_MODE_LOW_POWER:
+ case SCAN_MODE_BALANCED:
+ case SCAN_MODE_LOW_LATENCY:
+ case SCAN_MODE_AMBIENT_DISCOVERY:
+ mScanMode = scanMode;
+ break;
+ default:
+ throw new IllegalArgumentException("invalid scan mode " + scanMode);
+ }
+ return this;
+ }
+
+ /**
+ * Set callback type for Bluetooth LE scan.
+ *
+ * @param callbackType The callback type flags for the scan.
+ * @throws IllegalArgumentException If the {@code callbackType} is invalid.
+ */
+ public Builder setCallbackType(int callbackType) {
+
+ if (!isValidCallbackType(callbackType)) {
+ throw new IllegalArgumentException("invalid callback type - " + callbackType);
+ }
+ mCallbackType = callbackType;
+ return this;
+ }
+
+ // Returns true if the callbackType is valid.
+ private boolean isValidCallbackType(int callbackType) {
+ if (callbackType == CALLBACK_TYPE_ALL_MATCHES
+ || callbackType == CALLBACK_TYPE_FIRST_MATCH
+ || callbackType == CALLBACK_TYPE_MATCH_LOST) {
+ return true;
+ }
+ return callbackType == (CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST);
+ }
+
+ /**
+ * Set scan result type for Bluetooth LE scan.
+ *
+ * @param scanResultType Type for scan result, could be either {@link
+ * ScanSettings#SCAN_RESULT_TYPE_FULL} or {@link ScanSettings#SCAN_RESULT_TYPE_ABBREVIATED}.
+ * @throws IllegalArgumentException If the {@code scanResultType} is invalid.
+ * @hide
+ */
+ @SystemApi
+ public Builder setScanResultType(int scanResultType) {
+ if (scanResultType < SCAN_RESULT_TYPE_FULL
+ || scanResultType > SCAN_RESULT_TYPE_ABBREVIATED) {
+ throw new IllegalArgumentException(
+ "invalid scanResultType - " + scanResultType);
+ }
+ mScanResultType = scanResultType;
+ return this;
+ }
+
+ /**
+ * Set report delay timestamp for Bluetooth LE scan. If set to 0, you will be notified of
+ * scan results immediately. If &gt; 0, scan results are queued up and delivered after the
+ * requested delay or 5000 milliseconds (whichever is higher). Note scan results may be
+ * delivered sooner if the internal buffers fill up.
+ *
+ * @param reportDelayMillis how frequently scan results should be delivered in
+ * milliseconds
+ * @throws IllegalArgumentException if {@code reportDelayMillis} &lt; 0
+ */
+ public Builder setReportDelay(long reportDelayMillis) {
+ if (reportDelayMillis < 0) {
+ throw new IllegalArgumentException("reportDelay must be > 0");
+ }
+ mReportDelayMillis = reportDelayMillis;
+ return this;
+ }
+
+ /**
+ * Set the number of matches for Bluetooth LE scan filters hardware match
+ *
+ * @param numOfMatches The num of matches can be one of
+ * {@link ScanSettings#MATCH_NUM_ONE_ADVERTISEMENT}
+ * or {@link ScanSettings#MATCH_NUM_FEW_ADVERTISEMENT} or {@link
+ * ScanSettings#MATCH_NUM_MAX_ADVERTISEMENT}
+ * @throws IllegalArgumentException If the {@code matchMode} is invalid.
+ */
+ public Builder setNumOfMatches(int numOfMatches) {
+ if (numOfMatches < MATCH_NUM_ONE_ADVERTISEMENT
+ || numOfMatches > MATCH_NUM_MAX_ADVERTISEMENT) {
+ throw new IllegalArgumentException("invalid numOfMatches " + numOfMatches);
+ }
+ mNumOfMatchesPerFilter = numOfMatches;
+ return this;
+ }
+
+ /**
+ * Set match mode for Bluetooth LE scan filters hardware match
+ *
+ * @param matchMode The match mode can be one of {@link ScanSettings#MATCH_MODE_AGGRESSIVE}
+ * or {@link ScanSettings#MATCH_MODE_STICKY}
+ * @throws IllegalArgumentException If the {@code matchMode} is invalid.
+ */
+ public Builder setMatchMode(int matchMode) {
+ if (matchMode < MATCH_MODE_AGGRESSIVE
+ || matchMode > MATCH_MODE_STICKY) {
+ throw new IllegalArgumentException("invalid matchMode " + matchMode);
+ }
+ mMatchMode = matchMode;
+ return this;
+ }
+
+ /**
+ * Set whether only legacy advertisments should be returned in scan results.
+ * Legacy advertisements include advertisements as specified by the
+ * Bluetooth core specification 4.2 and below. This is true by default
+ * for compatibility with older apps.
+ *
+ * @param legacy true if only legacy advertisements will be returned
+ */
+ public Builder setLegacy(boolean legacy) {
+ mLegacy = legacy;
+ return this;
+ }
+
+ /**
+ * Set the Physical Layer to use during this scan.
+ * This is used only if {@link ScanSettings.Builder#setLegacy}
+ * is set to false.
+ * {@link android.bluetooth.BluetoothAdapter#isLeCodedPhySupported}
+ * may be used to check whether LE Coded phy is supported by calling
+ * {@link android.bluetooth.BluetoothAdapter#isLeCodedPhySupported}.
+ * Selecting an unsupported phy will result in failure to start scan.
+ *
+ * @param phy Can be one of {@link BluetoothDevice#PHY_LE_1M}, {@link
+ * BluetoothDevice#PHY_LE_CODED} or {@link ScanSettings#PHY_LE_ALL_SUPPORTED}
+ */
+ public Builder setPhy(int phy) {
+ mPhy = phy;
+ return this;
+ }
+
+ /**
+ * Build {@link ScanSettings}.
+ */
+ public ScanSettings build() {
+ return new ScanSettings(mScanMode, mCallbackType, mScanResultType,
+ mReportDelayMillis, mMatchMode,
+ mNumOfMatchesPerFilter, mLegacy, mPhy);
+ }
+ }
+}
diff --git a/framework/java/android/bluetooth/le/TransportBlock.java b/framework/java/android/bluetooth/le/TransportBlock.java
new file mode 100644
index 0000000000..18bad9c3c2
--- /dev/null
+++ b/framework/java/android/bluetooth/le/TransportBlock.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Wrapper for Transport Discovery Data Transport Blocks.
+ * This class represents a Transport Block from a Transport Discovery Data.
+ *
+ * @see TransportDiscoveryData
+ * @see AdvertiseData
+ */
+public final class TransportBlock implements Parcelable {
+ private static final String TAG = "TransportBlock";
+ private final int mOrgId;
+ private final int mTdsFlags;
+ private final int mTransportDataLength;
+ private final byte[] mTransportData;
+
+ /**
+ * Creates an instance of TransportBlock from raw data.
+ *
+ * @param orgId the Organization ID
+ * @param tdsFlags the TDS flags
+ * @param transportDataLength the total length of the Transport Data
+ * @param transportData the Transport Data
+ */
+ public TransportBlock(int orgId, int tdsFlags, int transportDataLength,
+ @Nullable byte[] transportData) {
+ mOrgId = orgId;
+ mTdsFlags = tdsFlags;
+ mTransportDataLength = transportDataLength;
+ mTransportData = transportData;
+ }
+
+ private TransportBlock(Parcel in) {
+ mOrgId = in.readInt();
+ mTdsFlags = in.readInt();
+ mTransportDataLength = in.readInt();
+ if (mTransportDataLength > 0) {
+ mTransportData = new byte[mTransportDataLength];
+ in.readByteArray(mTransportData);
+ } else {
+ mTransportData = null;
+ }
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mOrgId);
+ dest.writeInt(mTdsFlags);
+ dest.writeInt(mTransportDataLength);
+ if (mTransportData != null) {
+ dest.writeByteArray(mTransportData);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TransportBlock other = (TransportBlock) obj;
+ return Arrays.equals(toByteArray(), other.toByteArray());
+ }
+
+ public static final @NonNull Creator<TransportBlock> CREATOR = new Creator<TransportBlock>() {
+ @Override
+ public TransportBlock createFromParcel(Parcel in) {
+ return new TransportBlock(in);
+ }
+
+ @Override
+ public TransportBlock[] newArray(int size) {
+ return new TransportBlock[size];
+ }
+ };
+
+ /**
+ * Gets the Organization ID of the Transport Block which corresponds to one of the
+ * the Bluetooth SIG Assigned Numbers.
+ */
+ public int getOrgId() {
+ return mOrgId;
+ }
+
+ /**
+ * Gets the TDS flags of the Transport Block which represents the role of the device and
+ * information about its state and supported features.
+ */
+ public int getTdsFlags() {
+ return mTdsFlags;
+ }
+
+ /**
+ * Gets the total number of octets in the Transport Data field in this Transport Block.
+ */
+ public int getTransportDataLength() {
+ return mTransportDataLength;
+ }
+
+ /**
+ * Gets the Transport Data of the Transport Block which contains organization-specific data.
+ */
+ @Nullable
+ public byte[] getTransportData() {
+ return mTransportData;
+ }
+
+ /**
+ * Converts this TransportBlock to byte array
+ *
+ * @return byte array representation of this Transport Block or null if the conversion failed
+ */
+ @Nullable
+ public byte[] toByteArray() {
+ try {
+ ByteBuffer buffer = ByteBuffer.allocate(totalBytes());
+ buffer.put((byte) mOrgId);
+ buffer.put((byte) mTdsFlags);
+ buffer.put((byte) mTransportDataLength);
+ if (mTransportData != null) {
+ buffer.put(mTransportData);
+ }
+ return buffer.array();
+ } catch (BufferOverflowException e) {
+ Log.e(TAG, "Error converting to byte array: " + e.toString());
+ return null;
+ }
+ }
+
+ /**
+ * @return total byte count of this TransportBlock
+ */
+ public int totalBytes() {
+ // 3 uint8 + byte[] length
+ int size = 3 + mTransportDataLength;
+ return size;
+ }
+}
diff --git a/framework/java/android/bluetooth/le/TransportDiscoveryData.java b/framework/java/android/bluetooth/le/TransportDiscoveryData.java
new file mode 100644
index 0000000000..2b52f19798
--- /dev/null
+++ b/framework/java/android/bluetooth/le/TransportDiscoveryData.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Wrapper for Transport Discovery Data AD Type.
+ * This class contains the Transport Discovery Data AD Type Code as well as
+ * a list of potential Transport Blocks.
+ *
+ * @see AdvertiseData
+ */
+public final class TransportDiscoveryData implements Parcelable {
+ private static final String TAG = "TransportDiscoveryData";
+ private final int mTransportDataType;
+ private final List<TransportBlock> mTransportBlocks;
+
+ /**
+ * Creates a TransportDiscoveryData instance.
+ *
+ * @param transportDataType the Transport Discovery Data AD Type
+ * @param transportBlocks the list of Transport Blocks
+ */
+ public TransportDiscoveryData(int transportDataType,
+ @NonNull List<TransportBlock> transportBlocks) {
+ mTransportDataType = transportDataType;
+ mTransportBlocks = transportBlocks;
+ }
+
+ /**
+ * Creates a TransportDiscoveryData instance from byte arrays.
+ *
+ * Uses the transport discovery data bytes and parses them into an usable class.
+ *
+ * @param transportDiscoveryData the raw discovery data
+ */
+ public TransportDiscoveryData(@NonNull byte[] transportDiscoveryData) {
+ ByteBuffer byteBuffer = ByteBuffer.wrap(transportDiscoveryData);
+ mTransportBlocks = new ArrayList();
+ if (byteBuffer.remaining() > 0) {
+ mTransportDataType = byteBuffer.get();
+ } else {
+ mTransportDataType = -1;
+ }
+ try {
+ while (byteBuffer.remaining() > 0) {
+ int orgId = byteBuffer.get();
+ int tdsFlags = byteBuffer.get();
+ int transportDataLength = byteBuffer.get();
+ byte[] transportData = new byte[transportDataLength];
+ byteBuffer.get(transportData, 0, transportDataLength);
+ mTransportBlocks.add(new TransportBlock(orgId, tdsFlags,
+ transportDataLength, transportData));
+ }
+ } catch (BufferUnderflowException e) {
+ Log.e(TAG, "Error while parsing data: " + e.toString());
+ }
+ }
+
+ private TransportDiscoveryData(Parcel in) {
+ mTransportDataType = in.readInt();
+ mTransportBlocks = in.createTypedArrayList(TransportBlock.CREATOR);
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ TransportDiscoveryData other = (TransportDiscoveryData) obj;
+ return Arrays.equals(toByteArray(), other.toByteArray());
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mTransportDataType);
+ dest.writeTypedList(mTransportBlocks);
+ }
+
+ public static final @NonNull Creator<TransportDiscoveryData> CREATOR =
+ new Creator<TransportDiscoveryData>() {
+ @Override
+ public TransportDiscoveryData createFromParcel(Parcel in) {
+ return new TransportDiscoveryData(in);
+ }
+
+ @Override
+ public TransportDiscoveryData[] newArray(int size) {
+ return new TransportDiscoveryData[size];
+ }
+ };
+
+ /**
+ * Gets the transport data type.
+ */
+ public int getTransportDataType() {
+ return mTransportDataType;
+ }
+
+ /**
+ * @return the list of {@link TransportBlock} in this TransportDiscoveryData
+ * or an empty list if there are no Transport Blocks
+ */
+ @NonNull
+ public List<TransportBlock> getTransportBlocks() {
+ if (mTransportBlocks == null) {
+ return Collections.emptyList();
+ }
+ return mTransportBlocks;
+ }
+
+ /**
+ * Converts this TransportDiscoveryData to byte array
+ *
+ * @return byte array representation of this Transport Discovery Data or null if the
+ * conversion failed
+ */
+ @Nullable
+ public byte[] toByteArray() {
+ try {
+ ByteBuffer buffer = ByteBuffer.allocate(totalBytes());
+ buffer.put((byte) mTransportDataType);
+ for (TransportBlock transportBlock : getTransportBlocks()) {
+ buffer.put(transportBlock.toByteArray());
+ }
+ return buffer.array();
+ } catch (BufferOverflowException e) {
+ Log.e(TAG, "Error converting to byte array: " + e.toString());
+ return null;
+ }
+ }
+
+ /**
+ * @return total byte count of this TransportDataDiscovery
+ */
+ public int totalBytes() {
+ int size = 1; // Counting Transport Data Type here.
+ for (TransportBlock transportBlock : getTransportBlocks()) {
+ size += transportBlock.totalBytes();
+ }
+ return size;
+ }
+}
diff --git a/framework/java/android/bluetooth/le/TruncatedFilter.java b/framework/java/android/bluetooth/le/TruncatedFilter.java
new file mode 100644
index 0000000000..25925888a0
--- /dev/null
+++ b/framework/java/android/bluetooth/le/TruncatedFilter.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+
+import java.util.List;
+
+/**
+ * A special scan filter that lets the client decide how the scan record should be stored.
+ *
+ * @deprecated this is not used anywhere
+ *
+ * @hide
+ */
+@Deprecated
+@SystemApi
+@SuppressLint("AndroidFrameworkBluetoothPermission")
+public final class TruncatedFilter {
+ private final ScanFilter mFilter;
+ private final List<ResultStorageDescriptor> mStorageDescriptors;
+
+ /**
+ * Constructor for {@link TruncatedFilter}.
+ *
+ * @param filter Scan filter of the truncated filter.
+ * @param storageDescriptors Describes how the scan should be stored.
+ */
+ public TruncatedFilter(ScanFilter filter, List<ResultStorageDescriptor> storageDescriptors) {
+ mFilter = filter;
+ mStorageDescriptors = storageDescriptors;
+ }
+
+ /**
+ * Returns the scan filter.
+ */
+ public ScanFilter getFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Returns a list of descriptor for scan result storage.
+ */
+ public List<ResultStorageDescriptor> getStorageDescriptors() {
+ return mStorageDescriptors;
+ }
+
+
+}
diff --git a/framework/java/android/bluetooth/package.html b/framework/java/android/bluetooth/package.html
new file mode 100644
index 0000000000..d9ca4f1310
--- /dev/null
+++ b/framework/java/android/bluetooth/package.html
@@ -0,0 +1,38 @@
+<HTML>
+<BODY>
+<p>Provides classes that manage Bluetooth functionality, such as scanning for
+devices, connecting with devices, and managing data transfer between devices.
+The Bluetooth API supports both "Classic Bluetooth" and Bluetooth Low Energy.</p>
+
+<p>For more information about Classic Bluetooth, see the
+<a href="{@docRoot}guide/topics/connectivity/bluetooth.html">Bluetooth</a> guide.
+For more information about Bluetooth Low Energy, see the
+<a href="{@docRoot}guide/topics/connectivity/bluetooth-le.html">
+Bluetooth Low Energy</a> (BLE) guide.</p>
+{@more}
+
+<p>The Bluetooth APIs let applications:</p>
+<ul>
+ <li>Scan for other Bluetooth devices (including BLE devices).</li>
+ <li>Query the local Bluetooth adapter for paired Bluetooth devices.</li>
+ <li>Establish RFCOMM channels/sockets.</li>
+ <li>Connect to specified sockets on other devices.</li>
+ <li>Transfer data to and from other devices.</li>
+ <li>Communicate with BLE devices, such as proximity sensors, heart rate
+ monitors, fitness devices, and so on.</li>
+ <li>Act as a GATT client or a GATT server (BLE).</li>
+</ul>
+
+<p>
+To perform Bluetooth communication using these APIs, an application must
+declare the {@link android.Manifest.permission#BLUETOOTH} permission. Some
+additional functionality, such as requesting device discovery,
+also requires the {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+permission.
+</p>
+
+<p class="note"><strong>Note:</strong>
+Not all Android-powered devices provide Bluetooth functionality.</p>
+
+</BODY>
+</HTML>
diff --git a/framework/tests/Android.bp b/framework/tests/Android.bp
new file mode 100644
index 0000000000..a6a2fe5115
--- /dev/null
+++ b/framework/tests/Android.bp
@@ -0,0 +1,19 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "BluetoothTests",
+ // Include all test java files.
+ srcs: ["src/**/*.java"],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ static_libs: [
+ "junit",
+ "modules-utils-bytesmatcher",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+}
diff --git a/framework/tests/AndroidManifest.xml b/framework/tests/AndroidManifest.xml
new file mode 100644
index 0000000000..75583d5298
--- /dev/null
+++ b/framework/tests/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.bluetooth.tests"
+ android:sharedUserId="android.uid.bluetooth" >
+
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+ <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
+ <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.LOCAL_MAC_ADDRESS" />
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
+ <uses-permission android:name="android.permission.READ_SMS"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application >
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="android.bluetooth.BluetoothTestRunner"
+ android:targetPackage="com.android.bluetooth.tests"
+ android:label="Bluetooth Tests" />
+ <instrumentation android:name="android.bluetooth.BluetoothInstrumentation"
+ android:targetPackage="com.android.bluetooth.tests"
+ android:label="Bluetooth Test Utils" />
+
+</manifest>
diff --git a/framework/tests/AndroidTest.xml b/framework/tests/AndroidTest.xml
new file mode 100644
index 0000000000..f93c4ebf5b
--- /dev/null
+++ b/framework/tests/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 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.
+-->
+<configuration description="Config for Bluetooth test cases">
+ <option name="test-suite-tag" value="apct"/>
+ <option name="test-suite-tag" value="apct-instrumentation"/>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="BluetoothTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct"/>
+ <option name="test-tag" value="BluetoothTests"/>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.bluetooth.tests" />
+ <option name="hidden-api-checks" value="false"/>
+ <option name="runner" value="android.bluetooth.BluetoothTestRunner"/>
+ </test>
+</configuration>
diff --git a/framework/tests/OWNERS b/framework/tests/OWNERS
new file mode 100644
index 0000000000..98bb877162
--- /dev/null
+++ b/framework/tests/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/bluetooth/OWNERS
diff --git a/framework/tests/src/android/bluetooth/BluetoothCodecConfigTest.java b/framework/tests/src/android/bluetooth/BluetoothCodecConfigTest.java
new file mode 100644
index 0000000000..bd55426601
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothCodecConfigTest.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2018 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 android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for {@link BluetoothCodecConfig}.
+ * <p>
+ * To run this test, use:
+ * runtest --path core/tests/bluetoothtests/src/android/bluetooth/BluetoothCodecConfigTest.java
+ */
+public class BluetoothCodecConfigTest extends TestCase {
+ private static final int[] kCodecTypeArray = new int[] {
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID,
+ };
+ private static final int[] kCodecPriorityArray = new int[] {
+ BluetoothCodecConfig.CODEC_PRIORITY_DISABLED,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST,
+ };
+ private static final int[] kSampleRateArray = new int[] {
+ BluetoothCodecConfig.SAMPLE_RATE_NONE,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.SAMPLE_RATE_88200,
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.SAMPLE_RATE_176400,
+ BluetoothCodecConfig.SAMPLE_RATE_192000,
+ };
+ private static final int[] kBitsPerSampleArray = new int[] {
+ BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ };
+ private static final int[] kChannelModeArray = new int[] {
+ BluetoothCodecConfig.CHANNEL_MODE_NONE,
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ };
+ private static final long[] kCodecSpecific1Array = new long[] { 1000, 1001, 1002, 1003, };
+ private static final long[] kCodecSpecific2Array = new long[] { 2000, 2001, 2002, 2003, };
+ private static final long[] kCodecSpecific3Array = new long[] { 3000, 3001, 3002, 3003, };
+ private static final long[] kCodecSpecific4Array = new long[] { 4000, 4001, 4002, 4003, };
+
+ private static final int kTotalConfigs = kCodecTypeArray.length * kCodecPriorityArray.length *
+ kSampleRateArray.length * kBitsPerSampleArray.length * kChannelModeArray.length *
+ kCodecSpecific1Array.length * kCodecSpecific2Array.length * kCodecSpecific3Array.length *
+ kCodecSpecific4Array.length;
+
+ private int selectCodecType(int configId) {
+ int left = kCodecTypeArray.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecTypeArray.length;
+ return kCodecTypeArray[index];
+ }
+
+ private int selectCodecPriority(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecPriorityArray.length;
+ return kCodecPriorityArray[index];
+ }
+
+ private int selectSampleRate(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kSampleRateArray.length;
+ return kSampleRateArray[index];
+ }
+
+ private int selectBitsPerSample(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kBitsPerSampleArray.length;
+ return kBitsPerSampleArray[index];
+ }
+
+ private int selectChannelMode(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length * kChannelModeArray.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kChannelModeArray.length;
+ return kChannelModeArray[index];
+ }
+
+ private long selectCodecSpecific1(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length * kChannelModeArray.length * kCodecSpecific1Array.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecSpecific1Array.length;
+ return kCodecSpecific1Array[index];
+ }
+
+ private long selectCodecSpecific2(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length * kChannelModeArray.length * kCodecSpecific1Array.length *
+ kCodecSpecific2Array.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecSpecific2Array.length;
+ return kCodecSpecific2Array[index];
+ }
+
+ private long selectCodecSpecific3(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length * kChannelModeArray.length * kCodecSpecific1Array.length *
+ kCodecSpecific2Array.length * kCodecSpecific3Array.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecSpecific3Array.length;
+ return kCodecSpecific3Array[index];
+ }
+
+ private long selectCodecSpecific4(int configId) {
+ int left = kCodecTypeArray.length * kCodecPriorityArray.length * kSampleRateArray.length *
+ kBitsPerSampleArray.length * kChannelModeArray.length * kCodecSpecific1Array.length *
+ kCodecSpecific2Array.length * kCodecSpecific3Array.length *
+ kCodecSpecific4Array.length;
+ int right = kTotalConfigs / left;
+ int index = configId / right;
+ index = index % kCodecSpecific4Array.length;
+ return kCodecSpecific4Array[index];
+ }
+
+ @SmallTest
+ public void testBluetoothCodecConfig_valid_get_methods() {
+
+ for (int config_id = 0; config_id < kTotalConfigs; config_id++) {
+ int codec_type = selectCodecType(config_id);
+ int codec_priority = selectCodecPriority(config_id);
+ int sample_rate = selectSampleRate(config_id);
+ int bits_per_sample = selectBitsPerSample(config_id);
+ int channel_mode = selectChannelMode(config_id);
+ long codec_specific1 = selectCodecSpecific1(config_id);
+ long codec_specific2 = selectCodecSpecific2(config_id);
+ long codec_specific3 = selectCodecSpecific3(config_id);
+ long codec_specific4 = selectCodecSpecific4(config_id);
+
+ BluetoothCodecConfig bcc = buildBluetoothCodecConfig(codec_type, codec_priority,
+ sample_rate, bits_per_sample,
+ channel_mode, codec_specific1,
+ codec_specific2, codec_specific3,
+ codec_specific4);
+
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC) {
+ assertTrue(bcc.isMandatoryCodec());
+ } else {
+ assertFalse(bcc.isMandatoryCodec());
+ }
+
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC) {
+ assertEquals("SBC", bcc.getCodecName());
+ }
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC) {
+ assertEquals("AAC", bcc.getCodecName());
+ }
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX) {
+ assertEquals("aptX", bcc.getCodecName());
+ }
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD) {
+ assertEquals("aptX HD", bcc.getCodecName());
+ }
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC) {
+ assertEquals("LDAC", bcc.getCodecName());
+ }
+ if (codec_type == BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID) {
+ assertEquals("INVALID CODEC", bcc.getCodecName());
+ }
+
+ assertEquals(codec_type, bcc.getCodecType());
+ assertEquals(codec_priority, bcc.getCodecPriority());
+ assertEquals(sample_rate, bcc.getSampleRate());
+ assertEquals(bits_per_sample, bcc.getBitsPerSample());
+ assertEquals(channel_mode, bcc.getChannelMode());
+ assertEquals(codec_specific1, bcc.getCodecSpecific1());
+ assertEquals(codec_specific2, bcc.getCodecSpecific2());
+ assertEquals(codec_specific3, bcc.getCodecSpecific3());
+ assertEquals(codec_specific4, bcc.getCodecSpecific4());
+ }
+ }
+
+ @SmallTest
+ public void testBluetoothCodecConfig_equals() {
+ BluetoothCodecConfig bcc1 =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ BluetoothCodecConfig bcc2_same =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+ assertTrue(bcc1.equals(bcc2_same));
+
+ BluetoothCodecConfig bcc3_codec_type =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc3_codec_type));
+
+ BluetoothCodecConfig bcc4_codec_priority =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc4_codec_priority));
+
+ BluetoothCodecConfig bcc5_sample_rate =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc5_sample_rate));
+
+ BluetoothCodecConfig bcc6_bits_per_sample =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc6_bits_per_sample));
+
+ BluetoothCodecConfig bcc7_channel_mode =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc7_channel_mode));
+
+ BluetoothCodecConfig bcc8_codec_specific1 =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1001, 2000, 3000, 4000);
+ assertFalse(bcc1.equals(bcc8_codec_specific1));
+
+ BluetoothCodecConfig bcc9_codec_specific2 =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2002, 3000, 4000);
+ assertFalse(bcc1.equals(bcc9_codec_specific2));
+
+ BluetoothCodecConfig bcc10_codec_specific3 =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3003, 4000);
+ assertFalse(bcc1.equals(bcc10_codec_specific3));
+
+ BluetoothCodecConfig bcc11_codec_specific4 =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4004);
+ assertFalse(bcc1.equals(bcc11_codec_specific4));
+ }
+
+ private BluetoothCodecConfig buildBluetoothCodecConfig(int sourceCodecType,
+ int codecPriority, int sampleRate, int bitsPerSample, int channelMode,
+ long codecSpecific1, long codecSpecific2, long codecSpecific3, long codecSpecific4) {
+ return new BluetoothCodecConfig.Builder()
+ .setCodecType(sourceCodecType)
+ .setCodecPriority(codecPriority)
+ .setSampleRate(sampleRate)
+ .setBitsPerSample(bitsPerSample)
+ .setChannelMode(channelMode)
+ .setCodecSpecific1(codecSpecific1)
+ .setCodecSpecific2(codecSpecific2)
+ .setCodecSpecific3(codecSpecific3)
+ .setCodecSpecific4(codecSpecific4)
+ .build();
+
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothCodecStatusTest.java b/framework/tests/src/android/bluetooth/BluetoothCodecStatusTest.java
new file mode 100644
index 0000000000..1cb2dcae86
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothCodecStatusTest.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright 2018 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 android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Unit test cases for {@link BluetoothCodecStatus}.
+ * <p>
+ * To run this test, use:
+ * runtest --path core/tests/bluetoothtests/src/android/bluetooth/BluetoothCodecStatusTest.java
+ */
+public class BluetoothCodecStatusTest extends TestCase {
+
+ // Codec configs: A and B are same; C is different
+ private static final BluetoothCodecConfig config_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig config_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig config_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ // Local capabilities: A and B are same; C is different
+ private static final BluetoothCodecConfig local_capability1_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability1_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability1_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+
+ private static final BluetoothCodecConfig local_capability2_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability2_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability2_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability3_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability3_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability3_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability4_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability4_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability4_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability5_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability5_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig local_capability5_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+
+ // Selectable capabilities: A and B are same; C is different
+ private static final BluetoothCodecConfig selectable_capability1_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability1_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability1_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability2_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability2_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability2_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability3_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability3_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability3_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability4_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability4_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability4_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability5_A =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability5_B =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO |
+ BluetoothCodecConfig.CHANNEL_MODE_MONO,
+ 1000, 2000, 3000, 4000);
+
+ private static final BluetoothCodecConfig selectable_capability5_C =
+ buildBluetoothCodecConfig(BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC,
+ BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
+ BluetoothCodecConfig.SAMPLE_RATE_44100 |
+ BluetoothCodecConfig.SAMPLE_RATE_48000 |
+ BluetoothCodecConfig.SAMPLE_RATE_88200 |
+ BluetoothCodecConfig.SAMPLE_RATE_96000,
+ BluetoothCodecConfig.BITS_PER_SAMPLE_16 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_24 |
+ BluetoothCodecConfig.BITS_PER_SAMPLE_32,
+ BluetoothCodecConfig.CHANNEL_MODE_STEREO,
+ 1000, 2000, 3000, 4000);
+
+ private static final List<BluetoothCodecConfig> LOCAL_CAPABILITY_A =
+ new ArrayList() {{
+ add(local_capability1_A);
+ add(local_capability2_A);
+ add(local_capability3_A);
+ add(local_capability4_A);
+ add(local_capability5_A);
+ }};
+
+ private static final List<BluetoothCodecConfig> LOCAL_CAPABILITY_B =
+ new ArrayList() {{
+ add(local_capability1_B);
+ add(local_capability2_B);
+ add(local_capability3_B);
+ add(local_capability4_B);
+ add(local_capability5_B);
+ }};
+
+ private static final List<BluetoothCodecConfig> LOCAL_CAPABILITY_B_REORDERED =
+ new ArrayList() {{
+ add(local_capability5_B);
+ add(local_capability4_B);
+ add(local_capability2_B);
+ add(local_capability3_B);
+ add(local_capability1_B);
+ }};
+
+ private static final List<BluetoothCodecConfig> LOCAL_CAPABILITY_C =
+ new ArrayList() {{
+ add(local_capability1_C);
+ add(local_capability2_C);
+ add(local_capability3_C);
+ add(local_capability4_C);
+ add(local_capability5_C);
+ }};
+
+ private static final List<BluetoothCodecConfig> SELECTABLE_CAPABILITY_A =
+ new ArrayList() {{
+ add(selectable_capability1_A);
+ add(selectable_capability2_A);
+ add(selectable_capability3_A);
+ add(selectable_capability4_A);
+ add(selectable_capability5_A);
+ }};
+
+ private static final List<BluetoothCodecConfig> SELECTABLE_CAPABILITY_B =
+ new ArrayList() {{
+ add(selectable_capability1_B);
+ add(selectable_capability2_B);
+ add(selectable_capability3_B);
+ add(selectable_capability4_B);
+ add(selectable_capability5_B);
+ }};
+
+ private static final List<BluetoothCodecConfig> SELECTABLE_CAPABILITY_B_REORDERED =
+ new ArrayList() {{
+ add(selectable_capability5_B);
+ add(selectable_capability4_B);
+ add(selectable_capability2_B);
+ add(selectable_capability3_B);
+ add(selectable_capability1_B);
+ }};
+
+ private static final List<BluetoothCodecConfig> SELECTABLE_CAPABILITY_C =
+ new ArrayList() {{
+ add(selectable_capability1_C);
+ add(selectable_capability2_C);
+ add(selectable_capability3_C);
+ add(selectable_capability4_C);
+ add(selectable_capability5_C);
+ }};
+
+ private static final BluetoothCodecStatus bcs_A =
+ new BluetoothCodecStatus(config_A, LOCAL_CAPABILITY_A, SELECTABLE_CAPABILITY_A);
+ private static final BluetoothCodecStatus bcs_B =
+ new BluetoothCodecStatus(config_B, LOCAL_CAPABILITY_B, SELECTABLE_CAPABILITY_B);
+ private static final BluetoothCodecStatus bcs_B_reordered =
+ new BluetoothCodecStatus(config_B, LOCAL_CAPABILITY_B_REORDERED,
+ SELECTABLE_CAPABILITY_B_REORDERED);
+ private static final BluetoothCodecStatus bcs_C =
+ new BluetoothCodecStatus(config_C, LOCAL_CAPABILITY_C, SELECTABLE_CAPABILITY_C);
+
+ @SmallTest
+ public void testBluetoothCodecStatus_get_methods() {
+
+ assertTrue(Objects.equals(bcs_A.getCodecConfig(), config_A));
+ assertTrue(Objects.equals(bcs_A.getCodecConfig(), config_B));
+ assertFalse(Objects.equals(bcs_A.getCodecConfig(), config_C));
+
+ assertTrue(bcs_A.getCodecsLocalCapabilities().equals(LOCAL_CAPABILITY_A));
+ assertTrue(bcs_A.getCodecsLocalCapabilities().equals(LOCAL_CAPABILITY_B));
+ assertFalse(bcs_A.getCodecsLocalCapabilities().equals(LOCAL_CAPABILITY_C));
+
+ assertTrue(bcs_A.getCodecsSelectableCapabilities()
+ .equals(SELECTABLE_CAPABILITY_A));
+ assertTrue(bcs_A.getCodecsSelectableCapabilities()
+ .equals(SELECTABLE_CAPABILITY_B));
+ assertFalse(bcs_A.getCodecsSelectableCapabilities()
+ .equals(SELECTABLE_CAPABILITY_C));
+ }
+
+ @SmallTest
+ public void testBluetoothCodecStatus_equals() {
+ assertTrue(bcs_A.equals(bcs_B));
+ assertTrue(bcs_B.equals(bcs_A));
+ assertTrue(bcs_A.equals(bcs_B_reordered));
+ assertTrue(bcs_B_reordered.equals(bcs_A));
+ assertFalse(bcs_A.equals(bcs_C));
+ assertFalse(bcs_C.equals(bcs_A));
+ }
+
+ private static BluetoothCodecConfig buildBluetoothCodecConfig(int sourceCodecType,
+ int codecPriority, int sampleRate, int bitsPerSample, int channelMode,
+ long codecSpecific1, long codecSpecific2, long codecSpecific3, long codecSpecific4) {
+ return new BluetoothCodecConfig.Builder()
+ .setCodecType(sourceCodecType)
+ .setCodecPriority(codecPriority)
+ .setSampleRate(sampleRate)
+ .setBitsPerSample(bitsPerSample)
+ .setChannelMode(channelMode)
+ .setCodecSpecific1(codecSpecific1)
+ .setCodecSpecific2(codecSpecific2)
+ .setCodecSpecific3(codecSpecific3)
+ .setCodecSpecific4(codecSpecific4)
+ .build();
+
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothInstrumentation.java b/framework/tests/src/android/bluetooth/BluetoothInstrumentation.java
new file mode 100644
index 0000000000..37b2a50ed6
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothInstrumentation.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2014 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 android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Bundle;
+
+import junit.framework.Assert;
+
+import java.util.Set;
+
+public class BluetoothInstrumentation extends Instrumentation {
+
+ private BluetoothTestUtils mUtils = null;
+ private BluetoothAdapter mAdapter = null;
+ private Bundle mArgs = null;
+ private Bundle mSuccessResult = null;
+
+ private BluetoothTestUtils getBluetoothTestUtils() {
+ if (mUtils == null) {
+ mUtils = new BluetoothTestUtils(getContext(),
+ BluetoothInstrumentation.class.getSimpleName());
+ }
+ return mUtils;
+ }
+
+ private BluetoothAdapter getBluetoothAdapter() {
+ if (mAdapter == null) {
+ mAdapter = ((BluetoothManager)getContext().getSystemService(
+ Context.BLUETOOTH_SERVICE)).getAdapter();
+ }
+ return mAdapter;
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ mArgs = arguments;
+ // create the default result response, but only use it in success code path
+ mSuccessResult = new Bundle();
+ mSuccessResult.putString("result", "SUCCESS");
+ start();
+ }
+
+ @Override
+ public void onStart() {
+ String command = mArgs.getString("command");
+ if ("enable".equals(command)) {
+ enable();
+ } else if ("disable".equals(command)) {
+ disable();
+ } else if ("unpairAll".equals(command)) {
+ unpairAll();
+ } else if ("getName".equals(command)) {
+ getName();
+ } else if ("getAddress".equals(command)) {
+ getAddress();
+ } else if ("getBondedDevices".equals(command)) {
+ getBondedDevices();
+ } else {
+ finish(null);
+ }
+ }
+
+ public void enable() {
+ getBluetoothTestUtils().enable(getBluetoothAdapter());
+ finish(mSuccessResult);
+ }
+
+ public void disable() {
+ getBluetoothTestUtils().disable(getBluetoothAdapter());
+ finish(mSuccessResult);
+ }
+
+ public void unpairAll() {
+ getBluetoothTestUtils().unpairAll(getBluetoothAdapter());
+ finish(mSuccessResult);
+ }
+
+ public void getName() {
+ String name = getBluetoothAdapter().getName();
+ mSuccessResult.putString("name", name);
+ finish(mSuccessResult);
+ }
+
+ public void getAddress() {
+ String name = getBluetoothAdapter().getAddress();
+ mSuccessResult.putString("address", name);
+ finish(mSuccessResult);
+ }
+
+ public void getBondedDevices() {
+ Set<BluetoothDevice> devices = getBluetoothAdapter().getBondedDevices();
+ int i = 0;
+ for (BluetoothDevice device : devices) {
+ mSuccessResult.putString(String.format("device-%02d", i), device.getAddress());
+ i++;
+ }
+ finish(mSuccessResult);
+ }
+
+ public void finish(Bundle result) {
+ if (result == null) {
+ result = new Bundle();
+ }
+ finish(Activity.RESULT_OK, result);
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java b/framework/tests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java
new file mode 100644
index 0000000000..c3d707cd75
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothLeAudioCodecConfigTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2021 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 android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for {@link BluetoothLeAudioCodecConfig}.
+ */
+public class BluetoothLeAudioCodecConfigTest extends TestCase {
+ private int[] mCodecTypeArray = new int[] {
+ BluetoothLeAudioCodecConfig.SOURCE_CODEC_TYPE_LC3,
+ BluetoothLeAudioCodecConfig.SOURCE_CODEC_TYPE_INVALID,
+ };
+
+ @SmallTest
+ public void testBluetoothLeAudioCodecConfig_valid_get_methods() {
+
+ for (int codecIdx = 0; codecIdx < mCodecTypeArray.length; codecIdx++) {
+ int codecType = mCodecTypeArray[codecIdx];
+
+ BluetoothLeAudioCodecConfig leAudioCodecConfig =
+ buildBluetoothLeAudioCodecConfig(codecType);
+
+ if (codecType == BluetoothLeAudioCodecConfig.SOURCE_CODEC_TYPE_LC3) {
+ assertEquals("LC3", leAudioCodecConfig.getCodecName());
+ }
+ if (codecType == BluetoothLeAudioCodecConfig.SOURCE_CODEC_TYPE_INVALID) {
+ assertEquals("INVALID CODEC", leAudioCodecConfig.getCodecName());
+ }
+
+ assertEquals(codecType, leAudioCodecConfig.getCodecType());
+ }
+ }
+
+ private BluetoothLeAudioCodecConfig buildBluetoothLeAudioCodecConfig(int sourceCodecType) {
+ return new BluetoothLeAudioCodecConfig.Builder()
+ .setCodecType(sourceCodecType)
+ .build();
+
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothRebootStressTest.java b/framework/tests/src/android/bluetooth/BluetoothRebootStressTest.java
new file mode 100644
index 0000000000..33e9dd7fab
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothRebootStressTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.test.InstrumentationTestCase;
+
+/**
+ * Instrumentation test case for stress test involving rebooting the device.
+ * <p>
+ * This test case tests that bluetooth is enabled after a device reboot. Because
+ * the device will reboot, the instrumentation must be driven by a script on the
+ * host side.
+ */
+public class BluetoothRebootStressTest extends InstrumentationTestCase {
+ private static final String TAG = "BluetoothRebootStressTest";
+ private static final String OUTPUT_FILE = "BluetoothRebootStressTestOutput.txt";
+
+ private BluetoothTestUtils mTestUtils;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ Context context = getInstrumentation().getTargetContext();
+ mTestUtils = new BluetoothTestUtils(context, TAG, OUTPUT_FILE);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ mTestUtils.close();
+ }
+
+ /**
+ * Test method used to start the test by turning bluetooth on.
+ */
+ public void testStart() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ mTestUtils.enable(adapter);
+ }
+
+ /**
+ * Test method used in the middle iterations of the test to check if
+ * bluetooth is on. Does not toggle bluetooth after the check. Assumes that
+ * bluetooth has been turned on by {@code #testStart()}
+ */
+ public void testMiddleNoToggle() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ assertTrue(adapter.isEnabled());
+ }
+
+ /**
+ * Test method used in the middle iterations of the test to check if
+ * bluetooth is on. Toggles bluetooth after the check. Assumes that
+ * bluetooth has been turned on by {@code #testStart()}
+ */
+ public void testMiddleToggle() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ assertTrue(adapter.isEnabled());
+
+ mTestUtils.disable(adapter);
+ mTestUtils.enable(adapter);
+ }
+
+ /**
+ * Test method used in the stop the test by turning bluetooth off. Assumes
+ * that bluetooth has been turned on by {@code #testStart()}
+ */
+ public void testStop() {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ assertTrue(adapter.isEnabled());
+
+ mTestUtils.disable(adapter);
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothStressTest.java b/framework/tests/src/android/bluetooth/BluetoothStressTest.java
new file mode 100644
index 0000000000..89dbe3f75b
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothStressTest.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2010 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 android.content.Context;
+import android.test.InstrumentationTestCase;
+
+/**
+ * Stress test suite for Bluetooth related functions.
+ *
+ * Includes tests for enabling/disabling bluetooth, enabling/disabling discoverable mode,
+ * starting/stopping scans, connecting/disconnecting to HFP, A2DP, HID, PAN profiles, and verifying
+ * that remote connections/disconnections occur for the PAN profile.
+ * <p>
+ * This test suite uses {@link android.bluetooth.BluetoothTestRunner} to for parameters such as the
+ * number of iterations and the addresses of remote Bluetooth devices.
+ */
+public class BluetoothStressTest extends InstrumentationTestCase {
+ private static final String TAG = "BluetoothStressTest";
+ private static final String OUTPUT_FILE = "BluetoothStressTestOutput.txt";
+ /** The amount of time to sleep between issuing start/stop SCO in ms. */
+ private static final long SCO_SLEEP_TIME = 2 * 1000;
+
+ private BluetoothAdapter mAdapter;
+ private BluetoothTestUtils mTestUtils;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ Context context = getInstrumentation().getTargetContext();
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mTestUtils = new BluetoothTestUtils(context, TAG, OUTPUT_FILE);
+
+ // Start all tests in a disabled state.
+ if (mAdapter.isEnabled()) {
+ mTestUtils.disable(mAdapter);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mTestUtils.close();
+ }
+
+ /**
+ * Stress test for enabling and disabling Bluetooth.
+ */
+ public void testEnable() {
+ int iterations = BluetoothTestRunner.sEnableIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("enable iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disable(mAdapter);
+ }
+ }
+
+ /**
+ * Stress test for putting the device in and taking the device out of discoverable mode.
+ */
+ public void testDiscoverable() {
+ int iterations = BluetoothTestRunner.sDiscoverableIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ mTestUtils.enable(mAdapter);
+ mTestUtils.undiscoverable(mAdapter);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("discoverable iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.discoverable(mAdapter);
+ mTestUtils.undiscoverable(mAdapter);
+ }
+ }
+
+ /**
+ * Stress test for starting and stopping Bluetooth scans.
+ */
+ public void testScan() {
+ int iterations = BluetoothTestRunner.sScanIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ mTestUtils.enable(mAdapter);
+ mTestUtils.stopScan(mAdapter);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("scan iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.startScan(mAdapter);
+ mTestUtils.stopScan(mAdapter);
+ }
+ }
+
+ /**
+ * Stress test for enabling and disabling the PAN NAP profile.
+ */
+ public void testEnablePan() {
+ int iterations = BluetoothTestRunner.sEnablePanIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disablePan(mAdapter);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("testEnablePan iteration " + (i + 1) + " of "
+ + iterations);
+ mTestUtils.enablePan(mAdapter);
+ mTestUtils.disablePan(mAdapter);
+ }
+ }
+
+ /**
+ * Stress test for pairing and unpairing with a remote device.
+ * <p>
+ * In this test, the local device initiates pairing with a remote device, and then unpairs with
+ * the device after the pairing has successfully completed.
+ */
+ public void testPair() {
+ int iterations = BluetoothTestRunner.sPairIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("pair iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.unpair(mAdapter, device);
+ }
+ }
+
+ /**
+ * Stress test for accepting a pairing request and unpairing with a remote device.
+ * <p>
+ * In this test, the local device waits for a pairing request from a remote device. It accepts
+ * the request and then unpairs after the paring has successfully completed.
+ */
+ public void testAcceptPair() {
+ int iterations = BluetoothTestRunner.sPairIterations;
+ if (iterations == 0) {
+ return;
+ }
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("acceptPair iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.acceptPair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.unpair(mAdapter, device);
+ }
+ }
+
+ /**
+ * Stress test for connecting and disconnecting with an A2DP source.
+ * <p>
+ * In this test, the local device plays the role of an A2DP sink, and initiates connections and
+ * disconnections with an A2DP source.
+ */
+ public void testConnectA2dp() {
+ int iterations = BluetoothTestRunner.sConnectA2dpIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.A2DP, null);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("connectA2dp iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.A2DP,
+ String.format("connectA2dp(device=%s)", device));
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.A2DP,
+ String.format("disconnectA2dp(device=%s)", device));
+ }
+
+ mTestUtils.unpair(mAdapter, device);
+ }
+
+ /**
+ * Stress test for connecting and disconnecting the HFP with a hands free device.
+ * <p>
+ * In this test, the local device plays the role of an HFP audio gateway, and initiates
+ * connections and disconnections with a hands free device.
+ */
+ public void testConnectHeadset() {
+ int iterations = BluetoothTestRunner.sConnectHeadsetIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("connectHeadset iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.HEADSET,
+ String.format("connectHeadset(device=%s)", device));
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET,
+ String.format("disconnectHeadset(device=%s)", device));
+ }
+
+ mTestUtils.unpair(mAdapter, device);
+ }
+
+ /**
+ * Stress test for connecting and disconnecting with a HID device.
+ * <p>
+ * In this test, the local device plays the role of a HID host, and initiates connections and
+ * disconnections with a HID device.
+ */
+ public void testConnectInput() {
+ int iterations = BluetoothTestRunner.sConnectInputIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HID_HOST, null);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("connectInput iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.HID_HOST,
+ String.format("connectInput(device=%s)", device));
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HID_HOST,
+ String.format("disconnectInput(device=%s)", device));
+ }
+
+ mTestUtils.unpair(mAdapter, device);
+ }
+
+ /**
+ * Stress test for connecting and disconnecting with a PAN NAP.
+ * <p>
+ * In this test, the local device plays the role of a PANU, and initiates connections and
+ * disconnections with a NAP.
+ */
+ public void testConnectPan() {
+ int iterations = BluetoothTestRunner.sConnectPanIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("connectPan iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.connectPan(mAdapter, device);
+ mTestUtils.disconnectPan(mAdapter, device);
+ }
+
+ mTestUtils.unpair(mAdapter, device);
+ }
+
+ /**
+ * Stress test for verifying a PANU connecting and disconnecting with the device.
+ * <p>
+ * In this test, the local device plays the role of a NAP which a remote PANU connects and
+ * disconnects from.
+ */
+ public void testIncomingPanConnection() {
+ int iterations = BluetoothTestRunner.sConnectPanIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.disablePan(mAdapter);
+ mTestUtils.enablePan(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.acceptPair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("incomingPanConnection iteration " + (i + 1) + " of "
+ + iterations);
+ mTestUtils.incomingPanConnection(mAdapter, device);
+ mTestUtils.incomingPanDisconnection(mAdapter, device);
+ }
+
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.disablePan(mAdapter);
+ }
+
+ /**
+ * Stress test for verifying that AudioManager can open and close SCO connections.
+ * <p>
+ * In this test, a HSP connection is opened with an external headset and the SCO connection is
+ * repeatibly opened and closed.
+ */
+ public void testStartStopSco() {
+ int iterations = BluetoothTestRunner.sStartStopScoIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.unpair(mAdapter, device);
+ mTestUtils.pair(mAdapter, device, BluetoothTestRunner.sDevicePairPasskey,
+ BluetoothTestRunner.sDevicePairPin);
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.stopSco(mAdapter, device);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.writeOutput("startStopSco iteration " + (i + 1) + " of " + iterations);
+ mTestUtils.startSco(mAdapter, device);
+ sleep(SCO_SLEEP_TIME);
+ mTestUtils.stopSco(mAdapter, device);
+ sleep(SCO_SLEEP_TIME);
+ }
+
+ mTestUtils.disconnectProfile(mAdapter, device, BluetoothProfile.HEADSET, null);
+ mTestUtils.unpair(mAdapter, device);
+ }
+
+ /* Make sure there is at least 1 unread message in the last week on remote device */
+ public void testMceSetMessageStatus() {
+ int iterations = BluetoothTestRunner.sMceSetMessageStatusIterations;
+ if (iterations == 0) {
+ return;
+ }
+
+ BluetoothDevice device = mAdapter.getRemoteDevice(BluetoothTestRunner.sDeviceAddress);
+ mTestUtils.enable(mAdapter);
+ mTestUtils.connectProfile(mAdapter, device, BluetoothProfile.MAP_CLIENT, null);
+ mTestUtils.mceGetUnreadMessage(mAdapter, device);
+
+ for (int i = 0; i < iterations; i++) {
+ mTestUtils.mceSetMessageStatus(mAdapter, device, BluetoothMapClient.READ);
+ mTestUtils.mceSetMessageStatus(mAdapter, device, BluetoothMapClient.UNREAD);
+ }
+
+ /**
+ * It is hard to find device to support set undeleted status, so just
+ * set deleted in 1 iteration
+ **/
+ mTestUtils.mceSetMessageStatus(mAdapter, device, BluetoothMapClient.DELETED);
+ }
+
+ private void sleep(long time) {
+ try {
+ Thread.sleep(time);
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothTestRunner.java b/framework/tests/src/android/bluetooth/BluetoothTestRunner.java
new file mode 100644
index 0000000000..d19c2c3e7e
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothTestRunner.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2010 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 junit.framework.TestSuite;
+
+import android.os.Bundle;
+import android.test.InstrumentationTestRunner;
+import android.test.InstrumentationTestSuite;
+import android.util.Log;
+
+/**
+ * Instrumentation test runner for Bluetooth tests.
+ * <p>
+ * To run:
+ * <pre>
+ * {@code
+ * adb shell am instrument \
+ * [-e enable_iterations <iterations>] \
+ * [-e discoverable_iterations <iterations>] \
+ * [-e scan_iterations <iterations>] \
+ * [-e enable_pan_iterations <iterations>] \
+ * [-e pair_iterations <iterations>] \
+ * [-e connect_a2dp_iterations <iterations>] \
+ * [-e connect_headset_iterations <iterations>] \
+ * [-e connect_input_iterations <iterations>] \
+ * [-e connect_pan_iterations <iterations>] \
+ * [-e start_stop_sco_iterations <iterations>] \
+ * [-e mce_set_message_status_iterations <iterations>] \
+ * [-e pair_address <address>] \
+ * [-e headset_address <address>] \
+ * [-e a2dp_address <address>] \
+ * [-e input_address <address>] \
+ * [-e pan_address <address>] \
+ * [-e pair_pin <pin>] \
+ * [-e pair_passkey <passkey>] \
+ * -w com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner
+ * }
+ * </pre>
+ */
+public class BluetoothTestRunner extends InstrumentationTestRunner {
+ private static final String TAG = "BluetoothTestRunner";
+
+ public static int sEnableIterations = 100;
+ public static int sDiscoverableIterations = 1000;
+ public static int sScanIterations = 1000;
+ public static int sEnablePanIterations = 1000;
+ public static int sPairIterations = 100;
+ public static int sConnectHeadsetIterations = 100;
+ public static int sConnectA2dpIterations = 100;
+ public static int sConnectInputIterations = 100;
+ public static int sConnectPanIterations = 100;
+ public static int sStartStopScoIterations = 100;
+ public static int sMceSetMessageStatusIterations = 100;
+
+ public static String sDeviceAddress = "";
+ public static byte[] sDevicePairPin = {'1', '2', '3', '4'};
+ public static int sDevicePairPasskey = 123456;
+
+ @Override
+ public TestSuite getAllTests() {
+ TestSuite suite = new InstrumentationTestSuite(this);
+ suite.addTestSuite(BluetoothStressTest.class);
+ return suite;
+ }
+
+ @Override
+ public ClassLoader getLoader() {
+ return BluetoothTestRunner.class.getClassLoader();
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ String val = arguments.getString("enable_iterations");
+ if (val != null) {
+ try {
+ sEnableIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("discoverable_iterations");
+ if (val != null) {
+ try {
+ sDiscoverableIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("scan_iterations");
+ if (val != null) {
+ try {
+ sScanIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("enable_pan_iterations");
+ if (val != null) {
+ try {
+ sEnablePanIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("pair_iterations");
+ if (val != null) {
+ try {
+ sPairIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("connect_a2dp_iterations");
+ if (val != null) {
+ try {
+ sConnectA2dpIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("connect_headset_iterations");
+ if (val != null) {
+ try {
+ sConnectHeadsetIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("connect_input_iterations");
+ if (val != null) {
+ try {
+ sConnectInputIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("connect_pan_iterations");
+ if (val != null) {
+ try {
+ sConnectPanIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("start_stop_sco_iterations");
+ if (val != null) {
+ try {
+ sStartStopScoIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("mce_set_message_status_iterations");
+ if (val != null) {
+ try {
+ sMceSetMessageStatusIterations = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ val = arguments.getString("device_address");
+ if (val != null) {
+ sDeviceAddress = val;
+ }
+
+ val = arguments.getString("device_pair_pin");
+ if (val != null) {
+ byte[] pin = BluetoothDevice.convertPinToBytes(val);
+ if (pin != null) {
+ sDevicePairPin = pin;
+ }
+ }
+
+ val = arguments.getString("device_pair_passkey");
+ if (val != null) {
+ try {
+ sDevicePairPasskey = Integer.parseInt(val);
+ } catch (NumberFormatException e) {
+ // Invalid argument, fall back to default value
+ }
+ }
+
+ Log.i(TAG, String.format("enable_iterations=%d", sEnableIterations));
+ Log.i(TAG, String.format("discoverable_iterations=%d", sDiscoverableIterations));
+ Log.i(TAG, String.format("scan_iterations=%d", sScanIterations));
+ Log.i(TAG, String.format("pair_iterations=%d", sPairIterations));
+ Log.i(TAG, String.format("connect_a2dp_iterations=%d", sConnectA2dpIterations));
+ Log.i(TAG, String.format("connect_headset_iterations=%d", sConnectHeadsetIterations));
+ Log.i(TAG, String.format("connect_input_iterations=%d", sConnectInputIterations));
+ Log.i(TAG, String.format("connect_pan_iterations=%d", sConnectPanIterations));
+ Log.i(TAG, String.format("start_stop_sco_iterations=%d", sStartStopScoIterations));
+ Log.i(TAG, String.format("device_address=%s", sDeviceAddress));
+ Log.i(TAG, String.format("device_pair_pin=%s", new String(sDevicePairPin)));
+ Log.i(TAG, String.format("device_pair_passkey=%d", sDevicePairPasskey));
+
+ // Call onCreate last since we want to set the static variables first.
+ super.onCreate(arguments);
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothTestUtils.java b/framework/tests/src/android/bluetooth/BluetoothTestUtils.java
new file mode 100644
index 0000000000..409025bc67
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothTestUtils.java
@@ -0,0 +1,1649 @@
+/*
+ * Copyright (C) 2010 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 android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.Environment;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class BluetoothTestUtils extends Assert {
+
+ /** Timeout for enable/disable in ms. */
+ private static final int ENABLE_DISABLE_TIMEOUT = 20000;
+ /** Timeout for discoverable/undiscoverable in ms. */
+ private static final int DISCOVERABLE_UNDISCOVERABLE_TIMEOUT = 5000;
+ /** Timeout for starting/stopping a scan in ms. */
+ private static final int START_STOP_SCAN_TIMEOUT = 5000;
+ /** Timeout for pair/unpair in ms. */
+ private static final int PAIR_UNPAIR_TIMEOUT = 20000;
+ /** Timeout for connecting/disconnecting a profile in ms. */
+ private static final int CONNECT_DISCONNECT_PROFILE_TIMEOUT = 20000;
+ /** Timeout to start or stop a SCO channel in ms. */
+ private static final int START_STOP_SCO_TIMEOUT = 10000;
+ /** Timeout to connect a profile proxy in ms. */
+ private static final int CONNECT_PROXY_TIMEOUT = 5000;
+ /** Time between polls in ms. */
+ private static final int POLL_TIME = 100;
+ /** Timeout to get map message in ms. */
+ private static final int GET_UNREAD_MESSAGE_TIMEOUT = 10000;
+ /** Timeout to set map message status in ms. */
+ private static final int SET_MESSAGE_STATUS_TIMEOUT = 2000;
+
+ private abstract class FlagReceiver extends BroadcastReceiver {
+ private int mExpectedFlags = 0;
+ private int mFiredFlags = 0;
+ private long mCompletedTime = -1;
+
+ public FlagReceiver(int expectedFlags) {
+ mExpectedFlags = expectedFlags;
+ }
+
+ public int getFiredFlags() {
+ synchronized (this) {
+ return mFiredFlags;
+ }
+ }
+
+ public long getCompletedTime() {
+ synchronized (this) {
+ return mCompletedTime;
+ }
+ }
+
+ protected void setFiredFlag(int flag) {
+ synchronized (this) {
+ mFiredFlags |= flag;
+ if ((mFiredFlags & mExpectedFlags) == mExpectedFlags) {
+ mCompletedTime = System.currentTimeMillis();
+ }
+ }
+ }
+ }
+
+ private class BluetoothReceiver extends FlagReceiver {
+ private static final int DISCOVERY_STARTED_FLAG = 1;
+ private static final int DISCOVERY_FINISHED_FLAG = 1 << 1;
+ private static final int SCAN_MODE_NONE_FLAG = 1 << 2;
+ private static final int SCAN_MODE_CONNECTABLE_FLAG = 1 << 3;
+ private static final int SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG = 1 << 4;
+ private static final int STATE_OFF_FLAG = 1 << 5;
+ private static final int STATE_TURNING_ON_FLAG = 1 << 6;
+ private static final int STATE_ON_FLAG = 1 << 7;
+ private static final int STATE_TURNING_OFF_FLAG = 1 << 8;
+ private static final int STATE_GET_MESSAGE_FINISHED_FLAG = 1 << 9;
+ private static final int STATE_SET_MESSAGE_STATUS_FINISHED_FLAG = 1 << 10;
+
+ public BluetoothReceiver(int expectedFlags) {
+ super(expectedFlags);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(intent.getAction())) {
+ setFiredFlag(DISCOVERY_STARTED_FLAG);
+ } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(intent.getAction())) {
+ setFiredFlag(DISCOVERY_FINISHED_FLAG);
+ } else if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
+ int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, -1);
+ assertNotSame(-1, mode);
+ switch (mode) {
+ case BluetoothAdapter.SCAN_MODE_NONE:
+ setFiredFlag(SCAN_MODE_NONE_FLAG);
+ break;
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
+ setFiredFlag(SCAN_MODE_CONNECTABLE_FLAG);
+ break;
+ case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
+ setFiredFlag(SCAN_MODE_CONNECTABLE_DISCOVERABLE_FLAG);
+ break;
+ }
+ } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
+ assertNotSame(-1, state);
+ switch (state) {
+ case BluetoothAdapter.STATE_OFF:
+ setFiredFlag(STATE_OFF_FLAG);
+ break;
+ case BluetoothAdapter.STATE_TURNING_ON:
+ setFiredFlag(STATE_TURNING_ON_FLAG);
+ break;
+ case BluetoothAdapter.STATE_ON:
+ setFiredFlag(STATE_ON_FLAG);
+ break;
+ case BluetoothAdapter.STATE_TURNING_OFF:
+ setFiredFlag(STATE_TURNING_OFF_FLAG);
+ break;
+ }
+ }
+ }
+ }
+
+ private class PairReceiver extends FlagReceiver {
+ private static final int STATE_BONDED_FLAG = 1;
+ private static final int STATE_BONDING_FLAG = 1 << 1;
+ private static final int STATE_NONE_FLAG = 1 << 2;
+
+ private BluetoothDevice mDevice;
+ private int mPasskey;
+ private byte[] mPin;
+
+ public PairReceiver(BluetoothDevice device, int passkey, byte[] pin, int expectedFlags) {
+ super(expectedFlags);
+
+ mDevice = device;
+ mPasskey = passkey;
+ mPin = pin;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mDevice.equals(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE))) {
+ return;
+ }
+
+ if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) {
+ int varient = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, -1);
+ assertNotSame(-1, varient);
+ switch (varient) {
+ case BluetoothDevice.PAIRING_VARIANT_PIN:
+ case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
+ mDevice.setPin(mPin);
+ break;
+ case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
+ break;
+ case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
+ case BluetoothDevice.PAIRING_VARIANT_CONSENT:
+ mDevice.setPairingConfirmation(true);
+ break;
+ case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
+ break;
+ }
+ } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+ int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
+ assertNotSame(-1, state);
+ switch (state) {
+ case BluetoothDevice.BOND_NONE:
+ setFiredFlag(STATE_NONE_FLAG);
+ break;
+ case BluetoothDevice.BOND_BONDING:
+ setFiredFlag(STATE_BONDING_FLAG);
+ break;
+ case BluetoothDevice.BOND_BONDED:
+ setFiredFlag(STATE_BONDED_FLAG);
+ break;
+ }
+ }
+ }
+ }
+
+ private class ConnectProfileReceiver extends FlagReceiver {
+ private static final int STATE_DISCONNECTED_FLAG = 1;
+ private static final int STATE_CONNECTING_FLAG = 1 << 1;
+ private static final int STATE_CONNECTED_FLAG = 1 << 2;
+ private static final int STATE_DISCONNECTING_FLAG = 1 << 3;
+
+ private BluetoothDevice mDevice;
+ private int mProfile;
+ private String mConnectionAction;
+
+ public ConnectProfileReceiver(BluetoothDevice device, int profile, int expectedFlags) {
+ super(expectedFlags);
+
+ mDevice = device;
+ mProfile = profile;
+
+ switch (mProfile) {
+ case BluetoothProfile.A2DP:
+ mConnectionAction = BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED;
+ break;
+ case BluetoothProfile.HEADSET:
+ mConnectionAction = BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED;
+ break;
+ case BluetoothProfile.HID_HOST:
+ mConnectionAction = BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED;
+ break;
+ case BluetoothProfile.PAN:
+ mConnectionAction = BluetoothPan.ACTION_CONNECTION_STATE_CHANGED;
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ mConnectionAction = BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED;
+ break;
+ default:
+ mConnectionAction = null;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mConnectionAction != null && mConnectionAction.equals(intent.getAction())) {
+ if (!mDevice.equals(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE))) {
+ return;
+ }
+
+ int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+ assertNotSame(-1, state);
+ switch (state) {
+ case BluetoothProfile.STATE_DISCONNECTED:
+ setFiredFlag(STATE_DISCONNECTED_FLAG);
+ break;
+ case BluetoothProfile.STATE_CONNECTING:
+ setFiredFlag(STATE_CONNECTING_FLAG);
+ break;
+ case BluetoothProfile.STATE_CONNECTED:
+ setFiredFlag(STATE_CONNECTED_FLAG);
+ break;
+ case BluetoothProfile.STATE_DISCONNECTING:
+ setFiredFlag(STATE_DISCONNECTING_FLAG);
+ break;
+ }
+ }
+ }
+ }
+
+ private class ConnectPanReceiver extends ConnectProfileReceiver {
+ private int mRole;
+
+ public ConnectPanReceiver(BluetoothDevice device, int role, int expectedFlags) {
+ super(device, BluetoothProfile.PAN, expectedFlags);
+
+ mRole = role;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mRole != intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, -1)) {
+ return;
+ }
+
+ super.onReceive(context, intent);
+ }
+ }
+
+ private class StartStopScoReceiver extends FlagReceiver {
+ private static final int STATE_CONNECTED_FLAG = 1;
+ private static final int STATE_DISCONNECTED_FLAG = 1 << 1;
+
+ public StartStopScoReceiver(int expectedFlags) {
+ super(expectedFlags);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED.equals(intent.getAction())) {
+ int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE,
+ AudioManager.SCO_AUDIO_STATE_ERROR);
+ assertNotSame(AudioManager.SCO_AUDIO_STATE_ERROR, state);
+ switch(state) {
+ case AudioManager.SCO_AUDIO_STATE_CONNECTED:
+ setFiredFlag(STATE_CONNECTED_FLAG);
+ break;
+ case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
+ setFiredFlag(STATE_DISCONNECTED_FLAG);
+ break;
+ }
+ }
+ }
+ }
+
+
+ private class MceSetMessageStatusReceiver extends FlagReceiver {
+ private static final int MESSAGE_RECEIVED_FLAG = 1;
+ private static final int STATUS_CHANGED_FLAG = 1 << 1;
+
+ public MceSetMessageStatusReceiver(int expectedFlags) {
+ super(expectedFlags);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (BluetoothMapClient.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
+ String handle = intent.getStringExtra(BluetoothMapClient.EXTRA_MESSAGE_HANDLE);
+ assertNotNull(handle);
+ setFiredFlag(MESSAGE_RECEIVED_FLAG);
+ mMsgHandle = handle;
+ } else if (BluetoothMapClient.ACTION_MESSAGE_DELETED_STATUS_CHANGED.equals(intent.getAction())) {
+ int result = intent.getIntExtra(BluetoothMapClient.EXTRA_RESULT_CODE, BluetoothMapClient.RESULT_FAILURE);
+ assertEquals(result, BluetoothMapClient.RESULT_SUCCESS);
+ setFiredFlag(STATUS_CHANGED_FLAG);
+ } else if (BluetoothMapClient.ACTION_MESSAGE_READ_STATUS_CHANGED.equals(intent.getAction())) {
+ int result = intent.getIntExtra(BluetoothMapClient.EXTRA_RESULT_CODE, BluetoothMapClient.RESULT_FAILURE);
+ assertEquals(result, BluetoothMapClient.RESULT_SUCCESS);
+ setFiredFlag(STATUS_CHANGED_FLAG);
+ }
+ }
+ }
+
+ private BluetoothProfile.ServiceListener mServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ synchronized (this) {
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ mA2dp = (BluetoothA2dp) proxy;
+ break;
+ case BluetoothProfile.HEADSET:
+ mHeadset = (BluetoothHeadset) proxy;
+ break;
+ case BluetoothProfile.HID_HOST:
+ mInput = (BluetoothHidHost) proxy;
+ break;
+ case BluetoothProfile.PAN:
+ mPan = (BluetoothPan) proxy;
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ mMce = (BluetoothMapClient) proxy;
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ synchronized (this) {
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ mA2dp = null;
+ break;
+ case BluetoothProfile.HEADSET:
+ mHeadset = null;
+ break;
+ case BluetoothProfile.HID_HOST:
+ mInput = null;
+ break;
+ case BluetoothProfile.PAN:
+ mPan = null;
+ break;
+ case BluetoothProfile.MAP_CLIENT:
+ mMce = null;
+ break;
+ }
+ }
+ }
+ };
+
+ private List<BroadcastReceiver> mReceivers = new ArrayList<BroadcastReceiver>();
+
+ private BufferedWriter mOutputWriter;
+ private String mTag;
+ private String mOutputFile;
+
+ private Context mContext;
+ private BluetoothA2dp mA2dp = null;
+ private BluetoothHeadset mHeadset = null;
+ private BluetoothHidHost mInput = null;
+ private BluetoothPan mPan = null;
+ private BluetoothMapClient mMce = null;
+ private String mMsgHandle = null;
+
+ /**
+ * Creates a utility instance for testing Bluetooth.
+ *
+ * @param context The context of the application using the utility.
+ * @param tag The log tag of the application using the utility.
+ */
+ public BluetoothTestUtils(Context context, String tag) {
+ this(context, tag, null);
+ }
+
+ /**
+ * Creates a utility instance for testing Bluetooth.
+ *
+ * @param context The context of the application using the utility.
+ * @param tag The log tag of the application using the utility.
+ * @param outputFile The path to an output file if the utility is to write results to a
+ * separate file.
+ */
+ public BluetoothTestUtils(Context context, String tag, String outputFile) {
+ mContext = context;
+ mTag = tag;
+ mOutputFile = outputFile;
+
+ if (mOutputFile == null) {
+ mOutputWriter = null;
+ } else {
+ try {
+ mOutputWriter = new BufferedWriter(new FileWriter(new File(
+ Environment.getExternalStorageDirectory(), mOutputFile), true));
+ } catch (IOException e) {
+ Log.w(mTag, "Test output file could not be opened", e);
+ mOutputWriter = null;
+ }
+ }
+ }
+
+ /**
+ * Closes the utility instance and unregisters any BroadcastReceivers.
+ */
+ public void close() {
+ while (!mReceivers.isEmpty()) {
+ mContext.unregisterReceiver(mReceivers.remove(0));
+ }
+
+ if (mOutputWriter != null) {
+ try {
+ mOutputWriter.close();
+ } catch (IOException e) {
+ Log.w(mTag, "Test output file could not be closed", e);
+ }
+ }
+ }
+
+ /**
+ * Enables Bluetooth and checks to make sure that Bluetooth was turned on and that the correct
+ * actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void enable(BluetoothAdapter adapter) {
+ writeOutput("Enabling Bluetooth adapter.");
+ assertFalse(adapter.isEnabled());
+ int btState = adapter.getState();
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ return;
+ }
+ final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_ON) {
+ completionSemaphore.release();
+ }
+ }
+ };
+
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ // Note: for Wear Local Edition builds, which have Permission Review Mode enabled to
+ // obey China CMIIT, BluetoothAdapter may not startup immediately on methods enable/disable.
+ // So no assertion applied here.
+ adapter.enable();
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
+ writeOutput(String.format("enable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("enable() timeout: state=%d (expected %d)", btState,
+ BluetoothAdapter.STATE_ON));
+ }
+ }
+
+ /**
+ * Disables Bluetooth and checks to make sure that Bluetooth was turned off and that the correct
+ * actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void disable(BluetoothAdapter adapter) {
+ writeOutput("Disabling Bluetooth adapter.");
+ assertTrue(adapter.isEnabled());
+ int btState = adapter.getState();
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ return;
+ }
+ final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
+ BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_OFF) {
+ completionSemaphore.release();
+ }
+ }
+ };
+
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ // Note: for Wear Local Edition builds, which have Permission Review Mode enabled to
+ // obey China CMIIT, BluetoothAdapter may not startup immediately on methods enable/disable.
+ // So no assertion applied here.
+ adapter.disable();
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(ENABLE_DISABLE_TIMEOUT, TimeUnit.MILLISECONDS);
+ writeOutput(String.format("disable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("disable() timeout: state=%d (expected %d)", btState,
+ BluetoothAdapter.STATE_OFF));
+ }
+ }
+
+ /**
+ * Puts the local device into discoverable mode and checks to make sure that the local device
+ * is in discoverable mode and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void discoverable(BluetoothAdapter adapter) {
+ if (!adapter.isEnabled()) {
+ fail("discoverable() bluetooth not enabled");
+ }
+
+ int scanMode = adapter.getScanMode();
+ if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+ return;
+ }
+
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
+ return;
+ }
+ final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_NONE);
+ if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ completionSemaphore.release();
+ }
+ }
+ };
+
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ writeOutput(String.format("discoverable() completed in 0 ms"));
+ } catch (final InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("discoverable() timeout: scanMode=%d (expected %d)", scanMode,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE));
+ }
+ }
+
+ /**
+ * Puts the local device into connectable only mode and checks to make sure that the local
+ * device is in in connectable mode and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void undiscoverable(BluetoothAdapter adapter) {
+ if (!adapter.isEnabled()) {
+ fail("undiscoverable() bluetooth not enabled");
+ }
+
+ int scanMode = adapter.getScanMode();
+ if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
+ return;
+ }
+
+ final Semaphore completionSemaphore = new Semaphore(0);
+ final BroadcastReceiver receiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (!BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(action)) {
+ return;
+ }
+ final int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
+ BluetoothAdapter.SCAN_MODE_NONE);
+ if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
+ completionSemaphore.release();
+ }
+ }
+ };
+
+ final IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
+ mContext.registerReceiver(receiver, filter);
+ assertTrue(adapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+ boolean success = false;
+ try {
+ success = completionSemaphore.tryAcquire(DISCOVERABLE_UNDISCOVERABLE_TIMEOUT,
+ TimeUnit.MILLISECONDS);
+ writeOutput(String.format("undiscoverable() completed in 0 ms"));
+ } catch (InterruptedException e) {
+ // This should never happen but just in case it does, the test will fail anyway.
+ }
+ mContext.unregisterReceiver(receiver);
+ if (!success) {
+ fail(String.format("undiscoverable() timeout: scanMode=%d (expected %d)", scanMode,
+ BluetoothAdapter.SCAN_MODE_CONNECTABLE));
+ }
+ }
+
+ /**
+ * Starts a scan for remote devices and checks to make sure that the local device is scanning
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void startScan(BluetoothAdapter adapter) {
+ int mask = BluetoothReceiver.DISCOVERY_STARTED_FLAG;
+
+ if (!adapter.isEnabled()) {
+ fail("startScan() bluetooth not enabled");
+ }
+
+ if (adapter.isDiscovering()) {
+ return;
+ }
+
+ BluetoothReceiver receiver = getBluetoothReceiver(mask);
+
+ long start = System.currentTimeMillis();
+ assertTrue(adapter.startDiscovery());
+
+ while (System.currentTimeMillis() - start < START_STOP_SCAN_TIMEOUT) {
+ if (adapter.isDiscovering() && ((receiver.getFiredFlags() & mask) == mask)) {
+ writeOutput(String.format("startScan() completed in %d ms",
+ (receiver.getCompletedTime() - start)));
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("startScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
+ adapter.isDiscovering(), firedFlags, mask));
+ }
+
+ /**
+ * Stops a scan for remote devices and checks to make sure that the local device is not scanning
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void stopScan(BluetoothAdapter adapter) {
+ int mask = BluetoothReceiver.DISCOVERY_FINISHED_FLAG;
+
+ if (!adapter.isEnabled()) {
+ fail("stopScan() bluetooth not enabled");
+ }
+
+ if (!adapter.isDiscovering()) {
+ return;
+ }
+
+ BluetoothReceiver receiver = getBluetoothReceiver(mask);
+
+ long start = System.currentTimeMillis();
+ assertTrue(adapter.cancelDiscovery());
+
+ while (System.currentTimeMillis() - start < START_STOP_SCAN_TIMEOUT) {
+ if (!adapter.isDiscovering() && ((receiver.getFiredFlags() & mask) == mask)) {
+ writeOutput(String.format("stopScan() completed in %d ms",
+ (receiver.getCompletedTime() - start)));
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("stopScan() timeout: isDiscovering=%b, flags=0x%x (expected 0x%x)",
+ adapter.isDiscovering(), firedFlags, mask));
+
+ }
+
+ /**
+ * Enables PAN tethering on the local device and checks to make sure that tethering is enabled.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void enablePan(BluetoothAdapter adapter) {
+ if (mPan == null) mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
+ assertNotNull(mPan);
+
+ long start = System.currentTimeMillis();
+ mPan.setBluetoothTethering(true);
+ long stop = System.currentTimeMillis();
+ assertTrue(mPan.isTetheringOn());
+
+ writeOutput(String.format("enablePan() completed in %d ms", (stop - start)));
+ }
+
+ /**
+ * Disables PAN tethering on the local device and checks to make sure that tethering is
+ * disabled.
+ *
+ * @param adapter The BT adapter.
+ */
+ public void disablePan(BluetoothAdapter adapter) {
+ if (mPan == null) mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
+ assertNotNull(mPan);
+
+ long start = System.currentTimeMillis();
+ mPan.setBluetoothTethering(false);
+ long stop = System.currentTimeMillis();
+ assertFalse(mPan.isTetheringOn());
+
+ writeOutput(String.format("disablePan() completed in %d ms", (stop - start)));
+ }
+
+ /**
+ * Initiates a pairing with a remote device and checks to make sure that the devices are paired
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
+ * @param pin The pairing pin if pairing requires a pin. Any value if not.
+ */
+ public void pair(BluetoothAdapter adapter, BluetoothDevice device, int passkey, byte[] pin) {
+ pairOrAcceptPair(adapter, device, passkey, pin, true);
+ }
+
+ /**
+ * Accepts a pairing with a remote device and checks to make sure that the devices are paired
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
+ * @param pin The pairing pin if pairing requires a pin. Any value if not.
+ */
+ public void acceptPair(BluetoothAdapter adapter, BluetoothDevice device, int passkey,
+ byte[] pin) {
+ pairOrAcceptPair(adapter, device, passkey, pin, false);
+ }
+
+ /**
+ * Helper method used by {@link #pair(BluetoothAdapter, BluetoothDevice, int, byte[])} and
+ * {@link #acceptPair(BluetoothAdapter, BluetoothDevice, int, byte[])} to either pair or accept
+ * a pairing request.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param passkey The pairing passkey if pairing requires a passkey. Any value if not.
+ * @param pin The pairing pin if pairing requires a pin. Any value if not.
+ * @param shouldPair Whether to pair or accept the pair.
+ */
+ private void pairOrAcceptPair(BluetoothAdapter adapter, BluetoothDevice device, int passkey,
+ byte[] pin, boolean shouldPair) {
+ int mask = PairReceiver.STATE_BONDING_FLAG | PairReceiver.STATE_BONDED_FLAG;
+ long start = -1;
+ String methodName;
+ if (shouldPair) {
+ methodName = String.format("pair(device=%s)", device);
+ } else {
+ methodName = String.format("acceptPair(device=%s)", device);
+ }
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ PairReceiver receiver = getPairReceiver(device, passkey, pin, mask);
+
+ int state = device.getBondState();
+ switch (state) {
+ case BluetoothDevice.BOND_NONE:
+ assertFalse(adapter.getBondedDevices().contains(device));
+ start = System.currentTimeMillis();
+ if (shouldPair) {
+ assertTrue(device.createBond());
+ }
+ break;
+ case BluetoothDevice.BOND_BONDING:
+ mask = 0; // Don't check for received intents since we might have missed them.
+ break;
+ case BluetoothDevice.BOND_BONDED:
+ assertTrue(adapter.getBondedDevices().contains(device));
+ return;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < PAIR_UNPAIR_TIMEOUT) {
+ state = device.getBondState();
+ if (state == BluetoothDevice.BOND_BONDED && (receiver.getFiredFlags() & mask) == mask) {
+ assertTrue(adapter.getBondedDevices().contains(device));
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+ methodName, state, BluetoothDevice.BOND_BONDED, firedFlags, mask));
+ }
+
+ /**
+ * Deletes a pairing with a remote device and checks to make sure that the devices are unpaired
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void unpair(BluetoothAdapter adapter, BluetoothDevice device) {
+ int mask = PairReceiver.STATE_NONE_FLAG;
+ long start = -1;
+ String methodName = String.format("unpair(device=%s)", device);
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ PairReceiver receiver = getPairReceiver(device, 0, null, mask);
+
+ int state = device.getBondState();
+ switch (state) {
+ case BluetoothDevice.BOND_NONE:
+ assertFalse(adapter.getBondedDevices().contains(device));
+ removeReceiver(receiver);
+ return;
+ case BluetoothDevice.BOND_BONDING:
+ start = System.currentTimeMillis();
+ assertTrue(device.removeBond());
+ break;
+ case BluetoothDevice.BOND_BONDED:
+ assertTrue(adapter.getBondedDevices().contains(device));
+ start = System.currentTimeMillis();
+ assertTrue(device.removeBond());
+ break;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < PAIR_UNPAIR_TIMEOUT) {
+ if (device.getBondState() == BluetoothDevice.BOND_NONE
+ && (receiver.getFiredFlags() & mask) == mask) {
+ assertFalse(adapter.getBondedDevices().contains(device));
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+ methodName, state, BluetoothDevice.BOND_BONDED, firedFlags, mask));
+ }
+
+ /**
+ * Deletes all pairings of remote devices
+ * @param adapter the BT adapter
+ */
+ public void unpairAll(BluetoothAdapter adapter) {
+ Set<BluetoothDevice> devices = adapter.getBondedDevices();
+ for (BluetoothDevice device : devices) {
+ unpair(adapter, device);
+ }
+ }
+
+ /**
+ * Connects a profile from the local device to a remote device and checks to make sure that the
+ * profile is connected and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param profile The profile to connect. One of {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEADSET}, {@link BluetoothProfile#HID_HOST} or {@link BluetoothProfile#MAP_CLIENT}..
+ * @param methodName The method name to printed in the logs. If null, will be
+ * "connectProfile(profile=&lt;profile&gt;, device=&lt;device&gt;)"
+ */
+ public void connectProfile(BluetoothAdapter adapter, BluetoothDevice device, int profile,
+ String methodName) {
+ if (methodName == null) {
+ methodName = String.format("connectProfile(profile=%d, device=%s)", profile, device);
+ }
+ int mask = (ConnectProfileReceiver.STATE_CONNECTING_FLAG
+ | ConnectProfileReceiver.STATE_CONNECTED_FLAG);
+ long start = -1;
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ BluetoothProfile proxy = connectProxy(adapter, profile);
+ assertNotNull(proxy);
+
+ ConnectProfileReceiver receiver = getConnectProfileReceiver(device, profile, mask);
+
+ int state = proxy.getConnectionState(device);
+ switch (state) {
+ case BluetoothProfile.STATE_CONNECTED:
+ removeReceiver(receiver);
+ return;
+ case BluetoothProfile.STATE_CONNECTING:
+ mask = 0; // Don't check for received intents since we might have missed them.
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ case BluetoothProfile.STATE_DISCONNECTING:
+ start = System.currentTimeMillis();
+ if (profile == BluetoothProfile.A2DP) {
+ assertTrue(((BluetoothA2dp)proxy).connect(device));
+ } else if (profile == BluetoothProfile.HEADSET) {
+ assertTrue(((BluetoothHeadset)proxy).connect(device));
+ } else if (profile == BluetoothProfile.HID_HOST) {
+ assertTrue(((BluetoothHidHost)proxy).connect(device));
+ } else if (profile == BluetoothProfile.MAP_CLIENT) {
+ assertTrue(((BluetoothMapClient)proxy).connect(device));
+ }
+ break;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
+ state = proxy.getConnectionState(device);
+ if (state == BluetoothProfile.STATE_CONNECTED
+ && (receiver.getFiredFlags() & mask) == mask) {
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+ methodName, state, BluetoothProfile.STATE_CONNECTED, firedFlags, mask));
+ }
+
+ /**
+ * Disconnects a profile between the local device and a remote device and checks to make sure
+ * that the profile is disconnected and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param profile The profile to disconnect. One of {@link BluetoothProfile#A2DP},
+ * {@link BluetoothProfile#HEADSET}, or {@link BluetoothProfile#HID_HOST}.
+ * @param methodName The method name to printed in the logs. If null, will be
+ * "connectProfile(profile=&lt;profile&gt;, device=&lt;device&gt;)"
+ */
+ public void disconnectProfile(BluetoothAdapter adapter, BluetoothDevice device, int profile,
+ String methodName) {
+ if (methodName == null) {
+ methodName = String.format("disconnectProfile(profile=%d, device=%s)", profile, device);
+ }
+ int mask = (ConnectProfileReceiver.STATE_DISCONNECTING_FLAG
+ | ConnectProfileReceiver.STATE_DISCONNECTED_FLAG);
+ long start = -1;
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ BluetoothProfile proxy = connectProxy(adapter, profile);
+ assertNotNull(proxy);
+
+ ConnectProfileReceiver receiver = getConnectProfileReceiver(device, profile, mask);
+
+ int state = proxy.getConnectionState(device);
+ switch (state) {
+ case BluetoothProfile.STATE_CONNECTED:
+ case BluetoothProfile.STATE_CONNECTING:
+ start = System.currentTimeMillis();
+ if (profile == BluetoothProfile.A2DP) {
+ assertTrue(((BluetoothA2dp)proxy).disconnect(device));
+ } else if (profile == BluetoothProfile.HEADSET) {
+ assertTrue(((BluetoothHeadset)proxy).disconnect(device));
+ } else if (profile == BluetoothProfile.HID_HOST) {
+ assertTrue(((BluetoothHidHost)proxy).disconnect(device));
+ } else if (profile == BluetoothProfile.MAP_CLIENT) {
+ assertTrue(((BluetoothMapClient)proxy).disconnect(device));
+ }
+ break;
+ case BluetoothProfile.STATE_DISCONNECTED:
+ removeReceiver(receiver);
+ return;
+ case BluetoothProfile.STATE_DISCONNECTING:
+ mask = 0; // Don't check for received intents since we might have missed them.
+ break;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
+ state = proxy.getConnectionState(device);
+ if (state == BluetoothProfile.STATE_DISCONNECTED
+ && (receiver.getFiredFlags() & mask) == mask) {
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%x)",
+ methodName, state, BluetoothProfile.STATE_DISCONNECTED, firedFlags, mask));
+ }
+
+ /**
+ * Connects the PANU to a remote NAP and checks to make sure that the PANU is connected and that
+ * the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void connectPan(BluetoothAdapter adapter, BluetoothDevice device) {
+ connectPanOrIncomingPanConnection(adapter, device, true);
+ }
+
+ /**
+ * Checks that a remote PANU connects to the local NAP correctly and that the correct actions
+ * were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void incomingPanConnection(BluetoothAdapter adapter, BluetoothDevice device) {
+ connectPanOrIncomingPanConnection(adapter, device, false);
+ }
+
+ /**
+ * Helper method used by {@link #connectPan(BluetoothAdapter, BluetoothDevice)} and
+ * {@link #incomingPanConnection(BluetoothAdapter, BluetoothDevice)} to either connect to a
+ * remote NAP or verify that a remote device connected to the local NAP.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param connect If the method should initiate the connection (is PANU)
+ */
+ private void connectPanOrIncomingPanConnection(BluetoothAdapter adapter, BluetoothDevice device,
+ boolean connect) {
+ long start = -1;
+ int mask, role;
+ String methodName;
+
+ if (connect) {
+ methodName = String.format("connectPan(device=%s)", device);
+ mask = (ConnectProfileReceiver.STATE_CONNECTED_FLAG |
+ ConnectProfileReceiver.STATE_CONNECTING_FLAG);
+ role = BluetoothPan.LOCAL_PANU_ROLE;
+ } else {
+ methodName = String.format("incomingPanConnection(device=%s)", device);
+ mask = ConnectProfileReceiver.STATE_CONNECTED_FLAG;
+ role = BluetoothPan.LOCAL_NAP_ROLE;
+ }
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
+ assertNotNull(mPan);
+ ConnectPanReceiver receiver = getConnectPanReceiver(device, role, mask);
+
+ int state = mPan.getConnectionState(device);
+ switch (state) {
+ case BluetoothPan.STATE_CONNECTED:
+ removeReceiver(receiver);
+ return;
+ case BluetoothPan.STATE_CONNECTING:
+ mask = 0; // Don't check for received intents since we might have missed them.
+ break;
+ case BluetoothPan.STATE_DISCONNECTED:
+ case BluetoothPan.STATE_DISCONNECTING:
+ start = System.currentTimeMillis();
+ if (role == BluetoothPan.LOCAL_PANU_ROLE) {
+ Log.i("BT", "connect to pan");
+ assertTrue(mPan.connect(device));
+ }
+ break;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
+ state = mPan.getConnectionState(device);
+ if (state == BluetoothPan.STATE_CONNECTED
+ && (receiver.getFiredFlags() & mask) == mask) {
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
+ methodName, state, BluetoothPan.STATE_CONNECTED, firedFlags, mask));
+ }
+
+ /**
+ * Disconnects the PANU from a remote NAP and checks to make sure that the PANU is disconnected
+ * and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void disconnectPan(BluetoothAdapter adapter, BluetoothDevice device) {
+ disconnectFromRemoteOrVerifyConnectNap(adapter, device, true);
+ }
+
+ /**
+ * Checks that a remote PANU disconnects from the local NAP correctly and that the correct
+ * actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void incomingPanDisconnection(BluetoothAdapter adapter, BluetoothDevice device) {
+ disconnectFromRemoteOrVerifyConnectNap(adapter, device, false);
+ }
+
+ /**
+ * Helper method used by {@link #disconnectPan(BluetoothAdapter, BluetoothDevice)} and
+ * {@link #incomingPanDisconnection(BluetoothAdapter, BluetoothDevice)} to either disconnect
+ * from a remote NAP or verify that a remote device disconnected from the local NAP.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param disconnect Whether the method should connect or verify.
+ */
+ private void disconnectFromRemoteOrVerifyConnectNap(BluetoothAdapter adapter,
+ BluetoothDevice device, boolean disconnect) {
+ long start = -1;
+ int mask, role;
+ String methodName;
+
+ if (disconnect) {
+ methodName = String.format("disconnectPan(device=%s)", device);
+ mask = (ConnectProfileReceiver.STATE_DISCONNECTED_FLAG |
+ ConnectProfileReceiver.STATE_DISCONNECTING_FLAG);
+ role = BluetoothPan.LOCAL_PANU_ROLE;
+ } else {
+ methodName = String.format("incomingPanDisconnection(device=%s)", device);
+ mask = ConnectProfileReceiver.STATE_DISCONNECTED_FLAG;
+ role = BluetoothPan.LOCAL_NAP_ROLE;
+ }
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ mPan = (BluetoothPan) connectProxy(adapter, BluetoothProfile.PAN);
+ assertNotNull(mPan);
+ ConnectPanReceiver receiver = getConnectPanReceiver(device, role, mask);
+
+ int state = mPan.getConnectionState(device);
+ switch (state) {
+ case BluetoothPan.STATE_CONNECTED:
+ case BluetoothPan.STATE_CONNECTING:
+ start = System.currentTimeMillis();
+ if (role == BluetoothPan.LOCAL_PANU_ROLE) {
+ assertTrue(mPan.disconnect(device));
+ }
+ break;
+ case BluetoothPan.STATE_DISCONNECTED:
+ removeReceiver(receiver);
+ return;
+ case BluetoothPan.STATE_DISCONNECTING:
+ mask = 0; // Don't check for received intents since we might have missed them.
+ break;
+ default:
+ removeReceiver(receiver);
+ fail(String.format("%s invalid state: state=%d", methodName, state));
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < CONNECT_DISCONNECT_PROFILE_TIMEOUT) {
+ state = mPan.getConnectionState(device);
+ if (state == BluetoothHidHost.STATE_DISCONNECTED
+ && (receiver.getFiredFlags() & mask) == mask) {
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
+ methodName, state, BluetoothHidHost.STATE_DISCONNECTED, firedFlags, mask));
+ }
+
+ /**
+ * Opens a SCO channel using {@link android.media.AudioManager#startBluetoothSco()} and checks
+ * to make sure that the channel is opened and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void startSco(BluetoothAdapter adapter, BluetoothDevice device) {
+ startStopSco(adapter, device, true);
+ }
+
+ /**
+ * Closes a SCO channel using {@link android.media.AudioManager#stopBluetoothSco()} and checks
+ * to make sure that the channel is closed and that the correct actions were broadcast.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ */
+ public void stopSco(BluetoothAdapter adapter, BluetoothDevice device) {
+ startStopSco(adapter, device, false);
+ }
+ /**
+ * Helper method for {@link #startSco(BluetoothAdapter, BluetoothDevice)} and
+ * {@link #stopSco(BluetoothAdapter, BluetoothDevice)}.
+ *
+ * @param adapter The BT adapter.
+ * @param device The remote device.
+ * @param isStart Whether the SCO channel should be opened.
+ */
+ private void startStopSco(BluetoothAdapter adapter, BluetoothDevice device, boolean isStart) {
+ long start = -1;
+ int mask;
+ String methodName;
+
+ if (isStart) {
+ methodName = String.format("startSco(device=%s)", device);
+ mask = StartStopScoReceiver.STATE_CONNECTED_FLAG;
+ } else {
+ methodName = String.format("stopSco(device=%s)", device);
+ mask = StartStopScoReceiver.STATE_DISCONNECTED_FLAG;
+ }
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ AudioManager manager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ assertNotNull(manager);
+
+ if (!manager.isBluetoothScoAvailableOffCall()) {
+ fail(String.format("%s device does not support SCO", methodName));
+ }
+
+ boolean isScoOn = manager.isBluetoothScoOn();
+ if (isStart == isScoOn) {
+ return;
+ }
+
+ StartStopScoReceiver receiver = getStartStopScoReceiver(mask);
+ start = System.currentTimeMillis();
+ if (isStart) {
+ manager.startBluetoothSco();
+ } else {
+ manager.stopBluetoothSco();
+ }
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < START_STOP_SCO_TIMEOUT) {
+ isScoOn = manager.isBluetoothScoOn();
+ if (isStart == isScoOn && (receiver.getFiredFlags() & mask) == mask) {
+ long finish = receiver.getCompletedTime();
+ if (start != -1 && finish != -1) {
+ writeOutput(String.format("%s completed in %d ms", methodName,
+ (finish - start)));
+ } else {
+ writeOutput(String.format("%s completed", methodName));
+ }
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: on=%b (expected %b), flags=0x%x (expected 0x%x)",
+ methodName, isScoOn, isStart, firedFlags, mask));
+ }
+
+ /**
+ * Writes a string to the logcat and a file if a file has been specified in the constructor.
+ *
+ * @param s The string to be written.
+ */
+ public void writeOutput(String s) {
+ Log.i(mTag, s);
+ if (mOutputWriter == null) {
+ return;
+ }
+ try {
+ mOutputWriter.write(s + "\n");
+ mOutputWriter.flush();
+ } catch (IOException e) {
+ Log.w(mTag, "Could not write to output file", e);
+ }
+ }
+
+ public void mceGetUnreadMessage(BluetoothAdapter adapter, BluetoothDevice device) {
+ int mask;
+ String methodName = "getUnreadMessage";
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ mMce = (BluetoothMapClient) connectProxy(adapter, BluetoothProfile.MAP_CLIENT);
+ assertNotNull(mMce);
+
+ if (mMce.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
+ fail(String.format("%s device is not connected", methodName));
+ }
+
+ mMsgHandle = null;
+ mask = MceSetMessageStatusReceiver.MESSAGE_RECEIVED_FLAG;
+ MceSetMessageStatusReceiver receiver = getMceSetMessageStatusReceiver(device, mask);
+ assertTrue(mMce.getUnreadMessages(device));
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < GET_UNREAD_MESSAGE_TIMEOUT) {
+ if ((receiver.getFiredFlags() & mask) == mask) {
+ writeOutput(String.format("%s completed", methodName));
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
+ methodName, mMce.getConnectionState(device), BluetoothMapClient.STATE_CONNECTED, firedFlags, mask));
+ }
+
+ /**
+ * Set a message to read/unread/deleted/undeleted
+ */
+ public void mceSetMessageStatus(BluetoothAdapter adapter, BluetoothDevice device, int status) {
+ int mask;
+ String methodName = "setMessageStatus";
+
+ if (!adapter.isEnabled()) {
+ fail(String.format("%s bluetooth not enabled", methodName));
+ }
+
+ if (!adapter.getBondedDevices().contains(device)) {
+ fail(String.format("%s device not paired", methodName));
+ }
+
+ mMce = (BluetoothMapClient) connectProxy(adapter, BluetoothProfile.MAP_CLIENT);
+ assertNotNull(mMce);
+
+ if (mMce.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
+ fail(String.format("%s device is not connected", methodName));
+ }
+
+ assertNotNull(mMsgHandle);
+ mask = MceSetMessageStatusReceiver.STATUS_CHANGED_FLAG;
+ MceSetMessageStatusReceiver receiver = getMceSetMessageStatusReceiver(device, mask);
+
+ assertTrue(mMce.setMessageStatus(device, mMsgHandle, status));
+
+ long s = System.currentTimeMillis();
+ while (System.currentTimeMillis() - s < SET_MESSAGE_STATUS_TIMEOUT) {
+ if ((receiver.getFiredFlags() & mask) == mask) {
+ writeOutput(String.format("%s completed", methodName));
+ removeReceiver(receiver);
+ return;
+ }
+ sleep(POLL_TIME);
+ }
+
+ int firedFlags = receiver.getFiredFlags();
+ removeReceiver(receiver);
+ fail(String.format("%s timeout: state=%d (expected %d), flags=0x%x (expected 0x%s)",
+ methodName, mMce.getConnectionState(device), BluetoothPan.STATE_CONNECTED, firedFlags, mask));
+ }
+
+ private void addReceiver(BroadcastReceiver receiver, String[] actions) {
+ IntentFilter filter = new IntentFilter();
+ for (String action: actions) {
+ filter.addAction(action);
+ }
+ mContext.registerReceiver(receiver, filter);
+ mReceivers.add(receiver);
+ }
+
+ private BluetoothReceiver getBluetoothReceiver(int expectedFlags) {
+ String[] actions = {
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED,
+ BluetoothAdapter.ACTION_DISCOVERY_STARTED,
+ BluetoothAdapter.ACTION_SCAN_MODE_CHANGED,
+ BluetoothAdapter.ACTION_STATE_CHANGED};
+ BluetoothReceiver receiver = new BluetoothReceiver(expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private PairReceiver getPairReceiver(BluetoothDevice device, int passkey, byte[] pin,
+ int expectedFlags) {
+ String[] actions = {
+ BluetoothDevice.ACTION_PAIRING_REQUEST,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED};
+ PairReceiver receiver = new PairReceiver(device, passkey, pin, expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private ConnectProfileReceiver getConnectProfileReceiver(BluetoothDevice device, int profile,
+ int expectedFlags) {
+ String[] actions = {
+ BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED,
+ BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
+ BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED,
+ BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED};
+ ConnectProfileReceiver receiver = new ConnectProfileReceiver(device, profile,
+ expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private ConnectPanReceiver getConnectPanReceiver(BluetoothDevice device, int role,
+ int expectedFlags) {
+ String[] actions = {BluetoothPan.ACTION_CONNECTION_STATE_CHANGED};
+ ConnectPanReceiver receiver = new ConnectPanReceiver(device, role, expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private StartStopScoReceiver getStartStopScoReceiver(int expectedFlags) {
+ String[] actions = {AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED};
+ StartStopScoReceiver receiver = new StartStopScoReceiver(expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private MceSetMessageStatusReceiver getMceSetMessageStatusReceiver(BluetoothDevice device,
+ int expectedFlags) {
+ String[] actions = {BluetoothMapClient.ACTION_MESSAGE_RECEIVED,
+ BluetoothMapClient.ACTION_MESSAGE_READ_STATUS_CHANGED,
+ BluetoothMapClient.ACTION_MESSAGE_DELETED_STATUS_CHANGED};
+ MceSetMessageStatusReceiver receiver = new MceSetMessageStatusReceiver(expectedFlags);
+ addReceiver(receiver, actions);
+ return receiver;
+ }
+
+ private void removeReceiver(BroadcastReceiver receiver) {
+ mContext.unregisterReceiver(receiver);
+ mReceivers.remove(receiver);
+ }
+
+ private BluetoothProfile connectProxy(BluetoothAdapter adapter, int profile) {
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ if (mA2dp != null) {
+ return mA2dp;
+ }
+ break;
+ case BluetoothProfile.HEADSET:
+ if (mHeadset != null) {
+ return mHeadset;
+ }
+ break;
+ case BluetoothProfile.HID_HOST:
+ if (mInput != null) {
+ return mInput;
+ }
+ break;
+ case BluetoothProfile.PAN:
+ if (mPan != null) {
+ return mPan;
+ }
+ case BluetoothProfile.MAP_CLIENT:
+ if (mMce != null) {
+ return mMce;
+ }
+ break;
+ default:
+ return null;
+ }
+ adapter.getProfileProxy(mContext, mServiceListener, profile);
+ long s = System.currentTimeMillis();
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ while (mA2dp == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
+ sleep(POLL_TIME);
+ }
+ return mA2dp;
+ case BluetoothProfile.HEADSET:
+ while (mHeadset == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
+ sleep(POLL_TIME);
+ }
+ return mHeadset;
+ case BluetoothProfile.HID_HOST:
+ while (mInput == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
+ sleep(POLL_TIME);
+ }
+ return mInput;
+ case BluetoothProfile.PAN:
+ while (mPan == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
+ sleep(POLL_TIME);
+ }
+ return mPan;
+ case BluetoothProfile.MAP_CLIENT:
+ while (mMce == null && System.currentTimeMillis() - s < CONNECT_PROXY_TIMEOUT) {
+ sleep(POLL_TIME);
+ }
+ return mMce;
+ default:
+ return null;
+ }
+ }
+
+ private void sleep(long time) {
+ try {
+ Thread.sleep(time);
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/BluetoothUuidTest.java b/framework/tests/src/android/bluetooth/BluetoothUuidTest.java
new file mode 100644
index 0000000000..536d722679
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/BluetoothUuidTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 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 android.os.ParcelUuid;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for {@link BluetoothUuid}.
+ * <p>
+ * To run this test, use adb shell am instrument -e class 'android.bluetooth.BluetoothUuidTest' -w
+ * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
+ */
+public class BluetoothUuidTest extends TestCase {
+
+ @SmallTest
+ public void testUuidParser() {
+ byte[] uuid16 = new byte[] {
+ 0x0B, 0x11 };
+ assertEquals(ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+ BluetoothUuid.parseUuidFrom(uuid16));
+
+ byte[] uuid32 = new byte[] {
+ 0x0B, 0x11, 0x33, (byte) 0xFE };
+ assertEquals(ParcelUuid.fromString("FE33110B-0000-1000-8000-00805F9B34FB"),
+ BluetoothUuid.parseUuidFrom(uuid32));
+
+ byte[] uuid128 = new byte[] {
+ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, (byte) 0xFF };
+ assertEquals(ParcelUuid.fromString("FF0F0E0D-0C0B-0A09-0807-0060504030201"),
+ BluetoothUuid.parseUuidFrom(uuid128));
+ }
+
+ @SmallTest
+ public void testUuidType() {
+ assertTrue(BluetoothUuid.is16BitUuid(
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB")));
+ assertFalse(BluetoothUuid.is32BitUuid(
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB")));
+
+ assertFalse(BluetoothUuid.is16BitUuid(
+ ParcelUuid.fromString("FE33110B-0000-1000-8000-00805F9B34FB")));
+ assertTrue(BluetoothUuid.is32BitUuid(
+ ParcelUuid.fromString("FE33110B-0000-1000-8000-00805F9B34FB")));
+ assertFalse(BluetoothUuid.is32BitUuid(
+ ParcelUuid.fromString("FE33110B-1000-1000-8000-00805F9B34FB")));
+
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/le/AdvertiseDataTest.java b/framework/tests/src/android/bluetooth/le/AdvertiseDataTest.java
new file mode 100644
index 0000000000..e58d905357
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/le/AdvertiseDataTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for {@link AdvertiseData}.
+ * <p>
+ * To run the test, use adb shell am instrument -e class 'android.bluetooth.le.AdvertiseDataTest' -w
+ * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
+ */
+public class AdvertiseDataTest extends TestCase {
+
+ private AdvertiseData.Builder mAdvertiseDataBuilder;
+
+ @Override
+ protected void setUp() throws Exception {
+ mAdvertiseDataBuilder = new AdvertiseData.Builder();
+ }
+
+ @SmallTest
+ public void testEmptyData() {
+ Parcel parcel = Parcel.obtain();
+ AdvertiseData data = mAdvertiseDataBuilder.build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testEmptyServiceUuid() {
+ Parcel parcel = Parcel.obtain();
+ AdvertiseData data = mAdvertiseDataBuilder.setIncludeDeviceName(true).build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testEmptyManufacturerData() {
+ Parcel parcel = Parcel.obtain();
+ int manufacturerId = 50;
+ byte[] manufacturerData = new byte[0];
+ AdvertiseData data =
+ mAdvertiseDataBuilder.setIncludeDeviceName(true)
+ .addManufacturerData(manufacturerId, manufacturerData).build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testEmptyServiceData() {
+ Parcel parcel = Parcel.obtain();
+ ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ byte[] serviceData = new byte[0];
+ AdvertiseData data =
+ mAdvertiseDataBuilder.setIncludeDeviceName(true)
+ .addServiceData(uuid, serviceData).build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testServiceUuid() {
+ Parcel parcel = Parcel.obtain();
+ ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ ParcelUuid uuid2 = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+
+ AdvertiseData data =
+ mAdvertiseDataBuilder.setIncludeDeviceName(true)
+ .addServiceUuid(uuid).addServiceUuid(uuid2).build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testManufacturerData() {
+ Parcel parcel = Parcel.obtain();
+ ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ ParcelUuid uuid2 = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+
+ int manufacturerId = 50;
+ byte[] manufacturerData = new byte[] {
+ (byte) 0xF0, 0x00, 0x02, 0x15 };
+ AdvertiseData data =
+ mAdvertiseDataBuilder.setIncludeDeviceName(true)
+ .addServiceUuid(uuid).addServiceUuid(uuid2)
+ .addManufacturerData(manufacturerId, manufacturerData).build();
+
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+
+ @SmallTest
+ public void testServiceData() {
+ Parcel parcel = Parcel.obtain();
+ ParcelUuid uuid = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ byte[] serviceData = new byte[] {
+ (byte) 0xF0, 0x00, 0x02, 0x15 };
+ AdvertiseData data =
+ mAdvertiseDataBuilder.setIncludeDeviceName(true)
+ .addServiceData(uuid, serviceData).build();
+ data.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ AdvertiseData dataFromParcel =
+ AdvertiseData.CREATOR.createFromParcel(parcel);
+ assertEquals(data, dataFromParcel);
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/le/ScanFilterTest.java b/framework/tests/src/android/bluetooth/le/ScanFilterTest.java
new file mode 100644
index 0000000000..35da4bceb6
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/le/ScanFilterTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanRecord;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for Bluetooth LE scan filters.
+ * <p>
+ * To run this test, use adb shell am instrument -e class 'android.bluetooth.ScanFilterTest' -w
+ * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
+ */
+public class ScanFilterTest extends TestCase {
+
+ private static final String DEVICE_MAC = "01:02:03:04:05:AB";
+ private ScanResult mScanResult;
+ private ScanFilter.Builder mFilterBuilder;
+
+ @Override
+ protected void setUp() throws Exception {
+ byte[] scanRecord = new byte[] {
+ 0x02, 0x01, 0x1a, // advertising flags
+ 0x05, 0x02, 0x0b, 0x11, 0x0a, 0x11, // 16 bit service uuids
+ 0x04, 0x09, 0x50, 0x65, 0x64, // setName
+ 0x02, 0x0A, (byte) 0xec, // tx power level
+ 0x05, 0x16, 0x0b, 0x11, 0x50, 0x64, // service data
+ 0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // manufacturer specific data
+ 0x03, 0x50, 0x01, 0x02, // an unknown data type won't cause trouble
+ };
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ BluetoothDevice device = adapter.getRemoteDevice(DEVICE_MAC);
+ mScanResult = new ScanResult(device, ScanRecord.parseFromBytes(scanRecord),
+ -10, 1397545200000000L);
+ mFilterBuilder = new ScanFilter.Builder();
+ }
+
+ @SmallTest
+ public void testsetNameFilter() {
+ ScanFilter filter = mFilterBuilder.setDeviceName("Ped").build();
+ assertTrue("setName filter fails", filter.matches(mScanResult));
+
+ filter = mFilterBuilder.setDeviceName("Pem").build();
+ assertFalse("setName filter fails", filter.matches(mScanResult));
+
+ }
+
+ @SmallTest
+ public void testDeviceFilter() {
+ ScanFilter filter = mFilterBuilder.setDeviceAddress(DEVICE_MAC).build();
+ assertTrue("device filter fails", filter.matches(mScanResult));
+
+ filter = mFilterBuilder.setDeviceAddress("11:22:33:44:55:66").build();
+ assertFalse("device filter fails", filter.matches(mScanResult));
+ }
+
+ @SmallTest
+ public void testsetServiceUuidFilter() {
+ ScanFilter filter = mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB")).build();
+ assertTrue("uuid filter fails", filter.matches(mScanResult));
+
+ filter = mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB")).build();
+ assertFalse("uuid filter fails", filter.matches(mScanResult));
+
+ filter = mFilterBuilder
+ .setServiceUuid(ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"),
+ ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF"))
+ .build();
+ assertTrue("uuid filter fails", filter.matches(mScanResult));
+ }
+
+ @SmallTest
+ public void testsetServiceDataFilter() {
+ byte[] setServiceData = new byte[] {
+ 0x50, 0x64 };
+ ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+ ScanFilter filter = mFilterBuilder.setServiceData(serviceDataUuid, setServiceData).build();
+ assertTrue("service data filter fails", filter.matches(mScanResult));
+
+ byte[] emptyData = new byte[0];
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, emptyData).build();
+ assertTrue("service data filter fails", filter.matches(mScanResult));
+
+ byte[] prefixData = new byte[] {
+ 0x50 };
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, prefixData).build();
+ assertTrue("service data filter fails", filter.matches(mScanResult));
+
+ byte[] nonMatchData = new byte[] {
+ 0x51, 0x64 };
+ byte[] mask = new byte[] {
+ (byte) 0x00, (byte) 0xFF };
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData, mask).build();
+ assertTrue("partial service data filter fails", filter.matches(mScanResult));
+
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData).build();
+ assertFalse("service data filter fails", filter.matches(mScanResult));
+ }
+
+ @SmallTest
+ public void testManufacturerSpecificData() {
+ byte[] setManufacturerData = new byte[] {
+ 0x02, 0x15 };
+ int manufacturerId = 0xE0;
+ ScanFilter filter =
+ mFilterBuilder.setManufacturerData(manufacturerId, setManufacturerData).build();
+ assertTrue("manufacturer data filter fails", filter.matches(mScanResult));
+
+ byte[] emptyData = new byte[0];
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, emptyData).build();
+ assertTrue("manufacturer data filter fails", filter.matches(mScanResult));
+
+ byte[] prefixData = new byte[] {
+ 0x02 };
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, prefixData).build();
+ assertTrue("manufacturer data filter fails", filter.matches(mScanResult));
+
+ // Test data mask
+ byte[] nonMatchData = new byte[] {
+ 0x02, 0x14 };
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData).build();
+ assertFalse("manufacturer data filter fails", filter.matches(mScanResult));
+ byte[] mask = new byte[] {
+ (byte) 0xFF, (byte) 0x00
+ };
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData, mask).build();
+ assertTrue("partial setManufacturerData filter fails", filter.matches(mScanResult));
+ }
+
+ @SmallTest
+ public void testReadWriteParcel() {
+ ScanFilter filter = mFilterBuilder.build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setDeviceName("Ped").build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setDeviceAddress("11:22:33:44:55:66").build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB")).build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setServiceUuid(
+ ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"),
+ ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF")).build();
+ testReadWriteParcelForFilter(filter);
+
+ byte[] serviceData = new byte[] {
+ 0x50, 0x64 };
+
+ ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, serviceData).build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, new byte[0]).build();
+ testReadWriteParcelForFilter(filter);
+
+ byte[] serviceDataMask = new byte[] {
+ (byte) 0xFF, (byte) 0xFF };
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, serviceData, serviceDataMask)
+ .build();
+ testReadWriteParcelForFilter(filter);
+
+ byte[] manufacturerData = new byte[] {
+ 0x02, 0x15 };
+ int manufacturerId = 0xE0;
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, manufacturerData).build();
+ testReadWriteParcelForFilter(filter);
+
+ filter = mFilterBuilder.setServiceData(serviceDataUuid, new byte[0]).build();
+ testReadWriteParcelForFilter(filter);
+
+ byte[] manufacturerDataMask = new byte[] {
+ (byte) 0xFF, (byte) 0xFF
+ };
+ filter = mFilterBuilder.setManufacturerData(manufacturerId, manufacturerData,
+ manufacturerDataMask).build();
+ testReadWriteParcelForFilter(filter);
+ }
+
+ private void testReadWriteParcelForFilter(ScanFilter filter) {
+ Parcel parcel = Parcel.obtain();
+ filter.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ScanFilter filterFromParcel =
+ ScanFilter.CREATOR.createFromParcel(parcel);
+ assertEquals(filter, filterFromParcel);
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/le/ScanRecordTest.java b/framework/tests/src/android/bluetooth/le/ScanRecordTest.java
new file mode 100644
index 0000000000..4e817d4a0d
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/le/ScanRecordTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.os.ParcelUuid;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.internal.util.HexDump;
+import com.android.modules.utils.BytesMatcher;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Unit test cases for {@link ScanRecord}.
+ * <p>
+ * To run this test, use adb shell am instrument -e class 'android.bluetooth.ScanRecordTest' -w
+ * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
+ */
+public class ScanRecordTest extends TestCase {
+ /**
+ * Example raw beacons captured from a Blue Charm BC011
+ */
+ private static final String RECORD_URL = "0201060303AAFE1716AAFE10EE01626C7565636861726D626561636F6E730009168020691E0EFE13551109426C7565436861726D5F313639363835000000";
+ private static final String RECORD_UUID = "0201060303AAFE1716AAFE00EE626C7565636861726D31000000000001000009168020691E0EFE13551109426C7565436861726D5F313639363835000000";
+ private static final String RECORD_TLM = "0201060303AAFE1116AAFE20000BF017000008874803FB93540916802069080EFE13551109426C7565436861726D5F313639363835000000000000000000";
+ private static final String RECORD_IBEACON = "0201061AFF4C000215426C7565436861726D426561636F6E730EFE1355C509168020691E0EFE13551109426C7565436861726D5F31363936383500000000";
+
+ @SmallTest
+ public void testMatchesAnyField_Eddystone_Parser() {
+ final List<String> found = new ArrayList<>();
+ final Predicate<byte[]> matcher = (v) -> {
+ found.add(HexDump.toHexString(v));
+ return false;
+ };
+ ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_URL))
+ .matchesAnyField(matcher);
+
+ assertEquals(Arrays.asList(
+ "020106",
+ "0303AAFE",
+ "1716AAFE10EE01626C7565636861726D626561636F6E7300",
+ "09168020691E0EFE1355",
+ "1109426C7565436861726D5F313639363835"), found);
+ }
+
+ @SmallTest
+ public void testMatchesAnyField_Eddystone() {
+ final BytesMatcher matcher = BytesMatcher.decode("⊆0016AAFE/00FFFFFF");
+ assertMatchesAnyField(RECORD_URL, matcher);
+ assertMatchesAnyField(RECORD_UUID, matcher);
+ assertMatchesAnyField(RECORD_TLM, matcher);
+ assertNotMatchesAnyField(RECORD_IBEACON, matcher);
+ }
+
+ @SmallTest
+ public void testMatchesAnyField_iBeacon_Parser() {
+ final List<String> found = new ArrayList<>();
+ final Predicate<byte[]> matcher = (v) -> {
+ found.add(HexDump.toHexString(v));
+ return false;
+ };
+ ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(RECORD_IBEACON))
+ .matchesAnyField(matcher);
+
+ assertEquals(Arrays.asList(
+ "020106",
+ "1AFF4C000215426C7565436861726D426561636F6E730EFE1355C5",
+ "09168020691E0EFE1355",
+ "1109426C7565436861726D5F313639363835"), found);
+ }
+
+ @SmallTest
+ public void testMatchesAnyField_iBeacon() {
+ final BytesMatcher matcher = BytesMatcher.decode("⊆00FF4C0002/00FFFFFFFF");
+ assertNotMatchesAnyField(RECORD_URL, matcher);
+ assertNotMatchesAnyField(RECORD_UUID, matcher);
+ assertNotMatchesAnyField(RECORD_TLM, matcher);
+ assertMatchesAnyField(RECORD_IBEACON, matcher);
+ }
+
+ @SmallTest
+ public void testParser() {
+ byte[] scanRecord = new byte[] {
+ 0x02, 0x01, 0x1a, // advertising flags
+ 0x05, 0x02, 0x0b, 0x11, 0x0a, 0x11, // 16 bit service uuids
+ 0x04, 0x09, 0x50, 0x65, 0x64, // name
+ 0x02, 0x0A, (byte) 0xec, // tx power level
+ 0x05, 0x16, 0x0b, 0x11, 0x50, 0x64, // service data
+ 0x05, (byte) 0xff, (byte) 0xe0, 0x00, 0x02, 0x15, // manufacturer specific data
+ 0x03, 0x50, 0x01, 0x02, // an unknown data type won't cause trouble
+ };
+ ScanRecord data = ScanRecord.parseFromBytes(scanRecord);
+ assertEquals(0x1a, data.getAdvertiseFlags());
+ ParcelUuid uuid1 = ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB");
+ ParcelUuid uuid2 = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+ assertTrue(data.getServiceUuids().contains(uuid1));
+ assertTrue(data.getServiceUuids().contains(uuid2));
+
+ assertEquals("Ped", data.getDeviceName());
+ assertEquals(-20, data.getTxPowerLevel());
+
+ assertTrue(data.getManufacturerSpecificData().get(0x00E0) != null);
+ assertArrayEquals(new byte[] {
+ 0x02, 0x15 }, data.getManufacturerSpecificData().get(0x00E0));
+
+ assertTrue(data.getServiceData().containsKey(uuid2));
+ assertArrayEquals(new byte[] {
+ 0x50, 0x64 }, data.getServiceData().get(uuid2));
+ }
+
+ // Assert two byte arrays are equal.
+ private static void assertArrayEquals(byte[] expected, byte[] actual) {
+ if (!Arrays.equals(expected, actual)) {
+ fail("expected:<" + Arrays.toString(expected) +
+ "> but was:<" + Arrays.toString(actual) + ">");
+ }
+
+ }
+
+ private static void assertMatchesAnyField(String record, BytesMatcher matcher) {
+ assertTrue(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record))
+ .matchesAnyField(matcher));
+ }
+
+ private static void assertNotMatchesAnyField(String record, BytesMatcher matcher) {
+ assertFalse(ScanRecord.parseFromBytes(HexDump.hexStringToByteArray(record))
+ .matchesAnyField(matcher));
+ }
+}
diff --git a/framework/tests/src/android/bluetooth/le/ScanResultTest.java b/framework/tests/src/android/bluetooth/le/ScanResultTest.java
new file mode 100644
index 0000000000..01d5c593bf
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/le/ScanResultTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit test cases for Bluetooth LE scans.
+ * <p>
+ * To run this test, use adb shell am instrument -e class 'android.bluetooth.ScanResultTest' -w
+ * 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
+ */
+public class ScanResultTest extends TestCase {
+
+ /**
+ * Test read and write parcel of ScanResult
+ */
+ @SmallTest
+ public void testScanResultParceling() {
+ BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(
+ "01:02:03:04:05:06");
+ byte[] scanRecord = new byte[] {
+ 1, 2, 3 };
+ int rssi = -10;
+ long timestampMicros = 10000L;
+
+ ScanResult result = new ScanResult(device, ScanRecord.parseFromBytes(scanRecord), rssi,
+ timestampMicros);
+ Parcel parcel = Parcel.obtain();
+ result.writeToParcel(parcel, 0);
+ // Need to reset parcel data position to the beginning.
+ parcel.setDataPosition(0);
+ ScanResult resultFromParcel = ScanResult.CREATOR.createFromParcel(parcel);
+ assertEquals(result, resultFromParcel);
+ }
+
+}
diff --git a/framework/tests/src/android/bluetooth/le/ScanSettingsTest.java b/framework/tests/src/android/bluetooth/le/ScanSettingsTest.java
new file mode 100644
index 0000000000..7c42c3b467
--- /dev/null
+++ b/framework/tests/src/android/bluetooth/le/ScanSettingsTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.le;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Test for Bluetooth LE {@link ScanSettings}.
+ */
+public class ScanSettingsTest extends TestCase {
+
+ @SmallTest
+ public void testCallbackType() {
+ ScanSettings.Builder builder = new ScanSettings.Builder();
+ builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
+ builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
+ builder.setCallbackType(ScanSettings.CALLBACK_TYPE_MATCH_LOST);
+ builder.setCallbackType(
+ ScanSettings.CALLBACK_TYPE_FIRST_MATCH | ScanSettings.CALLBACK_TYPE_MATCH_LOST);
+ try {
+ builder.setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES | ScanSettings.CALLBACK_TYPE_MATCH_LOST);
+ fail("should have thrown IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // nothing to do
+ }
+
+ try {
+ builder.setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES |
+ ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
+ fail("should have thrown IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // nothing to do
+ }
+
+ try {
+ builder.setCallbackType(
+ ScanSettings.CALLBACK_TYPE_ALL_MATCHES |
+ ScanSettings.CALLBACK_TYPE_FIRST_MATCH |
+ ScanSettings.CALLBACK_TYPE_MATCH_LOST);
+ fail("should have thrown IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // nothing to do
+ }
+
+ }
+}
diff --git a/service/Android.bp b/service/Android.bp
new file mode 100644
index 0000000000..d08b023728
--- /dev/null
+++ b/service/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "services.bluetooth-sources",
+ srcs: [
+ "java/**/*.java",
+ ],
+ visibility: [
+ "//frameworks/base/services",
+ "//frameworks/base/services/core",
+ ],
+}
diff --git a/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java b/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java
new file mode 100644
index 0000000000..380b1f37b9
--- /dev/null
+++ b/service/java/com/android/server/bluetooth/BluetoothAirplaneModeListener.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019 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 com.android.server;
+
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * The BluetoothAirplaneModeListener handles system airplane mode change callback and checks
+ * whether we need to inform BluetoothManagerService on this change.
+ *
+ * The information of airplane mode turns on would not be passed to the BluetoothManagerService
+ * when Bluetooth is on and Bluetooth is in one of the following situations:
+ * 1. Bluetooth A2DP is connected.
+ * 2. Bluetooth Hearing Aid profile is connected.
+ * 3. Bluetooth LE Audio is connected
+ */
+class BluetoothAirplaneModeListener {
+ private static final String TAG = "BluetoothAirplaneModeListener";
+ @VisibleForTesting static final String TOAST_COUNT = "bluetooth_airplane_toast_count";
+
+ private static final int MSG_AIRPLANE_MODE_CHANGED = 0;
+
+ @VisibleForTesting static final int MAX_TOAST_COUNT = 10; // 10 times
+
+ private final BluetoothManagerService mBluetoothManager;
+ private final BluetoothAirplaneModeHandler mHandler;
+ private BluetoothModeChangeHelper mAirplaneHelper;
+
+ @VisibleForTesting int mToastCount = 0;
+
+ BluetoothAirplaneModeListener(BluetoothManagerService service, Looper looper, Context context) {
+ mBluetoothManager = service;
+
+ mHandler = new BluetoothAirplaneModeHandler(looper);
+ context.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
+ mAirplaneModeObserver);
+ }
+
+ private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean unused) {
+ // Post from system main thread to android_io thread.
+ Message msg = mHandler.obtainMessage(MSG_AIRPLANE_MODE_CHANGED);
+ mHandler.sendMessage(msg);
+ }
+ };
+
+ private class BluetoothAirplaneModeHandler extends Handler {
+ BluetoothAirplaneModeHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_AIRPLANE_MODE_CHANGED:
+ handleAirplaneModeChange();
+ break;
+ default:
+ Log.e(TAG, "Invalid message: " + msg.what);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Call after boot complete
+ */
+ @VisibleForTesting
+ void start(BluetoothModeChangeHelper helper) {
+ Log.i(TAG, "start");
+ mAirplaneHelper = helper;
+ mToastCount = mAirplaneHelper.getSettingsInt(TOAST_COUNT);
+ }
+
+ @VisibleForTesting
+ boolean shouldPopToast() {
+ if (mToastCount >= MAX_TOAST_COUNT) {
+ return false;
+ }
+ mToastCount++;
+ mAirplaneHelper.setSettingsInt(TOAST_COUNT, mToastCount);
+ return true;
+ }
+
+ @VisibleForTesting
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ void handleAirplaneModeChange() {
+ if (shouldSkipAirplaneModeChange()) {
+ Log.i(TAG, "Ignore airplane mode change");
+ // Airplane mode enabled when Bluetooth is being used for audio/headering aid.
+ // Bluetooth is not disabled in such case, only state is changed to
+ // BLUETOOTH_ON_AIRPLANE mode.
+ mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON,
+ BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
+ if (shouldPopToast()) {
+ mAirplaneHelper.showToastMessage();
+ }
+ return;
+ }
+ if (mAirplaneHelper != null) {
+ mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager);
+ }
+ }
+
+ @VisibleForTesting
+ boolean shouldSkipAirplaneModeChange() {
+ if (mAirplaneHelper == null) {
+ return false;
+ }
+ if (!mAirplaneHelper.isBluetoothOn() || !mAirplaneHelper.isAirplaneModeOn()
+ || !mAirplaneHelper.isMediaProfileConnected()) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java
new file mode 100644
index 0000000000..611a37de70
--- /dev/null
+++ b/service/java/com/android/server/bluetooth/BluetoothDeviceConfigListener.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 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 com.android.server;
+
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import java.util.ArrayList;
+
+/**
+ * The BluetoothDeviceConfigListener handles system device config change callback and checks
+ * whether we need to inform BluetoothManagerService on this change.
+ *
+ * The information of device config change would not be passed to the BluetoothManagerService
+ * when Bluetooth is on and Bluetooth is in one of the following situations:
+ * 1. Bluetooth A2DP is connected.
+ * 2. Bluetooth Hearing Aid profile is connected.
+ */
+class BluetoothDeviceConfigListener {
+ private static final String TAG = "BluetoothDeviceConfigListener";
+
+ private final BluetoothManagerService mService;
+ private final boolean mLogDebug;
+
+ BluetoothDeviceConfigListener(BluetoothManagerService service, boolean logDebug) {
+ mService = service;
+ mLogDebug = logDebug;
+ DeviceConfig.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_BLUETOOTH,
+ (Runnable r) -> r.run(),
+ mDeviceConfigChangedListener);
+ }
+
+ private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener =
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ if (!properties.getNamespace().equals(DeviceConfig.NAMESPACE_BLUETOOTH)) {
+ return;
+ }
+ if (mLogDebug) {
+ ArrayList<String> flags = new ArrayList<>();
+ for (String name : properties.getKeyset()) {
+ flags.add(name + "='" + properties.getString(name, "") + "'");
+ }
+ Slog.d(TAG, "onPropertiesChanged: " + String.join(",", flags));
+ }
+ boolean foundInit = false;
+ for (String name : properties.getKeyset()) {
+ if (name.startsWith("INIT_")) {
+ foundInit = true;
+ break;
+ }
+ }
+ if (!foundInit) {
+ return;
+ }
+ mService.onInitFlagsChanged();
+ }
+ };
+
+}
diff --git a/service/java/com/android/server/bluetooth/BluetoothManagerService.java b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
new file mode 100644
index 0000000000..4290e77dcb
--- /dev/null
+++ b/service/java/com/android/server/bluetooth/BluetoothManagerService.java
@@ -0,0 +1,2956 @@
+/*
+ * Copyright (C) 2012 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 com.android.server;
+
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.content.PermissionChecker.PERMISSION_HARD_DENIED;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProtoEnums;
+import android.bluetooth.IBluetooth;
+import android.bluetooth.IBluetoothCallback;
+import android.bluetooth.IBluetoothGatt;
+import android.bluetooth.IBluetoothHeadset;
+import android.bluetooth.IBluetoothManager;
+import android.bluetooth.IBluetoothManagerCallback;
+import android.bluetooth.IBluetoothProfileServiceConnection;
+import android.bluetooth.IBluetoothStateChangeCallback;
+import android.bluetooth.IBluetoothLeCallControl;
+import android.content.ActivityNotFoundException;
+import android.content.AttributionSource;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.PermissionChecker;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerExemptionManager;
+import android.os.Process;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+import android.util.FeatureFlagUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.pm.UserManagerInternal.UserRestrictionsListener;
+import com.android.server.pm.UserRestrictionsUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+class BluetoothManagerService extends IBluetoothManager.Stub {
+ private static final String TAG = "BluetoothManagerService";
+ private static final boolean DBG = true;
+
+ private static final String BLUETOOTH_PRIVILEGED =
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+
+ private static final int ACTIVE_LOG_MAX_SIZE = 20;
+ private static final int CRASH_LOG_MAX_SIZE = 100;
+
+ private static final int TIMEOUT_BIND_MS = 3000; //Maximum msec to wait for a bind
+ //Maximum msec to wait for service restart
+ private static final int SERVICE_RESTART_TIME_MS = 400;
+ //Maximum msec to wait for restart due to error
+ private static final int ERROR_RESTART_TIME_MS = 3000;
+ //Maximum msec to delay MESSAGE_USER_SWITCHED
+ private static final int USER_SWITCHED_TIME_MS = 200;
+ // Delay for the addProxy function in msec
+ private static final int ADD_PROXY_DELAY_MS = 100;
+ // Delay for retrying enable and disable in msec
+ private static final int ENABLE_DISABLE_DELAY_MS = 300;
+ private static final int DELAY_BEFORE_RESTART_DUE_TO_INIT_FLAGS_CHANGED_MS = 300;
+ private static final int DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS = 86400;
+
+ private static final int MESSAGE_ENABLE = 1;
+ private static final int MESSAGE_DISABLE = 2;
+ private static final int MESSAGE_HANDLE_ENABLE_DELAYED = 3;
+ private static final int MESSAGE_HANDLE_DISABLE_DELAYED = 4;
+ private static final int MESSAGE_REGISTER_STATE_CHANGE_CALLBACK = 30;
+ private static final int MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK = 31;
+ private static final int MESSAGE_BLUETOOTH_SERVICE_CONNECTED = 40;
+ private static final int MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED = 41;
+ private static final int MESSAGE_RESTART_BLUETOOTH_SERVICE = 42;
+ private static final int MESSAGE_BLUETOOTH_STATE_CHANGE = 60;
+ private static final int MESSAGE_TIMEOUT_BIND = 100;
+ private static final int MESSAGE_TIMEOUT_UNBIND = 101;
+ private static final int MESSAGE_GET_NAME_AND_ADDRESS = 200;
+ private static final int MESSAGE_USER_SWITCHED = 300;
+ private static final int MESSAGE_USER_UNLOCKED = 301;
+ private static final int MESSAGE_ADD_PROXY_DELAYED = 400;
+ private static final int MESSAGE_BIND_PROFILE_SERVICE = 401;
+ private static final int MESSAGE_RESTORE_USER_SETTING = 500;
+ private static final int MESSAGE_INIT_FLAGS_CHANGED = 600;
+
+ private static final int RESTORE_SETTING_TO_ON = 1;
+ private static final int RESTORE_SETTING_TO_OFF = 0;
+
+ private static final int MAX_ERROR_RESTART_RETRIES = 6;
+ private static final int MAX_WAIT_FOR_ENABLE_DISABLE_RETRIES = 10;
+
+ // Bluetooth persisted setting is off
+ private static final int BLUETOOTH_OFF = 0;
+ // Bluetooth persisted setting is on
+ // and Airplane mode won't affect Bluetooth state at start up
+ private static final int BLUETOOTH_ON_BLUETOOTH = 1;
+ // Bluetooth persisted setting is on
+ // but Airplane mode will affect Bluetooth state at start up
+ // and Airplane mode will have higher priority.
+ @VisibleForTesting
+ static final int BLUETOOTH_ON_AIRPLANE = 2;
+
+ private static final int SERVICE_IBLUETOOTH = 1;
+ private static final int SERVICE_IBLUETOOTHGATT = 2;
+
+ private final Context mContext;
+
+ // Locks are not provided for mName and mAddress.
+ // They are accessed in handler or broadcast receiver, same thread context.
+ private String mAddress;
+ private String mName;
+ private final ContentResolver mContentResolver;
+ private final int mUserId;
+ private final RemoteCallbackList<IBluetoothManagerCallback> mCallbacks;
+ private final RemoteCallbackList<IBluetoothStateChangeCallback> mStateChangeCallbacks;
+ private IBinder mBluetoothBinder;
+ private IBluetooth mBluetooth;
+ private IBluetoothGatt mBluetoothGatt;
+ private final ReentrantReadWriteLock mBluetoothLock = new ReentrantReadWriteLock();
+ private boolean mBinding;
+ private boolean mUnbinding;
+
+ private BluetoothModeChangeHelper mBluetoothModeChangeHelper;
+
+ private BluetoothAirplaneModeListener mBluetoothAirplaneModeListener;
+
+ private BluetoothDeviceConfigListener mBluetoothDeviceConfigListener;
+
+ // used inside handler thread
+ private boolean mQuietEnable = false;
+ private boolean mEnable;
+
+ private static CharSequence timeToLog(long timestamp) {
+ return android.text.format.DateFormat.format("MM-dd HH:mm:ss", timestamp);
+ }
+
+ /**
+ * Used for tracking apps that enabled / disabled Bluetooth.
+ */
+ private class ActiveLog {
+ private int mReason;
+ private String mPackageName;
+ private boolean mEnable;
+ private long mTimestamp;
+
+ ActiveLog(int reason, String packageName, boolean enable, long timestamp) {
+ mReason = reason;
+ mPackageName = packageName;
+ mEnable = enable;
+ mTimestamp = timestamp;
+ }
+
+ public String toString() {
+ return timeToLog(mTimestamp) + (mEnable ? " Enabled " : " Disabled ")
+ + " due to " + getEnableDisableReasonString(mReason) + " by " + mPackageName;
+ }
+
+ void dump(ProtoOutputStream proto) {
+ proto.write(BluetoothManagerServiceDumpProto.ActiveLog.TIMESTAMP_MS, mTimestamp);
+ proto.write(BluetoothManagerServiceDumpProto.ActiveLog.ENABLE, mEnable);
+ proto.write(BluetoothManagerServiceDumpProto.ActiveLog.PACKAGE_NAME, mPackageName);
+ proto.write(BluetoothManagerServiceDumpProto.ActiveLog.REASON, mReason);
+ }
+ }
+
+ private final LinkedList<ActiveLog> mActiveLogs = new LinkedList<>();
+ private final LinkedList<Long> mCrashTimestamps = new LinkedList<>();
+ private int mCrashes;
+ private long mLastEnabledTime;
+
+ // configuration from external IBinder call which is used to
+ // synchronize with broadcast receiver.
+ private boolean mQuietEnableExternal;
+ private boolean mEnableExternal;
+
+ // Map of apps registered to keep BLE scanning on.
+ private Map<IBinder, ClientDeathRecipient> mBleApps =
+ new ConcurrentHashMap<IBinder, ClientDeathRecipient>();
+
+ private int mState;
+ private final BluetoothHandler mHandler;
+ private int mErrorRecoveryRetryCounter;
+ private final int mSystemUiUid;
+
+ private boolean mIsHearingAidProfileSupported;
+
+ private AppOpsManager mAppOps;
+
+ // Save a ProfileServiceConnections object for each of the bound
+ // bluetooth profile services
+ private final Map<Integer, ProfileServiceConnections> mProfileServices = new HashMap<>();
+
+ private final boolean mWirelessConsentRequired;
+
+ private final IBluetoothCallback mBluetoothCallback = new IBluetoothCallback.Stub() {
+ @Override
+ public void onBluetoothStateChange(int prevState, int newState) throws RemoteException {
+ Message msg =
+ mHandler.obtainMessage(MESSAGE_BLUETOOTH_STATE_CHANGE, prevState, newState);
+ mHandler.sendMessage(msg);
+ }
+ };
+
+ private final UserRestrictionsListener mUserRestrictionsListener =
+ new UserRestrictionsListener() {
+ @Override
+ public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+ Bundle prevRestrictions) {
+
+ if (UserRestrictionsUtils.restrictionsChanged(prevRestrictions, newRestrictions,
+ UserManager.DISALLOW_BLUETOOTH_SHARING)) {
+ updateOppLauncherComponentState(userId,
+ newRestrictions.getBoolean(UserManager.DISALLOW_BLUETOOTH_SHARING));
+ }
+
+ // DISALLOW_BLUETOOTH can only be set by DO or PO on the system user.
+ if (userId == USER_SYSTEM
+ && UserRestrictionsUtils.restrictionsChanged(prevRestrictions,
+ newRestrictions, UserManager.DISALLOW_BLUETOOTH)) {
+ if (userId == USER_SYSTEM && newRestrictions.getBoolean(
+ UserManager.DISALLOW_BLUETOOTH)) {
+ updateOppLauncherComponentState(userId, true); // Sharing disallowed
+ sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_DISALLOWED,
+ mContext.getPackageName());
+ } else {
+ updateOppLauncherComponentState(userId, newRestrictions.getBoolean(
+ UserManager.DISALLOW_BLUETOOTH_SHARING));
+ }
+ }
+ }
+ };
+
+ @VisibleForTesting
+ public void onInitFlagsChanged() {
+ mHandler.removeMessages(MESSAGE_INIT_FLAGS_CHANGED);
+ mHandler.sendEmptyMessageDelayed(
+ MESSAGE_INIT_FLAGS_CHANGED,
+ DELAY_BEFORE_RESTART_DUE_TO_INIT_FLAGS_CHANGED_MS);
+ }
+
+ public boolean onFactoryReset(AttributionSource attributionSource) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+
+ // Wait for stable state if bluetooth is temporary state.
+ int state = getState();
+ if (state == BluetoothAdapter.STATE_BLE_TURNING_ON
+ || state == BluetoothAdapter.STATE_TURNING_ON
+ || state == BluetoothAdapter.STATE_TURNING_OFF) {
+ if (!waitForState(Set.of(BluetoothAdapter.STATE_BLE_ON, BluetoothAdapter.STATE_ON))) {
+ return false;
+ }
+ }
+
+ // Clear registered LE apps to force shut-off Bluetooth
+ clearBleApps();
+ state = getState();
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth == null) {
+ return false;
+ }
+ if (state == BluetoothAdapter.STATE_BLE_ON) {
+ addActiveLog(
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_FACTORY_RESET,
+ mContext.getPackageName(), false);
+ mBluetooth.onBrEdrDown(attributionSource);
+ return true;
+ } else if (state == BluetoothAdapter.STATE_ON) {
+ addActiveLog(
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_FACTORY_RESET,
+ mContext.getPackageName(), false);
+ mBluetooth.disable(attributionSource);
+ return true;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to shutdown Bluetooth", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ return false;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void onAirplaneModeChanged() {
+ synchronized (this) {
+ if (isBluetoothPersistedStateOn()) {
+ if (isAirplaneModeOn()) {
+ persistBluetoothSetting(BLUETOOTH_ON_AIRPLANE);
+ } else {
+ persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
+ }
+ }
+
+ int st = BluetoothAdapter.STATE_OFF;
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ st = mBluetooth.getState();
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call getState", e);
+ return;
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ Slog.d(TAG,
+ "Airplane Mode change - current state: " + BluetoothAdapter.nameForState(
+ st) + ", isAirplaneModeOn()=" + isAirplaneModeOn());
+
+ if (isAirplaneModeOn()) {
+ // Clear registered LE apps to force shut-off
+ clearBleApps();
+
+ // If state is BLE_ON make sure we trigger disableBLE
+ if (st == BluetoothAdapter.STATE_BLE_ON) {
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ addActiveLog(
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE,
+ mContext.getPackageName(), false);
+ mBluetooth.onBrEdrDown(mContext.getAttributionSource());
+ mEnable = false;
+ mEnableExternal = false;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call onBrEdrDown", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ } else if (st == BluetoothAdapter.STATE_ON) {
+ sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE,
+ mContext.getPackageName());
+ }
+ } else if (mEnableExternal) {
+ sendEnableMsg(mQuietEnableExternal,
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE,
+ mContext.getPackageName());
+ }
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED.equals(action)) {
+ String newName = intent.getStringExtra(BluetoothAdapter.EXTRA_LOCAL_NAME);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth Adapter name changed to " + newName + " by "
+ + mContext.getPackageName());
+ }
+ if (newName != null) {
+ storeNameAndAddress(newName, null);
+ }
+ } else if (BluetoothAdapter.ACTION_BLUETOOTH_ADDRESS_CHANGED.equals(action)) {
+ String newAddress = intent.getStringExtra(BluetoothAdapter.EXTRA_BLUETOOTH_ADDRESS);
+ if (newAddress != null) {
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth Adapter address changed to " + newAddress);
+ }
+ storeNameAndAddress(null, newAddress);
+ } else {
+ if (DBG) {
+ Slog.e(TAG, "No Bluetooth Adapter address parameter found");
+ }
+ }
+ } else if (Intent.ACTION_SETTING_RESTORED.equals(action)) {
+ final String name = intent.getStringExtra(Intent.EXTRA_SETTING_NAME);
+ if (Settings.Global.BLUETOOTH_ON.equals(name)) {
+ // The Bluetooth On state may be changed during system restore.
+ final String prevValue =
+ intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE);
+ final String newValue = intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE);
+
+ if (DBG) {
+ Slog.d(TAG,
+ "ACTION_SETTING_RESTORED with BLUETOOTH_ON, prevValue=" + prevValue
+ + ", newValue=" + newValue);
+ }
+
+ if ((newValue != null) && (prevValue != null) && !prevValue.equals(newValue)) {
+ Message msg = mHandler.obtainMessage(MESSAGE_RESTORE_USER_SETTING,
+ newValue.equals("0") ? RESTORE_SETTING_TO_OFF
+ : RESTORE_SETTING_TO_ON, 0);
+ mHandler.sendMessage(msg);
+ }
+ }
+ } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)
+ || BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(action)
+ || BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED.equals(action)) {
+ final int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
+ BluetoothProfile.STATE_CONNECTED);
+ if (mHandler.hasMessages(MESSAGE_INIT_FLAGS_CHANGED)
+ && state == BluetoothProfile.STATE_DISCONNECTED
+ && !mBluetoothModeChangeHelper.isMediaProfileConnected()) {
+ Slog.i(TAG, "Device disconnected, reactivating pending flag changes");
+ onInitFlagsChanged();
+ }
+ }
+ }
+ };
+
+ BluetoothManagerService(Context context) {
+ mHandler = new BluetoothHandler(IoThread.get().getLooper());
+
+ mContext = context;
+
+ mWirelessConsentRequired = context.getResources()
+ .getBoolean(com.android.internal.R.bool.config_wirelessConsentRequired);
+
+ mCrashes = 0;
+ mBluetooth = null;
+ mBluetoothBinder = null;
+ mBluetoothGatt = null;
+ mBinding = false;
+ mUnbinding = false;
+ mEnable = false;
+ mState = BluetoothAdapter.STATE_OFF;
+ mQuietEnableExternal = false;
+ mEnableExternal = false;
+ mAddress = null;
+ mName = null;
+ mErrorRecoveryRetryCounter = 0;
+ mContentResolver = context.getContentResolver();
+ mUserId = mContentResolver.getUserId();
+ // Observe BLE scan only mode settings change.
+ registerForBleScanModeChange();
+ mCallbacks = new RemoteCallbackList<IBluetoothManagerCallback>();
+ mStateChangeCallbacks = new RemoteCallbackList<IBluetoothStateChangeCallback>();
+
+ mIsHearingAidProfileSupported = context.getResources()
+ .getBoolean(com.android.internal.R.bool.config_hearing_aid_profile_supported);
+
+ // TODO: We need a more generic way to initialize the persist keys of FeatureFlagUtils
+ String value = SystemProperties.get(FeatureFlagUtils.PERSIST_PREFIX + FeatureFlagUtils.HEARING_AID_SETTINGS);
+ if (!TextUtils.isEmpty(value)) {
+ boolean isHearingAidEnabled = Boolean.parseBoolean(value);
+ Log.v(TAG, "set feature flag HEARING_AID_SETTINGS to " + isHearingAidEnabled);
+ FeatureFlagUtils.setEnabled(context, FeatureFlagUtils.HEARING_AID_SETTINGS, isHearingAidEnabled);
+ if (isHearingAidEnabled && !mIsHearingAidProfileSupported) {
+ // Overwrite to enable support by FeatureFlag
+ mIsHearingAidProfileSupported = true;
+ }
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED);
+ filter.addAction(BluetoothAdapter.ACTION_BLUETOOTH_ADDRESS_CHANGED);
+ filter.addAction(Intent.ACTION_SETTING_RESTORED);
+ filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
+ filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
+ mContext.registerReceiver(mReceiver, filter);
+
+ loadStoredNameAndAddress();
+ if (isBluetoothPersistedStateOn()) {
+ if (DBG) {
+ Slog.d(TAG, "Startup: Bluetooth persisted state is ON.");
+ }
+ mEnableExternal = true;
+ }
+
+ String airplaneModeRadios =
+ Settings.Global.getString(mContentResolver, Settings.Global.AIRPLANE_MODE_RADIOS);
+ if (airplaneModeRadios == null || airplaneModeRadios.contains(
+ Settings.Global.RADIO_BLUETOOTH)) {
+ mBluetoothAirplaneModeListener = new BluetoothAirplaneModeListener(
+ this, IoThread.get().getLooper(), context);
+ }
+
+ int systemUiUid = -1;
+ // Check if device is configured with no home screen, which implies no SystemUI.
+ boolean noHome = mContext.getResources().getBoolean(R.bool.config_noHomeScreen);
+ if (!noHome) {
+ PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
+ systemUiUid = pm.getPackageUid(pm.getSystemUiServiceComponent().getPackageName(),
+ MATCH_SYSTEM_ONLY, USER_SYSTEM);
+ }
+ if (systemUiUid >= 0) {
+ Slog.d(TAG, "Detected SystemUiUid: " + Integer.toString(systemUiUid));
+ } else {
+ // Some platforms, such as wearables do not have a system ui.
+ Slog.w(TAG, "Unable to resolve SystemUI's UID.");
+ }
+ mSystemUiUid = systemUiUid;
+ }
+
+ /**
+ * Returns true if airplane mode is currently on
+ */
+ private boolean isAirplaneModeOn() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ }
+
+ private boolean supportBluetoothPersistedState() {
+ return mContext.getResources().getBoolean(R.bool.config_supportBluetoothPersistedState);
+ }
+
+ /**
+ * Returns true if the Bluetooth saved state is "on"
+ */
+ private boolean isBluetoothPersistedStateOn() {
+ if (!supportBluetoothPersistedState()) {
+ return false;
+ }
+ int state = Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON, -1);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth persisted state: " + state);
+ }
+ return state != BLUETOOTH_OFF;
+ }
+
+ private boolean isBluetoothPersistedStateOnAirplane() {
+ if (!supportBluetoothPersistedState()) {
+ return false;
+ }
+ int state = Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON, -1);
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth persisted state: " + state);
+ }
+ return state == BLUETOOTH_ON_AIRPLANE;
+ }
+
+ /**
+ * Returns true if the Bluetooth saved state is BLUETOOTH_ON_BLUETOOTH
+ */
+ private boolean isBluetoothPersistedStateOnBluetooth() {
+ if (!supportBluetoothPersistedState()) {
+ return false;
+ }
+ return Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON,
+ BLUETOOTH_ON_BLUETOOTH) == BLUETOOTH_ON_BLUETOOTH;
+ }
+
+ /**
+ * Save the Bluetooth on/off state
+ */
+ private void persistBluetoothSetting(int value) {
+ if (DBG) {
+ Slog.d(TAG, "Persisting Bluetooth Setting: " + value);
+ }
+ // waive WRITE_SECURE_SETTINGS permission check
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ Settings.Global.BLUETOOTH_ON, value);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ /**
+ * Returns true if the Bluetooth Adapter's name and address is
+ * locally cached
+ * @return
+ */
+ private boolean isNameAndAddressSet() {
+ return mName != null && mAddress != null && mName.length() > 0 && mAddress.length() > 0;
+ }
+
+ /**
+ * Retrieve the Bluetooth Adapter's name and address and save it in
+ * in the local cache
+ */
+ private void loadStoredNameAndAddress() {
+ if (DBG) {
+ Slog.d(TAG, "Loading stored name and address");
+ }
+ if (mContext.getResources()
+ .getBoolean(com.android.internal.R.bool.config_bluetooth_address_validation)
+ && Settings.Secure.getIntForUser(mContentResolver,
+ Settings.Secure.BLUETOOTH_NAME, 0, mUserId)
+ == 0) {
+ // if the valid flag is not set, don't load the address and name
+ if (DBG) {
+ Slog.d(TAG, "invalid bluetooth name and address stored");
+ }
+ return;
+ }
+ mName = Settings.Secure.getStringForUser(
+ mContentResolver, Settings.Secure.BLUETOOTH_NAME, mUserId);
+ mAddress = Settings.Secure.getStringForUser(
+ mContentResolver, Settings.Secure.BLUETOOTH_ADDRESS, mUserId);
+ if (DBG) {
+ Slog.d(TAG, "Stored bluetooth Name=" + mName + ",Address=" + mAddress);
+ }
+ }
+
+ /**
+ * Save the Bluetooth name and address in the persistent store.
+ * Only non-null values will be saved.
+ * @param name
+ * @param address
+ */
+ private void storeNameAndAddress(String name, String address) {
+ if (name != null) {
+ Settings.Secure.putStringForUser(mContentResolver, Settings.Secure.BLUETOOTH_NAME, name,
+ mUserId);
+ mName = name;
+ if (DBG) {
+ Slog.d(TAG, "Stored Bluetooth name: " + Settings.Secure.getStringForUser(
+ mContentResolver, Settings.Secure.BLUETOOTH_NAME,
+ mUserId));
+ }
+ }
+
+ if (address != null) {
+ Settings.Secure.putStringForUser(mContentResolver, Settings.Secure.BLUETOOTH_ADDRESS,
+ address, mUserId);
+ mAddress = address;
+ if (DBG) {
+ Slog.d(TAG,
+ "Stored Bluetoothaddress: " + Settings.Secure.getStringForUser(
+ mContentResolver, Settings.Secure.BLUETOOTH_ADDRESS,
+ mUserId));
+ }
+ }
+
+ if ((name != null) && (address != null)) {
+ Settings.Secure.putIntForUser(mContentResolver, Settings.Secure.BLUETOOTH_ADDR_VALID, 1,
+ mUserId);
+ }
+ }
+
+ public IBluetooth registerAdapter(IBluetoothManagerCallback callback) {
+ if (callback == null) {
+ Slog.w(TAG, "Callback is null in registerAdapter");
+ return null;
+ }
+ synchronized (mCallbacks) {
+ mCallbacks.register(callback);
+ }
+ return mBluetooth;
+ }
+
+ public void unregisterAdapter(IBluetoothManagerCallback callback) {
+ if (callback == null) {
+ Slog.w(TAG, "Callback is null in unregisterAdapter");
+ return;
+ }
+ synchronized (mCallbacks) {
+ mCallbacks.unregister(callback);
+ }
+ }
+
+ public void registerStateChangeCallback(IBluetoothStateChangeCallback callback) {
+ if (callback == null) {
+ Slog.w(TAG, "registerStateChangeCallback: Callback is null!");
+ return;
+ }
+ Message msg = mHandler.obtainMessage(MESSAGE_REGISTER_STATE_CHANGE_CALLBACK);
+ msg.obj = callback;
+ mHandler.sendMessage(msg);
+ }
+
+ public void unregisterStateChangeCallback(IBluetoothStateChangeCallback callback) {
+ if (callback == null) {
+ Slog.w(TAG, "unregisterStateChangeCallback: Callback is null!");
+ return;
+ }
+ Message msg = mHandler.obtainMessage(MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK);
+ msg.obj = callback;
+ mHandler.sendMessage(msg);
+ }
+
+ public boolean isEnabled() {
+ return getState() == BluetoothAdapter.STATE_ON;
+ }
+
+ public int getState() {
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getState(): report OFF for non-active and non system user");
+ return BluetoothAdapter.STATE_OFF;
+ }
+
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ return mBluetooth.getState();
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "getState()", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ return BluetoothAdapter.STATE_OFF;
+ }
+
+ class ClientDeathRecipient implements IBinder.DeathRecipient {
+ private String mPackageName;
+
+ ClientDeathRecipient(String packageName) {
+ mPackageName = packageName;
+ }
+
+ public void binderDied() {
+ if (DBG) {
+ Slog.d(TAG, "Binder is dead - unregister " + mPackageName);
+ }
+
+ for (Map.Entry<IBinder, ClientDeathRecipient> entry : mBleApps.entrySet()) {
+ IBinder token = entry.getKey();
+ ClientDeathRecipient deathRec = entry.getValue();
+ if (deathRec.equals(this)) {
+ updateBleAppCount(token, false, mPackageName);
+ break;
+ }
+ }
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+ }
+
+ @Override
+ public boolean isBleScanAlwaysAvailable() {
+ if (isAirplaneModeOn() && !mEnable) {
+ return false;
+ }
+ try {
+ return Settings.Global.getInt(mContentResolver,
+ Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE) != 0;
+ } catch (SettingNotFoundException e) {
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isHearingAidProfileSupported() {
+ return mIsHearingAidProfileSupported;
+ }
+
+ private boolean isDeviceProvisioned() {
+ return Settings.Global.getInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED,
+ 0) != 0;
+ }
+
+ // Monitor change of BLE scan only mode settings.
+ private void registerForProvisioningStateChange() {
+ ContentObserver contentObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ if (!isDeviceProvisioned()) {
+ if (DBG) {
+ Slog.d(TAG, "DEVICE_PROVISIONED setting changed, but device is not "
+ + "provisioned");
+ }
+ return;
+ }
+ if (mHandler.hasMessages(MESSAGE_INIT_FLAGS_CHANGED)) {
+ Slog.i(TAG, "Device provisioned, reactivating pending flag changes");
+ onInitFlagsChanged();
+ }
+ }
+ };
+
+ mContentResolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), false,
+ contentObserver);
+ }
+
+ // Monitor change of BLE scan only mode settings.
+ private void registerForBleScanModeChange() {
+ ContentObserver contentObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ if (isBleScanAlwaysAvailable()) {
+ // Nothing to do
+ return;
+ }
+ // BLE scan is not available.
+ disableBleScanMode();
+ clearBleApps();
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ addActiveLog(BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
+ mContext.getPackageName(), false);
+ mBluetooth.onBrEdrDown(mContext.getAttributionSource());
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error when disabling bluetooth", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ }
+ };
+
+ mContentResolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE), false,
+ contentObserver);
+ }
+
+ // Disable ble scan only mode.
+ private void disableBleScanMode() {
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (mBluetooth != null && (mBluetooth.getState() != BluetoothAdapter.STATE_ON)) {
+ if (DBG) {
+ Slog.d(TAG, "Reseting the mEnable flag for clean disable");
+ }
+ mEnable = false;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "getState()", e);
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+ }
+
+ private int updateBleAppCount(IBinder token, boolean enable, String packageName) {
+ ClientDeathRecipient r = mBleApps.get(token);
+ if (r == null && enable) {
+ ClientDeathRecipient deathRec = new ClientDeathRecipient(packageName);
+ try {
+ token.linkToDeath(deathRec, 0);
+ } catch (RemoteException ex) {
+ throw new IllegalArgumentException("BLE app (" + packageName + ") already dead!");
+ }
+ mBleApps.put(token, deathRec);
+ if (DBG) {
+ Slog.d(TAG, "Registered for death of " + packageName);
+ }
+ } else if (!enable && r != null) {
+ // Unregister death recipient as the app goes away.
+ token.unlinkToDeath(r, 0);
+ mBleApps.remove(token);
+ if (DBG) {
+ Slog.d(TAG, "Unregistered for death of " + packageName);
+ }
+ }
+ int appCount = mBleApps.size();
+ if (DBG) {
+ Slog.d(TAG, appCount + " registered Ble Apps");
+ }
+ return appCount;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private boolean checkBluetoothPermissions(AttributionSource attributionSource, String message,
+ boolean requireForeground) {
+ if (isBluetoothDisallowed()) {
+ if (DBG) {
+ Slog.d(TAG, "checkBluetoothPermissions: bluetooth disallowed");
+ }
+ return false;
+ }
+ // Check if packageName belongs to callingUid
+ final int callingUid = Binder.getCallingUid();
+ final boolean isCallerSystem = UserHandle.getAppId(callingUid) == Process.SYSTEM_UID;
+ if (!isCallerSystem) {
+ checkPackage(callingUid, attributionSource.getPackageName());
+
+ if (requireForeground && !checkIfCallerIsForegroundUser()) {
+ Slog.w(TAG, "Not allowed for non-active and non system user");
+ return false;
+ }
+
+ if (!checkConnectPermissionForDataDelivery(mContext, attributionSource, message)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public boolean enableBle(AttributionSource attributionSource, IBinder token)
+ throws RemoteException {
+ final String packageName = attributionSource.getPackageName();
+ if (!checkBluetoothPermissions(attributionSource, "enableBle", false)) {
+ if (DBG) {
+ Slog.d(TAG, "enableBle(): bluetooth disallowed");
+ }
+ return false;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "enableBle(" + packageName + "): mBluetooth =" + mBluetooth
+ + " mBinding = " + mBinding + " mState = "
+ + BluetoothAdapter.nameForState(mState));
+ }
+ updateBleAppCount(token, true, packageName);
+
+ if (mState == BluetoothAdapter.STATE_ON
+ || mState == BluetoothAdapter.STATE_BLE_ON
+ || mState == BluetoothAdapter.STATE_TURNING_ON
+ || mState == BluetoothAdapter.STATE_TURNING_OFF
+ || mState == BluetoothAdapter.STATE_BLE_TURNING_ON) {
+ Log.d(TAG, "enableBLE(): Bluetooth is already enabled or is turning on");
+ return true;
+ }
+ synchronized (mReceiver) {
+ // waive WRITE_SECURE_SETTINGS permission check
+ sendEnableMsg(false, BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
+ packageName, true);
+ }
+ return true;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public boolean disableBle(AttributionSource attributionSource, IBinder token)
+ throws RemoteException {
+ final String packageName = attributionSource.getPackageName();
+ if (!checkBluetoothPermissions(attributionSource, "disableBle", false)) {
+ if (DBG) {
+ Slog.d(TAG, "disableBLE(): bluetooth disallowed");
+ }
+ return false;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "disableBle(" + packageName + "): mBluetooth =" + mBluetooth
+ + " mBinding = " + mBinding + " mState = "
+ + BluetoothAdapter.nameForState(mState));
+ }
+
+ if (mState == BluetoothAdapter.STATE_OFF) {
+ Slog.d(TAG, "disableBLE(): Already disabled");
+ return false;
+ }
+ updateBleAppCount(token, false, packageName);
+
+ if (mState == BluetoothAdapter.STATE_BLE_ON && !isBleAppPresent()) {
+ if (mEnable) {
+ disableBleScanMode();
+ }
+ if (!mEnableExternal) {
+ addActiveLog(BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
+ packageName, false);
+ sendBrEdrDownCallback(attributionSource);
+ }
+ }
+ return true;
+ }
+
+ // Clear all apps using BLE scan only mode.
+ private void clearBleApps() {
+ mBleApps.clear();
+ }
+
+ /** @hide */
+ public boolean isBleAppPresent() {
+ if (DBG) {
+ Slog.d(TAG, "isBleAppPresent() count: " + mBleApps.size());
+ }
+ return mBleApps.size() > 0;
+ }
+
+ /**
+ * Call IBluetooth.onLeServiceUp() to continue if Bluetooth should be on,
+ * call IBluetooth.onBrEdrDown() to disable if Bluetooth should be off.
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ private void continueFromBleOnState() {
+ if (DBG) {
+ Slog.d(TAG, "continueFromBleOnState()");
+ }
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth == null) {
+ Slog.e(TAG, "onBluetoothServiceUp: mBluetooth is null!");
+ return;
+ }
+ if (!mEnableExternal && !isBleAppPresent()) {
+ Slog.i(TAG, "Bluetooth was disabled while enabling BLE, disable BLE now");
+ mEnable = false;
+ mBluetooth.onBrEdrDown(mContext.getAttributionSource());
+ return;
+ }
+ if (isBluetoothPersistedStateOnBluetooth() || !isBleAppPresent()) {
+ // This triggers transition to STATE_ON
+ mBluetooth.onLeServiceUp(mContext.getAttributionSource());
+ persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call onServiceUp", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Inform BluetoothAdapter instances that BREDR part is down
+ * and turn off all service and stack if no LE app needs it
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ private void sendBrEdrDownCallback(AttributionSource attributionSource) {
+ if (DBG) {
+ Slog.d(TAG, "Calling sendBrEdrDownCallback callbacks");
+ }
+
+ if (mBluetooth == null) {
+ Slog.w(TAG, "Bluetooth handle is null");
+ return;
+ }
+
+ if (isBleAppPresent()) {
+ // Need to stay at BLE ON. Disconnect all Gatt connections
+ try {
+ mBluetoothGatt.unregAll(attributionSource);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to disconnect all apps.", e);
+ }
+ } else {
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ mBluetooth.onBrEdrDown(attributionSource);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Call to onBrEdrDown() failed.", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ }
+
+ }
+
+ public boolean enableNoAutoConnect(AttributionSource attributionSource) {
+ final String packageName = attributionSource.getPackageName();
+ if (!checkBluetoothPermissions(attributionSource, "enableNoAutoConnect", false)) {
+ if (DBG) {
+ Slog.d(TAG, "enableNoAutoConnect(): not enabling - bluetooth disallowed");
+ }
+ return false;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "enableNoAutoConnect(): mBluetooth =" + mBluetooth + " mBinding = "
+ + mBinding);
+ }
+
+ int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
+ if (callingAppId != Process.NFC_UID) {
+ throw new SecurityException("no permission to enable Bluetooth quietly");
+ }
+
+ synchronized (mReceiver) {
+ mQuietEnableExternal = true;
+ mEnableExternal = true;
+ sendEnableMsg(true,
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST, packageName);
+ }
+ return true;
+ }
+
+ public boolean enable(AttributionSource attributionSource) throws RemoteException {
+ final String packageName = attributionSource.getPackageName();
+ if (!checkBluetoothPermissions(attributionSource, "enable", true)) {
+ if (DBG) {
+ Slog.d(TAG, "enable(): not enabling - bluetooth disallowed");
+ }
+ return false;
+ }
+
+ final int callingUid = Binder.getCallingUid();
+ final boolean callerSystem = UserHandle.getAppId(callingUid) == Process.SYSTEM_UID;
+ if (!callerSystem && !isEnabled() && mWirelessConsentRequired
+ && startConsentUiIfNeeded(packageName,
+ callingUid, BluetoothAdapter.ACTION_REQUEST_ENABLE)) {
+ return false;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "enable(" + packageName + "): mBluetooth =" + mBluetooth + " mBinding = "
+ + mBinding + " mState = " + BluetoothAdapter.nameForState(mState));
+ }
+
+ synchronized (mReceiver) {
+ mQuietEnableExternal = false;
+ mEnableExternal = true;
+ // waive WRITE_SECURE_SETTINGS permission check
+ sendEnableMsg(false,
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST, packageName);
+ }
+ if (DBG) {
+ Slog.d(TAG, "enable returning");
+ }
+ return true;
+ }
+
+ public boolean disable(AttributionSource attributionSource, boolean persist)
+ throws RemoteException {
+ if (!persist) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ }
+
+ final String packageName = attributionSource.getPackageName();
+ if (!checkBluetoothPermissions(attributionSource, "disable", true)) {
+ if (DBG) {
+ Slog.d(TAG, "disable(): not disabling - bluetooth disallowed");
+ }
+ return false;
+ }
+
+ final int callingUid = Binder.getCallingUid();
+ final boolean callerSystem = UserHandle.getAppId(callingUid) == Process.SYSTEM_UID;
+ if (!callerSystem && isEnabled() && mWirelessConsentRequired
+ && startConsentUiIfNeeded(packageName,
+ callingUid, BluetoothAdapter.ACTION_REQUEST_DISABLE)) {
+ return false;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "disable(): mBluetooth = " + mBluetooth + " mBinding = " + mBinding);
+ }
+
+ synchronized (mReceiver) {
+ if (!isBluetoothPersistedStateOnAirplane()) {
+ if (persist) {
+ persistBluetoothSetting(BLUETOOTH_OFF);
+ }
+ mEnableExternal = false;
+ }
+ sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
+ packageName);
+ }
+ return true;
+ }
+
+ private boolean startConsentUiIfNeeded(String packageName,
+ int callingUid, String intentAction) throws RemoteException {
+ if (checkBluetoothPermissionWhenWirelessConsentRequired()) {
+ return false;
+ }
+ try {
+ // Validate the package only if we are going to use it
+ ApplicationInfo applicationInfo = mContext.getPackageManager()
+ .getApplicationInfoAsUser(packageName,
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ UserHandle.getUserId(callingUid));
+ if (applicationInfo.uid != callingUid) {
+ throw new SecurityException("Package " + packageName
+ + " not in uid " + callingUid);
+ }
+
+ Intent intent = new Intent(intentAction);
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
+ intent.setFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ try {
+ mContext.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // Shouldn't happen
+ Slog.e(TAG, "Intent to handle action " + intentAction + " missing");
+ return false;
+ }
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ /**
+ * Check if AppOpsManager is available and the packageName belongs to uid
+ *
+ * A null package belongs to any uid
+ */
+ private void checkPackage(int uid, String packageName) {
+ if (mAppOps == null) {
+ Slog.w(TAG, "checkPackage(): called before system boot up, uid "
+ + uid + ", packageName " + packageName);
+ throw new IllegalStateException("System has not boot yet");
+ }
+ if (packageName == null) {
+ Slog.w(TAG, "checkPackage(): called with null packageName from " + uid);
+ return;
+ }
+ try {
+ mAppOps.checkPackage(uid, packageName);
+ } catch (SecurityException e) {
+ Slog.w(TAG, "checkPackage(): " + packageName + " does not belong to uid " + uid);
+ throw new SecurityException(e.getMessage());
+ }
+ }
+
+ /**
+ * Check if the caller must still pass permission check or if the caller is exempted
+ * from the consent UI via the MANAGE_BLUETOOTH_WHEN_WIRELESS_CONSENT_REQUIRED check.
+ *
+ * Commands from some callers may be exempted from triggering the consent UI when
+ * enabling bluetooth. This exemption is checked via the
+ * MANAGE_BLUETOOTH_WHEN_WIRELESS_CONSENT_REQUIRED and allows calls to skip
+ * the consent UI where it may otherwise be required.
+ *
+ * @hide
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private boolean checkBluetoothPermissionWhenWirelessConsentRequired() {
+ int result = mContext.checkCallingPermission(
+ android.Manifest.permission.MANAGE_BLUETOOTH_WHEN_WIRELESS_CONSENT_REQUIRED);
+ return result == PackageManager.PERMISSION_GRANTED;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void unbindAndFinish() {
+ if (DBG) {
+ Slog.d(TAG, "unbindAndFinish(): " + mBluetooth + " mBinding = " + mBinding
+ + " mUnbinding = " + mUnbinding);
+ }
+
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (mUnbinding) {
+ return;
+ }
+ mUnbinding = true;
+ mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
+ mHandler.removeMessages(MESSAGE_BIND_PROFILE_SERVICE);
+ if (mBluetooth != null) {
+ //Unregister callback object
+ try {
+ mBluetooth.unregisterCallback(mBluetoothCallback,
+ mContext.getAttributionSource());
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Unable to unregister BluetoothCallback", re);
+ }
+ mBluetoothBinder = null;
+ mBluetooth = null;
+ mContext.unbindService(mConnection);
+ mUnbinding = false;
+ mBinding = false;
+ } else {
+ mUnbinding = false;
+ }
+ mBluetoothGatt = null;
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+ }
+
+ public IBluetoothGatt getBluetoothGatt() {
+ // sync protection
+ return mBluetoothGatt;
+ }
+
+ @Override
+ public boolean bindBluetoothProfileService(int bluetoothProfile,
+ IBluetoothProfileServiceConnection proxy) {
+ if (mState != BluetoothAdapter.STATE_ON) {
+ if (DBG) {
+ Slog.d(TAG, "Trying to bind to profile: " + bluetoothProfile
+ + ", while Bluetooth was disabled");
+ }
+ return false;
+ }
+ synchronized (mProfileServices) {
+ ProfileServiceConnections psc = mProfileServices.get(new Integer(bluetoothProfile));
+ if (psc == null) {
+ if (DBG) {
+ Slog.d(TAG, "Creating new ProfileServiceConnections object for" + " profile: "
+ + bluetoothProfile);
+ }
+
+ Intent intent;
+ if (bluetoothProfile == BluetoothProfile.HEADSET) {
+ intent = new Intent(IBluetoothHeadset.class.getName());
+ } else if (bluetoothProfile== BluetoothProfile.LE_CALL_CONTROL) {
+ intent = new Intent(IBluetoothLeCallControl.class.getName());
+ } else {
+ return false;
+ }
+
+ psc = new ProfileServiceConnections(intent);
+ if (!psc.bindService()) {
+ return false;
+ }
+
+ mProfileServices.put(new Integer(bluetoothProfile), psc);
+ }
+ }
+
+ // Introducing a delay to give the client app time to prepare
+ Message addProxyMsg = mHandler.obtainMessage(MESSAGE_ADD_PROXY_DELAYED);
+ addProxyMsg.arg1 = bluetoothProfile;
+ addProxyMsg.obj = proxy;
+ mHandler.sendMessageDelayed(addProxyMsg, ADD_PROXY_DELAY_MS);
+ return true;
+ }
+
+ @Override
+ public void unbindBluetoothProfileService(int bluetoothProfile,
+ IBluetoothProfileServiceConnection proxy) {
+ synchronized (mProfileServices) {
+ Integer profile = new Integer(bluetoothProfile);
+ ProfileServiceConnections psc = mProfileServices.get(profile);
+ if (psc == null) {
+ return;
+ }
+ psc.removeProxy(proxy);
+ if (psc.isEmpty()) {
+ // All prxoies are disconnected, unbind with the service.
+ try {
+ mContext.unbindService(psc);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Unable to unbind service with intent: " + psc.mIntent, e);
+ }
+ mProfileServices.remove(profile);
+ }
+ }
+ }
+
+ private void unbindAllBluetoothProfileServices() {
+ synchronized (mProfileServices) {
+ for (Integer i : mProfileServices.keySet()) {
+ ProfileServiceConnections psc = mProfileServices.get(i);
+ try {
+ mContext.unbindService(psc);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Unable to unbind service with intent: " + psc.mIntent, e);
+ }
+ psc.removeAllProxies();
+ }
+ mProfileServices.clear();
+ }
+ }
+
+ /**
+ * Send enable message and set adapter name and address. Called when the boot phase becomes
+ * PHASE_SYSTEM_SERVICES_READY.
+ */
+ public void handleOnBootPhase() {
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth boot completed");
+ }
+ mAppOps = mContext.getSystemService(AppOpsManager.class);
+ UserManagerInternal userManagerInternal =
+ LocalServices.getService(UserManagerInternal.class);
+ userManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
+ final boolean isBluetoothDisallowed = isBluetoothDisallowed();
+ if (isBluetoothDisallowed) {
+ return;
+ }
+ final boolean isSafeMode = mContext.getPackageManager().isSafeMode();
+ if (mEnableExternal && isBluetoothPersistedStateOnBluetooth() && !isSafeMode) {
+ if (DBG) {
+ Slog.d(TAG, "Auto-enabling Bluetooth.");
+ }
+ sendEnableMsg(mQuietEnableExternal,
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_SYSTEM_BOOT,
+ mContext.getPackageName());
+ } else if (!isNameAndAddressSet()) {
+ if (DBG) {
+ Slog.d(TAG, "Getting adapter name and address");
+ }
+ Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);
+ mHandler.sendMessage(getMsg);
+ }
+
+ mBluetoothModeChangeHelper = new BluetoothModeChangeHelper(mContext);
+ if (mBluetoothAirplaneModeListener != null) {
+ mBluetoothAirplaneModeListener.start(mBluetoothModeChangeHelper);
+ }
+ registerForProvisioningStateChange();
+ mBluetoothDeviceConfigListener = new BluetoothDeviceConfigListener(this, DBG);
+ }
+
+ /**
+ * Called when switching to a different foreground user.
+ */
+ public void handleOnSwitchUser(int userHandle) {
+ if (DBG) {
+ Slog.d(TAG, "User " + userHandle + " switched");
+ }
+ mHandler.obtainMessage(MESSAGE_USER_SWITCHED, userHandle, 0).sendToTarget();
+ }
+
+ /**
+ * Called when user is unlocked.
+ */
+ public void handleOnUnlockUser(int userHandle) {
+ if (DBG) {
+ Slog.d(TAG, "User " + userHandle + " unlocked");
+ }
+ mHandler.obtainMessage(MESSAGE_USER_UNLOCKED, userHandle, 0).sendToTarget();
+ }
+
+ /**
+ * This class manages the clients connected to a given ProfileService
+ * and maintains the connection with that service.
+ */
+ private final class ProfileServiceConnections
+ implements ServiceConnection, IBinder.DeathRecipient {
+ final RemoteCallbackList<IBluetoothProfileServiceConnection> mProxies =
+ new RemoteCallbackList<IBluetoothProfileServiceConnection>();
+ IBinder mService;
+ ComponentName mClassName;
+ Intent mIntent;
+ boolean mInvokingProxyCallbacks = false;
+
+ ProfileServiceConnections(Intent intent) {
+ mService = null;
+ mClassName = null;
+ mIntent = intent;
+ }
+
+ private boolean bindService() {
+ int state = BluetoothAdapter.STATE_OFF;
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ state = mBluetooth.getState();
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call getState", e);
+ return false;
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ if (state != BluetoothAdapter.STATE_ON) {
+ if (DBG) {
+ Slog.d(TAG, "Unable to bindService while Bluetooth is disabled");
+ }
+ return false;
+ }
+
+ if (mIntent != null && mService == null && doBind(mIntent, this, 0,
+ UserHandle.CURRENT_OR_SELF)) {
+ Message msg = mHandler.obtainMessage(MESSAGE_BIND_PROFILE_SERVICE);
+ msg.obj = this;
+ mHandler.sendMessageDelayed(msg, TIMEOUT_BIND_MS);
+ return true;
+ }
+ Slog.w(TAG, "Unable to bind with intent: " + mIntent);
+ return false;
+ }
+
+ private void addProxy(IBluetoothProfileServiceConnection proxy) {
+ mProxies.register(proxy);
+ if (mService != null) {
+ try {
+ proxy.onServiceConnected(mClassName, mService);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to connect to proxy", e);
+ }
+ } else {
+ if (!mHandler.hasMessages(MESSAGE_BIND_PROFILE_SERVICE, this)) {
+ Message msg = mHandler.obtainMessage(MESSAGE_BIND_PROFILE_SERVICE);
+ msg.obj = this;
+ mHandler.sendMessage(msg);
+ }
+ }
+ }
+
+ private void removeProxy(IBluetoothProfileServiceConnection proxy) {
+ if (proxy != null) {
+ if (mProxies.unregister(proxy)) {
+ try {
+ proxy.onServiceDisconnected(mClassName);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to disconnect proxy", e);
+ }
+ }
+ } else {
+ Slog.w(TAG, "Trying to remove a null proxy");
+ }
+ }
+
+ private void removeAllProxies() {
+ onServiceDisconnected(mClassName);
+ mProxies.kill();
+ }
+
+ private boolean isEmpty() {
+ return mProxies.getRegisteredCallbackCount() == 0;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ // remove timeout message
+ mHandler.removeMessages(MESSAGE_BIND_PROFILE_SERVICE, this);
+ mService = service;
+ mClassName = className;
+ try {
+ mService.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to linkToDeath", e);
+ }
+
+ if (mInvokingProxyCallbacks) {
+ Slog.e(TAG, "Proxy callbacks already in progress.");
+ return;
+ }
+ mInvokingProxyCallbacks = true;
+
+ final int n = mProxies.beginBroadcast();
+ try {
+ for (int i = 0; i < n; i++) {
+ try {
+ mProxies.getBroadcastItem(i).onServiceConnected(className, service);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to connect to proxy", e);
+ }
+ }
+ } finally {
+ mProxies.finishBroadcast();
+ mInvokingProxyCallbacks = false;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Log.e(TAG, "error unlinking to death", e);
+ }
+ mService = null;
+ mClassName = null;
+
+ if (mInvokingProxyCallbacks) {
+ Slog.e(TAG, "Proxy callbacks already in progress.");
+ return;
+ }
+ mInvokingProxyCallbacks = true;
+
+ final int n = mProxies.beginBroadcast();
+ try {
+ for (int i = 0; i < n; i++) {
+ try {
+ mProxies.getBroadcastItem(i).onServiceDisconnected(className);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to disconnect from proxy", e);
+ }
+ }
+ } finally {
+ mProxies.finishBroadcast();
+ mInvokingProxyCallbacks = false;
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ if (DBG) {
+ Slog.w(TAG, "Profile service for profile: " + mClassName + " died.");
+ }
+ onServiceDisconnected(mClassName);
+ // Trigger rebind
+ Message msg = mHandler.obtainMessage(MESSAGE_BIND_PROFILE_SERVICE);
+ msg.obj = this;
+ mHandler.sendMessageDelayed(msg, TIMEOUT_BIND_MS);
+ }
+ }
+
+ private void sendBluetoothStateCallback(boolean isUp) {
+ try {
+ int n = mStateChangeCallbacks.beginBroadcast();
+ if (DBG) {
+ Slog.d(TAG, "Broadcasting onBluetoothStateChange(" + isUp + ") to " + n
+ + " receivers.");
+ }
+ for (int i = 0; i < n; i++) {
+ try {
+ mStateChangeCallbacks.getBroadcastItem(i).onBluetoothStateChange(isUp);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call onBluetoothStateChange() on callback #" + i, e);
+ }
+ }
+ } finally {
+ mStateChangeCallbacks.finishBroadcast();
+ }
+ }
+
+ /**
+ * Inform BluetoothAdapter instances that Adapter service is up
+ */
+ private void sendBluetoothServiceUpCallback() {
+ synchronized (mCallbacks) {
+ try {
+ int n = mCallbacks.beginBroadcast();
+ Slog.d(TAG, "Broadcasting onBluetoothServiceUp() to " + n + " receivers.");
+ for (int i = 0; i < n; i++) {
+ try {
+ mCallbacks.getBroadcastItem(i).onBluetoothServiceUp(mBluetooth);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call onBluetoothServiceUp() on callback #" + i, e);
+ }
+ }
+ } finally {
+ mCallbacks.finishBroadcast();
+ }
+ }
+ }
+
+ /**
+ * Inform BluetoothAdapter instances that Adapter service is down
+ */
+ private void sendBluetoothServiceDownCallback() {
+ synchronized (mCallbacks) {
+ try {
+ int n = mCallbacks.beginBroadcast();
+ Slog.d(TAG, "Broadcasting onBluetoothServiceDown() to " + n + " receivers.");
+ for (int i = 0; i < n; i++) {
+ try {
+ mCallbacks.getBroadcastItem(i).onBluetoothServiceDown();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call onBluetoothServiceDown() on callback #" + i, e);
+ }
+ }
+ } finally {
+ mCallbacks.finishBroadcast();
+ }
+ }
+ }
+
+ public String getAddress(AttributionSource attributionSource) {
+ if (!checkConnectPermissionForDataDelivery(mContext, attributionSource, "getAddress")) {
+ return null;
+ }
+
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getAddress(): not allowed for non-active and non system user");
+ return null;
+ }
+
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.LOCAL_MAC_ADDRESS)
+ != PackageManager.PERMISSION_GRANTED) {
+ return BluetoothAdapter.DEFAULT_MAC_ADDRESS;
+ }
+
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ return mBluetooth.getAddressWithAttribution(attributionSource);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG,
+ "getAddress(): Unable to retrieve address remotely. Returning cached address",
+ e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ // mAddress is accessed from outside.
+ // It is alright without a lock. Here, bluetooth is off, no other thread is
+ // changing mAddress
+ return mAddress;
+ }
+
+ public String getName(AttributionSource attributionSource) {
+ if (!checkConnectPermissionForDataDelivery(mContext, attributionSource, "getName")) {
+ return null;
+ }
+
+ if ((Binder.getCallingUid() != Process.SYSTEM_UID) && (!checkIfCallerIsForegroundUser())) {
+ Slog.w(TAG, "getName(): not allowed for non-active and non system user");
+ return null;
+ }
+
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ return mBluetooth.getName(attributionSource);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "getName(): Unable to retrieve name remotely. Returning cached name", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ // mName is accessed from outside.
+ // It alright without a lock. Here, bluetooth is off, no other thread is
+ // changing mName
+ return mName;
+ }
+
+ private class BluetoothServiceConnection implements ServiceConnection {
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ String name = componentName.getClassName();
+ if (DBG) {
+ Slog.d(TAG, "BluetoothServiceConnection: " + name);
+ }
+ Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_CONNECTED);
+ if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
+ msg.arg1 = SERVICE_IBLUETOOTH;
+ } else if (name.equals("com.android.bluetooth.gatt.GattService")) {
+ msg.arg1 = SERVICE_IBLUETOOTHGATT;
+ } else {
+ Slog.e(TAG, "Unknown service connected: " + name);
+ return;
+ }
+ msg.obj = service;
+ mHandler.sendMessage(msg);
+ }
+
+ public void onServiceDisconnected(ComponentName componentName) {
+ // Called if we unexpectedly disconnect.
+ String name = componentName.getClassName();
+ if (DBG) {
+ Slog.d(TAG, "BluetoothServiceConnection, disconnected: " + name);
+ }
+ Message msg = mHandler.obtainMessage(MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED);
+ if (name.equals("com.android.bluetooth.btservice.AdapterService")) {
+ msg.arg1 = SERVICE_IBLUETOOTH;
+ } else if (name.equals("com.android.bluetooth.gatt.GattService")) {
+ msg.arg1 = SERVICE_IBLUETOOTHGATT;
+ } else {
+ Slog.e(TAG, "Unknown service disconnected: " + name);
+ return;
+ }
+ mHandler.sendMessage(msg);
+ }
+ }
+
+ private BluetoothServiceConnection mConnection = new BluetoothServiceConnection();
+
+ private class BluetoothHandler extends Handler {
+ boolean mGetNameAddressOnly = false;
+ private int mWaitForEnableRetry;
+ private int mWaitForDisableRetry;
+
+ BluetoothHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_NAME_AND_ADDRESS:
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_GET_NAME_AND_ADDRESS");
+ }
+ try {
+ mBluetoothLock.writeLock().lock();
+ if ((mBluetooth == null) && (!mBinding)) {
+ if (DBG) {
+ Slog.d(TAG, "Binding to service to get name and address");
+ }
+ mGetNameAddressOnly = true;
+ Message timeoutMsg = mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);
+ mHandler.sendMessageDelayed(timeoutMsg, TIMEOUT_BIND_MS);
+ Intent i = new Intent(IBluetooth.class.getName());
+ if (!doBind(i, mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ UserHandle.CURRENT)) {
+ mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);
+ } else {
+ mBinding = true;
+ }
+ } else if (mBluetooth != null) {
+ try {
+ storeNameAndAddress(
+ mBluetooth.getName(mContext.getAttributionSource()),
+ mBluetooth.getAddressWithAttribution(
+ mContext.getAttributionSource()));
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Unable to grab names", re);
+ }
+ if (mGetNameAddressOnly && !mEnable) {
+ unbindAndFinish();
+ }
+ mGetNameAddressOnly = false;
+ }
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+ break;
+
+ case MESSAGE_ENABLE:
+ int quietEnable = msg.arg1;
+ int isBle = msg.arg2;
+ if (mHandler.hasMessages(MESSAGE_HANDLE_DISABLE_DELAYED)
+ || mHandler.hasMessages(MESSAGE_HANDLE_ENABLE_DELAYED)) {
+ // We are handling enable or disable right now, wait for it.
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_ENABLE,
+ quietEnable, isBle), ENABLE_DISABLE_DELAY_MS);
+ break;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_ENABLE(" + quietEnable + "): mBluetooth = "
+ + mBluetooth);
+ }
+ mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mEnable = true;
+
+ if (isBle == 0) {
+ persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH);
+ }
+
+ // Use service interface to get the exact state
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ boolean isHandled = true;
+ int state = mBluetooth.getState();
+ switch (state) {
+ case BluetoothAdapter.STATE_BLE_ON:
+ if (isBle == 1) {
+ Slog.i(TAG, "Already at BLE_ON State");
+ } else {
+ Slog.w(TAG, "BT Enable in BLE_ON State, going to ON");
+ mBluetooth.onLeServiceUp(mContext.getAttributionSource());
+ }
+ break;
+ case BluetoothAdapter.STATE_BLE_TURNING_ON:
+ case BluetoothAdapter.STATE_TURNING_ON:
+ case BluetoothAdapter.STATE_ON:
+ Slog.i(TAG, "MESSAGE_ENABLE: already enabled");
+ break;
+ default:
+ isHandled = false;
+ break;
+ }
+ if (isHandled) break;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ mQuietEnable = (quietEnable == 1);
+ if (mBluetooth == null) {
+ handleEnable(mQuietEnable);
+ } else {
+ //
+ // We need to wait until transitioned to STATE_OFF and
+ // the previous Bluetooth process has exited. The
+ // waiting period has three components:
+ // (a) Wait until the local state is STATE_OFF. This
+ // is accomplished by sending delay a message
+ // MESSAGE_HANDLE_ENABLE_DELAYED
+ // (b) Wait until the STATE_OFF state is updated to
+ // all components.
+ // (c) Wait until the Bluetooth process exits, and
+ // ActivityManager detects it.
+ // The waiting for (b) and (c) is accomplished by
+ // delaying the MESSAGE_RESTART_BLUETOOTH_SERVICE
+ // message. The delay time is backed off if Bluetooth
+ // continuously failed to turn on itself.
+ //
+ mWaitForEnableRetry = 0;
+ Message enableDelayedMsg =
+ mHandler.obtainMessage(MESSAGE_HANDLE_ENABLE_DELAYED);
+ mHandler.sendMessageDelayed(enableDelayedMsg, ENABLE_DISABLE_DELAY_MS);
+ }
+ break;
+
+ case MESSAGE_DISABLE:
+ if (mHandler.hasMessages(MESSAGE_HANDLE_DISABLE_DELAYED) || mBinding
+ || mHandler.hasMessages(MESSAGE_HANDLE_ENABLE_DELAYED)) {
+ // We are handling enable or disable right now, wait for it.
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_DISABLE),
+ ENABLE_DISABLE_DELAY_MS);
+ break;
+ }
+
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_DISABLE: mBluetooth = " + mBluetooth
+ + ", mBinding = " + mBinding);
+ }
+ mHandler.removeMessages(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+
+ if (mEnable && mBluetooth != null) {
+ mWaitForDisableRetry = 0;
+ Message disableDelayedMsg =
+ mHandler.obtainMessage(MESSAGE_HANDLE_DISABLE_DELAYED, 0, 0);
+ mHandler.sendMessageDelayed(disableDelayedMsg, ENABLE_DISABLE_DELAY_MS);
+ } else {
+ mEnable = false;
+ handleDisable();
+ }
+ break;
+
+ case MESSAGE_HANDLE_ENABLE_DELAYED: {
+ // The Bluetooth is turning off, wait for STATE_OFF
+ if (mState != BluetoothAdapter.STATE_OFF) {
+ if (mWaitForEnableRetry < MAX_WAIT_FOR_ENABLE_DISABLE_RETRIES) {
+ mWaitForEnableRetry++;
+ Message enableDelayedMsg =
+ mHandler.obtainMessage(MESSAGE_HANDLE_ENABLE_DELAYED);
+ mHandler.sendMessageDelayed(enableDelayedMsg, ENABLE_DISABLE_DELAY_MS);
+ break;
+ } else {
+ Slog.e(TAG, "Wait for STATE_OFF timeout");
+ }
+ }
+ // Either state is changed to STATE_OFF or reaches the maximum retry, we
+ // should move forward to the next step.
+ mWaitForEnableRetry = 0;
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, getServiceRestartMs());
+ Slog.d(TAG, "Handle enable is finished");
+ break;
+ }
+
+ case MESSAGE_HANDLE_DISABLE_DELAYED: {
+ boolean disabling = (msg.arg1 == 1);
+ Slog.d(TAG, "MESSAGE_HANDLE_DISABLE_DELAYED: disabling:" + disabling);
+ if (!disabling) {
+ // The Bluetooth is turning on, wait for STATE_ON
+ if (mState != BluetoothAdapter.STATE_ON) {
+ if (mWaitForDisableRetry < MAX_WAIT_FOR_ENABLE_DISABLE_RETRIES) {
+ mWaitForDisableRetry++;
+ Message disableDelayedMsg = mHandler.obtainMessage(
+ MESSAGE_HANDLE_DISABLE_DELAYED, 0, 0);
+ mHandler.sendMessageDelayed(disableDelayedMsg,
+ ENABLE_DISABLE_DELAY_MS);
+ break;
+ } else {
+ Slog.e(TAG, "Wait for STATE_ON timeout");
+ }
+ }
+ // Either state is changed to STATE_ON or reaches the maximum retry, we
+ // should move forward to the next step.
+ mWaitForDisableRetry = 0;
+ mEnable = false;
+ handleDisable();
+ // Wait for state exiting STATE_ON
+ Message disableDelayedMsg =
+ mHandler.obtainMessage(MESSAGE_HANDLE_DISABLE_DELAYED, 1, 0);
+ mHandler.sendMessageDelayed(disableDelayedMsg, ENABLE_DISABLE_DELAY_MS);
+ } else {
+ // The Bluetooth is turning off, wait for exiting STATE_ON
+ if (mState == BluetoothAdapter.STATE_ON) {
+ if (mWaitForDisableRetry < MAX_WAIT_FOR_ENABLE_DISABLE_RETRIES) {
+ mWaitForDisableRetry++;
+ Message disableDelayedMsg = mHandler.obtainMessage(
+ MESSAGE_HANDLE_DISABLE_DELAYED, 1, 0);
+ mHandler.sendMessageDelayed(disableDelayedMsg,
+ ENABLE_DISABLE_DELAY_MS);
+ break;
+ } else {
+ Slog.e(TAG, "Wait for exiting STATE_ON timeout");
+ }
+ }
+ // Either state is exited from STATE_ON or reaches the maximum retry, we
+ // should move forward to the next step.
+ Slog.d(TAG, "Handle disable is finished");
+ }
+ break;
+ }
+
+ case MESSAGE_RESTORE_USER_SETTING:
+ if ((msg.arg1 == RESTORE_SETTING_TO_OFF) && mEnable) {
+ if (DBG) {
+ Slog.d(TAG, "Restore Bluetooth state to disabled");
+ }
+ persistBluetoothSetting(BLUETOOTH_OFF);
+ mEnableExternal = false;
+ sendDisableMsg(
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTORE_USER_SETTING,
+ mContext.getPackageName());
+ } else if ((msg.arg1 == RESTORE_SETTING_TO_ON) && !mEnable) {
+ if (DBG) {
+ Slog.d(TAG, "Restore Bluetooth state to enabled");
+ }
+ mQuietEnableExternal = false;
+ mEnableExternal = true;
+ // waive WRITE_SECURE_SETTINGS permission check
+ sendEnableMsg(false,
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTORE_USER_SETTING,
+ mContext.getPackageName());
+ }
+ break;
+ case MESSAGE_REGISTER_STATE_CHANGE_CALLBACK: {
+ IBluetoothStateChangeCallback callback =
+ (IBluetoothStateChangeCallback) msg.obj;
+ mStateChangeCallbacks.register(callback);
+ break;
+ }
+ case MESSAGE_UNREGISTER_STATE_CHANGE_CALLBACK: {
+ IBluetoothStateChangeCallback callback =
+ (IBluetoothStateChangeCallback) msg.obj;
+ mStateChangeCallbacks.unregister(callback);
+ break;
+ }
+ case MESSAGE_ADD_PROXY_DELAYED: {
+ ProfileServiceConnections psc = mProfileServices.get(msg.arg1);
+ if (psc == null) {
+ break;
+ }
+ IBluetoothProfileServiceConnection proxy =
+ (IBluetoothProfileServiceConnection) msg.obj;
+ psc.addProxy(proxy);
+ break;
+ }
+ case MESSAGE_BIND_PROFILE_SERVICE: {
+ ProfileServiceConnections psc = (ProfileServiceConnections) msg.obj;
+ removeMessages(MESSAGE_BIND_PROFILE_SERVICE, msg.obj);
+ if (psc == null) {
+ break;
+ }
+ psc.bindService();
+ break;
+ }
+ case MESSAGE_BLUETOOTH_SERVICE_CONNECTED: {
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_BLUETOOTH_SERVICE_CONNECTED: " + msg.arg1);
+ }
+
+ IBinder service = (IBinder) msg.obj;
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (msg.arg1 == SERVICE_IBLUETOOTHGATT) {
+ mBluetoothGatt =
+ IBluetoothGatt.Stub.asInterface(Binder.allowBlocking(service));
+ continueFromBleOnState();
+ break;
+ } // else must be SERVICE_IBLUETOOTH
+
+ //Remove timeout
+ mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);
+
+ mBinding = false;
+ mBluetoothBinder = service;
+ mBluetooth = IBluetooth.Stub.asInterface(Binder.allowBlocking(service));
+
+ if (!isNameAndAddressSet()) {
+ Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);
+ mHandler.sendMessage(getMsg);
+ if (mGetNameAddressOnly) {
+ return;
+ }
+ }
+
+ //Register callback object
+ try {
+ mBluetooth.registerCallback(mBluetoothCallback,
+ mContext.getAttributionSource());
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Unable to register BluetoothCallback", re);
+ }
+ //Inform BluetoothAdapter instances that service is up
+ sendBluetoothServiceUpCallback();
+
+ //Do enable request
+ try {
+ if (!mBluetooth.enable(mQuietEnable, mContext.getAttributionSource())) {
+ Slog.e(TAG, "IBluetooth.enable() returned false");
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call enable()", e);
+ }
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+
+ if (!mEnable) {
+ waitForState(Set.of(BluetoothAdapter.STATE_ON));
+ handleDisable();
+ waitForState(Set.of(BluetoothAdapter.STATE_OFF,
+ BluetoothAdapter.STATE_TURNING_ON,
+ BluetoothAdapter.STATE_TURNING_OFF,
+ BluetoothAdapter.STATE_BLE_TURNING_ON,
+ BluetoothAdapter.STATE_BLE_ON,
+ BluetoothAdapter.STATE_BLE_TURNING_OFF));
+ }
+ break;
+ }
+ case MESSAGE_BLUETOOTH_STATE_CHANGE: {
+ int prevState = msg.arg1;
+ int newState = msg.arg2;
+ if (DBG) {
+ Slog.d(TAG,
+ "MESSAGE_BLUETOOTH_STATE_CHANGE: " + BluetoothAdapter.nameForState(
+ prevState) + " > " + BluetoothAdapter.nameForState(
+ newState));
+ }
+ mState = newState;
+ bluetoothStateChangeHandler(prevState, newState);
+ // handle error state transition case from TURNING_ON to OFF
+ // unbind and rebind bluetooth service and enable bluetooth
+ if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_ON) && (newState
+ == BluetoothAdapter.STATE_OFF) && (mBluetooth != null) && mEnable) {
+ recoverBluetoothServiceFromError(false);
+ }
+ if ((prevState == BluetoothAdapter.STATE_TURNING_ON) && (newState
+ == BluetoothAdapter.STATE_BLE_ON) && (mBluetooth != null) && mEnable) {
+ recoverBluetoothServiceFromError(true);
+ }
+ // If we tried to enable BT while BT was in the process of shutting down,
+ // wait for the BT process to fully tear down and then force a restart
+ // here. This is a bit of a hack (b/29363429).
+ if ((prevState == BluetoothAdapter.STATE_BLE_TURNING_OFF) && (newState
+ == BluetoothAdapter.STATE_OFF)) {
+ if (mEnable) {
+ Slog.d(TAG, "Entering STATE_OFF but mEnabled is true; restarting.");
+ waitForState(Set.of(BluetoothAdapter.STATE_OFF));
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, getServiceRestartMs());
+ }
+ }
+ if (newState == BluetoothAdapter.STATE_ON
+ || newState == BluetoothAdapter.STATE_BLE_ON) {
+ // bluetooth is working, reset the counter
+ if (mErrorRecoveryRetryCounter != 0) {
+ Slog.w(TAG, "bluetooth is recovered from error");
+ mErrorRecoveryRetryCounter = 0;
+ }
+ }
+ break;
+ }
+ case MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED: {
+ Slog.e(TAG, "MESSAGE_BLUETOOTH_SERVICE_DISCONNECTED(" + msg.arg1 + ")");
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (msg.arg1 == SERVICE_IBLUETOOTH) {
+ // if service is unbinded already, do nothing and return
+ if (mBluetooth == null) {
+ break;
+ }
+ mBluetooth = null;
+ } else if (msg.arg1 == SERVICE_IBLUETOOTHGATT) {
+ mBluetoothGatt = null;
+ break;
+ } else {
+ Slog.e(TAG, "Unknown argument for service disconnect!");
+ break;
+ }
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+
+ // log the unexpected crash
+ addCrashLog();
+ addActiveLog(BluetoothProtoEnums.ENABLE_DISABLE_REASON_CRASH,
+ mContext.getPackageName(), false);
+ if (mEnable) {
+ mEnable = false;
+ // Send a Bluetooth Restart message
+ Message restartMsg =
+ mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, getServiceRestartMs());
+ }
+
+ sendBluetoothServiceDownCallback();
+
+ // Send BT state broadcast to update
+ // the BT icon correctly
+ if ((mState == BluetoothAdapter.STATE_TURNING_ON) || (mState
+ == BluetoothAdapter.STATE_ON)) {
+ bluetoothStateChangeHandler(BluetoothAdapter.STATE_ON,
+ BluetoothAdapter.STATE_TURNING_OFF);
+ mState = BluetoothAdapter.STATE_TURNING_OFF;
+ }
+ if (mState == BluetoothAdapter.STATE_TURNING_OFF) {
+ bluetoothStateChangeHandler(BluetoothAdapter.STATE_TURNING_OFF,
+ BluetoothAdapter.STATE_OFF);
+ }
+
+ mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
+ mState = BluetoothAdapter.STATE_OFF;
+ break;
+ }
+ case MESSAGE_RESTART_BLUETOOTH_SERVICE: {
+ mErrorRecoveryRetryCounter++;
+ Slog.d(TAG, "MESSAGE_RESTART_BLUETOOTH_SERVICE: retry count="
+ + mErrorRecoveryRetryCounter);
+ if (mErrorRecoveryRetryCounter < MAX_ERROR_RESTART_RETRIES) {
+ /* Enable without persisting the setting as
+ it doesnt change when IBluetooth
+ service restarts */
+ mEnable = true;
+ addActiveLog(BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTARTED,
+ mContext.getPackageName(), true);
+ handleEnable(mQuietEnable);
+ } else {
+ Slog.e(TAG, "Reach maximum retry to restart Bluetooth!");
+ }
+ break;
+ }
+ case MESSAGE_TIMEOUT_BIND: {
+ Slog.e(TAG, "MESSAGE_TIMEOUT_BIND");
+ mBluetoothLock.writeLock().lock();
+ mBinding = false;
+ mBluetoothLock.writeLock().unlock();
+ break;
+ }
+ case MESSAGE_TIMEOUT_UNBIND: {
+ Slog.e(TAG, "MESSAGE_TIMEOUT_UNBIND");
+ mBluetoothLock.writeLock().lock();
+ mUnbinding = false;
+ mBluetoothLock.writeLock().unlock();
+ break;
+ }
+
+ case MESSAGE_USER_SWITCHED: {
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_USER_SWITCHED");
+ }
+ mHandler.removeMessages(MESSAGE_USER_SWITCHED);
+
+ /* disable and enable BT when detect a user switch */
+ if (mBluetooth != null && isEnabled()) {
+ restartForReason(BluetoothProtoEnums.ENABLE_DISABLE_REASON_USER_SWITCH);
+ } else if (mBinding || mBluetooth != null) {
+ Message userMsg = mHandler.obtainMessage(MESSAGE_USER_SWITCHED);
+ userMsg.arg2 = 1 + msg.arg2;
+ // if user is switched when service is binding retry after a delay
+ mHandler.sendMessageDelayed(userMsg, USER_SWITCHED_TIME_MS);
+ if (DBG) {
+ Slog.d(TAG, "Retry MESSAGE_USER_SWITCHED " + userMsg.arg2);
+ }
+ }
+ break;
+ }
+ case MESSAGE_USER_UNLOCKED: {
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_USER_UNLOCKED");
+ }
+ mHandler.removeMessages(MESSAGE_USER_SWITCHED);
+
+ if (mEnable && !mBinding && (mBluetooth == null)) {
+ // We should be connected, but we gave up for some
+ // reason; maybe the Bluetooth service wasn't encryption
+ // aware, so try binding again.
+ if (DBG) {
+ Slog.d(TAG, "Enabled but not bound; retrying after unlock");
+ }
+ handleEnable(mQuietEnable);
+ }
+ break;
+ }
+ case MESSAGE_INIT_FLAGS_CHANGED: {
+ if (DBG) {
+ Slog.d(TAG, "MESSAGE_INIT_FLAGS_CHANGED");
+ }
+ mHandler.removeMessages(MESSAGE_INIT_FLAGS_CHANGED);
+ if (mBluetoothModeChangeHelper.isMediaProfileConnected()) {
+ Slog.i(TAG, "Delaying MESSAGE_INIT_FLAGS_CHANGED by "
+ + DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS
+ + " ms due to existing connections");
+ mHandler.sendEmptyMessageDelayed(
+ MESSAGE_INIT_FLAGS_CHANGED,
+ DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS);
+ break;
+ }
+ if (!isDeviceProvisioned()) {
+ Slog.i(TAG, "Delaying MESSAGE_INIT_FLAGS_CHANGED by "
+ + DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS
+ + "ms because device is not provisioned");
+ mHandler.sendEmptyMessageDelayed(
+ MESSAGE_INIT_FLAGS_CHANGED,
+ DELAY_FOR_RETRY_INIT_FLAG_CHECK_MS);
+ break;
+ }
+ if (mBluetooth != null && isEnabled()) {
+ Slog.i(TAG, "Restarting Bluetooth due to init flag change");
+ restartForReason(
+ BluetoothProtoEnums.ENABLE_DISABLE_REASON_INIT_FLAGS_CHANGED);
+ }
+ break;
+ }
+ }
+ }
+
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED
+ })
+ private void restartForReason(int reason) {
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ mBluetooth.unregisterCallback(mBluetoothCallback,
+ mContext.getAttributionSource());
+ }
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Unable to unregister", re);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ if (mState == BluetoothAdapter.STATE_TURNING_OFF) {
+ // MESSAGE_USER_SWITCHED happened right after MESSAGE_ENABLE
+ bluetoothStateChangeHandler(mState, BluetoothAdapter.STATE_OFF);
+ mState = BluetoothAdapter.STATE_OFF;
+ }
+ if (mState == BluetoothAdapter.STATE_OFF) {
+ bluetoothStateChangeHandler(mState, BluetoothAdapter.STATE_TURNING_ON);
+ mState = BluetoothAdapter.STATE_TURNING_ON;
+ }
+
+ waitForState(Set.of(BluetoothAdapter.STATE_ON));
+
+ if (mState == BluetoothAdapter.STATE_TURNING_ON) {
+ bluetoothStateChangeHandler(mState, BluetoothAdapter.STATE_ON);
+ }
+
+ unbindAllBluetoothProfileServices();
+ // disable
+ addActiveLog(reason, mContext.getPackageName(), false);
+ handleDisable();
+ // Pbap service need receive STATE_TURNING_OFF intent to close
+ bluetoothStateChangeHandler(BluetoothAdapter.STATE_ON,
+ BluetoothAdapter.STATE_TURNING_OFF);
+
+ boolean didDisableTimeout =
+ !waitForState(Set.of(BluetoothAdapter.STATE_OFF));
+
+ bluetoothStateChangeHandler(BluetoothAdapter.STATE_TURNING_OFF,
+ BluetoothAdapter.STATE_OFF);
+ sendBluetoothServiceDownCallback();
+
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (mBluetooth != null) {
+ mBluetooth = null;
+ // Unbind
+ mContext.unbindService(mConnection);
+ }
+ mBluetoothGatt = null;
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+
+ //
+ // If disabling Bluetooth times out, wait for an
+ // additional amount of time to ensure the process is
+ // shut down completely before attempting to restart.
+ //
+ if (didDisableTimeout) {
+ SystemClock.sleep(3000);
+ } else {
+ SystemClock.sleep(100);
+ }
+
+ mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
+ mState = BluetoothAdapter.STATE_OFF;
+ // enable
+ addActiveLog(reason, mContext.getPackageName(), true);
+ // mEnable flag could have been reset on disableBLE. Reenable it.
+ mEnable = true;
+ handleEnable(mQuietEnable);
+ }
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void handleEnable(boolean quietMode) {
+ mQuietEnable = quietMode;
+
+ try {
+ mBluetoothLock.writeLock().lock();
+ if ((mBluetooth == null) && (!mBinding)) {
+ Slog.d(TAG, "binding Bluetooth service");
+ //Start bind timeout and bind
+ Message timeoutMsg = mHandler.obtainMessage(MESSAGE_TIMEOUT_BIND);
+ mHandler.sendMessageDelayed(timeoutMsg, TIMEOUT_BIND_MS);
+ Intent i = new Intent(IBluetooth.class.getName());
+ if (!doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ UserHandle.CURRENT)) {
+ mHandler.removeMessages(MESSAGE_TIMEOUT_BIND);
+ } else {
+ mBinding = true;
+ }
+ } else if (mBluetooth != null) {
+ //Enable bluetooth
+ try {
+ if (!mBluetooth.enable(mQuietEnable, mContext.getAttributionSource())) {
+ Slog.e(TAG, "IBluetooth.enable() returned false");
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call enable()", e);
+ }
+ }
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+ }
+
+ boolean doBind(Intent intent, ServiceConnection conn, int flags, UserHandle user) {
+ ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
+ intent.setComponent(comp);
+ if (comp == null || !mContext.bindServiceAsUser(intent, conn, flags, user)) {
+ Slog.e(TAG, "Fail to bind to: " + intent);
+ return false;
+ }
+ return true;
+ }
+
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ private void handleDisable() {
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ if (DBG) {
+ Slog.d(TAG, "Sending off request.");
+ }
+ if (!mBluetooth.disable(mContext.getAttributionSource())) {
+ Slog.e(TAG, "IBluetooth.disable() returned false");
+ }
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to call disable()", e);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ }
+
+ private boolean checkIfCallerIsForegroundUser() {
+ int foregroundUser;
+ int callingUser = UserHandle.getCallingUserId();
+ int callingUid = Binder.getCallingUid();
+ final long callingIdentity = Binder.clearCallingIdentity();
+ UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ UserInfo ui = um.getProfileParent(callingUser);
+ int parentUser = (ui != null) ? ui.id : UserHandle.USER_NULL;
+ int callingAppId = UserHandle.getAppId(callingUid);
+ boolean valid = false;
+ try {
+ foregroundUser = ActivityManager.getCurrentUser();
+ valid = (callingUser == foregroundUser) || parentUser == foregroundUser
+ || callingAppId == Process.NFC_UID || callingAppId == mSystemUiUid
+ || callingAppId == Process.SHELL_UID;
+ if (DBG && !valid) {
+ Slog.d(TAG, "checkIfCallerIsForegroundUser: valid=" + valid + " callingUser="
+ + callingUser + " parentUser=" + parentUser + " foregroundUser="
+ + foregroundUser);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ return valid;
+ }
+
+ private void sendBleStateChanged(int prevState, int newState) {
+ if (DBG) {
+ Slog.d(TAG,
+ "Sending BLE State Change: " + BluetoothAdapter.nameForState(prevState) + " > "
+ + BluetoothAdapter.nameForState(newState));
+ }
+ // Send broadcast message to everyone else
+ Intent intent = new Intent(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
+ intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothAdapter.EXTRA_STATE, newState);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL, null, getTempAllowlistBroadcastOptions());
+ }
+
+ private boolean isBleState(int state) {
+ switch (state) {
+ case BluetoothAdapter.STATE_BLE_ON:
+ case BluetoothAdapter.STATE_BLE_TURNING_ON:
+ case BluetoothAdapter.STATE_BLE_TURNING_OFF:
+ return true;
+ }
+ return false;
+ }
+
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ private void bluetoothStateChangeHandler(int prevState, int newState) {
+ boolean isStandardBroadcast = true;
+ if (prevState == newState) { // No change. Nothing to do.
+ return;
+ }
+ // Notify all proxy objects first of adapter state change
+ if (newState == BluetoothAdapter.STATE_BLE_ON || newState == BluetoothAdapter.STATE_OFF) {
+ boolean intermediate_off = (prevState == BluetoothAdapter.STATE_TURNING_OFF
+ && newState == BluetoothAdapter.STATE_BLE_ON);
+
+ if (newState == BluetoothAdapter.STATE_OFF) {
+ // If Bluetooth is off, send service down event to proxy objects, and unbind
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth is complete send Service Down");
+ }
+ sendBluetoothServiceDownCallback();
+ unbindAndFinish();
+ sendBleStateChanged(prevState, newState);
+
+ /* Currently, the OFF intent is broadcasted externally only when we transition
+ * from TURNING_OFF to BLE_ON state. So if the previous state is a BLE state,
+ * we are guaranteed that the OFF intent has been broadcasted earlier and we
+ * can safely skip it.
+ * Conversely, if the previous state is not a BLE state, it indicates that some
+ * sort of crash has occurred, moving us directly to STATE_OFF without ever
+ * passing through BLE_ON. We should broadcast the OFF intent in this case. */
+ isStandardBroadcast = !isBleState(prevState);
+
+ } else if (!intermediate_off) {
+ // connect to GattService
+ if (DBG) {
+ Slog.d(TAG, "Bluetooth is in LE only mode");
+ }
+ if (mBluetoothGatt != null || !mContext.getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ continueFromBleOnState();
+ } else {
+ if (DBG) {
+ Slog.d(TAG, "Binding Bluetooth GATT service");
+ }
+ Intent i = new Intent(IBluetoothGatt.class.getName());
+ doBind(i, mConnection, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
+ UserHandle.CURRENT);
+ }
+ sendBleStateChanged(prevState, newState);
+ //Don't broadcase this as std intent
+ isStandardBroadcast = false;
+
+ } else if (intermediate_off) {
+ if (DBG) {
+ Slog.d(TAG, "Intermediate off, back to LE only mode");
+ }
+ // For LE only mode, broadcast as is
+ sendBleStateChanged(prevState, newState);
+ sendBluetoothStateCallback(false); // BT is OFF for general users
+ // Broadcast as STATE_OFF
+ newState = BluetoothAdapter.STATE_OFF;
+ sendBrEdrDownCallback(mContext.getAttributionSource());
+ }
+ } else if (newState == BluetoothAdapter.STATE_ON) {
+ boolean isUp = (newState == BluetoothAdapter.STATE_ON);
+ sendBluetoothStateCallback(isUp);
+ sendBleStateChanged(prevState, newState);
+
+ } else if (newState == BluetoothAdapter.STATE_BLE_TURNING_ON
+ || newState == BluetoothAdapter.STATE_BLE_TURNING_OFF) {
+ sendBleStateChanged(prevState, newState);
+ isStandardBroadcast = false;
+
+ } else if (newState == BluetoothAdapter.STATE_TURNING_ON
+ || newState == BluetoothAdapter.STATE_TURNING_OFF) {
+ sendBleStateChanged(prevState, newState);
+ }
+
+ if (isStandardBroadcast) {
+ if (prevState == BluetoothAdapter.STATE_BLE_ON) {
+ // Show prevState of BLE_ON as OFF to standard users
+ prevState = BluetoothAdapter.STATE_OFF;
+ }
+ if (DBG) {
+ Slog.d(TAG,
+ "Sending State Change: " + BluetoothAdapter.nameForState(prevState) + " > "
+ + BluetoothAdapter.nameForState(newState));
+ }
+ Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+ intent.putExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothAdapter.EXTRA_STATE, newState);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL, null,
+ getTempAllowlistBroadcastOptions());
+ }
+ }
+
+ private boolean waitForState(Set<Integer> states) {
+ int i = 0;
+ while (i < 10) {
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth == null) {
+ break;
+ }
+ if (states.contains(mBluetooth.getState())) {
+ return true;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "getState()", e);
+ break;
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+ SystemClock.sleep(300);
+ i++;
+ }
+ Slog.e(TAG, "waitForState " + states + " time out");
+ return false;
+ }
+
+ private void sendDisableMsg(int reason, String packageName) {
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISABLE));
+ addActiveLog(reason, packageName, false);
+ }
+
+ private void sendEnableMsg(boolean quietMode, int reason, String packageName) {
+ sendEnableMsg(quietMode, reason, packageName, false);
+ }
+
+ private void sendEnableMsg(boolean quietMode, int reason, String packageName, boolean isBle) {
+ mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE, quietMode ? 1 : 0,
+ isBle ? 1 : 0));
+ addActiveLog(reason, packageName, true);
+ mLastEnabledTime = SystemClock.elapsedRealtime();
+ }
+
+ private void addActiveLog(int reason, String packageName, boolean enable) {
+ synchronized (mActiveLogs) {
+ if (mActiveLogs.size() > ACTIVE_LOG_MAX_SIZE) {
+ mActiveLogs.remove();
+ }
+ mActiveLogs.add(
+ new ActiveLog(reason, packageName, enable, System.currentTimeMillis()));
+ }
+
+ int state = enable ? FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED :
+ FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED;
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED,
+ Binder.getCallingUid(), null, state, reason, packageName);
+ }
+
+ private void addCrashLog() {
+ synchronized (mCrashTimestamps) {
+ if (mCrashTimestamps.size() == CRASH_LOG_MAX_SIZE) {
+ mCrashTimestamps.removeFirst();
+ }
+ mCrashTimestamps.add(System.currentTimeMillis());
+ mCrashes++;
+ }
+ }
+
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ private void recoverBluetoothServiceFromError(boolean clearBle) {
+ Slog.e(TAG, "recoverBluetoothServiceFromError");
+ try {
+ mBluetoothLock.readLock().lock();
+ if (mBluetooth != null) {
+ //Unregister callback object
+ mBluetooth.unregisterCallback(mBluetoothCallback, mContext.getAttributionSource());
+ }
+ } catch (RemoteException re) {
+ Slog.e(TAG, "Unable to unregister", re);
+ } finally {
+ mBluetoothLock.readLock().unlock();
+ }
+
+ SystemClock.sleep(500);
+
+ // disable
+ addActiveLog(BluetoothProtoEnums.ENABLE_DISABLE_REASON_START_ERROR,
+ mContext.getPackageName(), false);
+ handleDisable();
+
+ waitForState(Set.of(BluetoothAdapter.STATE_OFF));
+
+ sendBluetoothServiceDownCallback();
+
+ try {
+ mBluetoothLock.writeLock().lock();
+ if (mBluetooth != null) {
+ mBluetooth = null;
+ // Unbind
+ mContext.unbindService(mConnection);
+ }
+ mBluetoothGatt = null;
+ } finally {
+ mBluetoothLock.writeLock().unlock();
+ }
+
+ mHandler.removeMessages(MESSAGE_BLUETOOTH_STATE_CHANGE);
+ mState = BluetoothAdapter.STATE_OFF;
+
+ if (clearBle) {
+ clearBleApps();
+ }
+
+ mEnable = false;
+
+ // Send a Bluetooth Restart message to reenable bluetooth
+ Message restartMsg = mHandler.obtainMessage(MESSAGE_RESTART_BLUETOOTH_SERVICE);
+ mHandler.sendMessageDelayed(restartMsg, ERROR_RESTART_TIME_MS);
+ }
+
+ private boolean isBluetoothDisallowed() {
+ final long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ return mContext.getSystemService(UserManager.class)
+ .hasUserRestriction(UserManager.DISALLOW_BLUETOOTH, UserHandle.SYSTEM);
+ } finally {
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ /**
+ * Disables BluetoothOppLauncherActivity component, so the Bluetooth sharing option is not
+ * offered to the user if Bluetooth or sharing is disallowed. Puts the component to its default
+ * state if Bluetooth is not disallowed.
+ *
+ * @param userId user to disable bluetooth sharing for.
+ * @param bluetoothSharingDisallowed whether bluetooth sharing is disallowed.
+ */
+ private void updateOppLauncherComponentState(int userId, boolean bluetoothSharingDisallowed) {
+ final ComponentName oppLauncherComponent = new ComponentName("com.android.bluetooth",
+ "com.android.bluetooth.opp.BluetoothOppLauncherActivity");
+ final int newState =
+ bluetoothSharingDisallowed ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
+ try {
+ final IPackageManager imp = AppGlobals.getPackageManager();
+ imp.setComponentEnabledSetting(oppLauncherComponent, newState,
+ PackageManager.DONT_KILL_APP, userId);
+ } catch (Exception e) {
+ // The component was not found, do nothing.
+ }
+ }
+
+ private int getServiceRestartMs() {
+ return (mErrorRecoveryRetryCounter + 1) * SERVICE_RESTART_TIME_MS;
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) {
+ return;
+ }
+ if ((args.length > 0) && args[0].startsWith("--proto")) {
+ dumpProto(fd);
+ return;
+ }
+ String errorMsg = null;
+
+ writer.println("Bluetooth Status");
+ writer.println(" enabled: " + isEnabled());
+ writer.println(" state: " + BluetoothAdapter.nameForState(mState));
+ writer.println(" address: " + mAddress);
+ writer.println(" name: " + mName);
+ if (mEnable) {
+ long onDuration = SystemClock.elapsedRealtime() - mLastEnabledTime;
+ String onDurationString = String.format(Locale.US, "%02d:%02d:%02d.%03d",
+ (int) (onDuration / (1000 * 60 * 60)),
+ (int) ((onDuration / (1000 * 60)) % 60), (int) ((onDuration / 1000) % 60),
+ (int) (onDuration % 1000));
+ writer.println(" time since enabled: " + onDurationString);
+ }
+
+ if (mActiveLogs.size() == 0) {
+ writer.println("\nBluetooth never enabled!");
+ } else {
+ writer.println("\nEnable log:");
+ for (ActiveLog log : mActiveLogs) {
+ writer.println(" " + log);
+ }
+ }
+
+ writer.println(
+ "\nBluetooth crashed " + mCrashes + " time" + (mCrashes == 1 ? "" : "s"));
+ if (mCrashes == CRASH_LOG_MAX_SIZE) {
+ writer.println("(last " + CRASH_LOG_MAX_SIZE + ")");
+ }
+ for (Long time : mCrashTimestamps) {
+ writer.println(" " + timeToLog(time));
+ }
+
+ writer.println("\n" + mBleApps.size() + " BLE app" + (mBleApps.size() == 1 ? "" : "s")
+ + " registered");
+ for (ClientDeathRecipient app : mBleApps.values()) {
+ writer.println(" " + app.getPackageName());
+ }
+
+ writer.println("\nBluetoothManagerService:");
+ writer.println(" mEnable:" + mEnable);
+ writer.println(" mQuietEnable:" + mQuietEnable);
+ writer.println(" mEnableExternal:" + mEnableExternal);
+ writer.println(" mQuietEnableExternal:" + mQuietEnableExternal);
+
+ writer.println("");
+ writer.flush();
+ if (args.length == 0) {
+ // Add arg to produce output
+ args = new String[1];
+ args[0] = "--print";
+ }
+
+ if (mBluetoothBinder == null) {
+ errorMsg = "Bluetooth Service not connected";
+ } else {
+ try {
+ mBluetoothBinder.dump(fd, args);
+ } catch (RemoteException re) {
+ errorMsg = "RemoteException while dumping Bluetooth Service";
+ }
+ }
+ if (errorMsg != null) {
+ writer.println(errorMsg);
+ }
+ }
+
+ private void dumpProto(FileDescriptor fd) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+ proto.write(BluetoothManagerServiceDumpProto.ENABLED, isEnabled());
+ proto.write(BluetoothManagerServiceDumpProto.STATE, mState);
+ proto.write(BluetoothManagerServiceDumpProto.STATE_NAME,
+ BluetoothAdapter.nameForState(mState));
+ proto.write(BluetoothManagerServiceDumpProto.ADDRESS, mAddress);
+ proto.write(BluetoothManagerServiceDumpProto.NAME, mName);
+ if (mEnable) {
+ proto.write(BluetoothManagerServiceDumpProto.LAST_ENABLED_TIME_MS, mLastEnabledTime);
+ }
+ proto.write(BluetoothManagerServiceDumpProto.CURR_TIMESTAMP_MS,
+ SystemClock.elapsedRealtime());
+ for (ActiveLog log : mActiveLogs) {
+ long token = proto.start(BluetoothManagerServiceDumpProto.ACTIVE_LOGS);
+ log.dump(proto);
+ proto.end(token);
+ }
+ proto.write(BluetoothManagerServiceDumpProto.NUM_CRASHES, mCrashes);
+ proto.write(BluetoothManagerServiceDumpProto.CRASH_LOG_MAXED,
+ mCrashes == CRASH_LOG_MAX_SIZE);
+ for (Long time : mCrashTimestamps) {
+ proto.write(BluetoothManagerServiceDumpProto.CRASH_TIMESTAMPS_MS, time);
+ }
+ proto.write(BluetoothManagerServiceDumpProto.NUM_BLE_APPS, mBleApps.size());
+ for (ClientDeathRecipient app : mBleApps.values()) {
+ proto.write(BluetoothManagerServiceDumpProto.BLE_APP_PACKAGE_NAMES,
+ app.getPackageName());
+ }
+ proto.flush();
+ }
+
+ private static String getEnableDisableReasonString(int reason) {
+ switch (reason) {
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST:
+ return "APPLICATION_REQUEST";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE:
+ return "AIRPLANE_MODE";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_DISALLOWED:
+ return "DISALLOWED";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTARTED:
+ return "RESTARTED";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_START_ERROR:
+ return "START_ERROR";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_SYSTEM_BOOT:
+ return "SYSTEM_BOOT";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_CRASH:
+ return "CRASH";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_USER_SWITCH:
+ return "USER_SWITCH";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTORE_USER_SETTING:
+ return "RESTORE_USER_SETTING";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_FACTORY_RESET:
+ return "FACTORY_RESET";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_INIT_FLAGS_CHANGED:
+ return "INIT_FLAGS_CHANGED";
+ case BluetoothProtoEnums.ENABLE_DISABLE_REASON_UNSPECIFIED:
+ default: return "UNKNOWN[" + reason + "]";
+ }
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private static boolean checkPermissionForDataDelivery(Context context, String permission,
+ AttributionSource attributionSource, String message) {
+ final int result = PermissionChecker.checkPermissionForDataDeliveryFromDataSource(
+ context, permission, PID_UNKNOWN,
+ new AttributionSource(context.getAttributionSource(), attributionSource), message);
+ if (result == PERMISSION_GRANTED) {
+ return true;
+ }
+
+ final String msg = "Need " + permission + " permission for " + attributionSource + ": "
+ + message;
+ if (result == PERMISSION_HARD_DENIED) {
+ throw new SecurityException(msg);
+ } else {
+ Log.w(TAG, msg);
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if the BLUETOOTH_CONNECT permission is granted for the calling app. Returns
+ * false if the result is a soft denial. Throws SecurityException if the result is a hard
+ * denial.
+ *
+ * <p>Should be used in situations where the app op should not be noted.
+ */
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public static boolean checkConnectPermissionForDataDelivery(
+ Context context, AttributionSource attributionSource, String message) {
+ return checkPermissionForDataDelivery(context, BLUETOOTH_CONNECT,
+ attributionSource, message);
+ }
+
+ static @NonNull Bundle getTempAllowlistBroadcastOptions() {
+ final long duration = 10_000;
+ final BroadcastOptions bOptions = BroadcastOptions.makeBasic();
+ bOptions.setTemporaryAppAllowlist(duration,
+ TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+ PowerExemptionManager.REASON_BLUETOOTH_BROADCAST, "");
+ return bOptions.toBundle();
+ }
+}
diff --git a/service/java/com/android/server/bluetooth/BluetoothModeChangeHelper.java b/service/java/com/android/server/bluetooth/BluetoothModeChangeHelper.java
new file mode 100644
index 0000000000..e5854c9682
--- /dev/null
+++ b/service/java/com/android/server/bluetooth/BluetoothModeChangeHelper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2020 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 com.android.server;
+
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothA2dp;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothProfile.ServiceListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.Settings;
+import android.widget.Toast;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper class that handles callout and callback methods without
+ * complex logic.
+ */
+public class BluetoothModeChangeHelper {
+ private volatile BluetoothA2dp mA2dp;
+ private volatile BluetoothHearingAid mHearingAid;
+ private volatile BluetoothLeAudio mLeAudio;
+ private final BluetoothAdapter mAdapter;
+ private final Context mContext;
+
+ BluetoothModeChangeHelper(Context context) {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mContext = context;
+
+ mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.A2DP);
+ mAdapter.getProfileProxy(mContext, mProfileServiceListener,
+ BluetoothProfile.HEARING_AID);
+ mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.LE_AUDIO);
+ }
+
+ private final ServiceListener mProfileServiceListener = new ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ // Setup Bluetooth profile proxies
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ mA2dp = (BluetoothA2dp) proxy;
+ break;
+ case BluetoothProfile.HEARING_AID:
+ mHearingAid = (BluetoothHearingAid) proxy;
+ break;
+ case BluetoothProfile.LE_AUDIO:
+ mLeAudio = (BluetoothLeAudio) proxy;
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ // Clear Bluetooth profile proxies
+ switch (profile) {
+ case BluetoothProfile.A2DP:
+ mA2dp = null;
+ break;
+ case BluetoothProfile.HEARING_AID:
+ mHearingAid = null;
+ break;
+ case BluetoothProfile.LE_AUDIO:
+ mLeAudio = null;
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ @VisibleForTesting
+ public boolean isMediaProfileConnected() {
+ return isA2dpConnected() || isHearingAidConnected() || isLeAudioConnected();
+ }
+
+ @VisibleForTesting
+ public boolean isBluetoothOn() {
+ final BluetoothAdapter adapter = mAdapter;
+ if (adapter == null) {
+ return false;
+ }
+ return adapter.getLeState() == BluetoothAdapter.STATE_ON;
+ }
+
+ @VisibleForTesting
+ public boolean isAirplaneModeOn() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ }
+
+ @VisibleForTesting
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
+ public void onAirplaneModeChanged(BluetoothManagerService managerService) {
+ managerService.onAirplaneModeChanged();
+ }
+
+ @VisibleForTesting
+ public int getSettingsInt(String name) {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ name, 0);
+ }
+
+ @VisibleForTesting
+ public void setSettingsInt(String name, int value) {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ name, value);
+ }
+
+ @VisibleForTesting
+ public void showToastMessage() {
+ Resources r = mContext.getResources();
+ final CharSequence text = r.getString(
+ R.string.bluetooth_airplane_mode_toast, 0);
+ Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+ }
+
+ private boolean isA2dpConnected() {
+ final BluetoothA2dp a2dp = mA2dp;
+ if (a2dp == null) {
+ return false;
+ }
+ return a2dp.getConnectedDevices().size() > 0;
+ }
+
+ private boolean isHearingAidConnected() {
+ final BluetoothHearingAid hearingAid = mHearingAid;
+ if (hearingAid == null) {
+ return false;
+ }
+ return hearingAid.getConnectedDevices().size() > 0;
+ }
+
+ private boolean isLeAudioConnected() {
+ final BluetoothLeAudio leAudio = mLeAudio;
+ if (leAudio == null) {
+ return false;
+ }
+ return leAudio.getConnectedDevices().size() > 0;
+ }
+}
diff --git a/service/java/com/android/server/bluetooth/BluetoothService.java b/service/java/com/android/server/bluetooth/BluetoothService.java
new file mode 100644
index 0000000000..1a1eecd0f4
--- /dev/null
+++ b/service/java/com/android/server/bluetooth/BluetoothService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2015 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 com.android.server;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.os.UserManager;
+
+import com.android.server.SystemService.TargetUser;
+
+class BluetoothService extends SystemService {
+ private BluetoothManagerService mBluetoothManagerService;
+ private boolean mInitialized = false;
+
+ public BluetoothService(Context context) {
+ super(context);
+ mBluetoothManagerService = new BluetoothManagerService(context);
+ }
+
+ private void initialize() {
+ if (!mInitialized) {
+ mBluetoothManagerService.handleOnBootPhase();
+ mInitialized = true;
+ }
+ }
+
+ @Override
+ public void onStart() {
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
+ publishBinderService(BluetoothAdapter.BLUETOOTH_MANAGER_SERVICE,
+ mBluetoothManagerService);
+ } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY &&
+ !UserManager.isHeadlessSystemUserMode()) {
+ initialize();
+ }
+ }
+
+ @Override
+ public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
+ if (!mInitialized) {
+ initialize();
+ } else {
+ mBluetoothManagerService.handleOnSwitchUser(to.getUserIdentifier());
+ }
+ }
+
+ @Override
+ public void onUserUnlocking(@NonNull TargetUser user) {
+ mBluetoothManagerService.handleOnUnlockUser(user.getUserIdentifier());
+ }
+}
diff --git a/service/tests/Android.bp b/service/tests/Android.bp
new file mode 100644
index 0000000000..6cfa7ecb63
--- /dev/null
+++ b/service/tests/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+filegroup {
+ name: "service-bluetooth-tests-sources",
+ srcs: [
+ "src/**/*.java",
+ ],
+ visibility: [
+ "//frameworks/base",
+ "//frameworks/base/services",
+ "//frameworks/base/services/tests/servicestests",
+ ],
+}
diff --git a/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java b/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
new file mode 100644
index 0000000000..a1d4c203de
--- /dev/null
+++ b/service/tests/src/com/android/server/BluetoothAirplaneModeListenerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2019 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 com.android.server;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.os.Looper;
+import android.provider.Settings;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAirplaneModeListenerTest {
+ private Context mContext;
+ private BluetoothAirplaneModeListener mBluetoothAirplaneModeListener;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothModeChangeHelper mHelper;
+
+ @Mock BluetoothManagerService mBluetoothManagerService;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+
+ mHelper = mock(BluetoothModeChangeHelper.class);
+ when(mHelper.getSettingsInt(BluetoothAirplaneModeListener.TOAST_COUNT))
+ .thenReturn(BluetoothAirplaneModeListener.MAX_TOAST_COUNT);
+ doNothing().when(mHelper).setSettingsInt(anyString(), anyInt());
+ doNothing().when(mHelper).showToastMessage();
+ doNothing().when(mHelper).onAirplaneModeChanged(any(BluetoothManagerService.class));
+
+ mBluetoothAirplaneModeListener = new BluetoothAirplaneModeListener(
+ mBluetoothManagerService, Looper.getMainLooper(), mContext);
+ mBluetoothAirplaneModeListener.start(mHelper);
+ }
+
+ @Test
+ public void testIgnoreOnAirplanModeChange() {
+ Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
+
+ when(mHelper.isBluetoothOn()).thenReturn(true);
+ Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
+
+ when(mHelper.isMediaProfileConnected()).thenReturn(true);
+ Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
+
+ when(mHelper.isAirplaneModeOn()).thenReturn(true);
+ Assert.assertTrue(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
+ }
+
+ @Test
+ public void testHandleAirplaneModeChange_InvokeAirplaneModeChanged() {
+ mBluetoothAirplaneModeListener.handleAirplaneModeChange();
+ verify(mHelper).onAirplaneModeChanged(mBluetoothManagerService);
+ }
+
+ @Test
+ public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_NotPopToast() {
+ mBluetoothAirplaneModeListener.mToastCount = BluetoothAirplaneModeListener.MAX_TOAST_COUNT;
+ when(mHelper.isBluetoothOn()).thenReturn(true);
+ when(mHelper.isMediaProfileConnected()).thenReturn(true);
+ when(mHelper.isAirplaneModeOn()).thenReturn(true);
+ mBluetoothAirplaneModeListener.handleAirplaneModeChange();
+
+ verify(mHelper).setSettingsInt(Settings.Global.BLUETOOTH_ON,
+ BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
+ verify(mHelper, times(0)).showToastMessage();
+ verify(mHelper, times(0)).onAirplaneModeChanged(mBluetoothManagerService);
+ }
+
+ @Test
+ public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_PopToast() {
+ mBluetoothAirplaneModeListener.mToastCount = 0;
+ when(mHelper.isBluetoothOn()).thenReturn(true);
+ when(mHelper.isMediaProfileConnected()).thenReturn(true);
+ when(mHelper.isAirplaneModeOn()).thenReturn(true);
+ mBluetoothAirplaneModeListener.handleAirplaneModeChange();
+
+ verify(mHelper).setSettingsInt(Settings.Global.BLUETOOTH_ON,
+ BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
+ verify(mHelper).showToastMessage();
+ verify(mHelper, times(0)).onAirplaneModeChanged(mBluetoothManagerService);
+ }
+
+ @Test
+ public void testIsPopToast_PopToast() {
+ mBluetoothAirplaneModeListener.mToastCount = 0;
+ Assert.assertTrue(mBluetoothAirplaneModeListener.shouldPopToast());
+ verify(mHelper).setSettingsInt(BluetoothAirplaneModeListener.TOAST_COUNT, 1);
+ }
+
+ @Test
+ public void testIsPopToast_NotPopToast() {
+ mBluetoothAirplaneModeListener.mToastCount = BluetoothAirplaneModeListener.MAX_TOAST_COUNT;
+ Assert.assertFalse(mBluetoothAirplaneModeListener.shouldPopToast());
+ verify(mHelper, times(0)).setSettingsInt(anyString(), anyInt());
+ }
+}