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.
The Bip 6 widget shows "open Zepp to get weather" when our v5 response
omits or empty-stubs any dataset the firmware asked for. Fixes three gaps:
- Implement createDailyAirQuality with a 7-day skeleton (placeholder
AQI 50, level "good"; real ingestion gated on WeatherSpec extension).
- Replace EmptyResponse "{}" for hourlyAirQuality and dailyTide with
populated shape stubs (metadata + empty items list).
- Add a defensive else branch for unknown dataset names that returns a
metadata+empty-items skeleton and logs the key at WARN, so future
firmware additions surface in logs without silently dropping keys.
See docs/bip6-weather-widget-research.md for the diagnosis.
Source: https://github.com/PeterXMR/Gadgetbridge
(cherry picked from commit 8b9d4f319ecc69dcb3c9ebbe77c35a06df7ebbbc)
The v5 place dataset hard-coded latitude/longitude to "0.0" and used a
constant locationKey "accu:123456", which prevented watches that were
never provisioned via the official Zepp app (Bip 6, Active 2, ...) from
accepting the response as authoritative — they treated the zero coords
as a missing-location signal and silently refused to display weather
(issue #5653).
- ZeppOsWeatherHandlerV5.createPlace: pull lat/lon from WeatherSpec
when set, else fall back to the user-configured location pref. Format
with Locale.ROOT and derive a stable per-location locationKey from a
hash of the coords so the watch can dedupe responses.
- ZeppOsWeatherService.onSendWeather: same fallback chain for the
CMD_SET_DEFAULT_LOCATION packet so the watch's default-location state
is bootstrapped with real coordinates instead of "1.234,-5.678".
Refs #5653.
source: https://github.com/PeterXMR/Gadgetbridge
(cherry picked from commit be87ac3364fbf5725f43e3dbac87ef2e103fdd89)
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.
The per-minute steps, calories and distance records set clientRecordVersion
to the metric value itself. Health Connect keeps the record with the highest
clientRecordVersion on a clientRecordId collision (newVersion >= existing
overwrites), so each minute became pinned to the largest value ever sent for
it. When a device delivers activity in two phases and revises a minute's value
downward between them, the corrected record carries a lower version and is
silently ignored, leaving Health Connect above the device's own total.
Stamp clientRecordVersion with the sync run's wall-clock instead, shared by
every record in the slice. A later run always outranks the value it previously
wrote for a minute, so the freshest re-read wins regardless of whether the
value rose or fell. The clientRecordId remains the dedup key and is unchanged.
The ACTIVITY sync cursor is a single (deviceId, dataType) timestamp shared by
the steps, active-calories and distance syncers. The cursor advances to the
latest record produced by any of them. When a device delivers calorie/distance
detail for the most recent minutes before the matching step detail, the cursor
moves past those minutes on the calorie front; the step minutes that arrive on
the next sync are then clipped by the slice start boundary and lost forever.
Resetting the HC sync state recovered them, confirming the data was present in
the database but skipped.
Give the per-minute steps, calories and distance records a deterministic
clientRecordId (gb-{type}-{manufacturer}-{model}-{endEpochSecond}) so that
re-emitting a record upserts instead of duplicating, mirroring SleepSyncer.
With records now idempotent, extend the per-syncer slice lower bound backwards
by a one-hour lookback so the trailing window is re-emitted on the next sync
and the late minutes are recovered. Heart rate is a grouped series keyed on its
start time, cannot be deduped this way, and does not extend the shared base, so
it keeps strict boundaries and never re-emits.
The Xiaomi two-phase activity fetch (today + past) could fire the data-finish
signal that triggers downstream consumers before all DETAILS files had been
parsed, so a consumer reading the database in that gap saw an incomplete
picture. Hold the finish signal until both fetch phases have delivered and
all files are parsed.
Use an AtomicBoolean for the fetch-hold flag and set awaitingPastResponse
before the fetch starts to avoid signalling between the two phases.