blob: a9ccea4740037f8fde6799a868dc5f43ef52d00a [file]
/*
* 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 static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
import static android.bluetooth.BluetoothUtils.executeFromBinder;
import static android.bluetooth.BluetoothUtils.logRemoteException;
import static java.util.Objects.requireNonNull;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.Hide;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresNoPermission;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.bluetooth.BluetoothGattCharacteristic.WriteType;
import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
import android.bluetooth.annotations.RequiresLegacyBluetoothPermission;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AttributionSource;
import android.os.Handler;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Log;
import com.android.bluetooth.flags.Flags;
import com.android.internal.annotations.GuardedBy;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executor;
/**
* 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.class.getSimpleName();
private static final boolean VDBG = Log.isLoggable("bluetooth", Log.VERBOSE);
private final IBluetoothGatt mService;
private volatile BluetoothGattCallback mCallback;
private final Executor mExecutor;
private final BluetoothDevice mDevice;
private final boolean mAutoConnect;
private boolean mClientRegistered;
private int mAuthRetryState = AUTH_RETRY_STATE_IDLE;
private final Object mStateLock = new Object();
@GuardedBy("mStateLock")
private int mConnState = CONN_STATE_CONNECTING;
private final Object mDeviceBusyLock = new Object();
@GuardedBy("mDeviceBusyLock")
private boolean mDeviceBusy = false;
private final int mTransport;
private final boolean mOpportunistic;
private final AttributionSource mAttributionSource;
private final BluetoothGattConnectionSettings mGattConnectionSettings;
private static final int AUTH_RETRY_STATE_IDLE = 0;
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_CLOSED = 4;
private static final int WRITE_CHARACTERISTIC_MAX_RETRIES = 5;
private static final int WRITE_CHARACTERISTIC_TIME_TO_WAIT = 10; // milliseconds
private final CopyOnWriteArrayList<BluetoothGattService> mServices =
new CopyOnWriteArrayList<>();
/** 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;
/**
* GATT connection timed out, likely due to the remote device being out of range or not
* advertising as connectable.
*/
public static final int GATT_CONNECTION_TIMEOUT = 0x93;
/** 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;
/**
* Connection parameter update - Request the priority preferred for Digital Car Key for a lower
* latency connection. This connection parameter will consume more power than {@link
* BluetoothGatt#CONNECTION_PRIORITY_BALANCED}, so it is recommended that apps do not use this
* unless it specifically fits their use case.
*/
public static final int CONNECTION_PRIORITY_DCK = 3;
/** Connection Subrate mode - Request to disable subrate mode. */
public static final int SUBRATE_MODE_OFF = 0;
/**
* Connection Subrate mode - Requests to enable subrate mode with parameters optimized for low
* burstiness, minimum power consumption. This is the most power-efficient subrate
* configuration.
*/
public static final int SUBRATE_MODE_LOW = 1;
/**
* Connection subrate mode - Requests to enable subrate mode using balanced parameters to
* provide a compromise between power savings and performance.
*/
public static final int SUBRATE_MODE_BALANCED = 2;
/**
* Connection subrate mode - Requests to enable subrate mode with parameters optimized for high
* burstiness, enhanced data transfer.
*/
public static final int SUBRATE_MODE_HIGH = 3;
/** Connection Subrate mode - System Update. */
public static final int SUBRATE_MODE_SYSTEM_UPDATE = 99;
/** Connection Subrate mode - No Update applied due to error. */
public static final int SUBRATE_MODE_NOT_UPDATED = 255;
@Hide
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"SUBRATE_MODE"},
value = {SUBRATE_MODE_OFF, SUBRATE_MODE_LOW, SUBRATE_MODE_BALANCED, SUBRATE_MODE_HIGH})
public @interface SubrateMode {}
/** Subrate modes update. */
@Hide
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"ON_SUBRATE_CHANGE_MODE"},
value = {
SUBRATE_MODE_OFF,
SUBRATE_MODE_LOW,
SUBRATE_MODE_BALANCED,
SUBRATE_MODE_HIGH,
SUBRATE_MODE_SYSTEM_UPDATE,
SUBRATE_MODE_NOT_UPDATED,
})
public @interface OnSubrateChangeModeValues {}
/** Subrate request return values. */
@Hide
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"ON_SUBRATE_CHANGE_STATUS"},
value = {
BluetoothStatusCodes.SUCCESS,
BluetoothStatusCodes.REASON_SYSTEM_POLICY,
BluetoothStatusCodes.ERROR_UNKNOWN,
BluetoothStatusCodes.ERROR_HARDWARE_GENERIC,
BluetoothStatusCodes.NOT_ALLOWED,
BluetoothStatusCodes.ERROR_REMOTE_OPERATION_NOT_SUPPORTED,
BluetoothStatusCodes.ERROR_LOCAL_NOT_ENOUGH_RESOURCES,
BluetoothStatusCodes.ERROR_BAD_PARAMETERS,
})
public @interface OnSubrateChangeStatusValues {}
/** 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. */
private final IBluetoothGattCallback mBluetoothGattCallback = new GattCallback();
private class GattCallback extends IBluetoothGattCallback.Stub {
/**
* 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) {
executeFromBinder(mExecutor, cb);
}
/** Application interface registered - app is ready to go */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onClientRegistered(int status) {
Log.d(TAG, "onClientRegistered(" + status + ")");
mClientRegistered = status == GATT_SUCCESS;
synchronized (mStateLock) {
if (mConnState == CONN_STATE_CLOSED) {
Log.d(TAG, "Client registration completed after closed, unregistering");
unregisterApp();
mCallback = null;
return;
}
if (VDBG) {
if (mConnState != CONN_STATE_CONNECTING) {
Log.e(TAG, "Bad connection state: " + mConnState);
}
}
}
if (status != GATT_SUCCESS) {
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onConnectionStateChange(
BluetoothGatt.this, GATT_FAILURE, STATE_DISCONNECTED);
}
});
synchronized (mStateLock) {
mConnState = CONN_STATE_IDLE;
}
return;
}
try {
// autoConnect is inverse of "isDirect"
boolean isAutoMtuEnabled = false;
if (Flags.gattConnSettings()) {
isAutoMtuEnabled = mGattConnectionSettings.isAutomaticMtuEnabled();
}
mService.clientConnect(
mBluetoothGattCallback,
mDevice,
mDevice.getAddressType(),
!mAutoConnect,
mTransport,
mOpportunistic,
isAutoMtuEnabled,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/** Phy update callback */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
Log.d(
TAG,
"onPhyUpdate() -"
+ (" status=" + status)
+ (" device=" + device)
+ (" txPhy=" + txPhy)
+ (" rxPhy=" + rxPhy));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onPhyUpdate(BluetoothGatt.this, txPhy, rxPhy, status);
}
});
}
/** Phy read callback */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
Log.d(
TAG,
"onPhyRead() -"
+ (" status=" + status)
+ (" device=" + device)
+ (" txPhy=" + txPhy)
+ (" rxPhy=" + rxPhy));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onPhyRead(BluetoothGatt.this, txPhy, rxPhy, status);
}
});
}
/** Client connection state changed */
@Hide
@Override
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public void onClientConnectionState(int status, boolean connected, BluetoothDevice device) {
Log.d(
TAG,
"onClientConnectionState() -"
+ (" status=" + status)
+ (" connected=" + connected)
+ (" device=" + device));
if (!mDevice.equals(device)) {
return;
}
int profileState = connected ? STATE_CONNECTED : STATE_DISCONNECTED;
if (!connected && !mAutoConnect) {
unregisterApp();
}
runOrQueueCallback(
() -> {
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
@RequiresNoPermission // Callback to app
public void onSearchComplete(
BluetoothDevice device, List<BluetoothGattService> services, int status) {
Log.d(TAG, "onSearchComplete(" + device + ", _, " + status + ")");
if (!mDevice.equals(device)) {
return;
}
for (BluetoothGattService s : services) {
// services we receive don't have device set properly.
s.setDevice(mDevice);
}
mServices.clear();
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(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onServicesDiscovered(BluetoothGatt.this, status);
}
});
}
/** Remote characteristic has been read. Updates the internal value. */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onCharacteristicRead(
BluetoothDevice device, int status, int handle, byte[] value) {
if (VDBG) {
Log.d(
TAG,
"onCharacteristicRead() -"
+ (" device=" + device)
+ (" handle=" + handle)
+ (" Status=" + status));
}
if (!mDevice.equals(device)) {
return;
}
synchronized (mDeviceBusyLock) {
mDeviceBusy = false;
}
if ((status == GATT_INSUFFICIENT_AUTHENTICATION
|| status == GATT_INSUFFICIENT_ENCRYPTION)
&& (mAuthRetryState != AUTH_RETRY_STATE_MITM)
&& mClientRegistered) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.readCharacteristic(
mBluetoothGattCallback, device, 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(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
if (status == 0) characteristic.setValue(value);
callback.onCharacteristicRead(
BluetoothGatt.this, characteristic, value, status);
}
});
}
/** Characteristic has been written to the remote device. Let the app know how we did... */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onCharacteristicWrite(
BluetoothDevice device, int status, int handle, byte[] value) {
if (VDBG) {
Log.d(
TAG,
"onCharacteristicWrite() -"
+ (" device=" + device)
+ (" handle=" + handle)
+ (" Status=" + status));
}
if (!mDevice.equals(device)) {
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) && mClientRegistered;
i++) {
requestStatus =
mService.writeCharacteristic(
mBluetoothGattCallback,
device,
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) {
Log.e(TAG, "", e);
}
}
mAuthRetryState++;
return;
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
mAuthRetryState = AUTH_RETRY_STATE_IDLE;
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onCharacteristicWrite(
BluetoothGatt.this, characteristic, status);
}
});
}
/** Remote characteristic has been updated. Updates the internal value. */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onNotify(BluetoothDevice device, int handle, byte[] value) {
if (VDBG) {
Log.d(TAG, "onNotify(" + device + ", " + handle + ", _)");
}
if (!mDevice.equals(device)) {
return;
}
BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice, handle);
if (characteristic == null) return;
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
characteristic.setValue(value);
callback.onCharacteristicChanged(
BluetoothGatt.this, characteristic, value);
}
});
}
/** Descriptor has been read. */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onDescriptorRead(BluetoothDevice device, int status, int handle, byte[] value) {
if (VDBG) {
Log.d(TAG, "onDescriptorRead(" + device + ", " + status + ", " + handle + ", _)");
}
if (!mDevice.equals(device)) {
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)
&& mClientRegistered) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.readDescriptor(
mBluetoothGattCallback, device, handle, authReq, mAttributionSource);
mAuthRetryState++;
return;
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
mAuthRetryState = AUTH_RETRY_STATE_IDLE;
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
if (status == 0) descriptor.setValue(value);
callback.onDescriptorRead(
BluetoothGatt.this, descriptor, status, value);
}
});
}
/** Descriptor write operation complete. */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onDescriptorWrite(
BluetoothDevice device, int status, int handle, byte[] value) {
if (VDBG) {
Log.d(TAG, "onDescriptorWrite(" + device + ", " + status + ", " + handle + ", _)");
}
if (!mDevice.equals(device)) {
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)
&& mClientRegistered) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.writeDescriptor(
mBluetoothGattCallback,
device,
handle,
authReq,
value,
mAttributionSource);
mAuthRetryState++;
return;
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
mAuthRetryState = AUTH_RETRY_STATE_IDLE;
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onDescriptorWrite(BluetoothGatt.this, descriptor, status);
}
});
}
/** Prepared write transaction completed (or aborted) */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onExecuteWrite(BluetoothDevice device, int status) {
if (VDBG) {
Log.d(TAG, "onExecuteWrite(" + device + ", " + status + ")");
}
if (!mDevice.equals(device)) {
return;
}
synchronized (mDeviceBusyLock) {
mDeviceBusy = false;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onReliableWriteCompleted(BluetoothGatt.this, status);
}
});
}
/** Remote device RSSI has been read */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onReadRemoteRssi(BluetoothDevice device, int rssi, int status) {
if (VDBG) {
Log.d(TAG, "onReadRemoteRssi(" + device + ", " + rssi + ", " + status + ")");
}
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
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
@RequiresNoPermission // Callback to app
public void onConfigureMTU(BluetoothDevice device, int mtu, int status) {
Log.d(TAG, "onConfigureMTU(" + device + ", " + mtu + ", " + status + ")");
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onMtuChanged(BluetoothGatt.this, mtu, status);
}
});
}
/** Callback invoked when the given connection is updated */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onConnectionUpdated(
BluetoothDevice device, int interval, int latency, int timeout, int status) {
Log.d(
TAG,
"onConnectionUpdated() -"
+ (" device=" + device)
+ (" interval=" + interval)
+ (" latency=" + latency)
+ (" timeout=" + timeout)
+ (" status=" + status));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
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
@RequiresNoPermission // Callback to app
public void onServiceChanged(BluetoothDevice device) {
Log.d(TAG, "onServiceChanged(" + device + ")");
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onServiceChanged(BluetoothGatt.this);
}
});
}
/** Callback invoked when the given connection's subrate is changed */
@Hide
@Override
@RequiresNoPermission // Callback to app
public void onSubrateChange(BluetoothDevice device, int subrateMode, int status) {
Log.d(
TAG,
"onSubrateChange() - "
+ (" subrateMode=" + subrateMode)
+ (" status=" + status));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onSubrateChange(BluetoothGatt.this, subrateMode, status);
}
});
}
/** Callback indicating whether GATT characteristics offload has been added. */
@Hide
@RequiresNoPermission
@FlaggedApi(Flags.FLAG_GATT_OFFLOAD_API)
/* package */ void onCharacteristicsOffloaded(
BluetoothDevice device, GattOffloadSession session, int status) {
Log.d(
TAG,
"onCharacteristicsOffloaded() -"
+ (" device=" + device)
+ (" session=" + session)
+ (" status=" + status));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onCharacteristicsOffloaded(
BluetoothGatt.this, session, status);
}
});
}
/** Callback indicating whether GATT characteristics offload has been removed. */
@Hide
@Override
@RequiresNoPermission
@FlaggedApi(Flags.FLAG_GATT_OFFLOAD_API)
public void onCharacteristicsUnoffloaded(
BluetoothDevice device, int sessionId, int status) {
Log.d(
TAG,
"onCharacteristicsUnoffloaded() -"
+ (" device=" + device)
+ (" sessionId=" + sessionId)
+ (" status=" + status));
if (!mDevice.equals(device)) {
return;
}
runOrQueueCallback(
() -> {
final BluetoothGattCallback callback = mCallback;
if (callback != null) {
callback.onCharacteristicsUnoffloaded(
BluetoothGatt.this, sessionId, status);
}
});
}
}
@FlaggedApi(Flags.FLAG_GATT_CONN_SETTINGS)
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
BluetoothGatt(
@NonNull IBluetoothGatt iGatt,
@NonNull BluetoothDevice device,
AttributionSource source,
BluetoothGattConnectionSettings gattConnectionSettings,
@NonNull BluetoothGattCallback callback,
@NonNull @CallbackExecutor Executor executor) {
mService = iGatt;
mDevice = device;
mTransport = gattConnectionSettings.getTransport();
mAutoConnect = gattConnectionSettings.isAutoConnectEnabled();
mOpportunistic = gattConnectionSettings.isOpportunisticEnabled();
mAttributionSource = source;
mCallback = callback;
mExecutor = executor;
mGattConnectionSettings = requireNonNull(gattConnectionSettings);
UUID uuid = UUID.randomUUID();
Log.d(TAG, "BluetoothGatt() UUID=" + uuid);
try {
mService.registerClient(
new ParcelUuid(uuid),
mBluetoothGattCallback,
false, // eattSupport
mTransport,
mAttributionSource);
} catch (RemoteException e) {
synchronized (mStateLock) {
mConnState = CONN_STATE_IDLE;
}
Log.e(TAG, "", e);
}
}
@Hide
@Override
@RequiresNoPermission
public void onServiceConnected(IBinder service) {}
@Hide
@Override
@RequiresNoPermission
public void onServiceDisconnected() {}
@Hide
@Override
@RequiresNoPermission
public BluetoothAdapter getAdapter() {
return null;
}
/**
* Close this Bluetooth GATT client.
*
* <p>Application should call this method as early as possible after it is done with this GATT
* client.
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public void close() {
Log.d(TAG, "close()");
unregisterApp();
mCallback = null;
synchronized (mStateLock) {
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;
}
/** Unregister the current application and callbacks. */
@UnsupportedAppUsage
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
private void unregisterApp() {
if (!mClientRegistered) return;
Log.d(TAG, "unregisterApp()");
try {
mService.unregisterClient(mBluetoothGattCallback, mAttributionSource);
mClientRegistered = false;
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* Disconnects an established connection, or cancels a connection attempt currently in progress.
*/
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public void disconnect() {
Log.d(TAG, "disconnect() - device: " + mDevice);
if (!mClientRegistered) return;
try {
mService.clientDisconnect(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public boolean connect() {
if (!mClientRegistered) {
synchronized (mStateLock) {
if (mConnState != CONN_STATE_IDLE) {
return false;
}
mConnState = CONN_STATE_CONNECTING;
}
UUID uuid = UUID.randomUUID();
Log.d(TAG, "reconnect from connect(), UUID=" + uuid);
try {
mService.registerClient(
new ParcelUuid(uuid),
mBluetoothGattCallback,
/* eatt_support= */ false,
mTransport,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
synchronized (mStateLock) {
mConnState = CONN_STATE_IDLE;
}
Log.e(TAG, "Failed to register callback");
return false;
}
return true;
}
try {
Log.d(TAG, "connect(void) - device: " + mDevice + ", auto=" + mAutoConnect);
// autoConnect is inverse of "isDirect"
boolean isAutoMtuEnabled = false;
if (Flags.gattConnSettings()) {
isAutoMtuEnabled = mGattConnectionSettings.isAutomaticMtuEnabled();
}
mService.clientConnect(
mBluetoothGattCallback,
mDevice,
mDevice.getAddressType(),
!mAutoConnect,
mTransport,
mOpportunistic,
isAutoMtuEnabled,
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.
* @param rxPhy preferred receiver PHY.
* @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(BLUETOOTH_CONNECT)
public void setPreferredPhy(
@BluetoothDevice.PhyMask int txPhy,
@BluetoothDevice.PhyMask int rxPhy,
int phyOptions) {
if (!mClientRegistered) return;
try {
mService.clientSetPreferredPhy(
mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public void readPhy() {
if (!mClientRegistered) return;
try {
mService.clientReadPhy(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public boolean discoverServices() {
Log.d(TAG, "discoverServices() - device: " + mDevice);
if (!mClientRegistered) return false;
try {
mService.discoverServices(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public boolean discoverServiceByUuid(UUID uuid) {
Log.d(TAG, "discoverServiceByUuid() - device: " + mDevice);
if (!mClientRegistered) return false;
try {
mService.discoverServiceByUuid(
mBluetoothGattCallback, mDevice, 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(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 (!mClientRegistered) 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(
mBluetoothGattCallback,
device,
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(
allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
conditional = true)
public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) {
if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid);
if (!mClientRegistered) return false;
synchronized (mDeviceBusyLock) {
if (mDeviceBusy) return false;
mDeviceBusy = true;
}
try {
mService.readUsingCharacteristicUuid(
mBluetoothGattCallback,
mDevice,
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 because it relies on a {@link
* BluetoothGattCharacteristic} object whose underlying fields are subject to change outside
* this method.
*/
@Deprecated
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(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_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(BLUETOOTH_CONNECT)
@WriteOperationReturnValues
public int writeCharacteristic(
@NonNull BluetoothGattCharacteristic characteristic,
@NonNull byte[] value,
@WriteType 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 (value.length > bluetooth.constants.Core.GATT_MAX_ATTR_LEN) {
throw new IllegalArgumentException(
"value should not be longer than max length of an attribute value");
}
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 (!mClientRegistered) {
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(
mBluetoothGattCallback,
device,
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) {
Log.e(TAG, "", e);
}
}
} catch (RemoteException e) {
Log.e(TAG, "", e);
synchronized (mDeviceBusyLock) {
mDeviceBusy = false;
}
throw e.rethrowAsRuntimeException();
}
if (requestStatus != BluetoothStatusCodes.SUCCESS) {
synchronized (mDeviceBusyLock) {
mDeviceBusy = false;
}
}
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(BLUETOOTH_CONNECT)
public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
if (VDBG) Log.d(TAG, "readDescriptor() - uuid: " + descriptor.getUuid());
if (!mClientRegistered) 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(
mBluetoothGattCallback,
device,
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 because it relies on a {@link BluetoothGattDescriptor} object
* whose underlying fields are subject to change outside this method.
*/
@Deprecated
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(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(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 (!mClientRegistered) {
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(
mBluetoothGattCallback,
device,
descriptor.getInstanceId(),
AUTHENTICATION_NONE,
value,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
synchronized (mDeviceBusyLock) {
mDeviceBusy = false;
}
throw e.rethrowAsRuntimeException();
}
}
/**
* 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(BLUETOOTH_CONNECT)
public boolean beginReliableWrite() {
if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice);
if (!mClientRegistered) return false;
try {
mService.beginReliableWrite(mDevice, 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(BLUETOOTH_CONNECT)
public boolean executeReliableWrite() {
if (VDBG) Log.d(TAG, "executeReliableWrite() - device: " + mDevice);
if (!mClientRegistered) return false;
synchronized (mDeviceBusyLock) {
if (mDeviceBusy) return false;
mDeviceBusy = true;
}
try {
mService.endReliableWrite(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public void abortReliableWrite() {
if (VDBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice);
if (!mClientRegistered) return;
try {
mService.endReliableWrite(mBluetoothGattCallback, mDevice, false, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* @deprecated Use {@link #abortReliableWrite()}
*/
@Deprecated
@RequiresBluetoothConnectPermission
@RequiresPermission(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(BLUETOOTH_CONNECT)
public boolean setCharacteristicNotification(
BluetoothGattCharacteristic characteristic, boolean enable) {
Log.d(
TAG,
"setCharacteristicNotification() - uuid: "
+ characteristic.getUuid()
+ " enable: "
+ enable);
if (!mClientRegistered) return false;
BluetoothGattService service = characteristic.getService();
if (service == null) return false;
BluetoothDevice device = service.getDevice();
if (device == null) return false;
try {
mService.registerForNotification(
mBluetoothGattCallback,
device,
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(BLUETOOTH_CONNECT)
public boolean refresh() {
Log.d(TAG, "refresh() - device: " + mDevice);
if (!mClientRegistered) return false;
try {
mService.refreshDevice(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public boolean readRemoteRssi() {
Log.d(TAG, "readRssi() - device: " + mDevice);
if (!mClientRegistered) return false;
try {
return mService.readRemoteRssi(mBluetoothGattCallback, mDevice, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
}
/**
* Request an MTU size used for a given connection. Please note that starting from Android 14,
* the Android Bluetooth stack requests the BLE ATT MTU to 517 bytes when the first GATT client
* requests an MTU, and disregards all subsequent MTU requests. Check out <a
* href="{@docRoot}about/versions/14/behavior-changes-all#mtu-set-to-517">MTU is set to 517 for
* the first GATT client requesting an MTU</a> for more information.
*
* <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(BLUETOOTH_CONNECT)
public boolean requestMtu(int mtu) {
Log.d(TAG, "configureMTU() - device: " + mDevice + " mtu: " + mtu);
if (!mClientRegistered) return false;
try {
mService.configureMTU(mBluetoothGattCallback, mDevice, 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} {@link
* BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}, or {@link
* BluetoothGatt#CONNECTION_PRIORITY_DCK}.
* @throws IllegalArgumentException If the parameters are outside of their specified range.
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean requestConnectionPriority(int connectionPriority) {
if (connectionPriority < CONNECTION_PRIORITY_BALANCED
|| connectionPriority > CONNECTION_PRIORITY_DCK) {
throw new IllegalArgumentException("connectionPriority not within valid range");
}
Log.d(TAG, "requestConnectionPriority() - params: " + connectionPriority);
if (!mClientRegistered) return false;
try {
mService.connectionParameterUpdate(
mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT)
public boolean requestLeConnectionUpdate(
int minConnectionInterval,
int maxConnectionInterval,
int slaveLatency,
int supervisionTimeout,
int minConnectionEventLen,
int maxConnectionEventLen) {
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 (!mClientRegistered) return false;
try {
mService.leConnectionUpdate(
mBluetoothGattCallback,
mDevice,
minConnectionInterval,
maxConnectionInterval,
slaveLatency,
supervisionTimeout,
minConnectionEventLen,
maxConnectionEventLen,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
}
return true;
}
/** Subrate request return values. */
@Hide
@Retention(RetentionPolicy.SOURCE)
@IntDef(
prefix = {"SUBRATE_REQUEST_RETURN"},
value = {
BluetoothStatusCodes.SUCCESS,
BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED,
BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED,
BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION,
BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED,
BluetoothStatusCodes.FEATURE_NOT_SUPPORTED,
BluetoothStatusCodes.ERROR_UNKNOWN
})
public @interface SubrateRequestReturnValues {}
/**
* Request LE subrate mode.
*
* <p>Configure/Request subrating with this API, sending a subrate request to the remote device
* based on Subrate Mode. This function should be used in conjunction with {@link
* requestConnectionPriority} to manage link latency and power consumption effectively.
*
* <p>This method requires the calling app to have the {@link
* android.Manifest.permission#BLUETOOTH_CONNECT} permission. Additionally, an app must either
* have the {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} or be associated with the
* Companion Device manager (see {@link android.companion.CompanionDeviceManager#associate(
* AssociationRequest, android.companion.CompanionDeviceManager.Callback, Handler)})
*
* @param subrateMode Request a specific subrate mode.
* @throws IllegalArgumentException If the parameters are outside of their specified range.
* @return true, if the request is send to the Bluetooth stack.
*/
@RequiresBluetoothConnectPermission
@RequiresPermission(
allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
conditional = true)
public @SubrateRequestReturnValues int requestSubrateMode(@SubrateMode int subrateMode) {
if (subrateMode < BluetoothGatt.SUBRATE_MODE_OFF
|| subrateMode > BluetoothGatt.SUBRATE_MODE_HIGH) {
throw new IllegalArgumentException("Subrate Mode not within valid range");
}
Log.d(TAG, "requestsubrateMode(" + subrateMode + ")");
if (!mClientRegistered) {
return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
}
try {
return mService.subrateModeRequest(
mBluetoothGattCallback, mDevice, subrateMode, mAttributionSource);
} catch (RemoteException e) {
logRemoteException(TAG, e);
return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
}
}
/**
* Initiates an offload of a specified list of {@link BluetoothGattCharacteristic}s to an
* endpoint.
*
* <p>This method enables a GATT Client application to delegate the handling of specific
* attribute handles associated with the provided {@code characteristics} to a endpoint after
* service discovery is complete.
*
* <p>This is an asynchronous operation. The result of the offload attempt, including the {@link
* GattOffloadSession} object on success, will be delivered via the {@link
* BluetoothGattCallback#onCharacteristicsOffloaded} callback.
*
* <p>The {@code service} parameter must already contain all {@link
* BluetoothGattCharacteristic}s specified in the {@code characteristics} list. The {@code
* characteristics} list itself should exclusively contain the {@link
* BluetoothGattCharacteristic}s intended for offloading.
*
* <p>It's important to note that {@link BluetoothGattDescriptor}s associated with the offloaded
* {@link BluetoothGattCharacteristic}s are NOT offloaded.
*
* <p>The current security state of the GATT connection should meet or exceed the permissions
* required for all {@link BluetoothGattCharacteristic}s being offloaded. If not, the host
* application will need to perform additional security steps (e.g., pairing) to fulfill these
* requirements.
*
* <p>The offload session is terminated if the GATT database is invalidated. When {@link
* BluetoothGattCallback#onCharacteristicsUnoffloaded} is invoked, the application is
* responsible for re-establishing an offload session by calling this method again if needed.
*
* @param service The {@link BluetoothGattService} that contains the characteristics to be
* offloaded. This service object should be obtained from the GATT server.
* @param characteristics A list of {@link BluetoothGattCharacteristic}s from the specified
* {@code service} that are to be offloaded.
* @param endpointId The unique identifier of the target endpoint within the hub.
* @param hubId The unique identifier of the hub to which the endpoint belongs.
* @return An integer status code indicating the immediate result of the request, such as {@link
* GattOffloadSession#STATUS_SUCCESS} if the request was initiated successfully. A non-zero
* status indicates an immediate failure to start the operation.
* @throws IllegalArgumentException if the service or characteristics are not valid.
* @throws IllegalStateException if GATT client offload is not supported.
*/
@Hide
@SystemApi
@FlaggedApi(Flags.FLAG_GATT_OFFLOAD_API)
@RequiresBluetoothConnectPermission
@RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
public @GattOffloadSession.Status int offloadCharacteristics(
@NonNull BluetoothGattService service,
@NonNull List<BluetoothGattCharacteristic> characteristics,
long endpointId,
long hubId) {
if (requireNonNull(service).getUuid() == null) {
Log.e(TAG, "GattService uuid is null");
return GattOffloadSession.STATUS_ILLEGAL_PARAMETER;
}
if (requireNonNull(characteristics).isEmpty()) {
Log.e(TAG, "GattCharacteristics are empty");
return GattOffloadSession.STATUS_ILLEGAL_PARAMETER;
}
StringBuilder builder = new StringBuilder("offloadCharacteristics{");
builder.append("serviceUuid=").append(service.getUuid());
for (BluetoothGattCharacteristic characteristic : characteristics) {
if (VDBG) {
builder.append(", CharUuid=")
.append(characteristic.getUuid())
.append(", CharInstanceId=")
.append(characteristic.getInstanceId());
}
if (characteristic.getInstanceId() == 0) {
Log.e(TAG, "Characteristic instanceId is not configured");
return GattOffloadSession.STATUS_ILLEGAL_PARAMETER;
}
if (characteristic.getService() == null) {
Log.e(TAG, "GattService is null in characteristic");
return GattOffloadSession.STATUS_ILLEGAL_PARAMETER;
}
if (!characteristic.getService().equals(service)) {
Log.e(
TAG,
"Characteristic not bound to input service: expected="
+ service.getUuid()
+ " actual="
+ characteristic.getService().getUuid());
return GattOffloadSession.STATUS_ILLEGAL_PARAMETER;
}
}
builder.append(", endpointId=").append(endpointId).append(", hubId=").append(hubId);
builder.append("}");
Log.d(TAG, builder.toString());
GattOffloadSession.InnerParcel sessionParcel;
try {
sessionParcel =
mService.offloadClientCharacteristics(
mBluetoothGattCallback,
mDevice,
service,
characteristics,
endpointId,
hubId,
mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return GattOffloadSession.STATUS_SERVICE_UNAVAILABLE;
}
@GattOffloadSession.Status int status = requireNonNull(sessionParcel).getStatus();
if (status == GattOffloadSession.STATUS_SUCCESS) {
((GattCallback) mBluetoothGattCallback)
.onCharacteristicsOffloaded(
mDevice,
new GattOffloadSession(
requireNonNull(sessionParcel).getSessionId(),
mDevice,
this,
null,
service,
characteristics,
endpointId,
hubId),
status);
}
return status;
}
/**
* Stops the offloading of characteristics associated with a given offload session.
*
* @param session The offload session to be terminated. This was returned by a previous
* successful call to {@link #offloadCharacteristics}.
* @throws IllegalArgumentException if session id is not valid
* @throws IllegalStateException if BluetoothGatt service not available.
*/
@Hide
@FlaggedApi(Flags.FLAG_GATT_OFFLOAD_API)
@RequiresBluetoothConnectPermission
@RequiresPermission(allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED})
public void unoffloadCharacteristics(@NonNull GattOffloadSession session) {
int sessionId = session.getSessionId();
Log.d(TAG, "unoffloadCharacteristics sessionId= " + sessionId);
if (sessionId == GattOffloadSession.OFFLOAD_SESSION_ID_UNKNOWN) {
throw new IllegalArgumentException("session id is not valid");
}
try {
mService.unoffloadClientCharacteristics(
mBluetoothGattCallback, mDevice, sessionId, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
}
/**
* @deprecated Not supported - please use {@link BluetoothManager#getConnectedDevices(int)} with
* {@link BluetoothProfile#GATT} as argument
* @throws UnsupportedOperationException on every call
*/
@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 on every call
*/
@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 on every call
*/
@Override
@RequiresNoPermission
@Deprecated
public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
throw new UnsupportedOperationException(
"Use BluetoothManager#getDevicesMatchingConnectionStates instead.");
}
}