diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 05fa9114fe..18264b5ed8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -338,6 +338,13 @@ + + + + + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothScanCallbackReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothScanCallbackReceiver.java new file mode 100644 index 0000000000..c0b3be9219 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothScanCallbackReceiver.java @@ -0,0 +1,80 @@ +/* Copyright (C) 2019 Andreas Böhler + + 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.externalevents; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanResult; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; + +public class BluetoothScanCallbackReceiver extends BroadcastReceiver { + + private static final Logger LOG = LoggerFactory.getLogger(BluetoothScanCallbackReceiver.class); + private String mSeenScanCallbackUUID = ""; + + @TargetApi(Build.VERSION_CODES.O) + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if(!action.equals("nodomain.freeyourgadget.gadgetbridge.blescancallback") || !intent.hasExtra("address") || !intent.hasExtra("uuid")) { + return; + } + + String wantedAddress = intent.getExtras().getString("address"); + String uuid = intent.getExtras().getString("uuid"); + + int bleCallbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1); + if(bleCallbackType != -1) { + //LOG.debug("Passive background scan callback type: " + bleCallbackType); + ArrayList scanResults = intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); + for(ScanResult result: scanResults) { + BluetoothDevice device = result.getDevice(); + if(device.getAddress().equals(wantedAddress) && !mSeenScanCallbackUUID.equals(uuid)) { + mSeenScanCallbackUUID = uuid; + LOG.info("ScanCallbackReceiver has found " + device.getAddress() + "(" + device.getName() + ")"); + BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner().stopScan(getScanCallbackIntent(GBApplication.getContext(), wantedAddress, uuid)); + GBApplication.deviceService().connect(DeviceHelper.getInstance().toSupportedDevice(device)); + + } + } + } + } + + @TargetApi(Build.VERSION_CODES.O) + public static PendingIntent getScanCallbackIntent(Context context, String address, String uuid) { + Intent intent = new Intent(context, BluetoothScanCallbackReceiver.class); + intent.setAction("nodomain.freeyourgadget.gadgetbridge.blescancallback"); + intent.putExtra("address", address); + intent.putExtra("uuid", uuid); + return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } +} 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 3c95bd61e7..2d8bf6998c 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; @@ -33,27 +34,30 @@ import java.util.Set; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.CheckInitializedAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile; +import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; /** * Abstract base class for all devices connected through Bluetooth Low Energy (LE) aka * 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,8 +74,16 @@ public AbstractBTLEDeviceSupport(Logger logger) { @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()); + GBPrefs prefs = GBApplication.getGBPrefs(); + boolean autoReconnectScan = GBPrefs.AUTO_RECONNECT_SCAN_DEFAULT; + if (prefs != null) { + autoReconnectScan = prefs.getAutoReconnectScan(); + } + // Override the user preference if required by the device + autoReconnectScan = autoReconnectScan || useBleScannerForReconnect(); + mQueue.setBleScannerForReconnect(autoReconnectScan); } return mQueue.connect(); } @@ -136,6 +148,19 @@ public TransactionBuilder performInitialized(String taskName) throws IOException 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 +212,14 @@ protected void addSupportedProfile(AbstractBleProfile profile) { 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 +370,33 @@ public void onSetFmFrequency(float frequency) { 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; + } + + public boolean useBleScannerForReconnect() { + 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 0000000000..0ab0d611cd --- /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 2d0d091693..d135ab6576 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 @@ -17,15 +17,26 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.btle; +import android.annotation.TargetApi; +import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattServer; +import android.bluetooth.BluetoothGattServerCallback; import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; import android.content.Context; +import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -35,6 +46,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; @@ -42,6 +55,7 @@ import androidx.annotation.Nullable; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; +import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothScanCallbackReceiver; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; @@ -56,18 +70,45 @@ 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 Handler mHandler = new Handler(); private final Context mContext; private CountDownLatch mWaitForActionResultLatch; + private CountDownLatch mWaitForServerActionResultLatch; private CountDownLatch mConnectionLatch; private BluetoothGattCharacteristic mWaitCharacteristic; private final InternalGattCallback internalGattCallback; - private boolean mAutoReconnect; + private final InternalGattServerCallback internalGattServerCallback; + private boolean mAutoReconnect = false; + + private BluetoothLeScanner mBluetoothScanner; + private boolean mUseBleScannerForReconnect = false; + private PendingIntent mScanCallbackIntent = null; + + private Runnable mRestartRunnable = new Runnable() { + @Override + public void run() { + LOG.info("Restarting background scan due to Android N limitations..."); + startBleBackgroundScan(); + } + }; + + private Runnable mReduceBleScanIntervalRunnable = new Runnable() { + @Override + public void run() { + LOG.info("Restarting BLE background scan with lower priority..."); + startBleBackgroundScan(false); + } + }; private Thread dispatchThread = new Thread("Gadgetbridge GATT Dispatcher") { @@ -77,7 +118,7 @@ public void run() { while (!mDisposed && !mCrashed) { try { - Transaction transaction = mTransactions.take(); + AbstractTransaction qTransaction = mTransactions.take(); if (!isConnected()) { LOG.debug("not connected, waiting for connection..."); @@ -94,37 +135,70 @@ public void run() { 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(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 (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 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 +217,13 @@ public void run() { } }; - 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(); } @@ -156,6 +232,10 @@ public void setAutoReconnect(boolean enable) { mAutoReconnect = enable; } + public void setBleScannerForReconnect(boolean enable) { + mUseBleScannerForReconnect = enable; + } + protected boolean isConnected() { return mGbDevice.isConnected(); } @@ -183,6 +263,21 @@ public boolean connect() { 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()) { @@ -210,6 +305,24 @@ private void setDeviceConnectionState(State newState) { public void disconnect() { synchronized (mGattMonitor) { LOG.debug("disconnect()"); + + BluetoothGattServer gattServer = mBluetoothGattServer; + if (gattServer != null) { + mBluetoothGattServer = null; + BluetoothManager bluetoothManager = (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE); + if (bluetoothManager == null) { + LOG.error("Error getting bluetoothManager"); + } else { + List devices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER); + for(BluetoothDevice device : devices) { + LOG.debug("Disconnecting device: " + device.getAddress()); + gattServer.cancelConnection(device); + } + } + gattServer.clearServices(); + gattServer.close(); + } + BluetoothGatt gatt = mBluetoothGatt; if (gatt != null) { mBluetoothGatt = null; @@ -226,10 +339,14 @@ private void handleDisconnected(int status) { internalGattCallback.reset(); mTransactions.clear(); mAbortTransaction = true; + mAbortServerTransaction = true; if (mWaitForActionResultLatch != null) { mWaitForActionResultLatch.countDown(); } - boolean wasInitialized = mGbDevice.isInitialized(); + if (mWaitForServerActionResultLatch != null) { + mWaitForServerActionResultLatch.countDown(); + } + setDeviceConnectionState(State.NOT_CONNECTED); // either we've been disconnected because the device is out of range @@ -239,7 +356,7 @@ private void handleDisconnected(int status) { // reconnecting automatically, so we try to fix this by re-creating mBluetoothGatt. // Not sure if this actually works without re-initializing the device... if (mBluetoothGatt != null) { - if (!wasInitialized || !maybeReconnect()) { + if (!maybeReconnect()) { disconnect(); // ensure that we start over cleanly next time } } @@ -252,16 +369,122 @@ private void handleDisconnected(int status) { */ private boolean maybeReconnect() { if (mAutoReconnect && mBluetoothGatt != null) { - LOG.info("Enabling automatic ble reconnect..."); - boolean result = mBluetoothGatt.connect(); - if (result) { - setDeviceConnectionState(State.WAITING_FOR_RECONNECT); + if(!mUseBleScannerForReconnect) { + LOG.info("Enabling automatic ble reconnect..."); + boolean result = mBluetoothGatt.connect(); + if (result) { + setDeviceConnectionState(State.WAITING_FOR_RECONNECT); + } + return result; + } else { + if(GBApplication.isRunningLollipopOrLater()) { + LOG.info("Enabling BLE background scan"); + disconnect(); // ensure that we start over cleanly next time + startBleBackgroundScan(); + setDeviceConnectionState(State.WAITING_FOR_RECONNECT); + return true; + } } - return result; } return false; } + @TargetApi(Build.VERSION_CODES.O) + PendingIntent getScanCallbackIntent(boolean newUuid) { + if(newUuid || mScanCallbackIntent == null) { + String uuid = UUID.randomUUID().toString(); + mScanCallbackIntent = BluetoothScanCallbackReceiver.getScanCallbackIntent(mContext, mGbDevice.getAddress(), uuid); + } + return mScanCallbackIntent; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void stopBleBackgroundScan() { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mHandler.removeCallbacks(mReduceBleScanIntervalRunnable); + if(mBluetoothScanner != null) { + mBluetoothScanner.stopScan(getScanCallbackIntent(false)); + } + } else { + mHandler.removeCallbacks(mRestartRunnable); + if(mBluetoothScanner != null) { + mBluetoothScanner.stopScan(mScanCallback); + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void startBleBackgroundScan() { + startBleBackgroundScan(true); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void startBleBackgroundScan(boolean highPowerMode) { + if(mBluetoothScanner == null) + mBluetoothScanner = mBluetoothAdapter.getBluetoothLeScanner(); + + ScanSettings settings; + if(highPowerMode) { + settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(); + } else { + settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_BALANCED) + .build(); + } + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + LOG.info("Using Android O+ BLE scanner"); + List filters = Collections.singletonList(new ScanFilter.Builder().build()); + mBluetoothScanner.stopScan(getScanCallbackIntent(false)); + mBluetoothScanner.startScan(filters, settings, getScanCallbackIntent(true)); + // If high power mode is requested, we scan for 5 minutes + // and then continue scanning with lower priority (scan mode balanced) in order + // to conserve power. + if(highPowerMode) { + mHandler.postDelayed(mReduceBleScanIntervalRunnable, 5 * 60 * 1000); + } + } + else { + LOG.info("Using Android L-N BLE scanner"); + List filters = Collections.singletonList(new ScanFilter.Builder().setDeviceAddress(mGbDevice.getAddress()).build()); + mBluetoothScanner.stopScan(mScanCallback); + mBluetoothScanner.startScan(filters, settings, mScanCallback); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mHandler.postDelayed(mRestartRunnable, 25 * 60 * 1000); + } + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private ScanCallback mScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + String deviceName = result.getDevice().getName(); + String deviceAddress = result.getDevice().getAddress(); + + LOG.info("Scanner: Found: " + deviceName + " " + deviceAddress); + // The filter already filtered for our specific device, so it is enough to connect to it + mBluetoothScanner.stopScan(mScanCallback); + mHandler.removeCallbacks(mRestartRunnable); + connect(); + setDeviceConnectionState(State.CONNECTING); + } + + @Override + public void onBatchScanResults(List results) { + for (ScanResult sr : results) { + LOG.info("ScanCallback.onBatchScanResults.each:" + sr.toString()); + } + } + + @Override + public void onScanFailed(int errorCode) { + LOG.error("ScanCallback.onScanFailed:" + errorCode); + } + }; + public void dispose() { if (mDisposed) { return; @@ -269,6 +492,9 @@ public void dispose() { mDisposed = true; // try { disconnect(); + if(mUseBleScannerForReconnect) { + stopBleBackgroundScan(); + } dispatchThread.interrupt(); dispatchThread = null; // dispatchThread.join(); @@ -289,6 +515,18 @@ public void add(Transaction transaction) { } } + /** + * 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 +537,12 @@ public void add(Transaction transaction) { public void insert(Transaction transaction) { LOG.debug("about to insert: " + transaction); if (!transaction.isEmpty()) { - List tail = new ArrayList<>(mTransactions.size() + 2); - mTransactions.drainTo(tail); + 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 +574,16 @@ private boolean checkCorrectGattInstance(BluetoothGatt gatt, String where) { 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 +801,90 @@ public void reset() { 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 0000000000..f5aab72862 --- /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 0000000000..03db77b449 --- /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 0000000000..bf4cad8df3 --- /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 0000000000..5a8e7693d3 --- /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 ba91feca72..eef19df950 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 @@ * * @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 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 GattCallback callback) { gattCallback = callback; } @@ -81,4 +63,9 @@ public void setGattCallback(@Nullable GattCallback callback) { 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 0000000000..817c25f325 --- /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 3820ab398f..0000000000 --- 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; - } - } - -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTThread.java deleted file mode 100644 index 97f3c44010..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGATTThread.java +++ /dev/null @@ -1,72 +0,0 @@ -/* Copyright (C) 2018-2019 Andreas Böhler - 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.content.Context; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CasioGATTThread extends Thread { - CasioGATTServer mServer = null; - private static final Logger LOG = LoggerFactory.getLogger(CasioGATTThread.class); - private boolean mStopFlag = false; - private final Object waitObject = new Object(); - - public CasioGATTThread(Context context, CasioGB6900DeviceSupport deviceSupport) - { - mServer = new CasioGATTServer(context, deviceSupport); - } - - public void setContext(Context ctx) { - mServer.setContext(ctx); - } - - @Override - public void run() { - if (!mServer.initialize()) { - LOG.error("Error initializing CasioGATTServer. Has the context been set?"); - return; - } - - long waitTime = 60 * 1000; - - while (!mStopFlag) { - synchronized (waitObject) { - try { - waitObject.wait(waitTime); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - if (mStopFlag) { - break; - } - } - mServer.close(); - } - - public void quit() { - mStopFlag = true; - synchronized (waitObject) { - waitObject.notify(); - } - } - - -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGB6900DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGB6900DeviceSupport.java index bed8ad0362..18606646e5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGB6900DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioGB6900DeviceSupport.java @@ -18,11 +18,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.Context; import android.net.Uri; +import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +38,7 @@ import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.devices.casiogb6900.CasioGB6900Constants; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; @@ -48,22 +52,24 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.ServerTransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class CasioGB6900DeviceSupport extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(CasioGB6900DeviceSupport.class); private ArrayList mCasioCharacteristics = new ArrayList(); - private CasioGATTThread mThread; private CasioHandlerThread mHandlerThread = null; private MusicSpec mBufferMusicSpec = null; private MusicStateSpec mBufferMusicStateSpec = null; private BluetoothGatt mBtGatt = null; private CasioGB6900Constants.Model mModel = CasioGB6900Constants.Model.MODEL_CASIO_GENERIC; - private byte[] mBleSettings = null; + private boolean mFirstConnect = false; - private static final int mCasioSleepTime = 80; + private static final int mCasioSleepTime = 50; public CasioGB6900DeviceSupport() { super(LOG); @@ -79,14 +85,30 @@ public CasioGB6900DeviceSupport() { addSupportedService(CasioGB6900Constants.TX_POWER_SERVICE_UUID); addSupportedService(CasioGB6900Constants.LINK_LOSS_SERVICE); addSupportedService(CasioGB6900Constants.IMMEDIATE_ALERT_SERVICE_UUID); - mThread = new CasioGATTThread(getContext(), this); + + BluetoothGattService casioGATTService = new BluetoothGattService(CasioGB6900Constants.WATCH_CTRL_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY); + BluetoothGattCharacteristic bluetoothGATTCharacteristic = new BluetoothGattCharacteristic(CasioGB6900Constants.KEY_CONTAINER_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PERMISSION_WRITE); + bluetoothGATTCharacteristic.setValue(new byte[0]); + + BluetoothGattCharacteristic bluetoothGATTCharacteristic2 = new BluetoothGattCharacteristic(CasioGB6900Constants.NAME_OF_APP_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED); + bluetoothGATTCharacteristic2.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); + + bluetoothGATTCharacteristic2.addDescriptor(bluetoothGattDescriptor); + + casioGATTService.addCharacteristic(bluetoothGATTCharacteristic); + casioGATTService.addCharacteristic(bluetoothGATTCharacteristic2); + + addSupportedServerService(casioGATTService); } @Override - public void setContext(GBDevice gbDevice, BluetoothAdapter btAdapter, Context context) { - super.setContext(gbDevice, btAdapter, context); - mThread.setContext(context); - mThread.start(); + public boolean connectFirstTime() { + GB.toast(getContext(), "After first connect, disable and enable bluetooth on your Casio watch to really connect", Toast.LENGTH_SHORT, GB.INFO); + mFirstConnect = true; + return super.connect(); } @Override @@ -103,12 +125,6 @@ private void close() { mHandlerThread.interrupt(); mHandlerThread = null; } - - if(mThread != null) { - mThread.quit(); - mThread.interrupt(); - mThread = null; - } } @Override @@ -121,6 +137,14 @@ public void onServicesDiscovered(BluetoothGatt gatt) { protected TransactionBuilder initializeDevice(TransactionBuilder builder) { LOG.info("Initializing"); + if(mFirstConnect) { + gbDevice.setState(GBDevice.State.INITIALIZED); + gbDevice.sendDeviceUpdateIntent(getContext()); + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + return builder; + } + String name = gbDevice.getName(); if(name.contains("5600B")) { @@ -131,17 +155,27 @@ protected TransactionBuilder initializeDevice(TransactionBuilder builder) { mModel = CasioGB6900Constants.Model.MODEL_CASIO_GENERIC; } + try { + new InitOperation(this, builder).perform(); + } catch (IOException e) { + GB.toast(getContext(), "Initializing Casio watch failed", Toast.LENGTH_SHORT, GB.ERROR, e); + } + /* gbDevice.setState(GBDevice.State.INITIALIZING); gbDevice.sendDeviceUpdateIntent(getContext()); + */ - addCharacteristics(); + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); - builder.setGattCallback(this); - enableNotifications(builder, true); + builder.setGattCallback(this); configureWatch(builder); + addCharacteristics(); + enableNotifications(builder, true); + LOG.info("Initialization Done"); return builder; @@ -251,52 +285,22 @@ private void writeCasioVirtualServerFeature(TransactionBuilder builder) { } } - private void readBleSettings() { - try { - TransactionBuilder builder = performInitialized("readBleSettings"); - builder.read(getCharacteristic(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID)); - builder.queue(getQueue()); - } catch(IOException e) { - LOG.error("Error reading BLE settings: " + e.getMessage()); - } - } - - private void configureBleSettings() { - // These values seem to improve connection stability _on my phone_ - // Maybe they should be configurable? - int slaveLatency = 2; - int connInterval = 300; - - mBleSettings[5] = (byte)(connInterval & 0xff); - mBleSettings[6] = (byte)((connInterval >> 8) & 0xff); - mBleSettings[7] = (byte)(slaveLatency & 0xff); - mBleSettings[8] = (byte)((slaveLatency >> 8) & 0xff); - - mBleSettings[9] = 0; // Setting for Disconnect!? - } - - private void writeBleSettings() { - try { - TransactionBuilder builder = performInitialized("writeBleSettings"); - builder.write(getCharacteristic(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID), mBleSettings); - builder.queue(getQueue()); - } catch(IOException e) { - LOG.error("Error writing BLE settings: " + e.getMessage()); - } - } private boolean handleInitResponse(byte data) { boolean handled = false; switch(data) { case (byte) 1: LOG.info("Initialization done, setting state to INITIALIZED"); - if(mHandlerThread == null) { - mHandlerThread = new CasioHandlerThread(getDevice(), getContext(), this); + if(mHandlerThread != null) { + if(mHandlerThread.isAlive()) { + mHandlerThread.quit(); + mHandlerThread.interrupt(); + } } + mHandlerThread = new CasioHandlerThread(getDevice(), getContext(), this); mHandlerThread.start(); gbDevice.setState(GBDevice.State.INITIALIZED); gbDevice.sendDeviceUpdateIntent(getContext()); - readBleSettings(); handled = true; break; default: @@ -351,7 +355,7 @@ private boolean handleServerFeatureRequests(byte data) { return true; } - private boolean handleCasioCom(byte[] data) { + private boolean handleCasioCom(byte[] data, boolean handleTime) { boolean handled = false; if(data.length < 3) { @@ -365,7 +369,11 @@ private boolean handleCasioCom(byte[] data) { handled = handleInitResponse(data[2]); break; case 2: - handled = handleTimeRequests(data[2]); + if(handleTime) { + handled = handleTimeRequests(data[2]); + } else { + handled = true; + } break; case 7: handled = handleServerFeatureRequests(data[2]); @@ -391,63 +399,6 @@ public boolean onCharacteristicRead(BluetoothGatt gatt, } LOG.info(str); } - else if(characteristicUUID.equals(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID)) { - mBleSettings = data; - String str = "Read Casio Setting for BLE: "; - for(int i=0; i> 6) & 0x03; - //LOG.info("Call Alert: " + callAlert); - //int mailAlert = (data[0] >> 2) & 0x03; - //LOG.info("Mail Alert: " + mailAlert); - //int snsAlert = (data[2] >> 4) & 0x03; - //LOG.info("SNS Alert: " + snsAlert); - //int calAlert = (data[1] >> 6) & 0x03; - //LOG.info("Calendart Alert: " + calAlert); - //int otherAlert = (data[0] & 0x03); - //LOG.info("Other Alert: " + otherAlert); - //int vibrationValue = (data[3] & 0x0f); - //LOG.info("Vibration Value: " + vibrationValue); - //int alarmValue = (data[3] >> 4) & 0x0f; - // Vibration pattern; A = 0, B = 1, C = 2 - //LOG.info("Alarm Value: " + alarmValue); - //int animationValue = data[4] & 0x40; - // Length of Alarm, only 2, 5 and 10 possible - //LOG.info("Animation Value: " + animationValue); - // 0 = on - // 64 = off - //int useDisableMtuReqBit = data[4] & 0x08; - // 8 = on - // 0 = off!? - //LOG.info("useDisableMtuReqBit: " + useDisableMtuReqBit); - - //int slaveLatency = ((data[7] & 0xff) | ((data[8] & 0xff) << 8)); - //int connInterval = ((data[5] & 0xff) | ((data[6] & 0xff) << 8)); - //LOG.info("Slave Latency: " + slaveLatency); - //LOG.info("Connection Interval: " + connInterval); - //LOG.info(str); - - configureBleSettings(); - writeBleSettings(); - } else { return super.onCharacteristicRead(gatt, characteristic, status); } @@ -466,11 +417,11 @@ public boolean onCharacteristicChanged(BluetoothGatt gatt, return true; if(characteristicUUID.equals(CasioGB6900Constants.CASIO_A_NOT_W_REQ_NOT)) { - handled = handleCasioCom(data); + handled = handleCasioCom(data, true); } if(characteristicUUID.equals(CasioGB6900Constants.CASIO_A_NOT_COM_SET_NOT)) { - handled = handleCasioCom(data); + handled = handleCasioCom(data, false); } if(characteristicUUID.equals(CasioGB6900Constants.ALERT_LEVEL_CHARACTERISTIC_UUID)) { @@ -501,6 +452,8 @@ public boolean onCharacteristicChanged(BluetoothGatt gatt, } private void showNotification(byte icon, String title, String message) { + if(!isConnected()) + return; try { TransactionBuilder builder = performInitialized("showNotification"); int len; @@ -558,6 +511,10 @@ public void onDeleteNotification(int id) { public void onSetAlarms(ArrayList alarms) { int alarmOffset = 4; byte[] data = new byte[20]; + + if(!isConnected()) + return; + for(int i=0; i alarms) { @Override public void onSetTime() { + if(!isConnected()) + return; + try { TransactionBuilder builder = performInitialized("SetTime"); writeCasioLocalTimeInformation(builder); @@ -622,6 +582,9 @@ public void onSetMusicState(MusicStateSpec stateSpec) { private void sendMusicInfo() { + if(!isConnected()) + return; + try { TransactionBuilder builder = performInitialized("sendMusicInfo"); String info = ""; @@ -716,6 +679,9 @@ public void onEnableRealtimeHeartRateMeasurement(boolean enable) { @Override public void onFindDevice(boolean start) { + if(!isConnected()) + return; + if(start) { try { TransactionBuilder builder = performInitialized("findDevice"); @@ -776,4 +742,100 @@ public void onTestNewFunction() { public void onSendWeather(WeatherSpec weatherSpec) { } + + @Override + public boolean 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 false; + } + + LOG.info("will send response to read request from device: " + device.getAddress()); + + try { + ServerTransactionBuilder builder = performServer("sendNameOfApp"); + builder.writeServerResponse(device, requestId, 0, offset, CasioGB6900Constants.MUSIC_MESSAGE.getBytes()); + builder.queue(getQueue()); + } catch (IOException e) { + LOG.warn("sendMusicInfo failed: " + e.getMessage()); + } + return true; + } + + 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 boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, + boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { + + GBDeviceEventMusicControl musicCmd = new GBDeviceEventMusicControl(); + if (!characteristic.getUuid().equals(CasioGB6900Constants.KEY_CONTAINER_CHARACTERISTIC_UUID)) { + LOG.warn("unexpected write request"); + return false; + } + + if((value[0] & 0x03) == 0) { + int button = value[1] & 0x0f; + LOG.info("Button pressed: " + button); + switch(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 false; + } + evaluateGBDeviceEvent(musicCmd); + } + else { + LOG.info("received from device: " + value.toString()); + } + return true; + } + + @Override + public boolean useBleScannerForReconnect() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioHandlerThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioHandlerThread.java index d049802c4a..63243602cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioHandlerThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/CasioHandlerThread.java @@ -33,7 +33,6 @@ public class CasioHandlerThread extends GBDeviceIoThread { private boolean mQuit = false; private CasioGB6900DeviceSupport mDeviceSupport; private final Object waitObject = new Object(); - //private CasioGATTServer mServer = null; private int TX_PERIOD = 60; @@ -43,7 +42,6 @@ public CasioHandlerThread(GBDevice gbDevice, Context context, CasioGB6900DeviceS super(gbDevice, context); LOG.info("Initializing Casio Handler Thread"); mQuit = false; - //mServer = new CasioGATTServer(context, deviceSupport); mDeviceSupport = deviceSupport; } @@ -51,13 +49,6 @@ public CasioHandlerThread(GBDevice gbDevice, Context context, CasioGB6900DeviceS public void run() { mQuit = false; - /* - if(!mServer.initialize()) { - LOG.error("Error initializing CasioGATTServer. Has the context been set?"); - return; - } - */ - long waitTime = TX_PERIOD * 1000; while (!mQuit) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/operations/InitOperation.java new file mode 100644 index 0000000000..211f3d26e8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/casiogb6900/operations/InitOperation.java @@ -0,0 +1,182 @@ +/* Copyright (C) 2016-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.devices.casiogb6900.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; + +import nodomain.freeyourgadget.gadgetbridge.devices.casiogb6900.CasioGB6900Constants; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900.CasioGB6900DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class InitOperation extends AbstractBTLEOperation { + private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); + + private final TransactionBuilder builder; + private byte[] mBleSettings = null; + + + public InitOperation(CasioGB6900DeviceSupport support, TransactionBuilder builder) { + super(support); + this.builder = builder; + builder.setGattCallback(this); + } + + @Override + protected void doPerform() throws IOException { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + TransactionBuilder builder = getSupport().createTransactionBuilder("readBleSettings"); + builder.setGattCallback(this); + builder.read(getCharacteristic(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID)); + getSupport().performImmediately(builder); + } + + @Override + public TransactionBuilder performInitialized(String taskName) throws IOException { + throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method"); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return super.onCharacteristicChanged(gatt, characteristic); + } + + private void configureBleSettings() { + // These values seem to improve connection stability _on my phone_ + // Maybe they should be configurable? + int slaveLatency = 2; + int connInterval = 300; + + mBleSettings[5] = (byte)(connInterval & 0xff); + mBleSettings[6] = (byte)((connInterval >> 8) & 0xff); + mBleSettings[7] = (byte)(slaveLatency & 0xff); + mBleSettings[8] = (byte)((slaveLatency >> 8) & 0xff); + + mBleSettings[9] = 0; // Setting for Disconnect!? + } + + private void writeBleSettings() { + try { + TransactionBuilder builder = getSupport().createTransactionBuilder("writeBleInit"); + builder.setGattCallback(this); + builder.write(getCharacteristic(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID), mBleSettings); + getSupport().performImmediately(builder); + } catch(IOException e) { + LOG.error("Error writing BLE settings: " + e.getMessage()); + } + } + + @Override + public boolean onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + + UUID characteristicUUID = characteristic.getUuid(); + byte[] data = characteristic.getValue(); + + if(data.length == 0) + return true; + + if(characteristicUUID.equals(CasioGB6900Constants.CASIO_SETTING_FOR_BLE_CHARACTERISTIC_UUID)) { + mBleSettings = data; + String str = "Read Casio Setting for BLE: "; + for(int i=0; i> 6) & 0x03; + //LOG.info("Call Alert: " + callAlert); + //int mailAlert = (data[0] >> 2) & 0x03; + //LOG.info("Mail Alert: " + mailAlert); + //int snsAlert = (data[2] >> 4) & 0x03; + //LOG.info("SNS Alert: " + snsAlert); + //int calAlert = (data[1] >> 6) & 0x03; + //LOG.info("Calendart Alert: " + calAlert); + //int otherAlert = (data[0] & 0x03); + //LOG.info("Other Alert: " + otherAlert); + //int vibrationValue = (data[3] & 0x0f); + //LOG.info("Vibration Value: " + vibrationValue); + //int alarmValue = (data[3] >> 4) & 0x0f; + // Vibration pattern; A = 0, B = 1, C = 2 + //LOG.info("Alarm Value: " + alarmValue); + //int animationValue = data[4] & 0x40; + // Length of Alarm, only 2, 5 and 10 possible + //LOG.info("Animation Value: " + animationValue); + // 0 = on + // 64 = off + //int useDisableMtuReqBit = data[4] & 0x08; + // 8 = on + // 0 = off!? + //LOG.info("useDisableMtuReqBit: " + useDisableMtuReqBit); + + //int slaveLatency = ((data[7] & 0xff) | ((data[8] & 0xff) << 8)); + //int connInterval = ((data[5] & 0xff) | ((data[6] & 0xff) << 8)); + //LOG.info("Slave Latency: " + slaveLatency); + //LOG.info("Connection Interval: " + connInterval); + //LOG.info(str); + + configureBleSettings(); + writeBleSettings(); + } + else { + return super.onCharacteristicRead(gatt, characteristic, status); + } + + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java index 13b8867c5e..36e6b58c6e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -24,6 +24,7 @@ public class GBPrefs { public static final String PACKAGE_BLACKLIST = "package_blacklist"; public static final String PACKAGE_PEBBLEMSG_BLACKLIST = "package_pebblemsg_blacklist"; public static final String CALENDAR_BLACKLIST = "calendar_blacklist"; + public static final String AUTO_RECONNECT_SCAN = "general_autoreconnectscan"; public static final String AUTO_RECONNECT = "general_autocreconnect"; private static final String AUTO_START = "general_autostartonboot"; public static final String AUTO_EXPORT_ENABLED = "auto_export_enabled"; @@ -35,6 +36,7 @@ public class GBPrefs { public static final String RTL_SUPPORT = "rtl"; public static final String RTL_CONTEXTUAL_ARABIC = "contextualArabic"; public static boolean AUTO_RECONNECT_DEFAULT = true; + public static boolean AUTO_RECONNECT_SCAN_DEFAULT = false; public static final String USER_NAME = "mi_user_alias"; public static final String USER_NAME_DEFAULT = "gadgetbridge-user"; @@ -53,6 +55,10 @@ public boolean getAutoReconnect() { return mPrefs.getBoolean(AUTO_RECONNECT, AUTO_RECONNECT_DEFAULT); } + public boolean getAutoReconnectScan() { + return mPrefs.getBoolean(AUTO_RECONNECT_SCAN, AUTO_RECONNECT_SCAN_DEFAULT); + } + public boolean getAutoStart() { return mPrefs.getBoolean(AUTO_START, AUTO_START_DEFAULT); } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 364cccd116..53fcd5e87c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -644,4 +644,5 @@ Modus-Konfiguration Konfiguration speichern Nicht verbunden, Alarm nicht eingestellt. - \ No newline at end of file + BLE Scanner zur Wiederherstellung der Verbindung verwenden + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ee0671f9c..b950d136a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -690,4 +690,5 @@ Mode Configuration Save Configuration Not connected, alarm not set. + Use BLE Scanner for Reconnect \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 2b451bf11a..b626a4e683 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -18,6 +18,11 @@ android:defaultValue="false" android:key="general_autocreconnect" android:title="@string/pref_title_general_autoreconnect" /> +