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 47deb27ec0..8dfd8191b6 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 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 68243a67e7..8b97683486 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" /> +