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.
Analog to IntentApiReceiver enhancement #6250
Adds an optional `device` extra containing the MAC address of the target gadget.
Missing `device` extra or a `null` value forward the intent payload to all initialised BangleJS.
The existing device preference filter (device_intents == true) remains unchanged.
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.