/* Copyright (C) 2015-2024 Andreas Böhler, Andreas Shimokawa, Carsten Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Gordon Williams, José Rebelo, Sergey Trofimov, Taavi Eomäe, Uwe Hermann, Yoran Vulker This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.btle; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattServer; import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; /** * One queue/thread per connectable device. */ @SuppressLint("MissingPermission") // if we're using this, we have bluetooth permissions public final class BtLEQueue { private static final Logger LOG = LoggerFactory.getLogger(BtLEQueue.class); private final Object mGattMonitor = new Object(); private final GBDevice mGbDevice; private final BluetoothAdapter mBluetoothAdapter; private BluetoothGatt mBluetoothGatt; private BluetoothGattServer mBluetoothGattServer; private final Set mSupportedServerServices; private final BlockingQueue mTransactions = new LinkedBlockingQueue<>(); private volatile boolean mDisposed; private volatile boolean mCrashed; private volatile boolean mAbortTransaction; private volatile boolean mAbortServerTransaction; private volatile boolean mPauseTransaction = false; private final Context mContext; private CountDownLatch mWaitForActionResultLatch; private CountDownLatch mWaitForServerActionResultLatch; private CountDownLatch mConnectionLatch; private BluetoothGattCharacteristic mWaitCharacteristic; private final InternalGattCallback internalGattCallback; private final InternalGattServerCallback internalGattServerCallback; private boolean mAutoReconnect; private boolean scanReconnect; private boolean mImplicitGattCallbackModify = true; private boolean mSendWriteRequestResponse = false; private Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") { @Override public void run() { LOG.debug("Queue Dispatch Thread started."); while (!mDisposed && !mCrashed) { try { AbstractTransaction qTransaction = mTransactions.take(); if (!isConnected()) { LOG.debug("not connected, waiting for connection..."); // TODO: request connection and initialization from the outside and wait until finished internalGattCallback.reset(); // wait until the connection succeeds before running the actions // Note that no automatic connection is performed. This has to be triggered // on the outside typically by the DeviceSupport. The reason is that // devices have different kinds of initializations and this class has no // idea about them. mConnectionLatch = new CountDownLatch(1); mConnectionLatch.await(); mConnectionLatch = null; } if(qTransaction instanceof ServerTransaction) { ServerTransaction serverTransaction = (ServerTransaction)qTransaction; internalGattServerCallback.setTransactionGattCallback(serverTransaction.getGattCallback()); mAbortServerTransaction = false; for (BtLEServerAction action : serverTransaction.getActions()) { if (mAbortServerTransaction) { // got disconnected LOG.info("Aborting running server transaction"); break; } if (LOG.isDebugEnabled()) { LOG.debug("About to run server action: " + action); } if (action.run(mBluetoothGattServer)) { // check again, maybe due to some condition, action did not need to write, so we can't wait boolean waitForResult = action.expectsResult(); if (waitForResult) { mWaitForServerActionResultLatch.await(); mWaitForServerActionResultLatch = null; if (mAbortServerTransaction) { break; } } } else { LOG.error("Server action returned false: {}", action); break; // abort the transaction } } } if(qTransaction instanceof Transaction) { Transaction transaction = (Transaction)qTransaction; LOG.trace("Changing gatt callback for {}? {}", transaction.getTaskName(), transaction.isModifyGattCallback()); if (mImplicitGattCallbackModify || transaction.isModifyGattCallback()) { internalGattCallback.setTransactionGattCallback(transaction.getGattCallback()); } mAbortTransaction = false; // Run all actions of the transaction until one doesn't succeed for (BtLEAction action : transaction.getActions()) { if (mAbortTransaction) { // got disconnected LOG.info("Aborting running transaction"); break; } while ((action instanceof WriteAction) && mPauseTransaction && !mAbortTransaction) { LOG.info("Pausing WriteAction"); try { Thread.sleep(100); } catch (Exception e) { LOG.info("Exception during pause: {}", e); break; } } mWaitCharacteristic = action.getCharacteristic(); mWaitForActionResultLatch = new CountDownLatch(1); if (LOG.isDebugEnabled()) { LOG.debug("About to run action: " + action); } if (action instanceof GattListenerAction) { // this special action overwrites the transaction gatt listener (if any), it must // always be the last action in the transaction internalGattCallback.setTransactionGattCallback(((GattListenerAction) action).getGattCallback()); } if (action.run(mBluetoothGatt)) { // check again, maybe due to some condition, action did not need to write, so we can't wait boolean waitForResult = action.expectsResult(); if (waitForResult) { mWaitForActionResultLatch.await(); mWaitForActionResultLatch = null; if (mAbortTransaction) { break; } } } else { LOG.error("Action returned false: {}", action); break; // abort the transaction } } } } catch (InterruptedException ignored) { mConnectionLatch = null; LOG.debug("Thread interrupted"); } catch (Throwable ex) { LOG.error("Queue Dispatch Thread died", ex); mCrashed = true; mConnectionLatch = null; } finally { mWaitForActionResultLatch = null; mWaitCharacteristic = null; } } LOG.info("Queue Dispatch Thread terminated."); } }; public BtLEQueue(BluetoothAdapter bluetoothAdapter, GBDevice gbDevice, GattCallback externalGattCallback, GattServerCallback externalGattServerCallback, Context context, Set supportedServerServices) { mBluetoothAdapter = bluetoothAdapter; mGbDevice = gbDevice; internalGattCallback = new InternalGattCallback(externalGattCallback); internalGattServerCallback = new InternalGattServerCallback(externalGattServerCallback); mContext = context; mSupportedServerServices = supportedServerServices; dispatchThread.start(); } public void setAutoReconnect(boolean enable) { mAutoReconnect = enable; } public void setScanReconnect(boolean enable){ this.scanReconnect = enable; } public void setImplicitGattCallbackModify(final boolean enable) { mImplicitGattCallbackModify = enable; } public void setSendWriteRequestResponse(final boolean enable) { mSendWriteRequestResponse = enable; } private boolean isConnected() { if (mGbDevice.isConnected()) { return true; } LOG.debug("isConnected(): current state = {}", mGbDevice.getState()); return false; } /** * Connects to the given remote device. Note that this does not perform any device * specific initialization. This should be done in the specific {@link DeviceSupport} * class. * * @return true whether the connection attempt was successfully triggered and false if that failed or if there is already a connection */ public boolean connect() { mPauseTransaction = false; if (isConnected()) { LOG.warn("Ingoring connect() because already connected."); return false; } synchronized (mGattMonitor) { if (mBluetoothGatt != null) { // Tribal knowledge says you're better off not reusing existing BluetoothGatt connections, // so create a new one. LOG.info("connect() requested -- disconnecting previous connection: " + mGbDevice.getName()); disconnect(); } } LOG.info("Attempting to connect to {}", mGbDevice.getName()); mBluetoothAdapter.cancelDiscovery(); BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); if(!mSupportedServerServices.isEmpty()) { BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { LOG.error("Error getting bluetoothManager"); return false; } mBluetoothGattServer = bluetoothManager.openGattServer(mContext, internalGattServerCallback); if (mBluetoothGattServer == null) { LOG.error("Error opening Gatt Server"); return false; } for(BluetoothGattService service : mSupportedServerServices) { mBluetoothGattServer.addService(service); } } synchronized (mGattMonitor) { // connectGatt with true doesn't really work ;( too often connection problems if (GBApplication.isRunningMarshmallowOrLater()) { mBluetoothGatt = remoteDevice.connectGatt(mContext, false, internalGattCallback, BluetoothDevice.TRANSPORT_LE); } else { mBluetoothGatt = remoteDevice.connectGatt(mContext, false, internalGattCallback); } } boolean result = mBluetoothGatt != null; if (result) { setDeviceConnectionState(State.CONNECTING); } return result; } private void setDeviceConnectionState(final State newState) { new Handler(Looper.getMainLooper()).post(() -> { LOG.debug("new device connection state: {}", newState); mGbDevice.setState(newState); mGbDevice.sendDeviceUpdateIntent(mContext, GBDevice.DeviceUpdateSubject.CONNECTION_STATE); }); } public void disconnect() { synchronized (mGattMonitor) { LOG.debug("disconnect()"); BluetoothGatt gatt = mBluetoothGatt; if (gatt != null) { mBluetoothGatt = null; LOG.info("Disconnecting BtLEQueue from GATT device"); gatt.disconnect(); gatt.close(); setDeviceConnectionState(State.NOT_CONNECTED); } mPauseTransaction = false; BluetoothGattServer gattServer = mBluetoothGattServer; if (gattServer != null) { mBluetoothGattServer = null; gattServer.clearServices(); gattServer.close(); } } } private void handleDisconnected(int status) { LOG.debug("handleDisconnected: {}", status); internalGattCallback.reset(); mTransactions.clear(); mPauseTransaction = false; mAbortTransaction = true; mAbortServerTransaction = true; if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } if (mWaitForServerActionResultLatch != null) { mWaitForServerActionResultLatch.countDown(); } setDeviceConnectionState(State.NOT_CONNECTED); // either we've been disconnected because the device is out of range // or because of an explicit @{link #disconnect()) // To support automatic reconnection, we keep the mBluetoothGatt instance // alive (we do not close() it). Unfortunately we sometimes have problems // reconnecting automatically, so we try to fix this by re-creating mBluetoothGatt. // Not sure if this actually works without re-initializing the device... if (mBluetoothGatt != null) { if (!maybeReconnect()) { disconnect(); // ensure that we start over cleanly next time } } } /** * Depending on certain criteria, connects to the BluetoothGatt. * * @return true if a reconnection attempt was made, or false otherwise */ private boolean maybeReconnect() { if (mAutoReconnect && mBluetoothGatt != null) { if(scanReconnect){ LOG.info("Waiting for BLE scan before attempting reconnection..."); setDeviceConnectionState(State.WAITING_FOR_SCAN); return true; } LOG.info("Enabling automatic ble reconnect..."); boolean result = mBluetoothGatt.connect(); mPauseTransaction = false; if (result) { setDeviceConnectionState(State.WAITING_FOR_RECONNECT); } return result; } return false; } public void setPaused(boolean paused) { mPauseTransaction = paused; } public void dispose() { if (mDisposed) { return; } mDisposed = true; // try { disconnect(); dispatchThread.interrupt(); dispatchThread = null; // dispatchThread.join(); // } catch (InterruptedException ex) { // LOG.error("Exception while disposing BtLEQueue", ex); // } } /** * Adds a transaction to the end of the queue. * * @param transaction */ public void add(Transaction transaction) { LOG.debug("about to add: {}", transaction); if (!transaction.isEmpty()) { mTransactions.add(transaction); } } /** * Aborts the currently running transaction */ public void abortCurrentTransaction() { mAbortTransaction = true; if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } } /** * Adds a serverTransaction to the end of the queue * * @param transaction */ public void add(ServerTransaction transaction) { LOG.debug("about to add: {}", transaction); if(!transaction.isEmpty()) { mTransactions.add(transaction); } } /** * Adds a transaction to the beginning of the queue. * Note that actions of the *currently executing* transaction * will still be executed before the given transaction. */ public void insert(Transaction transaction) { LOG.debug("about to insert: {}", transaction); if (!transaction.isEmpty()) { List tail = new ArrayList<>(mTransactions.size() + 2); //mTransactions.drainTo(tail); tail.addAll(mTransactions); mTransactions.clear(); mTransactions.add(transaction); mTransactions.addAll(tail); } } public void clear() { mTransactions.clear(); } /** * Retrieves a list of supported GATT services on the connected device. This should be * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. * * @return A {@code List} of supported services. */ public List getSupportedGattServices() { if (mBluetoothGatt == null) { LOG.warn("BluetoothGatt is null => no services available."); return Collections.emptyList(); } return mBluetoothGatt.getServices(); } /** @noinspection BooleanMethodIsAlwaysInverted*/ private boolean checkCorrectGattInstance(BluetoothGatt gatt, String where) { if (gatt != mBluetoothGatt && mBluetoothGatt != null) { LOG.warn("Ignoring event from wrong BluetoothGatt instance: {}; {}", where, gatt); return false; } return true; } /** @noinspection BooleanMethodIsAlwaysInverted*/ private boolean checkCorrectBluetoothDevice(BluetoothDevice device) { //BluetoothDevice clientDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); if(!device.getAddress().equals(mGbDevice.getAddress())) { // != clientDevice && clientDevice != null) { LOG.warn("Ignoring request from wrong Bluetooth device: {}", device.getAddress()); return false; } return true; } // Implements callback methods for GATT events that the app cares about. For example, // connection change and services discovered. private final class InternalGattCallback extends BluetoothGattCallback { private @Nullable GattCallback mTransactionGattCallback; private final GattCallback mExternalGattCallback; public InternalGattCallback(GattCallback externalGattCallback) { mExternalGattCallback = externalGattCallback; } public void setTransactionGattCallback(@Nullable GattCallback callback) { mTransactionGattCallback = callback; } private GattCallback getCallbackToUse() { if (mTransactionGattCallback != null) { return mTransactionGattCallback; } return mExternalGattCallback; } @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { LOG.debug("connection state change, newState: {}{}", newState, getStatusString(status)); synchronized (mGattMonitor) { if (mBluetoothGatt == null) { mBluetoothGatt = gatt; } } if (!checkCorrectGattInstance(gatt, "connection state event")) { return; } if (status != BluetoothGatt.GATT_SUCCESS) { LOG.warn("connection state event with error status {}", status); } switch (newState) { case BluetoothProfile.STATE_CONNECTED: LOG.info("Connected to GATT server."); setDeviceConnectionState(State.CONNECTED); // Attempts to discover services after successful connection. List cachedServices = gatt.getServices(); if (cachedServices != null && !cachedServices.isEmpty()) { LOG.info("Using cached services, skipping discovery"); onServicesDiscovered(gatt, BluetoothGatt.GATT_SUCCESS); } else { LOG.info("Attempting to start service discovery"); // discover services in the main thread (appears to fix Samsung connection problems) new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { if (mBluetoothGatt != null) { mBluetoothGatt.discoverServices(); } } }); } break; case BluetoothProfile.STATE_DISCONNECTED: LOG.info("Disconnected from GATT server."); handleDisconnected(status); break; case BluetoothProfile.STATE_CONNECTING: LOG.info("Connecting to GATT server..."); setDeviceConnectionState(State.CONNECTING); break; } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (!checkCorrectGattInstance(gatt, "services discovered: " + getStatusString(status))) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { if (getCallbackToUse() != null) { // only propagate the successful event getCallbackToUse().onServicesDiscovered(gatt); } if (mConnectionLatch != null) { mConnectionLatch.countDown(); } } else { LOG.warn("onServicesDiscovered received: {}", status); } } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { LOG.debug("characteristic write: {}{}", characteristic.getUuid(), getStatusString(status)); if (!checkCorrectGattInstance(gatt, "characteristic write")) { return; } if (getCallbackToUse() != null) { getCallbackToUse().onCharacteristicWrite(gatt, characteristic, status); } checkWaitingCharacteristic(characteristic, status); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { super.onMtuChanged(gatt, mtu, status); LOG.debug("mtu changed to {}{}", mtu, getStatusString(status)); if(getCallbackToUse() != null){ getCallbackToUse().onMtuChanged(gatt, mtu, status); } if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { LOG.debug( "characteristic read: {}{}{}", characteristic.getUuid(), getStatusString(status), status == BluetoothGatt.GATT_SUCCESS ? ": " + Logging.formatBytes(characteristic.getValue()) : "" ); if (!checkCorrectGattInstance(gatt, "characteristic read")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onCharacteristicRead(gatt, characteristic, status); } catch (Throwable ex) { LOG.error("onCharacteristicRead: {}", ex.getMessage(), ex); } } checkWaitingCharacteristic(characteristic, status); } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { LOG.debug("descriptor read: {}{}", descriptor.getUuid(), getStatusString(status)); if (!checkCorrectGattInstance(gatt, "descriptor read")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onDescriptorRead(gatt, descriptor, status); } catch (Throwable ex) { LOG.error("onDescriptorRead failed", ex); } } checkWaitingCharacteristic(descriptor.getCharacteristic(), status); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { LOG.debug("descriptor write: {}{}", descriptor.getUuid(), getStatusString(status)); if (!checkCorrectGattInstance(gatt, "descriptor write")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onDescriptorWrite(gatt, descriptor, status); } catch (Throwable ex) { LOG.error("onDescriptorWrite failed", ex); } } checkWaitingCharacteristic(descriptor.getCharacteristic(), status); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (LOG.isDebugEnabled()) { String content = Logging.formatBytes(characteristic.getValue()); LOG.debug("characteristic changed: {} value: {}", characteristic.getUuid(), content); } if (!checkCorrectGattInstance(gatt, "characteristic changed")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onCharacteristicChanged(gatt, characteristic); } catch (Throwable ex) { LOG.error("onCharacteristicChanged failed", ex); } } else { LOG.info("No gatt callback registered, ignoring characteristic change"); } } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { LOG.debug("remote rssi: {}{}", rssi, getStatusString(status)); if (!checkCorrectGattInstance(gatt, "remote rssi")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onReadRemoteRssi(gatt, rssi, status); } catch (Throwable ex) { LOG.error("onReadRemoteRssi failed", ex); } } } private void checkWaitingCharacteristic(BluetoothGattCharacteristic characteristic, int status) { if (status != BluetoothGatt.GATT_SUCCESS) { if (characteristic != null) { LOG.debug("failed btle action, aborting transaction: {}{}", characteristic.getUuid(), getStatusString(status)); } mAbortTransaction = true; } if (characteristic != null && BtLEQueue.this.mWaitCharacteristic != null && characteristic.getUuid().equals(BtLEQueue.this.mWaitCharacteristic.getUuid())) { if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } } else { if (BtLEQueue.this.mWaitCharacteristic != null) { LOG.error( "checkWaitingCharacteristic: mismatched characteristic received: {}", (characteristic != null && characteristic.getUuid() != null) ? characteristic.getUuid().toString() : "(null)" ); } } } private String getStatusString(int status) { return status == BluetoothGatt.GATT_SUCCESS ? " (success)" : " (failed: " + status + ")"; } public void reset() { if (LOG.isDebugEnabled()) { LOG.debug("internal gatt callback set to null"); } mTransactionGattCallback = null; } } // Implements callback methods for GATT server events that the app cares about. For example, // connection change and read/write requests. private final class InternalGattServerCallback extends BluetoothGattServerCallback { private @Nullable GattServerCallback mTransactionGattCallback; private final GattServerCallback mExternalGattServerCallback; public InternalGattServerCallback(GattServerCallback externalGattServerCallback) { mExternalGattServerCallback = externalGattServerCallback; } public void setTransactionGattCallback(@Nullable GattServerCallback callback) { mTransactionGattCallback = callback; } private GattServerCallback getCallbackToUse() { if (mTransactionGattCallback != null) { return mTransactionGattCallback; } return mExternalGattServerCallback; } @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { LOG.debug("gatt server connection state change, newState: {}{}", newState, getStatusString(status)); if(!checkCorrectBluetoothDevice(device)) { return; } if (status != BluetoothGatt.GATT_SUCCESS) { LOG.warn("gatt server connection state event with error status {}", status); } } private String getStatusString(int status) { return status == BluetoothGatt.GATT_SUCCESS ? " (success)" : " (failed: " + status + ")"; } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { if(!checkCorrectBluetoothDevice(device)) { return; } LOG.debug("characteristic read request: {} characteristic: {}", device.getAddress(), characteristic.getUuid()); if (getCallbackToUse() != null) { getCallbackToUse().onCharacteristicReadRequest(device, requestId, offset, characteristic); } } @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if(!checkCorrectBluetoothDevice(device)) { return; } LOG.debug("characteristic write request: {} characteristic: {}", device.getAddress(), characteristic.getUuid()); boolean success = false; if (getCallbackToUse() != null) { success = getCallbackToUse().onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); } if (responseNeeded && mSendWriteRequestResponse) { mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, new byte[0]); } } @Override public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { if(!checkCorrectBluetoothDevice(device)) { return; } LOG.debug("onDescriptorReadRequest: {}", device.getAddress()); if(getCallbackToUse() != null) { getCallbackToUse().onDescriptorReadRequest(device, requestId, offset, descriptor); } } @Override public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { if(!checkCorrectBluetoothDevice(device)) { return; } LOG.debug("onDescriptorWriteRequest: {}", device.getAddress()); boolean success = false; if(getCallbackToUse() != null) { success = getCallbackToUse().onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); } if (responseNeeded && mSendWriteRequestResponse) { mBluetoothGattServer.sendResponse(device, requestId, success ? BluetoothGatt.GATT_SUCCESS : BluetoothGatt.GATT_FAILURE, 0, new byte[0]); } } } }