PR #6195 widened the syncer guards to match Health Connect's enforced
bounds, but widened too much: it overlooked that some of Gadgetbridge's
in-band sentinel values fall inside HC's accepted range, so placeholder
readings leaked into Health Connect and showed up as spurious data points.
- HeartRate / RestingHeartRate: 255 is GB's "illegal value" marker for a
bad measurement (ActivitySample.getHeartRate), and it sits inside HC's
1..300 / 0..300 range. Exclude it explicitly while keeping HC's bounds.
- SpO2: 0 means "not measured" (sample providers return null on 0 and all
SpO2 charts filter getSpo2() > 0). Raise the lower bound to 1.
Add HeartRateSyncerTest covering the HeartRateSyncer.sync() path (sentinel
255 and 0 are not inserted; valid range passes) and extend
SyncerRangeValidationTest with the new boundaries.
Three lifecycle bugs in GarminSupport's transferNotification:
* addFileToDownloadList incremented totalSize unconditionally, so a directory listing arriving on connect posted a notification with the unlocalised "Unknown transfer" fallback title before any transfer started.
* The start/finish pair gated on gbDevice.isBusy(), which other paths set and clear — start could be skipped while the matching finish ran anyway, or vice versa.
* dispose() didn't finish the notification, leaving it pinned after a mid-sync disconnect.
Gate the increments and the start/finish pair on the private isBusyFetching flag, and finish + clear the flag in dispose().
Long-running per-line fetches (e.g. ExploreSync's catalog walk) hold the device's busy task for the entire session, so the busy→idle edge WorkoutListActivity refreshes on only fires at session end. Listen for ACTION_NEW_DATA too so each newly imported activity appears as it lands.
Add a silent loadSummaries path that skips the isLoading flips, and disable the RecyclerView change animator so the dashboard row's notifyItemChanged(0) doesn't cross-fade — both would otherwise read as a distracting blink during an active sync.
Wires the new ExploreSync sub-service into ProtocolBufferHandler and GarminSupport so a paired device's historical-line catalog can be fetched independently of FIT — which Garmin Connect Mobile irreversibly archives files out of after the first sync (see issues like #5694 and #5994). Activities the FIT path can no longer see become visible again here.
Each historical line is reconstructed into an ActivityTrack, written as GPX, and persisted as a BaseActivitySummary keyed on the first-point timestamp — the same surface FIT-imported workouts use, so UI and exporters need no ExploreSync awareness. GarminCoordinator now picks the activity track provider from the summary contents (gpxTrack vs rawDetailsPath) instead of hard-coding the FIT one.
Three call sites (FileTransferHandler, FitImporter, GarminSupport) each open-coded the same [TYPE]/[YEAR]/[TYPE]_[timestamp]_[suffix] filename shape and re-created their own SimpleDateFormat. Hoist the formatters (now thread-safe DateTimeFormatter) and the path builder into GarminUtils as buildExportPath(type, instant, suffix, ext).
FitImporter's null-type fallback now uses "NULL" consistently for both directory and filename, fixing a latent NPE.
Every watch-bound Smart RPC site followed the same shape: sendOutgoingMessage(taskName, protocolBufferHandler.prepareProtobufRequest(payload)). Fold it into a single sendProtobufRequest(taskName, payload) on GarminSupport. No behaviour change.
The watch encodes lat/lon as signed 32-bit semicircles (1<<31 == 180°). Move the bidirectional conversion behind named helpers backed by a single constant, and migrate toLocationData and FieldDefinitionCoordinate onto them.
- harmonise extra `address` to `device` - the old extra `address` logs a warning but is still supported
- support optional `device` extra for action nodomain.freeyourgadget.gadgetbridge.command.ACTIVITY_SYNC
- support optional `device` extra for action nodomain.freeyourgadget.gadgetbridge.command.DEBUG_SEND_NOTIFICATION
- support optional `device` extra for action nodomain.freeyourgadget.gadgetbridge.command.DEBUG_INCOMING_CALL
- support optional `device` extra for action nodomain.freeyourgadget.gadgetbridge.command.DEBUG_END_CALL
- support ComponentName extras for action nodomain.freeyourgadget.gadgetbridge.command.DEBUG_TEST_NEW_FUNCTION
- code review
See Freeyourgadget/website#247 for the documentation update.
EventHandler.onTestNewFunction
- add optional `options` Bundle parameter for receiving arguments
intent nodomain.freeyourgadget.gadgetbridge.command.DEBUG_TEST_NEW_FUNCTION
- add optional String extra `address` to specify the MAC address of the target device
- add optional Bundle extra `options` to pass arguments to EventHandler.onTestNewFunction
- construct a synthetic `options` bundle if the `options` extra is missing but there are `options_...` extras
Global setting `Settings / Developer options / Intent API / Allow Debug Command` must be enabled and the device(s) connected for the following examples.
Example to trigger onTestNewFunction for all currently connected devices:
`adb shell am broadcast -p nodomain.freeyourgadget.gadgetbridge -a "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_TEST_NEW_FUNCTION"`
Example to trigger onTestNewFunction for a specific device:
`adb shell am broadcast -p nodomain.freeyourgadget.gadgetbridge -a "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_TEST_NEW_FUNCTION" --es address "12:34:56:78:9A:BC" `
Example to trigger onTestNewFunction for a specific device with an option Bundle containing float x=4.2 and integer array y=[1,2,3] extras:
`adb.exe shell am broadcast -p nodomain.freeyourgadget.gadgetbridge -a "nodomain.freeyourgadget.gadgetbridge.command.DEBUG_TEST_NEW_FUNCTION" --es address "31:31:43:30:40:07" --ef options_x 4.2 --eia options_y 1,2,3`
Extras for some other types can also be specified via command line. See https://developer.android.com/tools/adb#IntentSpec for details.
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.