diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java index a14788adf..47268c48e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DebugActivity.java @@ -1005,10 +1005,15 @@ public class DebugActivity extends AbstractGBActivity { return; } DeviceType deviceType = DeviceType.values()[(int) deviceKey]; + String deviceName = deviceType.name(); + int deviceNameResource = deviceType.getDeviceCoordinator().getDeviceNameResource(); + if(deviceNameResource != 0){ + deviceName = context.getString(deviceNameResource); + } try ( DBHandler db = GBApplication.acquireDB()) { DaoSession daoSession = db.getDaoSession(); - GBDevice gbDevice = new GBDevice(deviceMac, deviceType.name(), "", null, deviceType); + GBDevice gbDevice = new GBDevice(deviceMac, deviceName, "", null, deviceType); gbDevice.setFirmwareVersion("N/A"); gbDevice.setFirmwareVersion2("N/A"); @@ -1018,7 +1023,7 @@ public class DebugActivity extends AbstractGBActivity { Device device = DBHelper.getDevice(gbDevice, daoSession); //the addition happens here Intent refreshIntent = new Intent(DeviceManager.ACTION_REFRESH_DEVICELIST); LocalBroadcastManager.getInstance(context).sendBroadcast(refreshIntent); - GB.toast(context, "Added test device: " + deviceType.name(), Toast.LENGTH_SHORT, GB.INFO); + GB.toast(context, "Added test device: " + deviceName, Toast.LENGTH_SHORT, GB.INFO); } catch ( Exception e) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index dd435cbf6..455eaa649 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -258,6 +258,16 @@ public class GBDeviceAdapterv2 extends ListAdapter= Build.VERSION_CODES.R) { createDynamicShortcut(device); } - GBApplication.deviceService(device).connect(); + handleDeviceConnect(device); } } }); @@ -853,7 +863,7 @@ public class GBDeviceAdapterv2 extends ListAdapter createBLEScanFilters() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 4e7b66ecc..1e57bfe38 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -123,6 +123,14 @@ public interface DeviceCoordinator { */ ConnectionType getConnectionType(); + /** + * Returns false is the Device is not connectable, + * only scannable, like beacons + * + * @return boolean + */ + boolean isConnectable(); + /** * Checks whether this coordinator handles the given candidate. * diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/scannable/ScannableDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/scannable/ScannableDeviceCoordinator.java new file mode 100644 index 000000000..97eae6bc0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/scannable/ScannableDeviceCoordinator.java @@ -0,0 +1,61 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.scannable; + +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; + +public class ScannableDeviceCoordinator extends AbstractBLEDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Override + public int[] getSupportedDeviceSpecificApplicationSettings() { + return new int[]{ + R.xml.devicesettings_scannable + }; + } + + @Override + public boolean isConnectable() { + return false; + } + + @Override + public String getManufacturer() { + return "unknown"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return null; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_scannable; + } + + @Override + public int getBatteryCount() { + return 0; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_scannable; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_scannable_disabled; + } +} 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 847069248..55308c9b8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDevice.java @@ -310,7 +310,7 @@ public class GBDevice implements Parcelable { } public boolean isConnected() { - return mState.equalsOrHigherThan(State.CONNECTED); + return mState == State.SCANNED || mState.equalsOrHigherThan(State.CONNECTED); } public boolean isInitializing() { @@ -318,7 +318,7 @@ public class GBDevice implements Parcelable { } public boolean isInitialized() { - return mState.equalsOrHigherThan(State.INITIALIZED); + return mState == State.SCANNED || mState.equalsOrHigherThan(State.INITIALIZED); } public boolean isConnecting() { @@ -728,6 +728,7 @@ public class GBDevice implements Parcelable { NOT_CONNECTED(R.string.not_connected), WAITING_FOR_RECONNECT(R.string.waiting_for_reconnect), WAITING_FOR_SCAN(R.string.device_state_waiting_scan), + SCANNED(R.string.state_scanned), CONNECTING(R.string.connecting), CONNECTED(R.string.connected, R.string.connecting), INITIALIZING(R.string.initializing, R.string.connecting), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index c43462951..33b098bbe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -151,6 +151,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.smaq2oss.SMAQ2OSSCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.soflow.SoFlowCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyLinkBudsCoordinator; @@ -374,6 +375,7 @@ public enum DeviceType { SONY_WENA_3(SonyWena3Coordinator.class), FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class), PIXOO(PixooCoordinator.class), + SCANNABLE(ScannableDeviceCoordinator.class), TEST(TestDeviceCoordinator.class); private DeviceCoordinator coordinator; 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 26ccda156..aa60908ce 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -255,6 +255,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere private OsmandEventReceiver mOsmandAidlHelper = null; + private HashMap deviceLastScannedTimestamps = new HashMap<>(); + private final String[] mMusicActions = { "com.android.music.metachanged", "com.android.music.playstatechanged", @@ -270,21 +272,26 @@ public class DeviceCommunicationService extends Service implements SharedPrefere private final String COMMAND_BLUETOOTH_CONNECT = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECT"; private final String ACTION_DEVICE_CONNECTED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_CONNECTED"; + private final String ACTION_DEVICE_SCANNED = "nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_SCANNED"; 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){ + private void sendDeviceAPIBroadcast(String address, String action){ if(!allowBluetoothIntentApi){ GB.log("not sending API event due to settings", GB.INFO, null); return; } - Intent intent = new Intent(ACTION_DEVICE_CONNECTED); + Intent intent = new Intent(action); intent.putExtra("EXTRA_DEVICE_ADDRESS", address); sendBroadcast(intent); } + private void sendDeviceConnectedBroadcast(String address){ + sendDeviceAPIBroadcast(address, ACTION_DEVICE_CONNECTED); + } + BroadcastReceiver bluetoothCommandReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -368,6 +375,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere LOG.debug("device state update reason"); sendDeviceConnectedBroadcast(device.getAddress()); sendCachedNotifications(device); + }else if(subject == GBDevice.DeviceUpdateSubject.CONNECTION_STATE && (device.getState() == GBDevice.State.SCANNED)){ + sendDeviceAPIBroadcast(device.getAddress(), ACTION_DEVICE_SCANNED); } }else if(BLEScanService.EVENT_DEVICE_FOUND.equals(action)){ String deviceAddress = intent.getStringExtra(BLEScanService.EXTRA_DEVICE_ADDRESS); @@ -382,6 +391,44 @@ public class DeviceCommunicationService extends Service implements SharedPrefere return; } + if(!target.getDeviceCoordinator().isConnectable()){ + int actualRSSI = intent.getIntExtra(BLEScanService.EXTRA_RSSI, 0); + Prefs prefs = new Prefs( + GBApplication.getDeviceSpecificSharedPrefs(target.getAddress()) + ); + long timeoutSeconds = prefs.getLong("devicesetting_scannable_debounce", 60); + long minimumUnseenSeconds = prefs.getLong("devicesetting_scannable_unseen", 0); + int thresholdRSSI = prefs.getInt("devicesetting_scannable_rssi", -100); + + if(actualRSSI < thresholdRSSI){ + LOG.debug("ignoring {} since RSSI is too low ({} < {})", deviceAddress, actualRSSI, thresholdRSSI); + return; + } + + Long lastSeenTimestamp = deviceLastScannedTimestamps.get(deviceAddress); + deviceLastScannedTimestamps.put(deviceAddress, System.currentTimeMillis()); + + if(lastSeenTimestamp != null){ + long secondsSince = (System.currentTimeMillis() - lastSeenTimestamp) / 1000; + if(secondsSince < minimumUnseenSeconds){ + LOG.debug("ignoring {}, since only {} seconds passed (< {})", deviceAddress, secondsSince, minimumUnseenSeconds); + return; + } + } + + target.setState(GBDevice.State.SCANNED); + target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.CONNECTION_STATE); + new Handler().postDelayed(() -> { + if(target.getState() != GBDevice.State.SCANNED){ + return; + } + deviceLastScannedTimestamps.put(target.getAddress(), System.currentTimeMillis()); + target.setState(GBDevice.State.WAITING_FOR_SCAN); + target.sendDeviceUpdateIntent(DeviceCommunicationService.this, GBDevice.DeviceUpdateSubject.CONNECTION_STATE); + }, timeoutSeconds * 1000); + return; + } + connectToDevice(target); } } @@ -501,6 +548,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } private void connectToDevice(GBDevice device, boolean firstTime){ + if(!device.getDeviceCoordinator().isConnectable()){ + GB.toast("Cannot connect to Scannable Device", Toast.LENGTH_SHORT, GB.INFO); + return; + } + List gbDevs = null; boolean fromExtra = false; 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 index 56bedcdfb..e47551af2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLEScanService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLEScanService.java @@ -62,6 +62,7 @@ public class BLEScanService extends Service { public static final String EXTRA_DEVICE = "EXTRA_DEVICE"; public static final String EXTRA_DEVICE_ADDRESS = "EXTRA_DEVICE_ADDRESS"; + public static final String EXTRA_RSSI = "EXTRA_RSSI"; // 5 minutes scan restart interval private final int DELAY_SCAN_RESTART = 5 * 60 * 1000; @@ -100,6 +101,7 @@ public class BLEScanService extends Service { Intent intent = new Intent(EVENT_DEVICE_FOUND); intent.putExtra(EXTRA_DEVICE_ADDRESS, device.getAddress()); + intent.putExtra(EXTRA_RSSI, result.getRssi()); localBroadcastManager.sendBroadcast(intent); // device found, attempt connection diff --git a/app/src/main/res/drawable/ic_device_scannable.xml b/app/src/main/res/drawable/ic_device_scannable.xml new file mode 100644 index 000000000..ba39de948 --- /dev/null +++ b/app/src/main/res/drawable/ic_device_scannable.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_device_scannable_disabled.xml b/app/src/main/res/drawable/ic_device_scannable_disabled.xml new file mode 100644 index 000000000..3518e3caa --- /dev/null +++ b/app/src/main/res/drawable/ic_device_scannable_disabled.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e501c5bbe..f708ff271 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2711,4 +2711,12 @@ This device is already bound in Android settings, which can make pairing fail for some devices.\n\nIf adding the device fails, please remove it in Android settings and try again. Companion device Pair this device as companion?.\n\nThis is recommended for some functions such as find device, and provides a better connection. + Scannable device + Scanned + Scannable debounce timeout (seconds) + Minimum unseen time (seconds) + Minimal RSSI threshold + After being scanned, the device will stick as scanned and ignored for the specified amount of time + After being scanned, the device has to be unseen for this amount of time before being registered again + The minimum RSSI threshold for detection diff --git a/app/src/main/res/xml/devicesettings_scannable.xml b/app/src/main/res/xml/devicesettings_scannable.xml new file mode 100644 index 000000000..40793c625 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_scannable.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file