diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 90b26bde7..cc014a9dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -434,6 +434,12 @@ android:name=".devices.pinetime.PineTimeDFUService" android:label="PineTime Nordic DFU service" /> + + + { + GB.toast(GBApplication.getContext().getString(R.string.prompt_restart_gadgetbridge), Toast.LENGTH_LONG, GB.INFO); + return true; + }); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java index 2b254f334..1451d051f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -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; + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index f701ce681..51d00f795 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index ce9f0a0ec..c0d7cdbf2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -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 deviceStructs = new ArrayList<>(1); private final HashMap> 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 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 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 gbAllDevs = GBApplication.app().getDeviceManager().getDevices(); + Set 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 lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet()); + if (!lastDeviceAddresses.contains(btDeviceAddress)) { + lastDeviceAddresses = new HashSet(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 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 gbAllDevs = GBApplication.app().getDeviceManager().getDevices(); - Set 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 lastDeviceAddresses = prefs.getStringSet(GBPrefs.LAST_DEVICE_ADDRESSES, Collections.emptySet()); - if (!lastDeviceAddresses.contains(btDeviceAddress)) { - lastDeviceAddresses = new HashSet(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 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) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java index 95b5d06d6..a27165535 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupport.java @@ -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 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java index 70effe0c3..78c17c0ef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/ServiceDeviceSupport.java @@ -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(); 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 7ecd3ce26..834b65fd4 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 @@ -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()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLEScanService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLEScanService.java new file mode 100644 index 000000000..db3823adb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLEScanService.java @@ -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 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 scanFilters = null; + + if(applyFilters) { + List 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"); + } +} \ No newline at end of file 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 83bdb5695..8654086cc 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 @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index f711c58a9..7afd5aa42 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -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); 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 c1799e886..7294cf9ee 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GBPrefs.java @@ -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); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a1455b53..231d862c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2684,4 +2684,8 @@ Could not post ongoing notification due to missing permission Features Enabled features for this test device + Waiting for device scan + Reconnect by BLE scan + Wait for device scan instead of blind connection attempts + Please restart GB in order to take effect. diff --git a/app/src/main/res/xml/discovery_pairing_preferences.xml b/app/src/main/res/xml/discovery_pairing_preferences.xml index c21b4f7a5..9c427ae3d 100644 --- a/app/src/main/res/xml/discovery_pairing_preferences.xml +++ b/app/src/main/res/xml/discovery_pairing_preferences.xml @@ -23,4 +23,11 @@ android:summary="@string/discover_unsupported_devices_description" android:title="@string/discover_unsupported_devices" app:iconSpaceReserved="false" /> +