/* Copyright (C) 2015-2021 Andreas Böhler, Andreas Shimokawa, Carsten Pfeiffer, Cre3per, Daniel Dakhno, Daniele Gobbetti, Sergey Trofimov, Taavi Eomäe, Uwe Hermann 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.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. */ 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 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 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("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.toString()); 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.getMessage(), 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 setImplicitGattCallbackModify(final boolean enable) { mImplicitGattCallbackModify = enable; } public void setSendWriteRequestResponse(final boolean enable) { mSendWriteRequestResponse = enable; } protected boolean isConnected() { return mGbDevice.isConnected(); } /** * 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(State newState) { 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) { 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. * * @param 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(); } private boolean checkCorrectGattInstance(BluetoothGatt gatt, String where) { if (gatt != mBluetoothGatt && mBluetoothGatt != null) { LOG.info("Ignoring event from wrong BluetoothGatt instance: " + where + "; " + gatt); return false; } return true; } private boolean checkCorrectBluetoothDevice(BluetoothDevice device) { //BluetoothDevice clientDevice = mBluetoothAdapter.getRemoteDevice(mGbDevice.getAddress()); if(!device.getAddress().equals(mGbDevice.getAddress())) { // != clientDevice && clientDevice != null) { LOG.info("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.size() > 0) { 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); if(getCallbackToUse() != null){ getCallbackToUse().onMtuChanged(gatt, mtu, status); } mWaitForActionResultLatch.countDown(); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { LOG.debug("characteristic read: " + characteristic.getUuid() + getStatusString(status)); 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: " + ex.getMessage(), 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: " + ex.getMessage(), 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: " + characteristic.getUuid() + " value: " + content); } if (!checkCorrectGattInstance(gatt, "characteristic changed")) { return; } if (getCallbackToUse() != null) { try { getCallbackToUse().onCharacteristicChanged(gatt, characteristic); } catch (Throwable ex) { LOG.error("onCharaceristicChanged: " + ex.getMessage(), ex); } } else { LOG.info("No gattcallback 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: " + ex.getMessage(), 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("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("characterstic read request: " + device.getAddress() + " characteristic: " + 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: " + device.getAddress() + " characteristic: " + 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]); } } } }