diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6bfd292b98..b1ff8c8b79 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -343,6 +343,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..0324718534
--- /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);
+ }
+}
\ No newline at end of file
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 04f8d1cd1d..d19dd2c5ba 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
@@ -34,11 +34,13 @@
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
@@ -74,6 +76,14 @@ public boolean connect() {
if (mQueue == null) {
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();
}
@@ -385,4 +395,8 @@ public boolean onDescriptorReadRequest(BluetoothDevice device, int requestId, in
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/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java
index 52b6e96d39..dcaf4fa91d 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,6 +17,8 @@
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;
@@ -28,7 +30,13 @@
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;
@@ -39,6 +47,7 @@
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;
@@ -46,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;
@@ -69,6 +79,8 @@ public final class BtLEQueue {
private volatile boolean mAbortTransaction;
private volatile boolean mAbortServerTransaction;
+ private final Handler mHandler = new Handler();
+
private final Context mContext;
private CountDownLatch mWaitForActionResultLatch;
private CountDownLatch mWaitForServerActionResultLatch;
@@ -76,7 +88,27 @@ public final class BtLEQueue {
private BluetoothGattCharacteristic mWaitCharacteristic;
private final InternalGattCallback internalGattCallback;
private final InternalGattServerCallback internalGattServerCallback;
- private boolean mAutoReconnect;
+ 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") {
@@ -200,6 +232,10 @@ public void setAutoReconnect(boolean enable) {
mAutoReconnect = enable;
}
+ public void setBleScannerForReconnect(boolean enable) {
+ mUseBleScannerForReconnect = enable;
+ }
+
protected boolean isConnected() {
return mGbDevice.isConnected();
}
@@ -269,6 +305,23 @@ 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;
@@ -277,12 +330,6 @@ public void disconnect() {
gatt.close();
setDeviceConnectionState(State.NOT_CONNECTED);
}
- BluetoothGattServer gattServer = mBluetoothGattServer;
- if (gattServer != null) {
- mBluetoothGattServer = null;
- gattServer.clearServices();
- gattServer.close();
- }
}
}
@@ -299,8 +346,6 @@ private void handleDisconnected(int status) {
mWaitForServerActionResultLatch.countDown();
}
- boolean wasInitialized = mGbDevice.isInitialized();
-
setDeviceConnectionState(State.NOT_CONNECTED);
// either we've been disconnected because the device is out of range
@@ -310,7 +355,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
}
}
@@ -323,16 +368,121 @@ 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;
@@ -340,6 +490,9 @@ public void dispose() {
mDisposed = true;
// try {
disconnect();
+ if(mUseBleScannerForReconnect) {
+ stopBleBackgroundScan();
+ }
dispatchThread.interrupt();
dispatchThread = null;
// dispatchThread.join();
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/strings.xml b/app/src/main/res/values/strings.xml
index e4792757c4..6692a59213 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -692,4 +692,5 @@
Mode Configuration
Save Configuration
Not connected, alarm not set.
+ Use BLE Scanner for Reconnect
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 03f4e8552a..e723fe59cf 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" />
+