diff options
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—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—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 > 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} < 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=<profile>, device=<device>)" + */ + 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=<profile>, device=<device>)" + */ + 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()); + } +} |