This commit is contained in:
Andreas Böhler 2019-05-09 10:27:55 +02:00
parent fb04e0b48f
commit 340bbec8ee
7 changed files with 281 additions and 15 deletions

View File

@ -343,6 +343,13 @@
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".externalevents.BluetoothScanCallbackReceiver"
android:exported="false">
<intent-filter>
<action android:name="nodomain.freeyourgadget.gadgetbridge.blescancallback" />
</intent-filter>
</receiver>
<receiver
android:name=".service.receivers.GBMusicControlReceiver"
android:exported="false">

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<ScanResult> 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);
}
}

View File

@ -34,11 +34,13 @@ import java.util.Map;
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 abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
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 abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
return false;
}
public boolean useBleScannerForReconnect() {
return false;
}
}

View File

@ -17,6 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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.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;
@ -39,6 +47,7 @@ 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;
@ -46,6 +55,7 @@ import java.util.concurrent.LinkedBlockingQueue;
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 final class BtLEQueue {
mAutoReconnect = enable;
}
public void setBleScannerForReconnect(boolean enable) {
mUseBleScannerForReconnect = enable;
}
protected boolean isConnected() {
return mGbDevice.isConnected();
}
@ -269,6 +305,23 @@ public final class BtLEQueue {
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<BluetoothDevice> 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 final class BtLEQueue {
gatt.close();
setDeviceConnectionState(State.NOT_CONNECTED);
}
BluetoothGattServer gattServer = mBluetoothGattServer;
if (gattServer != null) {
mBluetoothGattServer = null;
gattServer.clearServices();
gattServer.close();
}
}
}
@ -299,8 +346,6 @@ public final class BtLEQueue {
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 @@ public final class BtLEQueue {
// 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 @@ public final class BtLEQueue {
*/
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<ScanFilter> 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<ScanFilter> 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<ScanResult> 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 final class BtLEQueue {
mDisposed = true;
// try {
disconnect();
if(mUseBleScannerForReconnect) {
stopBleBackgroundScan();
}
dispatchThread.interrupt();
dispatchThread = null;
// dispatchThread.join();

View File

@ -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 class GBPrefs {
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);
}

View File

@ -692,4 +692,5 @@
<string name="mode_configuration">Mode Configuration</string>
<string name="save_configuration">Save Configuration</string>
<string name="appwidget_not_connected">Not connected, alarm not set.</string>
<string name="pref_title_general_autoreconnectscan">Use BLE Scanner for Reconnect</string>
</resources>

View File

@ -18,6 +18,11 @@
android:defaultValue="false"
android:key="general_autocreconnect"
android:title="@string/pref_title_general_autoreconnect" />
<CheckBoxPreference
android:layout="@layout/preference_checkbox"
android:defaultValue="false"
android:key="general_autoreconnectscan"
android:title="@string/pref_title_general_autoreconnectscan" />
<ListPreference
android:defaultValue="default"
android:key="audio_player"