diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
index 3c95bd61e..bdad45b77 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java
@@ -17,6 +17,7 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.btle;
+import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
@@ -44,16 +45,17 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBlePro
* Bluetooth Smart.
*
* The connection to the device and all communication is made with a generic {@link BtLEQueue}.
- * Messages to the device are encoded as {@link BtLEAction actions} that are grouped with a
- * {@link Transaction} and sent via {@link BtLEQueue}.
+ * Messages to the device are encoded as {@link BtLEAction actions} or {@link BtLEServerAction actions}
+ * that are grouped with a {@link Transaction} or {@link ServerTransaction} and sent via {@link BtLEQueue}.
*
* @see TransactionBuilder
* @see BtLEQueue
*/
-public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback {
+public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback {
private BtLEQueue mQueue;
private Map mAvailableCharacteristics;
private final Set mSupportedServices = new HashSet<>(4);
+ private final Set mSupportedServerServices = new HashSet<>(4);
private Logger logger;
private final List> mSupportedProfiles = new ArrayList<>();
@@ -70,7 +72,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
@Override
public boolean connect() {
if (mQueue == null) {
- mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, getContext());
+ mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, this, getContext(), mSupportedServerServices);
mQueue.setAutoReconnect(getAutoReconnect());
}
return mQueue.connect();
@@ -136,6 +138,19 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
return createTransactionBuilder(taskName);
}
+ public ServerTransactionBuilder createServerTransactionBuilder(String taskName) {
+ return new ServerTransactionBuilder(taskName);
+ }
+
+ public ServerTransactionBuilder performServer(String taskName) throws IOException {
+ if (!isConnected()) {
+ if(!connect()) {
+ throw new IOException("1: Unable to connect to device: " + getDevice());
+ }
+ }
+ return createServerTransactionBuilder(taskName);
+ }
+
/**
* Ensures that the device is connected and (only then) performs the actions of the given
* transaction builder.
@@ -187,6 +202,14 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
mSupportedProfiles.add(profile);
}
+ /**
+ * Subclasses should call this method to add server services they support.
+ * @param service
+ */
+ protected void addSupportedServerService(BluetoothGattService service) {
+ mSupportedServerServices.add(service);
+ }
+
/**
* Returns the characteristic matching the given UUID. Only characteristics
* are returned whose service is marked as supported.
@@ -337,4 +360,29 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
public void onSetLedColor(int color) {
}
+
+ @Override
+ public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
+
+ }
+
+ @Override
+ public boolean onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
+ return false;
+ }
+
+ @Override
+ public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ return false;
+ }
+
+ @Override
+ public boolean onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
+ return false;
+ }
+
+ @Override
+ public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ return false;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractTransaction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractTransaction.java
new file mode 100644
index 000000000..0ab0d611c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractTransaction.java
@@ -0,0 +1,46 @@
+/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Andreas Boehler
+
+ 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 java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public abstract class AbstractTransaction {
+ private final String mName;
+ private final long creationTimestamp = System.currentTimeMillis();
+
+ public AbstractTransaction(String taskName) {
+ this.mName = taskName;
+ }
+
+ public String getTaskName() {
+ return mName;
+ }
+
+ protected String getCreationTime() {
+ return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date(creationTimestamp));
+ }
+
+ public abstract int getActionCount();
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "%s: Transaction task: %s with %d actions", getCreationTime(), getTaskName(), getActionCount());
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
index 2d0d09169..e0d6941f4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
@@ -23,7 +23,10 @@ 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;
@@ -35,6 +38,7 @@ 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;
@@ -56,17 +60,22 @@ public final class BtLEQueue {
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 final BlockingQueue mTransactions = new LinkedBlockingQueue<>();
private volatile boolean mDisposed;
private volatile boolean mCrashed;
private volatile boolean mAbortTransaction;
+ private volatile boolean mAbortServerTransaction;
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 Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") {
@@ -77,7 +86,7 @@ public final class BtLEQueue {
while (!mDisposed && !mCrashed) {
try {
- Transaction transaction = mTransactions.take();
+ AbstractTransaction qTransaction = mTransactions.take();
if (!isConnected()) {
LOG.debug("not connected, waiting for connection...");
@@ -94,37 +103,70 @@ public final class BtLEQueue {
mConnectionLatch = null;
}
- 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;
- }
- 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;
- }
+ 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 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;
+ 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;
+ }
+ 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
}
- } else {
- LOG.error("Action returned false: " + action);
- break; // abort the transaction
}
}
} catch (InterruptedException ignored) {
@@ -143,11 +185,13 @@ public final class BtLEQueue {
}
};
- public BtLEQueue(BluetoothAdapter bluetoothAdapter, GBDevice gbDevice, GattCallback externalGattCallback, Context context) {
+ 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();
}
@@ -183,6 +227,21 @@ public final class BtLEQueue {
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()) {
@@ -218,6 +277,12 @@ public final class BtLEQueue {
gatt.close();
setDeviceConnectionState(State.NOT_CONNECTED);
}
+ BluetoothGattServer gattServer = mBluetoothGattServer;
+ if (gattServer != null) {
+ mBluetoothGattServer = null;
+ gattServer.clearServices();
+ gattServer.close();
+ }
}
}
@@ -226,10 +291,16 @@ public final class BtLEQueue {
internalGattCallback.reset();
mTransactions.clear();
mAbortTransaction = true;
+ mAbortServerTransaction = true;
if (mWaitForActionResultLatch != null) {
mWaitForActionResultLatch.countDown();
}
+ if (mWaitForServerActionResultLatch != null) {
+ mWaitForServerActionResultLatch.countDown();
+ }
+
boolean wasInitialized = mGbDevice.isInitialized();
+
setDeviceConnectionState(State.NOT_CONNECTED);
// either we've been disconnected because the device is out of range
@@ -289,6 +360,18 @@ public final class BtLEQueue {
}
}
+ /**
+ * 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
@@ -299,8 +382,12 @@ public final class BtLEQueue {
public void insert(Transaction transaction) {
LOG.debug("about to insert: " + transaction);
if (!transaction.isEmpty()) {
- List tail = new ArrayList<>(mTransactions.size() + 2);
- mTransactions.drainTo(tail);
+ List tail = new ArrayList<>(mTransactions.size() + 2);
+ //mTransactions.drainTo(tail);
+ for( AbstractTransaction t : mTransactions) {
+ tail.add(t);
+ }
+ mTransactions.clear();
mTransactions.add(transaction);
mTransactions.addAll(tail);
}
@@ -332,6 +419,16 @@ public final class BtLEQueue {
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 {
@@ -549,4 +646,90 @@ public final class BtLEQueue {
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());
+ if (getCallbackToUse() != null) {
+ getCallbackToUse().onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
+ }
+ }
+
+ @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());
+ if(getCallbackToUse() != null) {
+ getCallbackToUse().onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
+ }
+ }
+ }
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEServerAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEServerAction.java
new file mode 100644
index 000000000..f5aab7286
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEServerAction.java
@@ -0,0 +1,75 @@
+/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, 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.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+import android.bluetooth.BluetoothGattService;
+
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+
+/**
+ * The Bluedroid implementation only allows performing one GATT request at a time.
+ * As they are asynchronous anyway, we encapsulate every GATT request (read and write)
+ * inside a runnable action.
+ *
+ * These actions are then executed one after another, ensuring that every action's result
+ * has been posted before invoking the next action.
+ */
+public abstract class BtLEServerAction {
+ private final BluetoothDevice device;
+ private final long creationTimestamp;
+
+ public BtLEServerAction(BluetoothDevice device) {
+ this.device = device;
+ creationTimestamp = System.currentTimeMillis();
+ }
+
+
+ public BluetoothDevice getDevice() {
+ return this.device;
+ }
+
+ /**
+ * Returns true if this action expects an (async) result which must
+ * be waited for, before continuing with other actions.
+ *
+ * This is needed because the current Bluedroid stack can only deal
+ * with one single bluetooth operation at a time.
+ */
+ public abstract boolean expectsResult();
+
+ /**
+ * Executes this action, e.g. reads or write a GATT characteristic.
+ *
+ * @return true if the action was successful, false otherwise
+ */
+ public abstract boolean run(BluetoothGattServer server);
+
+
+ protected String getCreationTime() {
+ return DateTimeUtils.formatDateTime(new Date(creationTimestamp));
+ }
+
+ public String toString() {
+ return getCreationTime() + ":" + getClass().getSimpleName() + " on device: " + getDevice().getAddress();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattServerCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattServerCallback.java
new file mode 100644
index 000000000..03db77b44
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/GattServerCallback.java
@@ -0,0 +1,60 @@
+package nodomain.freeyourgadget.gadgetbridge.service.btle;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattServerCallback;
+
+public interface GattServerCallback {
+
+ /**
+ * @param device
+ * @param status
+ * @param newState
+ * @see BluetoothGattServerCallback#onConnectionStateChange(BluetoothDevice, int, int)
+ */
+ void onConnectionStateChange(BluetoothDevice device, int status, int newState);
+
+ /**
+ * @param device
+ * @param requestId
+ * @param offset
+ * @param characteristic
+ * @see BluetoothGattServerCallback#onCharacteristicReadRequest(BluetoothDevice, int, int, BluetoothGattCharacteristic)
+ */
+ boolean onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic);
+
+ /**
+ * @param device
+ * @param requestId
+ * @param characteristic
+ * @param preparedWrite
+ * @param responseNeeded
+ * @param offset
+ * @param value
+ * @see BluetoothGattServerCallback#onCharacteristicWriteRequest(BluetoothDevice, int, BluetoothGattCharacteristic, boolean, boolean, int, byte[])
+ */
+ boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value);
+
+ /**
+ * @param device
+ * @param requestId
+ * @param offset
+ * @param descriptor
+ * @see BluetoothGattServerCallback#onDescriptorReadRequest(BluetoothDevice, int, int, BluetoothGattDescriptor)
+ */
+ boolean onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor);
+
+ /**
+ * @param device
+ * @param requestId
+ * @param descriptor
+ * @param preparedWrite
+ * @param responseNeeded
+ * @param offset
+ * @param value
+ * @see BluetoothGattServerCallback#onDescriptorWriteRequest(BluetoothDevice, int, BluetoothGattDescriptor, boolean, boolean, int, byte[])
+ */
+ boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value);
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransaction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransaction.java
new file mode 100644
index 000000000..bf4cad8df
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransaction.java
@@ -0,0 +1,76 @@
+/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
+
+ 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Groups a bunch of {@link BtLEServerAction actions} together, making sure
+ * that upon failure of one action, all subsequent actions are discarded.
+ *
+ * @author TREND
+ */
+public class ServerTransaction extends AbstractTransaction {
+ private final List mActions = new ArrayList<>(4);
+ private
+ @Nullable
+ GattServerCallback gattCallback;
+
+ public ServerTransaction(String taskName) {
+ super(taskName);
+ }
+
+ public void add(BtLEServerAction action) {
+ mActions.add(action);
+ }
+
+ public List getActions() {
+ return Collections.unmodifiableList(mActions);
+ }
+
+ public boolean isEmpty() {
+ return mActions.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "%s: Transaction task: %s with %d actions", getCreationTime(), getTaskName(), mActions.size());
+ }
+
+ public void setGattCallback(@Nullable GattServerCallback callback) {
+ gattCallback = callback;
+ }
+
+ /**
+ * Returns the GattServerCallback for this transaction, or null if none.
+ */
+ public
+ @Nullable
+ GattServerCallback getGattCallback() {
+ return gattCallback;
+ }
+
+ @Override
+ public int getActionCount() {
+ return mActions.size();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransactionBuilder.java
new file mode 100644
index 000000000..5a8e7693d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransactionBuilder.java
@@ -0,0 +1,87 @@
+/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer
+
+ 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.BluetoothDevice;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import androidx.annotation.Nullable;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ServerResponseAction;
+
+public class ServerTransactionBuilder {
+ private static final Logger LOG = LoggerFactory.getLogger(ServerTransactionBuilder.class);
+
+ private final ServerTransaction mTransaction;
+ private boolean mQueued;
+
+ public ServerTransactionBuilder(String taskName) {
+ mTransaction = new ServerTransaction(taskName);
+ }
+
+ public ServerTransactionBuilder writeServerResponse(BluetoothDevice device, int requestId, int status, int offset, byte[] data) {
+ if(device == null) {
+ LOG.warn("Unable to write to device: null");
+ return this;
+ }
+ ServerResponseAction action = new ServerResponseAction(device, requestId, status, offset, data);
+ return add(action);
+ }
+
+ public ServerTransactionBuilder add(BtLEServerAction action) {
+ mTransaction.add(action);
+ return this;
+ }
+
+ /**
+ * Sets a GattServerCallback instance that will be called when the transaction is executed,
+ * resulting in GattServerCallback events.
+ *
+ * @param callback the callback to set, may be null
+ */
+ public void setGattCallback(@Nullable GattServerCallback callback) {
+ mTransaction.setGattCallback(callback);
+ }
+
+ public
+ @Nullable
+ GattServerCallback getGattCallback() {
+ return mTransaction.getGattCallback();
+ }
+
+ /**
+ * To be used as the final step to execute the transaction by the given queue.
+ *
+ * @param queue
+ */
+ public void queue(BtLEQueue queue) {
+ if (mQueued) {
+ throw new IllegalStateException("This builder had already been queued. You must not reuse it.");
+ }
+ mQueued = true;
+ queue.add(mTransaction);
+ }
+
+ public ServerTransaction getTransaction() {
+ return mTransaction;
+ }
+
+ public String getTaskName() {
+ return mTransaction.getTaskName();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/Transaction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/Transaction.java
index ba91feca7..eef19df95 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/Transaction.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/Transaction.java
@@ -17,12 +17,9 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.btle;
-import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
-import java.util.Locale;
import androidx.annotation.Nullable;
@@ -32,20 +29,14 @@ import androidx.annotation.Nullable;
*
* @author TREND
*/
-public class Transaction {
- private final String mName;
+public class Transaction extends AbstractTransaction {
private final List mActions = new ArrayList<>(4);
- private final long creationTimestamp = System.currentTimeMillis();
private
@Nullable
GattCallback gattCallback;
public Transaction(String taskName) {
- this.mName = taskName;
- }
-
- public String getTaskName() {
- return mName;
+ super(taskName);
}
public void add(BtLEAction action) {
@@ -60,15 +51,6 @@ public class Transaction {
return mActions.isEmpty();
}
- protected String getCreationTime() {
- return DateFormat.getTimeInstance(DateFormat.MEDIUM).format(new Date(creationTimestamp));
- }
-
- @Override
- public String toString() {
- return String.format(Locale.US, "%s: Transaction task: %s with %d actions", getCreationTime(), getTaskName(), mActions.size());
- }
-
public void setGattCallback(@Nullable GattCallback callback) {
gattCallback = callback;
}
@@ -81,4 +63,9 @@ public class Transaction {
GattCallback getGattCallback() {
return gattCallback;
}
+
+ @Override
+ public int getActionCount() {
+ return mActions.size();
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ServerResponseAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ServerResponseAction.java
new file mode 100644
index 000000000..817c25f32
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/ServerResponseAction.java
@@ -0,0 +1,72 @@
+/* Copyright (C) 2015-2018 Andreas Shimokawa, Carsten Pfeiffer, Daniele
+ Gobbetti, 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.actions;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattServer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.Logging;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEServerAction;
+
+/**
+ * Invokes a response on a given GATT characteristic read.
+ * The result status will be made available asynchronously through the
+ * {@link BluetoothGattCallback}
+ */
+public class ServerResponseAction extends BtLEServerAction {
+ private static final Logger LOG = LoggerFactory.getLogger(ServerResponseAction.class);
+
+ private final byte[] value;
+ private final int requestId;
+ private final int status;
+ private final int offset;
+
+ public ServerResponseAction(BluetoothDevice device, int requestId, int status, int offset, byte[] data) {
+ super(device);
+ this.value = data;
+ this.requestId = requestId;
+ this.status = status;
+ this.offset = offset;
+ }
+
+ @Override
+ public boolean run(BluetoothGattServer server) {
+ return writeValue(server, getDevice(), requestId, status, offset, value);
+ }
+
+ protected boolean writeValue(BluetoothGattServer gattServer, BluetoothDevice device, int requestId, int status, int offset, byte[] value) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("writing to server: " + device.getAddress() + ": " + Logging.formatBytes(value));
+ }
+
+ return gattServer.sendResponse(device, requestId, 0, offset, value);
+ }
+
+ protected final byte[] getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean expectsResult() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTServer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTServer.java
deleted file mode 100644
index 3820ab398..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTServer.java
+++ /dev/null
@@ -1,228 +0,0 @@
-/* Copyright (C) 2018-2019 Andreas Böhler, Daniele Gobbetti
- based on code from BlueWatcher, https://github.com/masterjc/bluewatcher
-
- 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.devices.casiogb6900;
-
-import android.bluetooth.BluetoothDevice;
-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.content.Context;
-import android.content.Intent;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
-import nodomain.freeyourgadget.gadgetbridge.devices.casiogb6900.CasioGB6900Constants;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
-import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
-import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
-
-class CasioGATTServer extends BluetoothGattServerCallback {
- private static final Logger LOG = LoggerFactory.getLogger(CasioGATTServer.class);
-
- private Context mContext;
- private BluetoothGattServer mBluetoothGattServer;
- private CasioGB6900DeviceSupport mDeviceSupport = null;
- private final GBDeviceEventMusicControl musicCmd = new GBDeviceEventMusicControl();
-
- CasioGATTServer(Context context, CasioGB6900DeviceSupport deviceSupport) {
- mContext = context;
- mDeviceSupport = deviceSupport;
- }
-
- public void setContext(Context ctx) {
- mContext = ctx;
- }
-
- boolean initialize() {
- if(mContext == null) {
- return false;
- }
-
- BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
- if (bluetoothManager == null) {
- return false;
- }
- mBluetoothGattServer = bluetoothManager.openGattServer(mContext, this);
- if (mBluetoothGattServer == null) {
- return false;
- }
-
- BluetoothGattService casioGATTService = new BluetoothGattService(CasioGB6900Constants.WATCH_CTRL_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
- BluetoothGattCharacteristic bluetoothgGATTCharacteristic = new BluetoothGattCharacteristic(CasioGB6900Constants.KEY_CONTAINER_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE);
- bluetoothgGATTCharacteristic.setValue(new byte[0]);
-
- BluetoothGattCharacteristic bluetoothgGATTCharacteristic2 = new BluetoothGattCharacteristic(CasioGB6900Constants.NAME_OF_APP_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
- bluetoothgGATTCharacteristic2.setValue(CasioGB6900Constants.MUSIC_MESSAGE.getBytes());
-
- BluetoothGattDescriptor bluetoothGattDescriptor = new BluetoothGattDescriptor(CasioGB6900Constants.CCC_DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_READ | BluetoothGattDescriptor.PERMISSION_WRITE);
- bluetoothGattDescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
-
- bluetoothgGATTCharacteristic2.addDescriptor(bluetoothGattDescriptor);
-
- casioGATTService.addCharacteristic(bluetoothgGATTCharacteristic);
- casioGATTService.addCharacteristic(bluetoothgGATTCharacteristic2);
- mBluetoothGattServer.addService(casioGATTService);
-
- return true;
- }
-
- @Override
- public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
-
- if (!characteristic.getUuid().equals(CasioGB6900Constants.NAME_OF_APP_CHARACTERISTIC_UUID)) {
- LOG.warn("unexpected read request");
- return;
- }
-
- LOG.info("will send response to read request from device: " + device.getAddress());
-
- if (!this.mBluetoothGattServer.sendResponse(device, requestId, 0, offset, CasioGB6900Constants.MUSIC_MESSAGE.getBytes())) {
- LOG.warn("error sending response");
- }
- }
- private GBDeviceEventMusicControl.Event parse3Button(int button) {
- GBDeviceEventMusicControl.Event event;
- switch(button) {
- case 3:
- event = GBDeviceEventMusicControl.Event.NEXT;
- break;
- case 2:
- event = GBDeviceEventMusicControl.Event.PREVIOUS;
- break;
- case 1:
- event = GBDeviceEventMusicControl.Event.PLAYPAUSE;
- break;
- default:
- LOG.warn("Unhandled button received: " + button);
- event = GBDeviceEventMusicControl.Event.UNKNOWN;
- }
- return event;
- }
-
- private GBDeviceEventMusicControl.Event parse2Button(int button) {
- GBDeviceEventMusicControl.Event event;
- switch(button) {
- case 2:
- event = GBDeviceEventMusicControl.Event.PLAYPAUSE;
- break;
- case 1:
- event = GBDeviceEventMusicControl.Event.NEXT;
- break;
- default:
- LOG.warn("Unhandled button received: " + button);
- event = GBDeviceEventMusicControl.Event.UNKNOWN;
- }
- return event;
- }
-
- @Override
- public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic,
- boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
-
- if (!characteristic.getUuid().equals(CasioGB6900Constants.KEY_CONTAINER_CHARACTERISTIC_UUID)) {
- LOG.warn("unexpected write request");
- return;
- }
-
- if(mDeviceSupport == null) {
- LOG.warn("mDeviceSupport is null, did initialization complete?");
- return;
- }
-
- if((value[0] & 0x03) == 0) {
- int button = value[1] & 0x0f;
- LOG.info("Button pressed: " + button);
- switch(mDeviceSupport.getModel())
- {
- case MODEL_CASIO_5600B:
- musicCmd.event = parse2Button(button);
- break;
- case MODEL_CASIO_6900B:
- musicCmd.event = parse3Button(button);
- break;
- case MODEL_CASIO_GENERIC:
- musicCmd.event = parse3Button(button);
- break;
- default:
- LOG.warn("Unhandled device");
- return;
- }
- mDeviceSupport.evaluateGBDeviceEvent(musicCmd);
- mDeviceSupport.evaluateGBDeviceEvent(musicCmd);
- }
- else {
- LOG.info("received from device: " + value.toString());
- }
- }
-
- @Override
- public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
-
- LOG.info("Connection state change for device: " + device.getAddress() + " status = " + status + " newState = " + newState);
- if (newState == BluetoothGattServer.STATE_DISCONNECTED) {
- LOG.info("CASIO GATT server noticed disconnect.");
- }
- if (newState == BluetoothGattServer.STATE_CONNECTED) {
- GBDevice.State devState = mDeviceSupport.getDevice().getState();
- Intent deviceCommunicationServiceIntent = new Intent(mContext, DeviceCommunicationService.class);
- if (devState.equals(GBDevice.State.WAITING_FOR_RECONNECT) || devState.equals(GBDevice.State.NOT_CONNECTED)) {
- LOG.info("Forcing re-connect because GATT server has been reconnected.");
- deviceCommunicationServiceIntent.setAction(DeviceService.ACTION_CONNECT);
- deviceCommunicationServiceIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
- LocalBroadcastManager.getInstance(mContext).sendBroadcast(deviceCommunicationServiceIntent);
- //PendingIntent reconnectPendingIntent = PendingIntent.getService(mContext, 2, deviceCommunicationServiceIntent, PendingIntent.FLAG_UPDATE_CURRENT);
- //builder.addAction(R.drawable.ic_notification, context.getString(R.string.controlcenter_connect), reconnectPendingIntent);
- }
- }
- }
-
- @Override
- public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor,
- boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
-
- LOG.info("onDescriptorWriteRequest() notifications enabled = " + (value[0] == 1));
- if (!this.mBluetoothGattServer.sendResponse(device, requestId, 0, offset, value)) {
- LOG.warn("onDescriptorWriteRequest() error sending response!");
- }
- }
-
- @Override
- public void onServiceAdded(int status, BluetoothGattService service) {
- LOG.info("onServiceAdded() status = " + status + " service = " + service.getUuid());
- }
-
- @Override
- public void onNotificationSent(BluetoothDevice bluetoothDevice, int status) {
- LOG.info("onNotificationSent() status = " + status + " to device " + bluetoothDevice.getAddress());
- }
-
- void close() {
- if (mBluetoothGattServer != null) {
- mBluetoothGattServer.clearServices();
- mBluetoothGattServer.close();
- mBluetoothGattServer = null;
- }
- }
-
-}