mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Health Connect: Debounce event-triggered sync and fix resting HR lower bound
Two robustness fixes for the event-triggered Health Connect sync. Debounce: an activity fetch can broadcast ACTION_NEW_DATA several times in quick succession - multi-phase fetches and some chatty drivers emit the finish signal mid-fetch - and each broadcast immediately enqueued a sync. A sync that runs while the fetch is still in progress reads a half-populated database and can drop or duplicate records for the in-flight range. Enqueue the per-device worker with a short initial delay and ExistingWorkPolicy.REPLACE, so a burst of broadcasts collapses into one sync that runs once the fetch has settled. The per-device work name is unchanged (devices debounce independently) and the manual/debug sync paths use separate work names, so "Sync now" is unaffected. Resting heart rate: the platform RestingHeartRateRecord rejects bpm < 1, but the filter allowed 0..300, so a 0 bpm sample threw IllegalArgumentException and aborted that data type's slice. Filter 1..300 instead, matching the regular HeartRate syncer. Update the existing boundary test accordingly.
This commit is contained in:
+11
-4
@@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@@ -43,6 +44,11 @@ import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectSync
|
||||
|
||||
public class NewDataReceiver extends BroadcastReceiver {
|
||||
private static final String HEALTH_CONNECT_SYNC_WORKER_TAG = "HealthConnectSyncWorker";
|
||||
// Debounce window: a fetch can emit several ACTION_NEW_DATA broadcasts in quick succession
|
||||
// (multi-phase or chatty drivers fire mid-fetch), and a sync that runs against a half-fetched
|
||||
// database can miss or duplicate records. Delay the sync and restart the timer on every new
|
||||
// broadcast (REPLACE), so a single sync runs once the fetch has settled.
|
||||
private static final long DEBOUNCE_SECONDS = 10;
|
||||
private final Logger LOG = LoggerFactory.getLogger(NewDataReceiver.class);
|
||||
private Context context;
|
||||
private boolean registered = false;
|
||||
@@ -91,12 +97,12 @@ public class NewDataReceiver extends BroadcastReceiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// For device-specific syncs, use a device-specific work name and APPEND policy
|
||||
// This ensures each device's HC sync is queued and executed sequentially
|
||||
// Per-device unique work name so each device debounces independently.
|
||||
String workName = HEALTH_CONNECT_SYNC_WORKER_TAG + "_" + deviceAddress;
|
||||
|
||||
OneTimeWorkRequest syncRequest = new OneTimeWorkRequest.Builder(HealthConnectSyncWorker.class)
|
||||
.addTag(HEALTH_CONNECT_SYNC_WORKER_TAG)
|
||||
.setInitialDelay(DEBOUNCE_SECONDS, TimeUnit.SECONDS)
|
||||
.setInputData(
|
||||
new Data.Builder()
|
||||
.putString(HealthConnectSyncWorker.INPUT_DEVICE_ADDRESS, deviceAddress)
|
||||
@@ -106,10 +112,11 @@ public class NewDataReceiver extends BroadcastReceiver {
|
||||
|
||||
LOG.debug("Scheduling HC sync for device: {} with work name: {}", deviceAddress, workName);
|
||||
|
||||
// Use APPEND so multiple syncs for the same device queue up
|
||||
// REPLACE so a burst of broadcasts during one fetch cancels the pending (delayed) sync
|
||||
// and re-arms it; the sync runs once, DEBOUNCE_SECONDS after the last broadcast.
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
workName,
|
||||
ExistingWorkPolicy.APPEND,
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
syncRequest
|
||||
);
|
||||
}
|
||||
|
||||
+4
-3
@@ -45,9 +45,10 @@ internal object RestingHeartRateSyncer : AbstractTimeSampleSyncer<HeartRateSampl
|
||||
metadata: Metadata,
|
||||
deviceName: String
|
||||
): RestingHeartRateRecord? {
|
||||
// HC enforces 0..300 bpm; exclude 255, the bad-measurement sentinel GB uses for HR values.
|
||||
if (sample.heartRate !in 0..300 || sample.heartRate == 255) {
|
||||
logger.skipOutOfRange(deviceName, "RestingHeartRate", sample.heartRate, "0..300 bpm (255 excluded)")
|
||||
// HC enforces 1..300 bpm (the platform RestingHeartRateRecord rejects 0, unlike the
|
||||
// androidx client); also exclude 255, the bad-measurement sentinel GB uses for HR values.
|
||||
if (sample.heartRate !in 1..300 || sample.heartRate == 255) {
|
||||
logger.skipOutOfRange(deviceName, "RestingHeartRate", sample.heartRate, "1..300 bpm (255 excluded)")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -191,7 +191,12 @@ class SyncerRangeValidationTest {
|
||||
|
||||
@Test
|
||||
fun restingHr_lowerBoundary_accepted() {
|
||||
assertNotNull(RestingHeartRateSyncer.convertSample(hrSample(0), offset, metadata, device))
|
||||
assertNotNull(RestingHeartRateSyncer.convertSample(hrSample(1), offset, metadata, device))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun restingHr_zero_dropped() {
|
||||
assertNull(RestingHeartRateSyncer.convertSample(hrSample(0), offset, metadata, device))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user