| /* |
| * 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.FlaggedApi; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.RequiresNoPermission; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SuppressLint; |
| 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.Build; |
| 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 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_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 |
| // Max length of an attribute value, defined in gatt_api.h |
| private static final int GATT_MAX_ATTR_LEN = 512; |
| |
| 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; |
| |
| /** |
| * GATT connection timed out, likely due to the remote device being out of range or not |
| * advertising as connectable. |
| */ |
| @FlaggedApi(Flags.FLAG_ENUMERATE_GATT_ERRORS) |
| 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 request - Balanced. |
| * |
| * @hide |
| */ |
| public static final int SUBRATE_REQUEST_MODE_BALANCED = 0; |
| |
| /** |
| * Connection subrate request - High. |
| * |
| * @hide |
| */ |
| public static final int SUBRATE_REQUEST_MODE_HIGH = 1; |
| |
| /** |
| * Connection Subrate Request - Low Power. |
| * |
| * @hide |
| */ |
| public static final int SUBRATE_REQUEST_MODE_LOW_POWER = 2; |
| |
| /** @hide */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef( |
| prefix = {"SUBRATE_REQUEST_MODE"}, |
| value = { |
| SUBRATE_REQUEST_MODE_BALANCED, |
| SUBRATE_REQUEST_MODE_HIGH, |
| SUBRATE_REQUEST_MODE_LOW_POWER, |
| }) |
| public @interface SubrateRequestMode {} |
| |
| /** |
| * 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)); |
| } |
| mClientIf = clientIf; |
| synchronized (mStateLock) { |
| if (mConnState == CONN_STATE_CLOSED) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "Client registration completed after closed," |
| + " unregistering"); |
| } |
| unregisterApp(); |
| return; |
| } |
| if (VDBG) { |
| if (mConnState != CONN_STATE_CONNECTING) { |
| Log.e(TAG, "Bad connection state: " + mConnState); |
| } |
| } |
| } |
| 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 { |
| // autoConnect is inverse of "isDirect" |
| mService.clientConnect( |
| mClientIf, |
| mDevice.getAddress(), |
| mDevice.getAddressType(), |
| !mAutoConnect, |
| mTransport, |
| mOpportunistic, |
| mPhy, |
| mAttributionSource); |
| } 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); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * 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) { |
| Log.e(TAG, "", 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); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * 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); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Callback invoked when the given connection's subrate is changed |
| * |
| * @hide |
| */ |
| @Override |
| public void onSubrateChange( |
| String address, |
| int subrateFactor, |
| int latency, |
| int contNum, |
| int timeout, |
| int status) { |
| Log.d( |
| TAG, |
| "onSubrateChange() - " |
| + (" Device=" + address) |
| + (" subrateFactor=" + subrateFactor) |
| + (" latency=" + latency) |
| + (" contNum=" + contNum) |
| + (" 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.onSubrateChange( |
| BluetoothGatt.this, |
| subrateFactor, |
| latency, |
| contNum, |
| timeout, |
| status); |
| } |
| } |
| }); |
| } |
| }; |
| |
| /* 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; |
| } |
| |
| /** @hide */ |
| @Override |
| public void onServiceConnected(IBinder service) {} |
| |
| /** @hide */ |
| @Override |
| public void onServiceDisconnected() {} |
| |
| /** @hide */ |
| @Override |
| 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(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 eattSupport 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 eattSupport) { |
| 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, eattSupport, 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 (mService == null || mClientIf == 0) return; |
| if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf); |
| |
| 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 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 + ", 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); |
| 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 { |
| if (DBG) { |
| Log.d(TAG, "connect(void) - device: " + mDevice + ", auto=" + mAutoConnect); |
| } |
| |
| // autoConnect is inverse of "isDirect" |
| mService.clientConnect( |
| mClientIf, |
| mDevice.getAddress(), |
| mDevice.getAddressType(), |
| !mAutoConnect, |
| 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); |
| 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); |
| 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 because it relies on a {@link |
| * BluetoothGattCharacteristic} object whose underlying fields are subject to change outside |
| * this method. |
| */ |
| @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_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, |
| @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 > 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 (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) { |
| Log.e(TAG, "", e); |
| } |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "", e); |
| synchronized (mDeviceBusyLock) { |
| mDeviceBusy = false; |
| } |
| throw e.rethrowAsRuntimeException(); |
| } |
| if (Flags.gattFixDeviceBusy()) { |
| 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(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 because it relies on a {@link BluetoothGattDescriptor} object |
| * whose underlying fields are subject to change outside this method. |
| */ |
| @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; |
| } |
| 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(android.Manifest.permission.BLUETOOTH_CONNECT) |
| public boolean beginReliableWrite() { |
| if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice); |
| 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); |
| 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); |
| 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); |
| 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); |
| 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. 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(android.Manifest.permission.BLUETOOTH_CONNECT) |
| public boolean requestMtu(int mtu) { |
| if (DBG) { |
| Log.d(TAG, "configureMTU() - device: " + mDevice + " 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} {@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(android.Manifest.permission.BLUETOOTH_CONNECT) |
| public boolean requestConnectionPriority(int connectionPriority) { |
| if (connectionPriority < CONNECTION_PRIORITY_BALANCED |
| || connectionPriority > CONNECTION_PRIORITY_DCK) { |
| 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; |
| } |
| |
| /** |
| * Request LE subrate mode. |
| * |
| * <p>This function will send a LE subrate request to the remote device. |
| * |
| * @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. |
| * @hide |
| */ |
| @RequiresBluetoothConnectPermission |
| @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) |
| public boolean requestSubrateMode(@SubrateRequestMode int subrateMode) { |
| if (subrateMode < SUBRATE_REQUEST_MODE_BALANCED |
| || subrateMode > SUBRATE_REQUEST_MODE_LOW_POWER) { |
| throw new IllegalArgumentException("Subrate Mode not within valid range"); |
| } |
| |
| if (DBG) { |
| Log.d(TAG, "requestsubrateMode() - subrateMode: " + subrateMode); |
| } |
| if (mService == null || mClientIf == 0) { |
| return false; |
| } |
| |
| try { |
| mService.subrateModeRequest( |
| mClientIf, mDevice.getAddress(), subrateMode, mAttributionSource); |
| } catch (RemoteException e) { |
| Log.e(TAG, "", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Request a LE subrate request. |
| * |
| * <p>This function will send a LE subrate 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 bleSubrateRequest( |
| int subrateMin, |
| int subrateMax, |
| int maxLatency, |
| int contNumber, |
| int supervisionTimeout) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "bleSubrateRequest() - subrateMin=" |
| + subrateMin |
| + " subrateMax=" |
| + (subrateMax) |
| + " maxLatency= " |
| + maxLatency |
| + "contNumber=" |
| + contNumber |
| + " supervisionTimeout=" |
| + supervisionTimeout); |
| } |
| if (mService == null || mClientIf == 0) { |
| return false; |
| } |
| |
| try { |
| mService.leSubrateRequest( |
| mClientIf, |
| mDevice.getAddress(), |
| subrateMin, |
| subrateMax, |
| maxLatency, |
| contNumber, |
| supervisionTimeout, |
| 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 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."); |
| } |
| } |