Device connection: added device scan reconnection logic

This commit is contained in:
Daniel Dakhno 2024-02-14 03:25:26 +01:00
parent b8b8b5f681
commit 00c0a8e897
14 changed files with 689 additions and 176 deletions

View File

@ -434,6 +434,12 @@
android:name=".devices.pinetime.PineTimeDFUService"
android:label="PineTime Nordic DFU service" />
<service
android:name=".service.btle.BLEScanService"
android:enabled="true"
android:exported="false">
</service>
<receiver
android:name=".externalevents.GenericWeatherReceiver"
android:enabled="true"

View File

@ -17,12 +17,15 @@
package nodomain.freeyourgadget.gadgetbridge.activities.discovery;
import android.os.Bundle;
import android.widget.Toast;
import androidx.preference.PreferenceFragmentCompat;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractPreferenceFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivityV2;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DiscoveryPairingPreferenceActivity extends AbstractSettingsActivityV2 {
@Override
@ -38,8 +41,14 @@ public class DiscoveryPairingPreferenceActivity extends AbstractSettingsActivity
public static class DiscoveryPairingPreferenceFragment extends AbstractPreferenceFragment {
static final String FRAGMENT_TAG = "DISCOVERY_PAIRING_PREFERENCES_FRAGMENT";
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
setPreferencesFromResource(R.xml.discovery_pairing_preferences, rootKey);
findPreference("prefs_general_key_auto_reconnect_scan").setOnPreferenceChangeListener((preference, newValue) -> {
GB.toast(GBApplication.getContext().getString(R.string.prompt_restart_gadgetbridge), Toast.LENGTH_LONG, GB.INFO);
return true;
});
}
}
}

View File

@ -431,33 +431,14 @@ public class GBDevice implements Parcelable {
* Set simple to true to get this behavior.
*/
private String getStateString(boolean simple) {
switch (mState) {
case NOT_CONNECTED:
return GBApplication.getContext().getString(R.string.not_connected);
case WAITING_FOR_RECONNECT:
return GBApplication.getContext().getString(R.string.waiting_for_reconnect);
case CONNECTING:
return GBApplication.getContext().getString(R.string.connecting);
case CONNECTED:
if (simple) {
return GBApplication.getContext().getString(R.string.connecting);
}
return GBApplication.getContext().getString(R.string.connected);
case INITIALIZING:
if (simple) {
return GBApplication.getContext().getString(R.string.connecting);
}
return GBApplication.getContext().getString(R.string.initializing);
case AUTHENTICATION_REQUIRED:
return GBApplication.getContext().getString(R.string.authentication_required);
case AUTHENTICATING:
return GBApplication.getContext().getString(R.string.authenticating);
case INITIALIZED:
if (simple) {
return GBApplication.getContext().getString(R.string.connected);
}
return GBApplication.getContext().getString(R.string.initialized);
}
try{
// TODO: not sure if this is really neccessary...
if(simple){
return GBApplication.getContext().getString(mState.getSimpleStringId());
}
return GBApplication.getContext().getString(mState.getStringId());
}catch (Exception e){}
return GBApplication.getContext().getString(R.string.unknown_state);
}
@ -744,20 +725,40 @@ public class GBDevice implements Parcelable {
}
public enum State {
// Note: the order is important!
NOT_CONNECTED,
WAITING_FOR_RECONNECT,
CONNECTING,
CONNECTED,
INITIALIZING,
AUTHENTICATION_REQUIRED, // some kind of pairing is required by the device
AUTHENTICATING, // some kind of pairing is requested by the device
NOT_CONNECTED(R.string.not_connected),
WAITING_FOR_RECONNECT(R.string.waiting_for_reconnect),
WAITING_FOR_SCAN(R.string.device_state_waiting_scan),
CONNECTING(R.string.connecting),
CONNECTED(R.string.connected, R.string.connecting),
INITIALIZING(R.string.initializing, R.string.connecting),
AUTHENTICATION_REQUIRED(R.string.authentication_required), // some kind of pairing is required by the device
AUTHENTICATING(R.string.authenticating), // some kind of pairing is requested by the device
/**
* Means that the device is connected AND all the necessary initialization steps
* have been performed. At the very least, this means that basic information like
* device name, firmware version, hardware revision (as applicable) is available
* in the GBDevice.
*/
INITIALIZED,
INITIALIZED(R.string.initialized, R.string.connected);
private int stringId, simpleStringId;
State(int stringId, int simpleStringId) {
this.stringId = stringId;
this.simpleStringId = simpleStringId;
}
State(int stringId) {
this(stringId, stringId);
}
public int getStringId() {
return stringId;
}
public int getSimpleStringId() {
return simpleStringId;
}
}
}

View File

@ -127,7 +127,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
protected GBDevice gbDevice;
private BluetoothAdapter btAdapter;
private Context context;
private boolean autoReconnect;
private boolean autoReconnect, scanReconnect;
@ -171,6 +171,16 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
return autoReconnect;
}
@Override
public void setScanReconnect(boolean scanReconnect) {
this.scanReconnect = scanReconnect;
}
@Override
public boolean getScanReconnect(){
return this.scanReconnect;
}
@Override
public boolean getImplicitCallbackModify() {
return true;

View File

@ -97,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLEScanService;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.AutoConnectIntervalReceiver;
import nodomain.freeyourgadget.gadgetbridge.service.receivers.GBAutoFetchReceiver;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
@ -226,8 +227,6 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
@SuppressLint("StaticFieldLeak") // only used for test cases
private static DeviceSupportFactory DEVICE_SUPPORT_FACTORY = null;
private boolean mStarted = false;
private DeviceSupportFactory mFactory;
private final ArrayList<DeviceStruct> deviceStructs = new ArrayList<>(1);
private final HashMap<String, ArrayList<Intent>> cachedNotifications = new HashMap<>();
@ -273,6 +272,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private final String ACTION_DEVICE_CONNECTED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECTED";
private final int NOTIFICATIONS_CACHE_MAX = 10; // maximum amount of notifications to cache per device while disconnected
private boolean allowBluetoothIntentApi = false;
private boolean reconnectViaScan = GBPrefs.RECONNECT_SCAN_DEFAULT;
private void sendDeviceConnectedBroadcast(String address){
if(!allowBluetoothIntentApi){
@ -369,6 +369,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
sendDeviceConnectedBroadcast(device.getAddress());
sendCachedNotifications(device);
}
}else if(BLEScanService.EVENT_DEVICE_FOUND.equals(action)){
String deviceAddress = intent.getStringExtra(BLEScanService.EXTRA_DEVICE_ADDRESS);
GBDevice target = GBApplication
.app()
.getDeviceManager()
.getDeviceByAddress(deviceAddress);
if(target == null){
LOG.error("onReceive: device not found");
return;
}
connectToDevice(target);
}
}
};
@ -400,24 +414,20 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
setReceiversEnableState(enableReceivers, anyDeviceInitialized, features, devicesWithCalendar);
}
@Override
public void onCreate() {
LOG.debug("DeviceCommunicationService is being created");
super.onCreate();
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED));
mFactory = getDeviceSupportFactory();
private void registerInternalReceivers(){
IntentFilter localFilter = new IntentFilter();
localFilter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
localFilter.addAction(BLEScanService.EVENT_DEVICE_FOUND);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, localFilter);
}
private void registerExternalReceivers(){
mBlueToothConnectReceiver = new BluetoothConnectReceiver(this);
registerReceiver(mBlueToothConnectReceiver, new IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED));
mAutoConnectInvervalReceiver= new AutoConnectIntervalReceiver(this);
registerReceiver(mAutoConnectInvervalReceiver, new IntentFilter("GB_RECONNECT"));
if (hasPrefs()) {
getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this);
allowBluetoothIntentApi = getPrefs().getBoolean(GBPrefs.PREF_ALLOW_INTENT_API, false);
}
IntentFilter bluetoothCommandFilter = new IntentFilter();
bluetoothCommandFilter.addAction(COMMAND_BLUETOOTH_CONNECT);
registerReceiver(bluetoothCommandReceiver, bluetoothCommandFilter);
@ -429,6 +439,46 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
registerReceiver(intentApiReceiver, intentApiReceiver.buildFilter());
}
@Override
public void onCreate() {
LOG.debug("DeviceCommunicationService is being created");
super.onCreate();
mFactory = getDeviceSupportFactory();
registerInternalReceivers();
registerExternalReceivers();
if (hasPrefs()) {
getPrefs().getPreferences().registerOnSharedPreferenceChangeListener(this);
allowBluetoothIntentApi = getPrefs().getBoolean(GBPrefs.PREF_ALLOW_INTENT_API, false);
reconnectViaScan = getGBPrefs().getAutoReconnectByScan();
}
startForeground();
if(reconnectViaScan) {
scanAllDevices();
Intent scanServiceIntent = new Intent(this, BLEScanService.class);
startService(scanServiceIntent);
}
}
private void scanAllDevices(){
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
for(GBDevice device : devices){
if(device.getState() != GBDevice.State.NOT_CONNECTED){
continue;
}
boolean shouldAutoConnect = getGBPrefs().getAutoReconnect(device);
if(!shouldAutoConnect){
continue;
}
createDeviceStruct(device);
device.setState(GBDevice.State.WAITING_FOR_SCAN);
device.sendDeviceUpdateIntent(this);
}
}
private DeviceSupportFactory getDeviceSupportFactory() {
if (DEVICE_SUPPORT_FACTORY != null) {
return DEVICE_SUPPORT_FACTORY;
@ -436,144 +486,136 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
return new DeviceSupportFactory(this);
}
private void createDeviceStruct(GBDevice target){
DeviceStruct registeredStruct = new DeviceStruct();
registeredStruct.setDevice(target);
registeredStruct.setCoordinator(target.getDeviceCoordinator());
deviceStructs.add(registeredStruct);
}
private void connectToDevice(GBDevice device){
connectToDevice(device, false);
}
private void connectToDevice(GBDevice device, boolean firstTime){
List<GBDevice> gbDevs = null;
boolean fromExtra = false;
Prefs prefs = getPrefs();
if (device != null) {
gbDevs = new ArrayList<>();
gbDevs.add(device);
fromExtra = true;
} else if (prefs.getBoolean(GBPrefs.RECONNECT_ONLY_TO_CONNECTED, true)) {
List<GBDevice> gbAllDevs = GBApplication.app().getDeviceManager().getDevices();
Set<String> lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet());
if (gbAllDevs != null && !gbAllDevs.isEmpty() && !lastDeviceAddresses.isEmpty()) {
gbDevs = new ArrayList<>();
for(GBDevice gbDev : gbAllDevs) {
if (lastDeviceAddresses.contains(gbDev.getAddress())) {
gbDevs.add(gbDev);
}
}
}
} else {
gbDevs = GBApplication.app().getDeviceManager().getDevices();
}
if(gbDevs == null || gbDevs.size() == 0) {
return;
}
for(GBDevice gbDevice : gbDevs) {
String btDeviceAddress = gbDevice.getAddress();
boolean autoReconnect = GBPrefs.AUTO_RECONNECT_DEFAULT;
if (prefs != null && prefs.getPreferences() != null) {
autoReconnect = getGBPrefs().getAutoReconnect(gbDevice);
if(!fromExtra && !autoReconnect) {
continue;
}
Set<String> lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet());
if (!lastDeviceAddresses.contains(btDeviceAddress)) {
lastDeviceAddresses = new HashSet<String>(lastDeviceAddresses);
lastDeviceAddresses.add(btDeviceAddress);
prefs.getPreferences().edit().putStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, lastDeviceAddresses).apply();
}
}
if(!fromExtra && !autoReconnect) {
continue;
}
DeviceStruct registeredStruct = getDeviceStructOrNull(gbDevice);
if(registeredStruct != null){
boolean deviceAlreadyConnected = isDeviceConnecting(registeredStruct.getDevice()) || isDeviceConnected(registeredStruct.getDevice());
if(deviceAlreadyConnected){
break;
}
try {
removeDeviceSupport(gbDevice);
} catch (DeviceNotFoundException e) {
e.printStackTrace();
}
}else{
createDeviceStruct(gbDevice);
}
try {
DeviceSupport deviceSupport = mFactory.createDeviceSupport(gbDevice);
if (deviceSupport != null) {
setDeviceSupport(gbDevice, deviceSupport);
if (firstTime) {
deviceSupport.connectFirstTime();
} else {
deviceSupport.setAutoReconnect(autoReconnect);
deviceSupport.setScanReconnect(reconnectViaScan);
deviceSupport.connect();
}
} else {
GB.toast(this, getString(R.string.cannot_connect, "Can't create device support"), Toast.LENGTH_SHORT, GB.ERROR);
}
} catch (Exception e) {
GB.toast(this, getString(R.string.cannot_connect, e.getMessage()), Toast.LENGTH_SHORT, GB.ERROR, e);
}
for(DeviceStruct struct2 : deviceStructs){
struct2.getDevice().sendDeviceUpdateIntent(this);
}
}
}
@Override
public synchronized int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
LOG.info("no intent");
return START_NOT_STICKY;
return START_STICKY;
}
String action = intent.getAction();
boolean firstTime = intent.getBooleanExtra(EXTRA_CONNECT_FIRST_TIME, false);
if (action == null) {
LOG.info("no action");
return START_NOT_STICKY;
return START_STICKY;
}
LOG.debug("Service startcommand: " + action);
if (!action.equals(ACTION_START) && !action.equals(ACTION_CONNECT)) {
if (!mStarted) {
// using the service before issuing ACTION_START
LOG.info("Must start service with " + ACTION_START + " or " + ACTION_CONNECT + " before using it: " + action);
return START_NOT_STICKY;
}
// TODO
/*if (mDeviceSupport == null || (!isInitialized() && !action.equals(ACTION_DISCONNECT) && (!mDeviceSupport.useAutoConnect() || isConnected()))) {
// trying to send notification without valid Bluetooth connection
if (mGBDevice != null) {
// at least send back the current device state
mGBDevice.sendDeviceUpdateIntent(this);
}
return START_STICKY;
}*/
}
// when we get past this, we should have valid mDeviceSupport and mGBDevice instances
GBDevice targetDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
Prefs prefs = getPrefs();
switch (action) {
case ACTION_START:
start();
break;
case ACTION_CONNECT:
start(); // ensure started
List<GBDevice> gbDevs = null;
boolean fromExtra = false;
GBDevice extraDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (extraDevice != null) {
gbDevs = new ArrayList<>();
gbDevs.add(extraDevice);
fromExtra = true;
} else if (prefs.getBoolean(GBPrefs.RECONNECT_ONLY_TO_CONNECTED, true)) {
List<GBDevice> gbAllDevs = GBApplication.app().getDeviceManager().getDevices();
Set<String> lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet());
if (gbAllDevs != null && !gbAllDevs.isEmpty() && !lastDeviceAddresses.isEmpty()) {
gbDevs = new ArrayList<>();
for(GBDevice gbDev : gbAllDevs) {
if (lastDeviceAddresses.contains(gbDev.getAddress())) {
gbDevs.add(gbDev);
}
}
}
} else {
gbDevs = GBApplication.app().getDeviceManager().getDevices();
}
if(gbDevs == null || gbDevs.size() == 0) {
return START_NOT_STICKY;
}
for(GBDevice gbDevice : gbDevs) {
String btDeviceAddress = gbDevice.getAddress();
boolean autoReconnect = GBPrefs.AUTO_RECONNECT_DEFAULT;
if (prefs != null && prefs.getPreferences() != null) {
autoReconnect = getGBPrefs().getAutoReconnect(gbDevice);
if(!fromExtra && !autoReconnect) {
continue;
}
Set<String> lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet());
if (!lastDeviceAddresses.contains(btDeviceAddress)) {
lastDeviceAddresses = new HashSet<String>(lastDeviceAddresses);
lastDeviceAddresses.add(btDeviceAddress);
prefs.getPreferences().edit().putStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, lastDeviceAddresses).apply();
}
}
if(!fromExtra && !autoReconnect) {
continue;
}
DeviceStruct registeredStruct = getDeviceStructOrNull(gbDevice);
if(registeredStruct != null){
boolean deviceAlreadyConnected = isDeviceConnecting(registeredStruct.getDevice()) || isDeviceConnected(registeredStruct.getDevice());
if(deviceAlreadyConnected){
break;
}
try {
removeDeviceSupport(gbDevice);
} catch (DeviceNotFoundException e) {
e.printStackTrace();
}
}else{
registeredStruct = new DeviceStruct();
registeredStruct.setDevice(gbDevice);
registeredStruct.setCoordinator(gbDevice.getDeviceCoordinator());
deviceStructs.add(registeredStruct);
}
try {
DeviceSupport deviceSupport = mFactory.createDeviceSupport(gbDevice);
if (deviceSupport != null) {
setDeviceSupport(gbDevice, deviceSupport);
if (firstTime) {
deviceSupport.connectFirstTime();
} else {
deviceSupport.setAutoReconnect(autoReconnect);
deviceSupport.connect();
}
} else {
GB.toast(this, getString(R.string.cannot_connect, "Can't create device support"), Toast.LENGTH_SHORT, GB.ERROR);
}
} catch (Exception e) {
GB.toast(this, getString(R.string.cannot_connect, e.getMessage()), Toast.LENGTH_SHORT, GB.ERROR, e);
}
for(DeviceStruct struct2 : deviceStructs){
struct2.getDevice().sendDeviceUpdateIntent(this);
}
}
boolean firstTime = intent.getBooleanExtra(EXTRA_CONNECT_FIRST_TIME, false);
connectToDevice(targetDevice, firstTime);
break;
default:
GBDevice targetedDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
ArrayList<GBDevice> targetedDevices = new ArrayList<>();
if(targetedDevice != null){
targetedDevices.add(targetedDevice);
if(targetDevice != null){
targetedDevices.add(targetDevice);
}else{
for(GBDevice device : getGBDevices()){
if(isDeviceInitialized(device)){
@ -1042,16 +1084,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
throw new DeviceNotFoundException(device);
}
private void start() {
if (!mStarted) {
GB.createNotificationChannels(this);
startForeground(GB.NOTIFICATION_ID, GB.createNotification(getString(R.string.gadgetbridge_running), this));
mStarted = true;
}
}
public boolean isStarted() {
return mStarted;
private void startForeground() {
GB.createNotificationChannels(this);
startForeground(GB.NOTIFICATION_ID, GB.createNotification(getString(R.string.gadgetbridge_running), this));
}
private boolean isDeviceConnected(GBDevice device) {

View File

@ -114,6 +114,10 @@ public interface DeviceSupport extends EventHandler {
*/
boolean getAutoReconnect();
void setScanReconnect(boolean enable);
boolean getScanReconnect();
/**
* Returns whether the gatt callback should be implicitly set to the one on the transaction,
* even if it was not set directly on the transaction. If true, the gatt callback will always

View File

@ -99,6 +99,16 @@ public class ServiceDeviceSupport implements DeviceSupport {
return delegate.getAutoReconnect();
}
@Override
public void setScanReconnect(boolean enable) {
delegate.setScanReconnect(enable);
}
@Override
public boolean getScanReconnect(){
return delegate.getScanReconnect();
}
@Override
public boolean getImplicitCallbackModify() {
return delegate.getImplicitCallbackModify();

View File

@ -78,6 +78,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im
if (mQueue == null) {
mQueue = new BtLEQueue(getBluetoothAdapter(), getDevice(), this, this, getContext(), mSupportedServerServices);
mQueue.setAutoReconnect(getAutoReconnect());
mQueue.setScanReconnect(getScanReconnect());
mQueue.setImplicitGattCallbackModify(getImplicitCallbackModify());
mQueue.setSendWriteRequestResponse(getSendWriteRequestResponse());
}

View File

@ -0,0 +1,407 @@
package nodomain.freeyourgadget.gadgetbridge.service.btle;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
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.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.IBinder;
import android.widget.RemoteViews;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class BLEScanService extends Service {
public static final String COMMAND_SCAN_DEVICE = "nodomain.freeyourgadget.gadgetbridge.service.ble.scan.command.START_SCAN_FOR_DEVICE";
public static final String COMMAND_START_SCAN_ALL = "nodomain.freeyourgadget.gadgetbridge.service.ble.scan.command.START_SCAN_ALL";
public static final String COMMAND_STOP_SCAN_ALL = "nodomain.freeyourgadget.gadgetbridge.service.ble.scan.command.STOP_SCAN_ALL";
public static final String EVENT_DEVICE_FOUND = "nodomain.freeyourgadget.gadgetbridge.service.ble.scan.event.DEVICE_FOUND";
public static final String EXTRA_DEVICE = "EXTRA_DEVICE";
public static final String EXTRA_DEVICE_ADDRESS = "EXTRA_DEVICE_ADDRESS";
// 5 minutes scan restart interval
private final int DELAY_SCAN_RESTART = 5 * 60 * 1000;
private LocalBroadcastManager localBroadcastManager;
private NotificationManager notificationManager;
private BluetoothManager bluetoothManager;
private BluetoothLeScanner scanner;
private Logger LOG = LoggerFactory.getLogger(getClass());
// private final ArrayList<ScanFilter> currentFilters = new ArrayList<>();
private enum ScanningState {
NOT_SCANNING,
SCANNING_WITHOUT_FILTERS,
SCANNING_WITH_FILTERS;
public boolean isDoingAnyScan(){
return ordinal() > NOT_SCANNING.ordinal();
}
public boolean shouldDiscardAfterFirstMatch(){
return this == SCANNING_WITH_FILTERS;
}
};
private ScanningState currentState = ScanningState.NOT_SCANNING;
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
BluetoothDevice device = result.getDevice();
LOG.debug("onScanResult: " + result);
Intent intent = new Intent(EVENT_DEVICE_FOUND);
intent.putExtra(EXTRA_DEVICE_ADDRESS, device.getAddress());
localBroadcastManager.sendBroadcast(intent);
// device found, attempt connection
// stop scanning for device for now
// will restart when connection attempt fails
if(currentState.shouldDiscardAfterFirstMatch()) {
// stopScanningForDevice(device.getAddress());
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
LOG.error("onScanFailed: " + errorCode);
updateNotification("Scan failed: " + errorCode);
}
};
@Override
public void onCreate() {
super.onCreate();
bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
scanner = bluetoothManager.getAdapter().getBluetoothLeScanner();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
registerReceivers();
this.startForeground();
if(scanner == null){
updateNotification("Waiting for bluetooth...");
}else{
restartScan(true);
}
// schedule after 5 seconds to fix weird timing of both services
scheduleRestartScan(5000);
}
private void scheduleRestartScan(){
scheduleRestartScan(DELAY_SCAN_RESTART);
}
private void scheduleRestartScan(long millis){
Handler handler = new Handler();
handler.postDelayed(() -> {
LOG.debug("restarting scan...");
try {
restartScan(true);
}catch (Exception e){
LOG.error("error during scheduled scan restart", e);
}
scheduleRestartScan();
}, millis);
}
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceivers();
}
private void updateNotification(boolean isScanning, int scannedDeviceCount){
notificationManager.notify(
GB.NOTIFICATION_ID_SCAN,
createNotification(isScanning, scannedDeviceCount)
);
}
private void updateNotification(String content){
notificationManager.notify(
GB.NOTIFICATION_ID_SCAN,
createNotification(content, R.drawable.ic_bluetooth)
);
}
private Notification createNotification(boolean isScanning, int scannedDevicesCount){
int icon = R.drawable.ic_bluetooth;
String content = "Not scanning";
if(isScanning){
icon = R.drawable.ic_bluetooth_searching;
if(scannedDevicesCount == 1) {
content = String.format("Scanning %d device", scannedDevicesCount);
}else if(scannedDevicesCount > 1){
content = String.format("Scanning %d devices", scannedDevicesCount);
}else{
content = "Scanning all devices";
}
}
return createNotification(content, icon);
}
private Notification createNotification(String content, int icon){
return new NotificationCompat
.Builder(this, GB.NOTIFICATION_CHANNEL_ID)
.setContentTitle("Scan service")
.setContentText(content)
.setSmallIcon(icon)
.build();
}
private void startForeground(){
Notification serviceNotification = createNotification(false, 0);
super.startForeground(GB.NOTIFICATION_ID_SCAN, serviceNotification);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent == null){
return START_STICKY;
}
String action = intent.getAction();
if(action == null){
return START_STICKY;
}
switch (action) {
case COMMAND_SCAN_DEVICE:
handleScanDevice(intent);
break;
case COMMAND_START_SCAN_ALL:
handleScanAll(intent);
break;
case COMMAND_STOP_SCAN_ALL:
handleStopScanAll(intent);
break;
default:
return START_STICKY;
}
return START_STICKY;
}
private void handleStopScanAll(Intent intent){
restartScan(true);
}
private void handleScanAll(Intent intent){
if(currentState != ScanningState.SCANNING_WITHOUT_FILTERS){
restartScan(false);
}
}
private void handleScanDevice(Intent intent){
/*
GBDevice device = intent.getParcelableExtra(EXTRA_DEVICE);
if(device == null){
return;
}
scanForDevice(device);
*/
restartScan(true);
}
/*private boolean isDeviceIncludedInCurrentFilters(GBDevice device){
for(ScanFilter currentFilter : currentFilters){
if(device.getAddress().equals(currentFilter.getDeviceAddress())){
return true;
}
}
return false;
}
*/
/*
private void stopScanningForDevice(GBDevice device){
this.stopScanningForDevice(device.getAddress());
}
*/
/*
private void stopScanningForDevice(String deviceAddress){
currentFilters.removeIf(scanFilter -> scanFilter
.getDeviceAddress()
.equals(deviceAddress)
);
restartScan(true);
}
*/
/*
private void scanForDevice(GBDevice device){
if(isDeviceIncludedInCurrentFilters(device)){
// already scanning for device
return;
}
ScanFilter deviceFilter = new ScanFilter.Builder()
.setDeviceAddress(device.getAddress())
.build();
currentFilters.add(deviceFilter);
// restart scan here
restartScan(true);
}
*/
BroadcastReceiver deviceStateUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
GBDevice.DeviceUpdateSubject subject =
(GBDevice.DeviceUpdateSubject)
intent.getSerializableExtra(GBDevice.EXTRA_UPDATE_SUBJECT);
if(subject != GBDevice.DeviceUpdateSubject.CONNECTION_STATE){
return;
}
restartScan(true);
}
};
BroadcastReceiver bluetoothStateChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if(intent == null){
return;
}
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch(state) {
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
updateNotification("Waiting for bluetooth...");
break;
case BluetoothAdapter.STATE_ON:
restartScan(true);
break;
}
}
};
private void registerReceivers(){
localBroadcastManager.registerReceiver(
deviceStateUpdateReceiver,
new IntentFilter(GBDevice.ACTION_DEVICE_CHANGED)
);
registerReceiver(
bluetoothStateChangedReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
);
}
private void unregisterReceivers(){
localBroadcastManager.unregisterReceiver(deviceStateUpdateReceiver);
unregisterReceiver(bluetoothStateChangedReceiver);
}
private void restartScan(boolean applyFilters){
if(scanner == null){
scanner = bluetoothManager.getAdapter().getBluetoothLeScanner();
}
if(scanner == null){
// at this point we should already be waiting for bluetooth to turn back on
LOG.debug("cannot enable scan since bluetooth seems off (scanner == null)");
return;
}
if(bluetoothManager.getAdapter().getState() != BluetoothAdapter.STATE_ON){
// again, we should be waiting for the adapter to turn on again
LOG.debug("Bluetooth adapter state off");
return;
}
if(currentState.isDoingAnyScan()){
scanner.stopScan(scanCallback);
}
ArrayList<ScanFilter> scanFilters = null;
if(applyFilters) {
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
scanFilters = new ArrayList<>(devices.size());
for (GBDevice device : devices) {
if (device.getState() == GBDevice.State.WAITING_FOR_SCAN) {
scanFilters.add(new ScanFilter.Builder()
.setDeviceAddress(device.getAddress())
.build()
);
}
}
if(scanFilters.size() == 0){
// no need to start scanning
LOG.debug("restartScan: stopping BLE scan, no devices");
currentState = ScanningState.NOT_SCANNING;
updateNotification(false, 0);
return;
}
}
ScanSettings scanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // enforced anyway in background
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setLegacy(false)
.build();
scanner.startScan(scanFilters, scanSettings, scanCallback);
if(applyFilters) {
LOG.debug("restartScan: started scan for " + scanFilters.size() + " devices");
updateNotification(true, scanFilters.size());
currentState = ScanningState.SCANNING_WITH_FILTERS;
}else{
LOG.debug("restartScan: started scan for all devices");
updateNotification(true, 0);
currentState = ScanningState.SCANNING_WITHOUT_FILTERS;
}
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}

View File

@ -81,6 +81,7 @@ public final class BtLEQueue {
private final InternalGattCallback internalGattCallback;
private final InternalGattServerCallback internalGattServerCallback;
private boolean mAutoReconnect;
private boolean scanReconnect;
private boolean mImplicitGattCallbackModify = true;
private boolean mSendWriteRequestResponse = false;
@ -218,6 +219,10 @@ public final class BtLEQueue {
mAutoReconnect = enable;
}
public void setScanReconnect(boolean enable){
this.scanReconnect = enable;
}
public void setImplicitGattCallbackModify(final boolean enable) {
mImplicitGattCallbackModify = enable;
}
@ -355,6 +360,12 @@ public final class BtLEQueue {
*/
private boolean maybeReconnect() {
if (mAutoReconnect && mBluetoothGatt != null) {
if(scanReconnect){
LOG.info("Waiting for BLE scan before attempting reconnection...");
setDeviceConnectionState(State.WAITING_FOR_SCAN);
return true;
}
LOG.info("Enabling automatic ble reconnect...");
boolean result = mBluetoothGatt.connect();
mPauseTransaction = false;

View File

@ -79,6 +79,7 @@ public class GB {
public static final int NOTIFICATION_ID_EXPORT_FAILED = 5;
public static final int NOTIFICATION_ID_PHONE_FIND = 6;
public static final int NOTIFICATION_ID_GPS = 7;
public static final int NOTIFICATION_ID_SCAN = 8;
public static final int NOTIFICATION_ID_ERROR = 42;
private static final Logger LOG = LoggerFactory.getLogger(GB.class);

View File

@ -59,6 +59,9 @@ public class GBPrefs {
public static boolean AUTO_RECONNECT_DEFAULT = true;
public static final String PREF_ALLOW_INTENT_API = "prefs_key_allow_bluetooth_intent_api";
public static final String RECONNECT_SCAN_KEY = "prefs_general_key_auto_reconnect_scan";
public static final boolean RECONNECT_SCAN_DEFAULT = false;
public static final String USER_NAME = "mi_user_alias";
public static final String USER_NAME_DEFAULT = "gadgetbridge-user";
private static final String USER_BIRTHDAY = "";
@ -80,6 +83,10 @@ public class GBPrefs {
return deviceSpecificPreferences.getBoolean(DEVICE_AUTO_RECONNECT, AUTO_RECONNECT_DEFAULT);
}
public boolean getAutoReconnectByScan() {
return mPrefs.getBoolean(RECONNECT_SCAN_KEY, RECONNECT_SCAN_DEFAULT);
}
public boolean getAutoStart() {
return mPrefs.getBoolean(AUTO_START, AUTO_START_DEFAULT);
}

View File

@ -2684,4 +2684,8 @@
<string name="warning_missing_notification_permission">Could not post ongoing notification due to missing permission</string>
<string name="pref_test_features_title">Features</string>
<string name="pref_test_features_summary">Enabled features for this test device</string>
<string name="device_state_waiting_scan">Waiting for device scan</string>
<string name="auto_reconnect_ble_scan_title">Reconnect by BLE scan</string>
<string name="auto_reconnect_ble_scan_summary">Wait for device scan instead of blind connection attempts</string>
<string name="prompt_restart_gadgetbridge">Please restart GB in order to take effect.</string>
</resources>

View File

@ -23,4 +23,11 @@
android:summary="@string/discover_unsupported_devices_description"
android:title="@string/discover_unsupported_devices"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="prefs_general_key_auto_reconnect_scan"
android:title="@string/auto_reconnect_ble_scan_title"
android:summary="@string/auto_reconnect_ble_scan_summary"
android:layout="@layout/preference_checkbox"
app:iconSpaceReserved="false" />
</PreferenceScreen>