| /* |
| * 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 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.Binder; |
| 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; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| |
| /** |
| * 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); |
| |
| @UnsupportedAppUsage private final IBluetoothGatt mService; |
| @UnsupportedAppUsage private volatile BluetoothGattCallback mCallback; |
| private Handler mHandler; |
| private final BluetoothDevice mDevice; |
| @UnsupportedAppUsage private boolean mAutoConnect; |
| private boolean mClientRegistered; |
| |
| @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 final int mTransport; |
| |
| private final int mPhy; |
| private final 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 final CopyOnWriteArrayList<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. |
| */ |
| 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. */ |
| 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) { |
| if (mHandler != null) { |
| executeFromBinder(mHandler::post, cb); |
| return; |
| } |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| cb.run(); |
| } catch (Exception ex) { |
| Log.w(TAG, "Unhandled exception in callback", ex); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| } |
| |
| /** |
| * Application interface registered - app is ready to go |
| * |
| * @hide |
| */ |
| @Override |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| @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(); |
| if (Flags.unregisterGattClientDisconnected()) { |
| 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" |
| mService.clientConnect( |
| mBluetoothGattCallback, |
| mDevice, |
| mDevice.getAddressType(), |
| !mAutoConnect, |
| mTransport, |
| mOpportunistic, |
| mPhy, |
| 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 (Flags.unregisterGattClientDisconnected() && !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); |
| } |
| |
| if (Flags.fixBluetoothGattGettingDuplicateServices()) { |
| 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 |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| @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 |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| @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 |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| @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 |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| @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 subrateFactor, |
| int latency, |
| int contNum, |
| int timeout, |
| int status) { |
| Log.d( |
| TAG, |
| "onSubrateChange() - " |
| + (" device=" + device) |
| + (" subrateFactor=" + subrateFactor) |
| + (" latency=" + latency) |
| + (" contNum=" + contNum) |
| + (" timeout=" + timeout) |
| + (" status=" + status)); |
| |
| if (!mDevice.equals(device)) { |
| return; |
| } |
| |
| runOrQueueCallback( |
| () -> { |
| 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 CopyOnWriteArrayList<>(); |
| |
| mConnState = CONN_STATE_IDLE; |
| mAuthRetryState = AUTH_RETRY_STATE_IDLE; |
| } |
| |
| /** @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(); |
| if (Flags.unregisterGattClientDisconnected()) { |
| mCallback = null; |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Register an application callback to start using GATT. |
| * |
| * <p>This is an asynchronous call. If registration is successful, client connection will be |
| * initiated. |
| * |
| * @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(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. If registration is successful, client connection will be |
| * initiated. |
| * |
| * @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(BLUETOOTH_CONNECT) |
| private boolean registerApp( |
| BluetoothGattCallback callback, Handler handler, boolean eattSupport) { |
| Log.d(TAG, "registerApp()"); |
| if (mService == null) return false; |
| |
| mCallback = callback; |
| mHandler = handler; |
| UUID uuid = UUID.randomUUID(); |
| 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(BLUETOOTH_CONNECT) |
| private void unregisterApp() { |
| if (mService == null || !mClientRegistered) return; |
| Log.d(TAG, "unregisterApp()"); |
| |
| try { |
| if (!Flags.unregisterGattClientDisconnected()) { |
| mCallback = null; |
| } |
| mService.unregisterClient(mBluetoothGattCallback, mAttributionSource); |
| mClientRegistered = false; |
| } 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(BLUETOOTH_CONNECT) |
| /*package*/ boolean connect( |
| Boolean autoConnect, BluetoothGattCallback callback, Handler handler) { |
| 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(BLUETOOTH_CONNECT) |
| public void disconnect() { |
| Log.d(TAG, "cancelOpen() - device: " + mDevice); |
| if (mService == null || !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 (mService == null) return false; |
| if (!mClientRegistered) { |
| if (!Flags.unregisterGattClientDisconnected()) { |
| return false; |
| } |
| 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, |
| 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" |
| mService.clientConnect( |
| mBluetoothGattCallback, |
| mDevice, |
| 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(BLUETOOTH_CONNECT) |
| public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) { |
| if (mService == null || !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 (mService == null || !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 (mService == null || !mClientRegistered) return false; |
| |
| if (!Flags.fixBluetoothGattGettingDuplicateServices()) { |
| mServices.clear(); |
| } |
| |
| 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 (mService == null || !mClientRegistered) return false; |
| |
| if (!Flags.fixBluetoothGattGettingDuplicateServices()) { |
| mServices.clear(); |
| } |
| |
| 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 (mService == null || !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(BLUETOOTH_CONNECT) |
| public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) { |
| if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid); |
| if (mService == null || !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 > 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 || !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 (mService == null || !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 (mService == null || !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 (mService == null || !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 (mService == null || !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 (mService == null || !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 (mService == null || !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 (mService == null || !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 (mService == null || !mClientRegistered) return false; |
| |
| try { |
| mService.readRemoteRssi(mBluetoothGattCallback, mDevice, 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(BLUETOOTH_CONNECT) |
| public boolean requestMtu(int mtu) { |
| Log.d(TAG, "configureMTU() - device: " + mDevice + " mtu: " + mtu); |
| if (mService == null || !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 (mService == null || !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 (mService == null || !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; |
| } |
| |
| /** |
| * Request LE subrate mode. |
| * |
| * <p>This function will send a LE subrate request to the remote device. |
| * |
| * <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. |
| * @hide |
| */ |
| @RequiresBluetoothConnectPermission |
| @RequiresPermission( |
| allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED}, |
| conditional = true) |
| public int 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"); |
| } |
| |
| Log.d(TAG, "requestsubrateMode(" + subrateMode + ")"); |
| if (mService == null || !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; |
| } |
| } |
| |
| /** |
| * @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."); |
| } |
| } |