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.Collections;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
@@ -43,6 +44,11 @@ import nodomain.freeyourgadget.gadgetbridge.util.healthconnect.HealthConnectSync
|
|||||||
|
|
||||||
public class NewDataReceiver extends BroadcastReceiver {
|
public class NewDataReceiver extends BroadcastReceiver {
|
||||||
private static final String HEALTH_CONNECT_SYNC_WORKER_TAG = "HealthConnectSyncWorker";
|
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 final Logger LOG = LoggerFactory.getLogger(NewDataReceiver.class);
|
||||||
private Context context;
|
private Context context;
|
||||||
private boolean registered = false;
|
private boolean registered = false;
|
||||||
@@ -91,12 +97,12 @@ public class NewDataReceiver extends BroadcastReceiver {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For device-specific syncs, use a device-specific work name and APPEND policy
|
// Per-device unique work name so each device debounces independently.
|
||||||
// This ensures each device's HC sync is queued and executed sequentially
|
|
||||||
String workName = HEALTH_CONNECT_SYNC_WORKER_TAG + "_" + deviceAddress;
|
String workName = HEALTH_CONNECT_SYNC_WORKER_TAG + "_" + deviceAddress;
|
||||||
|
|
||||||
OneTimeWorkRequest syncRequest = new OneTimeWorkRequest.Builder(HealthConnectSyncWorker.class)
|
OneTimeWorkRequest syncRequest = new OneTimeWorkRequest.Builder(HealthConnectSyncWorker.class)
|
||||||
.addTag(HEALTH_CONNECT_SYNC_WORKER_TAG)
|
.addTag(HEALTH_CONNECT_SYNC_WORKER_TAG)
|
||||||
|
.setInitialDelay(DEBOUNCE_SECONDS, TimeUnit.SECONDS)
|
||||||
.setInputData(
|
.setInputData(
|
||||||
new Data.Builder()
|
new Data.Builder()
|
||||||
.putString(HealthConnectSyncWorker.INPUT_DEVICE_ADDRESS, deviceAddress)
|
.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);
|
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(
|
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||||
workName,
|
workName,
|
||||||
ExistingWorkPolicy.APPEND,
|
ExistingWorkPolicy.REPLACE,
|
||||||
syncRequest
|
syncRequest
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -45,9 +45,10 @@ internal object RestingHeartRateSyncer : AbstractTimeSampleSyncer<HeartRateSampl
|
|||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
deviceName: String
|
deviceName: String
|
||||||
): RestingHeartRateRecord? {
|
): RestingHeartRateRecord? {
|
||||||
// HC enforces 0..300 bpm; exclude 255, the bad-measurement sentinel GB uses for HR values.
|
// HC enforces 1..300 bpm (the platform RestingHeartRateRecord rejects 0, unlike the
|
||||||
if (sample.heartRate !in 0..300 || sample.heartRate == 255) {
|
// androidx client); also exclude 255, the bad-measurement sentinel GB uses for HR values.
|
||||||
logger.skipOutOfRange(deviceName, "RestingHeartRate", sample.heartRate, "0..300 bpm (255 excluded)")
|
if (sample.heartRate !in 1..300 || sample.heartRate == 255) {
|
||||||
|
logger.skipOutOfRange(deviceName, "RestingHeartRate", sample.heartRate, "1..300 bpm (255 excluded)")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -191,7 +191,12 @@ class SyncerRangeValidationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun restingHr_lowerBoundary_accepted() {
|
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
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user