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/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 2d0d09169..28b9e16ab 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,9 +38,10 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.BlockingQueue; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingQueue; import androidx.annotation.Nullable; import nodomain.freeyourgadget.gadgetbridge.GBApplication; @@ -53,20 +57,27 @@ public final class BtLEQueue { private static final Logger LOG = LoggerFactory.getLogger(BtLEQueue.class); private final Object mGattMonitor = new Object(); + private final Object mTransactionMonitor = 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 final Queue mTransactions = new ConcurrentLinkedQueue<>(); + private final Queue mServerTransactions = new ConcurrentLinkedQueue<>(); 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 +88,16 @@ public final class BtLEQueue { while (!mDisposed && !mCrashed) { try { - Transaction transaction = mTransactions.take(); + LOG.info("waiting..."); + synchronized (mTransactionMonitor) { + try { + mTransactionMonitor.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + Transaction transaction = mTransactions.poll(); + ServerTransaction serverTransaction = mServerTransactions.poll(); if (!isConnected()) { LOG.debug("not connected, waiting for connection..."); @@ -94,37 +114,68 @@ 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(serverTransaction != null) { + 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(transaction != 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; + } + } + } 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 +194,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 +236,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 +286,12 @@ public final class BtLEQueue { gatt.close(); setDeviceConnectionState(State.NOT_CONNECTED); } + BluetoothGattServer gattServer = mBluetoothGattServer; + if (gattServer != null) { + mBluetoothGattServer = null; + gattServer.clearServices(); + gattServer.close(); + } } } @@ -226,9 +300,16 @@ public final class BtLEQueue { internalGattCallback.reset(); mTransactions.clear(); mAbortTransaction = true; + mAbortServerTransaction = true; if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } + if (mWaitForServerActionResultLatch != null) { + mWaitForServerActionResultLatch.countDown(); + } + synchronized(mTransactionMonitor) { + mTransactionMonitor.notify(); + } boolean wasInitialized = mGbDevice.isInitialized(); setDeviceConnectionState(State.NOT_CONNECTED); @@ -286,6 +367,24 @@ public final class BtLEQueue { LOG.debug("about to add: " + transaction); if (!transaction.isEmpty()) { mTransactions.add(transaction); + synchronized(mTransactionMonitor) { + mTransactionMonitor.notify(); + } + } + } + + /** + * 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()) { + mServerTransactions.add(transaction); + synchronized(mTransactionMonitor) { + mTransactionMonitor.notify(); + } } } @@ -300,14 +399,25 @@ public final class BtLEQueue { LOG.debug("about to insert: " + transaction); if (!transaction.isEmpty()) { List tail = new ArrayList<>(mTransactions.size() + 2); - mTransactions.drainTo(tail); + //mTransactions.drainTo(tail); + for( Transaction t : mTransactions) { + tail.add(t); + } + mTransactions.clear(); mTransactions.add(transaction); mTransactions.addAll(tail); + synchronized(mTransactionMonitor) { + mTransactionMonitor.notify(); + } } } public void clear() { mTransactions.clear(); + mServerTransactions.clear(); + synchronized(mTransactionMonitor) { + mTransactionMonitor.notify(); + } } /** @@ -332,6 +442,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 +669,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..701aec798 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/ServerTransaction.java @@ -0,0 +1,83 @@ +/* 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.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; + +/** + * 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 { + private final String mName; + private final List mActions = new ArrayList<>(4); + private final long creationTimestamp = System.currentTimeMillis(); + private + @Nullable + GattServerCallback gattCallback; + + public ServerTransaction(String taskName) { + this.mName = taskName; + } + + public String getTaskName() { + return mName; + } + + public void add(BtLEServerAction action) { + mActions.add(action); + } + + public List getActions() { + return Collections.unmodifiableList(mActions); + } + + public boolean isEmpty() { + 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 GattServerCallback callback) { + gattCallback = callback; + } + + /** + * Returns the GattServerCallback for this transaction, or null if none. + */ + public + @Nullable + GattServerCallback getGattCallback() { + return gattCallback; + } +} 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/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; + } +}