TemperatureSyncer is the only syncer implementing HealthConnectSyncer
directly instead of inheriting from AbstractTimeSampleSyncer or
AbstractActivitySampleSyncer, both of which populate latestRecordTimestamp
in their base sync(). Its hand-rolled sync() never set the field, so the
orchestrator had nothing to advance the persisted cursor with and the
TEMPERATURE cursor stayed pinned at its starting value. Every sync then
re-read from that frozen point to now and re-inserted the whole span; the
per-record clientRecordId dedup hid the duplicates in Health Connect, so it
was silent but re-inserted weeks of records on each run.
Return the furthest-forward edge of the inserted records (body record time,
skin record endTime) as latestRecordTimestamp on the success path. The no-op
early returns keep it null, which correctly holds the cursor, matching every
other syncer's "nothing synced" behaviour. The cursor self-heals on the next
successful sync; no reset needed.
LOG.atLevel(logLevel).log(...) in NativeFITMessage.getFieldDefinition
uses the SLF4J 2.0 fluent API, which logback-android 3.0.0 does not
implement. This throws AbstractMethodError at runtime, crashing the
FitAsyncProcessor thread and aborting the entire Garmin device sync
when a .FIT file triggers the type/size mismatch path.
Replace with a switch dispatching to LOG.debug/info/warn.
Health Connect enforces hard min/max bounds on each record-type field; a
single out-of-range value caused the record constructor to throw and
killed the entire batch insert. The trigger was a Garmin Venu 3
producing HRV > 200 ms, but several syncers shared the same gap (no
upper bound, only > 0 or no validation at all).
Add explicit per-syncer numeric guards matching the bounds enforced by
HC's record constructors, drop out-of-range samples, log them with the
existing [HC_SYNC] prefix, and continue the sync.
Bounds applied (per-record HC enforcement):
- HRV: 1..200 ms
- VO2Max: 0..100 ml/kg/min
- Weight: 0..1000 kg
- BloodGlucose: 0..900.91 mg/dL (= 50 mmol/L) -- previously unvalidated
- RespiratoryRate: 0..1000 breaths/min
- HeartRate / RestingHR: 1..300 / 0..300 bpm (widened from 20..250 to
match HC's contract; downstream consumers can apply physiological
filters)
- SpO2: 0..100 %
- Steps: 1..1000000 per record
- ActiveCalories: 0..1000000 kcal per record
- Distance: 0..1000000 m per record
- Skin temperature delta: -30..30 °C
- Body temperature: existing physiological clamp 25..45 °C kept (HC has
no enforced bound)
Add Logger.skipOutOfRange extension in syncers/SyncerLogging.kt and
relax HC_SYNC_TAG visibility from private to internal so syncers can
share it.
HeartRateSync aggregates dropped-sample counts into one summary log
line per slice rather than per-sample, to avoid log spam from glitchy
firmware.
convertSample visibility relaxed from protected to internal in the two
abstract base syncers so the new boundary tests can call it directly.
30 new unit tests in SyncerRangeValidationTest cover boundary
conditions per syncer.
ExerciseRoute requires strictly increasing timestamps; duplicate-second
points from device GPS parsers (e.g. Xiaomi Smart Band 9 Pro outdoor
cycling) caused IllegalArgumentException at construction time, aborting
the workout sync entirely.
Extract route construction into buildSanitisedRoute, which:
- drops points outside the workout time window
- drops points with non-finite or out-of-range lat/lng (HC's Location
constructor enforces lat in [-90, 90], lng in [-180, 180])
- drops points with non-finite or negative hdop/vdop accuracy (HC
rejects negative accuracy)
- deduplicates points by Instant
- returns null if fewer than two usable points remain
- catches IllegalArgumentException as a safety net so future HC
invariants degrade gracefully (workout saves without route rather
than the whole sync aborting)
Logs dropped points with the [HC_SYNC] prefix so users can grep.
Only commit gap time to awakeSleepDuration when sleep actually resumes
(bridge confirmed), instead of speculatively counting all non-sleep
time as awake. Fixes total duration exceeding session span.
Fix dead code bug where MAX_WAKE_PHASE_LENGTH could never trigger
because Block A unconditionally nulled sleepStart before Block B
could check it. Non-sleep samples now only break a session if the
user has step activity or the gap exceeds 1 hour. Bridged gaps are
counted as awake sleep duration.
See: #5757
getActivityKindForSample was written before AWAKE_SLEEP existed in
ActivityKind (April 2024 vs August 2024), so decoded stage 5 (awake
during sleep) fell through to UNKNOWN. This caused the sleep overlay
to skip awake periods, which may fragment sleep sessions in
SleepAnalysis and result in missing or truncated Health Connect
sleep records.
See: https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/5920
RecordedWorkoutSyncer is only invoked for devices where supportsActivityTracks() is true,
and all such devices also support activity tracking — so the !supportsActivityTracking
guards were always false, making addHeartRateRecord and addStepsRecord unreachable.
HR and steps are already synced by HeartRateSyncer and StepsSyncer at activity level.
SleepSyncer was inserting the same sleep session in multiple slices when
the session spanned a slice boundary. The 1-day look-back caused the
session to be re-discovered in adjacent slices, and the overlap check
allowed it to be synced in each one.
Assign each session to the slice where its start time falls
([sliceStart, sliceEnd)). This ownership rule ensures each session is
synced exactly once regardless of how many slices it overlaps with.
Add a 12-hour look-forward to the DB query for SLEEP so that sessions
starting near the slice end are fetched with their full data, preventing
truncation at the slice boundary.
Fixes: https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/5873
RecordedWorkoutSyncer was writing heart rate, steps, and calorie records
that overlap with the per-minute data already synced by HeartRateSyncer,
StepsSyncer, and ActiveCaloriesSyncer in the ACTIVITY data type sync.
The supportsActiveCalories guard from 8c575535b only covered
processDetailedWorkout but not processAggregateWorkout, which is the
code path hit when no track file is available. No guards existed for
heart rate or steps in either path.
Skip workout-level aggregate HR and steps when supportsActivityTracking
(covered by ACTIVITY sync), and skip workout-level calories when
supportsActiveCalories (covered by ActiveCaloriesSyncer). Per-second HR
from detailed workout track data is always synced as it is more granular
than the per-minute activity HR.
The sync state cursor was advancing to the slice end timestamp
unconditionally, even when no records were written. Late-arriving data
(sleep, workouts, VO2Max, etc.) that arrives after the cursor has
already advanced past it would be permanently skipped.
Each syncer now reports the latest timestamp of records actually
inserted into Health Connect. The sync cursor advances to
max(currentPersisted, latestRecordTimestamp) instead of
sliceEndTimestamp. If no records were written for a slice, the cursor
stays put. This handles all late-arriving data generically without
needing per-data-type special-casing.
- Add "Today" and "distant past" option to time period filter dropdown
- Store selected filter in SharedPreferences for persistence across sessions
- Auto-restore last selected filter when opening workout filter settings
- Today filter shows workouts from midnight to current time, distant pass shows all workouts
Use ActivityKind.getCycleUnit() to determine if cadence should be synced.
Only sync StepsCadenceRecord for step-based activities (walking, running, hiking) and CyclingPedalingCadenceRecord for cycling activities.
Skip cadence syncing for activities with other cycle units (strokes, jumps, reps, swings) as Health Connect lacks appropriate cadence record types.
Note: Swimming workouts with stroke data (freestyle, backstroke, etc.) could be synced as ExerciseSegment records in a future enhancement.
- Fix calorie mapping: caloriesBurnt represents active calories, not total calories
- Parse RAW_SUMMARY_DATA when SUMMARY_DATA JSON is empty (fixes Amazfit/ZeppOS devices)
- Use device-specific ActivitySummaryParser to extract workout metrics from binary protobuf data
- Sync ActiveCaloriesBurnedRecord separately from total calories
- Add StepsRecord for step count during workouts
- Add intelligent cadence syncing (StepsCadenceRecord for running/walking, CyclingPedalingCadenceRecord for cycling)
- Add required Health Connect permissions to manifest
- Update HealthConnectPermissionManager with all workout permissions
- Add supportsWorkoutActivityTypesConfiguration() to enable workout settings for devices without display
- Add supportsWorkoutDetectionCategories() to control visibility of workout categories
- Update ZeppOsSettingsCustomizer to hide workout categories preference for devices that don't support it
- Enable workout detection sensitivity configuration for Amazfit Helio Strap
- Workout categories selector is hidden for Helio Strap, only alert and sensitivity settings are shown
This allows the Helio Strap to configure automatic workout detection sensitivity despite having no display.
Changed boundary checks from exclusive [start, end) to inclusive [start, end] to ensure samples occurring exactly at slice boundaries are synced correctly. This fixes the issue where the last sample in the database would never get synced because it was filtered out by the exclusive end boundary check while the sync state advanced to its timestamp.
Affected syncers:
- AbstractTimeSampleSyncer (HRV, SpO2, Weight, etc.)
- StepsSyncer
- HeartRateSyncer
- SleepSyncer
Changed getLastSampleTimestamp() for WORKOUTS to use workout EndTime instead of
StartTime. Previously, using StartTime caused the sync state to advance only to
the start of the last workout, resulting in that workout being re-synced on every
sync run and any new workouts started after it being missed until the next sync.
Now correctly advances to workout end time, ensuring proper incremental sync.
- Add PERMISSION_WRITE_EXERCISE_ROUTE import to HealthConnectPermissionManager
- Include ElevationGainedRecord, PowerRecord, SpeedRecord, and ExerciseRoute permissions in requiredHealthConnectPermissions
- Add WORKOUTS enum to HealthConnectDataType with comprehensive permission mapping
- Handle WORKOUTS branches in HealthConnectUtils when expressions for exhaustiveness
Body Temperature
Skin Temperature
Distance
Exercise
Heart rate
Heart rate variability
Oxygen saturation
Sleep
Steps
Total calories burned
VO2 max
Weight
The feature is configurable via Settings->External Integrations->Health Connect
Based on https://codeberg.org/LLan/Gadgetbridge
Kudos to LLan for the preliminary work!
Implements HuamiHrvSummarySampleProvider to compute HRV summary statistics
on-demand from per-minute HRV value samples. Calculates weekly averages,
last night values, baselines, and HRV status (LOW/UNBALANCED/BALANCED).
This enables full HRV dashboard and status screen functionality for devices
that only provide raw HRV measurements without pre-computed summaries.
- Created HuamiHrvSummarySampleProvider with computed summary logic
- Added getHrvSummarySampleProvider() override in HuamiCoordinator
- Computes 7-day rolling average, last-night stats, and 14-day baselines
- Works retroactively with existing historical HRV data