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; - } - } - -}