/*
 * 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)
                                        + (" connected=" + connected)
                                        + (" 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. 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(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. 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(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.");
    }
}
