tracelet
Production-grade background geolocation for Flutter. Battery-conscious tracking, geofencing, SQLite persistence, HTTP sync, and headless execution for iOS & Android.
3.8.7
9.5K downloads/mo
#3006 most downloaded on pub.dev
Ikolvi/Tracelet
What this package is like to depend on
Last release 7 days ago
16 Aug 2026
Ships on a steady schedule
a new release about every 8 days
Nearly every release is documented
notes for 127 of 128 stable releases
41 versions withdrawn
withdrawn after publishing
6 months old
172 releases · first in 2026
172 releases in the last 12 months
see the full history below
Release timeline
172 releases · Feb 2026 to Aug 2026Releases
latest 60 of 172-
3.8.716 Aug 2026Release notes
Open source →FIX: (Android, iOS) the events that explain a background tracking failure are recorded on the always-on lifecycle channel, so a release build can report them. A released app runs at the default
logLevel, where everydebugline is discarded — which is why diagnosing the last round took repeated captures and a source read rather than one report. Now recorded regardless of level, and all per-session rather than per-fix: the continuous location stream starting and stopping (the transition the OS location indicator follows, so "the icon disappeared" is answerable); the app moving to background and to foreground — iOS never observed the background edge at all, so a report could show the app coming back but never leaving; foreground-service promotion and demotion, naming the demotion window in which a task removal is fatal; a pace machine refused a stale fix's speed, and the moment it gets a current one again; and a session start that declined to seed the pace machine because no fix had been resolved yet. The stale-fix lines are emitted once per run rather than once per fix, so a long run of them costs one line.FIX: (Android, iOS) a stale cached fix can no longer stand a session down the moment it wakes. Both platforms deliver a cached last-known fix as soon as location updates restart, carrying the speed from whenever it was taken — which, on a session the accelerometer has just woken, is from before the device stopped. The field trace shows the wake and the stand-down in the same second:
speed-motion: STATIONARY -> MOVING — manual pace changeimmediately followed byMOVING -> SLOWING — speed=0.10 m/s, then STATIONARY again 30 s later. Walking with the app backgrounded or killed therefore produced a cycle — the accelerometer wakes the stream, a stale reading stands it down, the stream stops — rather than tracking, with the location indicator flickering off and staying off. Only a fix less than ten seconds old may now drive the pace machine. Persistence and dispatch are untouched: a cached fix is still a real position, and the processor's own gates decide whether to keep it; it is only its speed that says nothing about the present.FIX: (Android, iOS) tracking survives being backgrounded or killed while moving. On startup the speed state machine was seeded with
LocationEngine.lastEffectiveSpeed, which is0.0on a process that has not yet handled a fix — exactly the state a killed-state relaunch or a background takeover begins in. A session that had just resumed as MOVING was therefore told it was stopped: it dropped to SLOWING immediately and,speedStationaryDelaylater, to STATIONARY, which switches off the continuous stream. The device had not moved differently and nothing else had changed; the location indicator simply disappeared shortly after the app left the screen. A field trace shows it twice in one minute —boot-tracking: bootstrapping … speedState=MOVINGfollowed within the same second byMOVING -> SLOWING — speed=0.00 m/s, and 30 s laterSLOWING -> STATIONARY — countdown expired.0.0means "no speed reported", not "stopped", so the machine is now seeded only when this process has actually resolved a speed. The same fabricated zero is gone from the periodic-fix path, which fed0.0into the machine whenever a periodic fix arrived without a speed.FIX: (Android, iOS) a walking user is no longer classified as stationary.
MotionConfig.speedMovingThresholddefaulted to 1.5 m/s — above an average walking pace of ~1.4 m/s — and a single threshold governed both directions with no band between them, so an ordinary walk straddled it: wake at 1.50-1.55 m/s, drop to SLOWING a second later at 1.31-1.48, run the countdown to STATIONARY with 26-28 consecutive fixes just below, wake again. Reaching STATIONARY switches the session to periodic fixes, which is what a walking user reports as "tracking stopped on its own". The entry threshold is now 0.9 m/s, comfortably below a slow walk and well above the 0.1-0.3 m/s a parked device reports from GPS noise, and leaving MOVING uses a separate, lower threshold — the newMotionConfig.speedStationaryThreshold, defaulting to 65 % of the entry one and clamped to it if set higher. The gap between the two is a hysteresis band, so a pace that varies either side of the entry threshold no longer oscillates. Both values are configurable; apps that tunedspeedMovingThresholdthemselves are unaffected.FIX: (Android) a stationary session that declines a wake can still be woken by the next one.
TYPE_SIGNIFICANT_MOTIONis a one-shot trigger sensor — firing consumes the registration — and the wake path tore down both wake sources before asking the motion coordinator what to do with the event. When the coordinator declined it (returning no action, leaving the session stationary), nothing re-armed either one: significant motion was consumed, shake monitoring had been stopped, and the accelerometer had been switched to stillness detection, which by construction only notices the device stopping. In the foreground that self-heals, because the CPU stays awake and a later shake or periodic fix rescues it; backgrounded it does not, sinceTYPE_ACCELEROMETERis a non-wakeup sensor and delivers nothing while the device is suspended. The session stayed stationary until tracking was restarted by hand. A declined wake now restores stationary monitoring, re-arming both sources, and says so on the lifecycle channel.FIX: (Android, iOS) tracking no longer freezes mid-walk and then jumps. Three faults compounded into one failure. The battery-budget engine measured drain from a single pair of battery-level readings five minutes apart — a level iOS reports in 5 % steps — so one reporting step read as 60 %/hr against a 3 %/hr budget and throttled a device that was draining normally. It throttled by writing its output into your live configuration, where
distanceFilter: 0(the documented "record every fix" opt-out) multiplied to 0 and was clamped up to 10 m: the processor's protection for a configured zero reads the base tuning that write had just replaced, nothing could ever restore it, andTracelet.activeConfigbegan reporting a configuration you had never set. With a distance gate now in play, adaptive sampling multiplied it by an unbounded activity and battery factor — 750 m for aStillclassification below 50 % battery — and because the processor's anchor advances only on an accepted fix, once nothing was accepted nothing could be. Four minutes of walking produced 59 rejections, zero locations and a frozen odometer. The jump was the same bug's second act: the implied-speed guard divides by the anchor's age, so a 1.65 km cell fix arriving 196 s later reads as 8.4 m/s and clears any ceiling meant for a car. Now: drain is measured over at least 15 minutes and discounted by one reporting step, so a figure only counts when it beats the budget by more than the measurement can resolve; throttling is a bounded five-rung ladder applied as an overlay that never touches your configuration, moves one rung per two consecutive conclusive windows, throttles sampling cadence before accuracy, relaxes the tracking accuracy gate whenever it does coarsen accuracy, and drops to zero on a charger; adaptive sampling may only delay a fix, with anything clearing the un-inflateddistanceFilteradmitted after 60 s — which leaves a genuinely parked device silent, since its jitter never clears that filter; and the implied-speed guard measures from the last fix the processor observed rather than the last one it accepted, which turns that same 8.4 m/s jump into 51 m/s. When nothing has been observed at all the anchor is re-seeded: the position is taken, but the span contributes no odometer distance and no derived speed (#393, #394, #395, #396).FIX: (Android, iOS) a released app can report a stalled location stream. Everything explaining one was logged at
debug, which Flutter's defaultinfo— and a direct SDK consumer's defaultoff— discards, so the bug report contained none of it; and the rejection line named only a reason and a speed, leaving an 8 m distance gate and an inflated 750 m one indistinguishable in a log. Stalls, recoveries, battery-budget throttle movements, idle-escape admissions and anchor re-seeds now go to the always-on lifecycle channel that bypasseslogLevel(the #318 mechanism), staying affordable because all of them are per-session rather than per-fix events. The per-fix rejection line keeps its level and its frequency but now carries the accuracy, the distance moved, the effective gate, the anchor age and the thresholds in force (#397).FIX: (Dart) the Doctor bug report says which Tracelet produced it. The report opened with a generation timestamp and, optionally, the host app's own version, so triage began by asking — and a report pasted into an issue weeks later could not answer at all.
traceletVersionis now exported frompackage:traceletand kept in lockstep with the pubspec by the release hook, and the report header carries it. The report also gained a Location stream health section: stalls, recoveries and throttle movements lifted out of the general log, each stall line carrying the rejection histogram, the gate the last fix was measured against, the configured gate beside it and the thresholds in force (#398). -
3.8.616 Aug 2026 withdrawnRelease notes
Open source →FIX: (iOS) an app with Swift Package Manager disabled links again. Flutter installs plugins as
:pathpods, and the published podspecs pointeds.source :httpat the GitHub Release zips — a source CocoaPods never downloads for path pods, soTraceletCore.xcframework/TraceletSyncFFI.xcframeworkwere simply absent and the build failed atldwith hundreds of undefined UniFFI symbols. The podspecs now fetch and checksum their own binary during evaluation, and each links the framework it vendors, which CocoaPods does only for a dependency's vendored frameworks (#390). -
3.8.6-alpha.115 Aug 2026 pre-release withdrawnNothing published for this version
-
3.8.515 Aug 2026 withdrawnRelease notes
Open source →FIX: (Android, iOS, web)
Tracelet.setOdometer()moves the reference the odometer measures from, not just the total. Distance is accumulated from an anchor held by the location processor, and setting the odometer wrote the counter alone — so the next accepted fix immediately added the whole span since the previous one and the value you had just set survived exactly one fix. The everyday form is "reset to zero, then start tracking": the trip began with however far the device had been carried while it was not being tracked. Only the odometer anchor is cleared, never the tracking one — that decides whether the next fix clearsdistanceFilter, so setting a counter must not quietly change which locations are recorded. All three platforms had the defect independently (#387).FIX: (Android, iOS) a session that starts stationary now acquires its first location.
motion.isMovingdefaults tofalse, sostart()took its stationary branch — and that branch acquired nothing at all: no continuous stream (by design), and in SMART mode no stationary schedule either, because the coordinator's posture is synced from the pace just committed and its inputs are only then pushed to stationary, so it reports no mode change and arms nothing. The only one-shot in the engine was fired from a stationary → moving transition, which a session that begins stationary never takes. An app could callstart(), leave the device on a desk, and never receive a singleonLocation— the workaround being handed out,motion: MotionConfig(isMoving: true), bought that first fix with a full-rate GPS stream nobody asked for.start()now takes one fix and routes it through the ordinary pipeline, so it is filtered, odometer-counted, persisted under yourpersistModeand dispatched like any other location, while the pace you asked to start in is left alone. It is skipped when a stream is already running (a moving start, or the in-app-evaluated geofence branch of a stationary one) and on resume, so the killed-state relaunch path is unchanged. The anchor contributes only a measured speed: both platforms and the Rust processor derive speed from their own last fix when the platform reports none, andstop()clears neither, so a device carried between two sessions in one process would otherwise derive a credible-looking speed and wake a session you explicitly asked to begin stationary. A real Doppler reading still counts, so a device that genuinely is moving at a stationary start is detected on that same fix. The fix that later wakes the session — whether fromchangePace(true)or from the accelerometer — is still delivered too: it sits withindistanceFilterof the anchor and would otherwise be dropped as a duplicate, so being told you are moving would come with no position to go with it. This behaviour existed until 3.2.0, which replaced an unconditional acquisition with the pace branch and left only the ongoing feed behind (#385).FIX: (Android, iOS)
PersistenceConfig.persistModeis applied to geofence ENTER/EXIT records. It only ever gated ordinary GPS fixes, sopersistMode: 'location'andpersistMode: 'none'still wrote every fence crossing to the local database and uploaded it in the next HTTP batch — an app that chosenoneprecisely to get geofence callbacks without the SDK retaining a location history was accumulating a record of every fence it crossed, with nothing in the logs to say so. Crossings are now persisted only underallandgeofence. YouronGeofencelistener is unaffected in every mode: only the database write is gated, sononekeeps its documented behaviour of firing events without storing anything. The mode is read at the moment of each crossing, so a mid-sessionsetConfigapplies to the next one (#383). -
3.8.413 Aug 2026 withdrawnRelease notes
Open source →FEAT: (Android, iOS) the tracking notification and the Live Activity can show a self-ticking elapsed timer, rendered by the OS rather than by rewriting the text on a timer of your own.
ForegroundServiceConfiggainsnotificationStartedAt(epoch ms) andnotificationShowTimer;LiveActivityConfiggainsstartedAtandshowTimer; andForegroundServiceConfig.notificationOnlyAlertOncemaps to Android'ssetOnlyAlertOnce, so later reposts replace the notification silently instead of replaying its sound and vibration.startedAtis supplied by the app rather than taken fromstart(), because the period a user cares about often began before tracking did, or survives a tracking restart. Everything here is additive — the new keys are absent unless set, so both platforms' config managers never see them and existing callers are unaffected — and updating a timer goes through the ordinary partialsetConfig, where aforegroundService-only write leaveshttp.urlandhttp.headersuntouched (#376).FIX: (Android)
ForegroundServiceConfig.showNotificationOnPauseOnlyno longer costs you whatAppConfig.stopOnTerminate: falsepromises. Hiding the notification demotes the foreground service, and a process holding no foreground service is one Android kills when its task is removed — so swiping the app from recents in the few hundred milliseconds between it leaving the screen and the notification being re-posted killed the process, with no headless task, no events and no logs. The setting is now ignored whilestopOnTerminateis false, and the SDK says so once on the lifecycle log channel; setstopOnTerminate: trueif you would rather have the hidden notification.getForegroundServiceHealth()gains a fourthlastForegroundPromotionResultvalue,suppressed, for a notification hidden on purpose, andlastForegroundTransitionAtnow stamps real state changes rather than every notification re-post (#378).FIX: (Android) events reach the registered headless task after the UI engine detaches while another plugin's background engine keeps the process alive. With, say, firebase_messaging's background service in the app, task removal left the SDK broadcasting into a fan-out with no members at all — a silent no-op — so native tracking ran on while Dart received nothing (#371).
-
3.8.312 Aug 2026 withdrawnRelease notes
Open source →FIX: (Android, iOS) telematics events survive a failed sync instead of being deleted by it. With
HttpConfig.syncTelematicsenabled, an upload failure — an offline device, a refused connection — cleared the entire stored telematics table rather than leaving the events queued for the next attempt. Events are now settled only when the request carrying them actually succeeded, and only over the id range that was uploaded (#366).FIX: (Android, iOS)
HttpConfig.syncTelematicshas an effect. The flag round-tripped through config and read back correctly fromState.config, but the native sync path looked it up in a structure that no longer existed by the time it ran, so it evaluated false on every sync and telematics were never attached (#370).FEAT:
TelematicsRecordexposesspeedandvalue— the speed at the event in m/s, and the measurement that triggered it (g for harsh driving events and impacts, km/h over the limit for speeding).severityremains the normalized 0–1 flag; these are the physical quantities behind it. Both reachedonDrivingEventalready but were dropped before storage, so they were missing from stored history and from every synced payload. Nullable: they read0for simulated events and for rows written before the migration, andnullonly if the native side did not report the field (#367).FEAT:
HttpConfig.telematicsUrlworks. Set it — withsyncTelematicsenabled — to POST telematics to their own endpoint as{"telematics": [...]}instead of attaching them to the location payload, using the same headers, timeouts, retries and SSL pinning asurl. Previously the value was accepted, stored, and ignored. Leave it unset to keep the existing behaviour (#368).NOTE:
syncTelematicsandtelematicsUrlare documented for the first time; both existed and neither appeared anywhere in the docs. To stop using a separate endpoint after setting one, pass an empty string rather thannull— config is merged, not replaced, sonullmeans "leave whatever is there". Raised in #356, which remains open for trip persistence and sync.FIX: (Android, iOS, web)
maxDaysToPersistandmaxRecordsToPersistare enforced against the local queue again. Both were accepted byready()/setConfig()and read back correctly inState.config, and then applied by nothing — no code path scoped aDELETEagainstlocation_events— so the queue grew without bound however they were set, which for an app offline for a long stretch is a storage problem rather than a correctness nitpick. The caps were real up to 3.0;2afc926f("prepare for 3.1.0") migrated the persist path on both platforms off the platform-nativeTraceletDatabaseclasses and onto the shared Rust core, and the retention calls — which lived in the same function body and hung off the samedbobject — were deleted along with it, with no equivalent added to the core to point them at. The amortization counter, its constant and a docstring claiming the function "also runs retention pruning" all survived, which is why the gap read as implemented (#361).BREAKING:
PersistenceConfig.maxDaysToPersistnow defaults to3days rather than1. The default has always been documented as1, but with nothing enforcing it the value never mattered; switching a one-day window on unannounced would have had an app offline over a weekend lose everything but its last day.3matcheslogMaxDays. Pass-1to retain indefinitely and rely onmaxRecordsToPersistalone, or set1to keep the previously documented figure (#361).NOTE: pruning is amortized over 100 inserts rather than run on every insert, so a
COUNT-and-DELETEis not attached to every GPS fix. The queue is bounded bymaxRecordsToPersist + 100and is cut back to the cap itself at each prune; it is a bound on growth, not a per-insert invariant. The first insert of a process prunes, so a backlog inherited from a build that never enforced the caps is cleared on the next fix (#361). -
3.8.211 Aug 2026 withdrawnRelease notes
Open source →FIX: (Android, iOS) geofence ENTER/EXIT no longer stop firing when the location filter tightens. Proximity registration — which decides which fences the OS is watching, and so is the whole feature in standard mode — rode the persistence-filtered location stream, so 3.8.0's transport-mode auto-tune (#299) could reject every fix, freeze registration, and leave no crossing ever reported again (#352).
FIX: (Android, iOS) geofences added alongside continuous tracking with
addGeofence()/addGeofences()now survive task removal and are restored after a reboot. Only a dedicatedstartGeofences()session set the tracking mode both paths keyed off, so astart()session with standalone fences lost every one of them on the first task removal, with nothing to re-register them — continuous tracking kept working, so the geofence feature could die silently (#353).FEAT: (Android, iOS) geofences smaller than the ~100 m the OS can resolve are now supported instead of silently never firing. A sub-100 m circle — and any polygon, which at default settings was never evaluated at all — is now owned by the in-app evaluator and decided against its true radius, with the OS region registered at 100 m purely as a wake-up. Note the cost: a fence the OS cannot serve needs the location stream (and on Android its foreground service) running, which an OS-resolvable fence does not (#355).
FIX: (Android, iOS)
notifyOnEntry,notifyOnExit,notifyOnDwellandloiteringDelayare now persisted with the geofence. The columns existed but were never written or read, so DWELL stopped working permanently after the first restore and an explicitly configurednotifyOnExit: falsewas silently reverted (#355).FIX: (Android, iOS) an in-app-evaluated geofence no longer goes quiet once the app is killed. The stationary throttle added in #319 stops the location stream on the premise that nothing needs it while the device is still — which a sub-100 m fence, decided from that stream, breaks (#355).
FIX: (Android, iOS) a geofence added after
start()now gets the fix cadence it is decided from, so EXIT no longer fires late or not at all. The cadence was settled once, atstart()— before any fence was registered, in the ordinarystart()-then-addGeofence()order — and the fence set now drives it at every point it changes, including back down again when the last such fence is removed (#357).FIX: (Android) a device carried while walking is no longer declared stationary. Accelerometer stillness was tested with a scalar that cannot see a vector which is rotating rather than growing, so a phone held or pocketed at a tilt scored as still and the stop timeout ended the session a minute later (#357).
FIX: (Android) geofence crossings reach the registered headless task again. A headless engine spawned for anything else — a custom sync body, say — joined the event fan-out and swallowed every subsequent crossing for the rest of the process: evaluated, logged, persisted and synced natively, but never delivered to the app, and dropped in silence (#358).
-
3.8.111 Aug 2026 withdrawnRelease notes
Open source →FIX: (Android) a headless task after task removal could silently never fire — the engine spawn now retries and reports failures instead of stalling forever (#331).
FIX: (iOS) a registered custom sync body builder is now honored on every path — a background relaunch no longer silently posts the SDK's default payload instead of it (#340).
FIX: (Android, iOS) the speed-motion state machine no longer reports STATIONARY from a filtered 0 m/s fix, treats an unavailable GPS speed as standing still, or double-emits a transition (#332, #333, #334, #335, #337).
FIX: (Android, iOS) a near-zero time delta between fixes no longer derives an implausible fallback speed that wakes a parked device (#342).
FIX: (Android, iOS)
start(isMoving: false)no longer permanently deafens the SMART motion coordinator to the accelerometer (#344).FIX: transport-mode auto-tuning no longer overrides an explicitly configured
distanceFilter: 0(#346).FIX: (Android, iOS) only a resumed session inherits the previous session's speed-motion pace (#348).
-
3.8.008 Aug 2026 withdrawnRelease notes
Open source →FIX:
Config.toMap()omits a section entirely when it carries nothing, soconst Config().toMap()is now empty rather than sixteen empty sub-maps. Each section was guarded withif (geo != null), which is dead code — the fields are non-nullable with defaults — the same mistake already fixed one level down forgeo.filterandandroid.foregroundService, and the analyzer had been reporting all sixteen. Nothing downstream read them (the wire format is the PigeontoTlConfig(), not this;Config.fromMapfalls back through an absent section, andactiveConfigis a resolved config afterready(), so every section still carries fields), but a partial update that changes nothing should not look like it touched every section — least of all in a pasted bug report.AttestationToken.toMap()loses the same dead guards ontoken,timestampandprovider: a token is a result, not a partial config, and those three are never absent (#326).FIX: the always-on lifecycle channel now records a tracking session's own boundaries —
session: startandsession: stop— on both platforms. It recorded what the background and killed-state pipelines did but never that a session began or ended, andstart()/stop()log only atinfo, so at any stricter level the trail could not say the one thing that answers most "it stopped tracking overnight" reports: tracking was stopped. iOS already wroterelaunch: declined to resume — tracking was stopped before termination, pointing at a stop the reader had no way to see. Every entry carries the mode and the strategy the session actually ran with, which is the whole diagnosis for periodic mode (WorkManager is throttled in Doze, exact alarms are not, a foreground service is neither); Android markssetConfig()'s in-place restart asrestart=trueso a config change does not read as the session ending, and itsLocationService.onDestroymoves fromdebugto the channel, so a service reclaimed by the OS is finally distinguishable from one that was never created (#324).NOTE: this also made the #318 verification card meaningful on a repeat run. Every other emitter reachable from a foreground
start()/stop()is either a one-shot per process (Android'sservice: onCreate, iOS'srelaunch:/termination:) or fires only on a real motion transition — so a card that clears the log first passed once after a fresh launch and reported a regression on every run after it, with nothing wrong in the SDK (#324).FIX:
setConfig()is now a genuine partial update across the wholeConfigmodel, not just the foreground service. Both native merges skip fields they do not receive — Android's says so outright ("a partial setConfig() must not overwrite existing non-null config with defaults") and iOS has the identical guard — but every field of the Dart model was non-nullable with a default, sotoMap()emitted all of them and a null was never sent. Those guards were correct and unreachable, andsetConfig(const Config())serialised a complete configuration built entirely of defaults, wrote it over everything stored —stopOnTerminate,startOnBoot,distanceFilter,desiredAccuracy, the HTTP settings, the iOS keep-alive flags — and persisted the result. iOS was worse in kind: its bridge builds one flat dictionary with no section boundaries, and the fields it reset (showsBackgroundLocationIndicator,preventSuspend,useBackgroundActivitySession) are the ones that keep background tracking alive, so a partial update silently degraded it. Every field now records whether it was supplied, separately from its value: the getters still return non-nullable values with the same defaults, so reading a config is unchanged, buttoMap()/toTlConfig()omit what was never set (#321).FEAT:
ready()andreset()now send a fully resolved configuration — every field pinned to its effective value — whilesetConfig()sends only what the caller set. This split is what makes omission safe: the baseline is always established explicitly, so the platforms' own defaults never have to match Dart's for correctness.Config.resolved()andConfig.mergedWith()are public, andTracelet.activeConfigapplies the same merge locally so it keeps reporting what the platform actually holds (#321).NOTE: passing a value equal to its default is still an explicit write, so a flag can always be set back — "unset" means not provided, never equal to the default. Dropping default-valued fields would have been a cheaper fix and a worse bug, making a default unreachable once changed. The one behaviour change to be aware of:
setConfig(const Config())used to reset everything and now changes nothing. Useready()orreset()to replace the baseline (#321).FIX: a partial
setConfig()no longer overwrites the persisted foreground-service configuration with defaults.setConfig()merges into the configuration the platform already stored, and that merge skips fields it does not receive — but everyForegroundServiceConfigfield was non-nullable with a default, soConfig.toMap()emitted all of them andconst Config()serialised a complete section built entirely of defaults. The platform could not distinguish that from a deliberate configuration, wrote it over the stored values, and persisted the result. The reported symptom wasshowNotificationOnPauseOnly: truequietly reverting, so the tracking notification appeared while the app was foregrounded; the same call also reset the title, text, channel and priority. It looked like a regression becauseready()re-sends the full config, so a fresh install always looked correct and only a later partialsetConfig()broke it. Each field now records whether it was supplied, separately from its value: the getters still return non-nullable values with the same defaults, so reading a config is unchanged, buttoMap()/toTlConfig()omit fields that were never set. Passing a value equal to the default is still an explicit write, so a flag can always be set back tofalse.Tracelet.activeConfigapplies the same merge locally, so it keeps reporting what the platform actually holds (#320).NOTE: this covers the
foregroundServicesection only. Every other section ofConfigstill replaces rather than merges —setConfig(const Config())resetsstopOnTerminate,distanceFilter, the HTTP settings and the rest to their defaults. Making the whole publicConfigmodel unset-aware across all four platforms is tracked as #321.FIX: (Android) killed-state tracking no longer keeps running continuous GPS after the motion subsystems settle back to stationary. The engine's mode is switched only from a motion transition, but
MotionDetector.onManualPaceChange()swaps its sensor set between the shake/significant-motion and stillness configurations directly, without routing throughdeclareStationary()— so no transition is emitted, and the engine stays continuous for the rest of the process lifetime with the OS location indicator pinned on and fixes landing every couple of seconds. A field report showed a singleisMoving=truetransition followed by 87 s of a demonstrably still device (peak 0.02 g against a 2.0 g threshold) still persisting continuous fixes, with the detector already back in its stationary configuration.startBootTracking()reconciled this once at bootstrap, which is why it only appeared mid-session — and why reopening the app showed a stationary pace while the location indicator stayed on. The reconciliation now runs on every heartbeat, so a missed transition costs one interval instead of the session, and the correction is recorded as a lifecycle entry (#319).FIX: (Android) the per-sample
[SHAKE]accelerometer trace moved fromdebugtoverbose. Every persisted line is a SQLite insert sharing one row cap with everything else, so atdebugthis single statement dominated the log — a device sampling at ~200 Hz emitted ~4 lines/s, turning the entire 2000-row table over about every 8 minutes. The database never grew (the cap held) but the retention window collapsed from the configured 3 days to minutes, evicting the background events the developer turned logging up to investigate. Turning logging up must not destroy the evidence (#319).FIX: (iOS) the engine is re-aligned with the committed motion state on every heartbeat, matching the Android reconciliation. iOS has no reproduction of the Android trigger and is less exposed to it —
CMMotionActivityManagerruns continuously regardless of tracking mode, so there is noonManualPaceChangeequivalent — but the failure class is identical and silent: the persisted state reads stationary while the engine keeps running continuous updates, costing battery for the rest of the session. Any ordering that reaches it (a queued callback landing after a mode switch, a force-switch bailing on itsenabledguard after the state was written) is now bounded to one heartbeat interval and recorded as a lifecycle entry rather than being invisible (#319).FEAT: (iOS) background and killed-state diagnostics are now persisted to the log table regardless of
logLevel, matching the Android channel. iOS has no separate killed-state pipeline — a relaunched process runs the same handlers as a foreground one — so what it records is the boundaries: the termination that registered significant-location monitoring for relaunch, the relaunch that resumed (and in which mode), and, when a relaunch declines to resume, which precondition failed. A downgrade from Always authorization silently disables tracking on that path and is now visible rather than inferred. Motion-state transitions carry whether the session launched in the background, so a trace distinguishes "the relaunched session never detected motion" from "it detected motion and the problem is downstream". Entries recorded before the database is open are buffered and flushed once it is; retention is unchanged and shares the existing row and day caps (#318).FEAT: (Android) background and killed-state diagnostics are now persisted to the log table regardless of
logLevel. The entries that explain a background failure — motion-state transitions on both the in-app and killed-state paths, service start/stop and sticky restarts, and boot/task-removal bootstrap outcomes — were all written atdebug, so the default level dropped them: a Flutter app (defaultinfo) had a populated log table containing none of the answers, and a direct native SDK consumer (defaultoff) had nothing at all. They now go through a curated, low-frequency lifecycle channel that bypasses the level gate, and entries recorded before the database is open are buffered and flushed once it is — so a bootstrap that never completed still leaves a trail. Retention is unchanged and already bounded on two axes (a row cap from the configured level, pluslogMaxDays), so worst-case database size does not move. Ordinarydebug/verboselogging stays gated bylogLevelexactly as before (#318).FIX: a crash ML model whose declared feature names the SDK cannot supply is now rejected at load instead of loading into a state that silently disables crash detection. The hosts map declared names through a lookup that defaults misses to
0.0, so a model trained on other names scored an all-zero feature vector on every window — and because a probability of0.0still satisfiescrashProba >= 0, the detector stayed in ML Replace mode and never fell back to the g-threshold rule. Crash detection was dead while the SDK reported the model ready. A model declaring no features at all (previously allowed through by aserdedefault) is rejected for the same reason (#309).FIX: the crash model's
peak_g,mean_gandgyro_peak_dpsare aggregated over the same ~16 s window the model was trained on. They were taken from the single 1 s window being scored whilespeed_max/dvalready used a 16 s history, so the feature vector straddled two time bases and none of it matched training —mean_gworst of all, since the mean over a 1 s window containing a spike is nothing like the mean over 16 s of driving. Detection still evaluates once a second; only the features widen (#310).FIX: (iOS) the loaded crash model is no longer read and written across threads without synchronization. It was written from the loader's background queue and read from the main run loop — an unsynchronized cross-thread ARC retain/release on a class reference, a crash risk rather than merely a stale read. Android already guarded the same field with
@Volatile(#311).FIX: the crash speed gate uses the pre-impact speed rather than the latest fix. A collision collapses speed within 1–2 s against ~1 Hz GPS and 1 Hz accelerometer windows, so a post-impact fix could land before the impact window was scored, dropping the reported speed below
crashMinSpeedKmhand failing the gate that both the rule and the ML path sit behind — losing the crash. The on-foot fall context derives from the same value so the two branches stay coherent (#312).FIX:
getTelematicsEvents()returns the most recent events, regardless of sync state. It shared the sync batcher's query (WHERE synced = 0 ORDER BY id ASC), so it returned the oldest events rather than the newest and emptied out entirely oncesyncTelematicswas enabled — contradicting both its own documentation and the Doctor's "most recent" bug-report section. Sync keeps the original query; the history API has its own (#313).FIX: the encrypted crash-model cache is keyed by model URL, so repointing
crashModelUrlinvalidates it even when the optionalcrashModelSha256is absent — previously one fixed filename meant the old blob was loaded forever. On iOS the cache directory (Library/Application Support, which iOS does not create by default) is now created before writing, and a failed write is logged rather than swallowed; the model was silently re-downloaded on everyready()and behaviour-configsetConfig()(#314).FIX: standard (low-power) geofence-only tracking no longer restores as continuous tracking after a reboot, a task removal, or a killed-state relaunch. Every restore path started the full location engine regardless of
geofenceModeHighAccuracy, so a geofence-only app silently converted to continuous GPS for the rest of the process lifetime — on iOS bringing back the persistent blue location indicator that #210 removed, and on Android running a foreground service solely for geofencing, which Google Play prohibits as of 2026-10-28. The restore paths now branch ongeofenceModeHighAccuracyexactly asstartGeofences()does. On Android the boot service still starts long enough to re-register the fences (Play Services clears them on reboot) and then stands itself down (#316).FIX: (Android) boot and task-removal tracking retries with backoff when the SDK cannot bootstrap, instead of returning and never trying again.
START_STICKYonly redelivers after the process is killed, and the service is alive at that point — it just returned early — so nothing called it again short of the user reopening the app, while the foreground notification advertised an active session that was tracking nothing. After the retries are exhausted the service stops rather than leaving that notification standing (#317). -
3.8.0-beta.206 Aug 2026 pre-releaseRelease notes
Open source →FEAT:
Tracelet.getCurrentLocationTuning()reports the location-filter thresholds actually in force, read back from the native processor rather than from the config you set.activeConfigis a Dart-side mirror of the lastConfigpassed in, so it cannot tell you whether a value reached the filter that uses it — and it cannot show a transport-mode auto-tune, which changes these thresholds with no config call at all. Returnsnullbefore a tracking session has built a processor, and alwaysnullon Web (#303).FIX: the method-channel
androidconfig block now carries the samegeofenceModeHighAccuracyvalue as thegeofenceblock. Native reads the key from both, so emitting the raw Android-only flag in one and the OR'd value in the other made behavior depend on which block the platform happened to consult (#305).FIX: (Android + iOS) the location-filter configuration now reaches the Rust processor at runtime.
setConfig()rebuilt the processor only for a short list of location keys, sotrackingAccuracyThreshold,odometerAccuracyThreshold,maxImpliedSpeed,filterPolicy,enableAdaptiveMode,rejectMockLocations,mockDetectionLevel, the sparse-update trio anduseKalmanFilterwere accepted, cached, and then ignored until the next cold start. BecauseLocationProcessorcaptured its base thresholds at construction, this also broke the #301 guarantee that disablingautoTuneFromTransportModerestores "the values you configured" — it restored the values captured when the processor was built. A newset_base_tuningcarries thresholds in without dropping the positional anchor a rebuild would cost (the reasonretuneexists, #299); the remaining constructor-only parameters trigger a targeted rebuild, and the Kalman filter is toggled independently of the processor (#303).FIX: (iOS) the whole
LocationFilterblock now reaches the config cache. Every transport serializes it as afiltersub-map nested insidegeo, but the iOSConfigManagerflattened only one level and then stored that block as a single opaque value no getter ever read — sotrackingAccuracyThreshold,odometerAccuracyThreshold,maxImpliedSpeed,rejectMockLocations,mockDetectionLevelanduseKalmanFilterwere pinned to their defaults (100 m / 0 m / 80 m/s / off) no matter what was configured, atready()as well as at runtime. The change detection added above compared key names absent from both config snapshots, so a filter change triggered neither the targeted rebuild norsetBaseTuning— the fix landed on a bridge that was never connected. Caches persisted in the nested shape are lifted on load, becauseautoResumeTracking()starts the pipeline off the persisted cache with noready()in between (#303).FIX: (Android + iOS)
LocationFilter.policyis now applied. It is serialized underpolicyby every transport, whilegetFilterPolicy()— and the processor-rebuild key list — readfilterPolicy, so the value was cached under a name nothing asked for and the policy stayed atadjusthowever it was configured. It is renamed during flattening, so both readers see it (#303).FIX: (Android + iOS)
logMaxDaysis now applied. Both platforms pruned logs only by a row count derived fromlogLevel, so the configured retention window was accepted and discarded —logMaxDays: 3andlogMaxDays: 90behaved identically. Log pruning now enforces both caps (#304).FIX: (iOS)
disableLocationAuthorizationAlertis now honored. The permission manager requested authorization unconditionally, so apps that wanted to own their pre-permission UX could not suppress the system prompt. It now reports the current status instead of prompting (#304).FIX: (Web)
GeofenceConfig.geofenceModeHighAccuracyis now honored. The web plugin read only the deprecatedAndroidConfig.geofenceModeHighAccuracy, so setting the documented cross-platform flag had no effect on web. It is now OR'd with the deprecated flag, matching both Pigeon hosts, andgeofenceExitAccuracyMaxis carried too (#305).FIX: the method-channel transport no longer drops geofence configuration.
_geofenceToMapemitted two of the five geofence keys, silently discardinggeofenceModeHighAccuracy(so no OR was performed on that path),geofenceInitialTrigger, andgeofenceExitAccuracyMax— the #276 tunable (#305).FIX: the four built-in
TraceletProfilepresets now set high-accuracy geofencing through the cross-platformgeofenceblock instead of the deprecatedandroidone.TraceletProfile.highAccuracytherefore enables it on iOS as well, and the deprecated field has no remaining internal dependants (#305).FIX: the pure-Dart
GeofenceEvaluatornow applies thegeofenceExitAccuracyMaxpolicy (-1full gating,0disabled,Nclamp). It is documented as a mirror of the Rust core but gated EXIT on raw accuracy, diverging from both native managers, which pass accuracy througheffectiveExitAccuracyfirst (#276). The parameter defaults to-1, so existing callers are unaffected (#306).FIX: the Rust geofence evaluator drops pending exit confirmations for fences a re-index no longer covers.
clear()andremove_geofence()both pruned them;index_geofences()did not, so a half-accumulated count survived and could contribute to a later EXIT (#306).DEPRECATED:
AuditConfig.includeExtrasInHashandAttestationConfig.verificationUrlwere never implemented — no platform reads either value. They are now annotated so the analyzer surfaces it, rather than being silently discarded at runtime. Implementing them is not a patch: the first changes what the audit chain hashes and so invalidates every existing chain, and the second needs a token-verification transport (#304). -
3.8.0-beta06 Aug 2026 pre-releaseRelease notes
Open source →FEAT: (Android + iOS)
ModeChangeEvent.appliedTuningreports the four thresholds a committed transport mode put in force —distanceFilter,trackingAccuracyThreshold,odometerAccuracyThresholdandmaxImpliedSpeed— and isnullwhen auto-tuning is off or the mode isunknown. Both SDKs already put these on the native mode-change payload in 3.8.0-alpha, butTlModeChangeEventcarried onlymodeandconfidence, so the plugin dispatchers dropped them: an auto-tune was silent for exactly the Flutter apps documented as being able to observe it (#301).FIX: (Android + iOS) turning
autoTuneFromTransportModeoff at runtime now restores the thresholds you configured.applyTransportModeTuningreturned early when the flag was false, before it could restore anything, and the flag does not trigger a processor rebuild — so a session that had auto-tuned towalkingkept the walking thresholds in force indefinitely after the feature was switched off. DisablingenableFusedClassifierhad the same effect, since destroying the classifier means no further mode change ever arrives to undo the tuning (#301).FIX: (Android + iOS)
setConfig()no longer silently drops an active auto-tune. A location-key change rebuilds the location processor from the configured values, but the classifier's committed mode was unchanged, so the tuning was not re-applied until a different mode committed — a user who stayed on foot across thesetConfig()kept the base thresholds whileonModeChangestill reportedwalking. Both SDKs now re-align the processor with the committed mode after any reconfiguration (#301).FIX: (Android + iOS) enabling
enableFusedClassifierthroughsetConfig()while already tracking now starts the ~1 Hz accelerometer-window loop that drives the classifier. It was started only fromstart(), so a mid-session enable produced a classifier that never classified — and, with auto-tuning on, never retuned. Configuring the classifier atready()was unaffected (#301).FIX: (iOS)
autoTuneFromTransportModeis now watched in the behaviour-key comparison that decides whethersetConfig()rebuilds the behaviour engines, matching Android (#301). -
3.8.0-alpha05 Aug 2026 pre-releaseRelease notes
Open source →FIX: (Android + iOS)
useKalmanFilter: truenow affects recorded distance, not just the rendered track. The odometer accumulated raw inter-fix deltas because Kalman smoothing ran after the location processor and fed onlycoords, so enabling the filter visibly smoothed the map while distance kept integrating GPS jitter — over-reporting badly on foot, where noise is large relative to real displacement. Smoothing now runs before the processor on both platforms, so the distance filter, accuracy filter, implied-speed filter and the odometer all operate on the de-noised track. Every fix feeds the filter (not only accepted ones), keeping its velocity estimate continuous across rejections (#299).FIX: (Android + iOS)
fusedClassifierAuthoritative: truenow genuinely drives sampling. The adaptive sampler was built from the raw platform Activity-Recognition value rather than the effective activity, so the fused classifier never reachedAdaptiveSamplingEnginedespite the option being documented as overriding the platform activity for sampling (#299).FIX: (iOS) the
classifierconfig section was dropped entirely when flattening the plugin config, makingenableFusedClassifier— and every other classifier option — undeliverable from Flutter on iOS (#299).FIX: (Android + iOS) a fix too coarse for the odometer now defers its distance instead of deleting it.
odometerAccuracyThresholdblocked such a fix from contributing, but the odometer's reference point advanced anyway — so the ground actually covered during that segment was lost for good, and the tighter the gate the more distance silently went missing. The odometer now keeps its own anchor, advanced only by fixes that clear the gate, so the next trustworthy fix books the whole span it covered. Without this, tightening the gate (which transport-mode auto-tuning does on foot) traded over-reporting for systematic under-reporting (#299).FEAT: (Android + iOS) new
ClassifierConfig.autoTuneFromTransportMode(defaultfalse). When enabled, a committed transport mode retunesdistanceFilter,trackingAccuracyThreshold,odometerAccuracyThresholdandmaxImpliedSpeedfor that mode — tighter on foot, looser in a vehicle — from a table shared by both platforms via the Rust core. This removes the hand-tuning that distance-accurate tracking previously required, and which neither the app developer nor the end user can get right in advance: a single session routinely contains walking, jogging and running. Retuning happens only on a committed change (confidence-gated and debounced bymodeSwitchDwellMs), never per accelerometer window, and swaps thresholds in place so the odometer stays continuous across the change. A mode ofunknownrestores your configured values, and the applied thresholds ride on themodechangepayload so an auto-tune is never silent. RequiresenableFusedClassifier: true(#299). -
3.7.605 Aug 2026Release notes
Open source →FIX: (Android + iOS) high-accuracy geofence ENTER/EXIT transitions no longer intermittently fail to fire for a stationary device. In
geofenceModeHighAccuracythe crossing evaluator is now fed the raw fix stream and the provider is requested with time-based delivery (minUpdateDistanceMeters = 0/kCLDistanceFilterNone), decoupling geofence evaluation from the tracking distance filter. That filter is a persistence-volume control, but it was also gating the fixes reaching the evaluator: a device standing still on a stable provider — GMS FusedLocationProvider (Android, since 3.7.3) or CoreLocation with a distance filter (iOS) — never moves the filter's distance, so no fix was delivered and no transition was evaluated (field logs show an iPhone parked inside a 50 m zone emitting many ENTERs and never an EXIT). Persistence is unchanged — the RustLocationProcessorkeeps its distance filter, so stored/synced location volume does not increase — and the decision logic (accuracy gating #274, 2-fix confirmation #294, hysteresis) is untouched; it simply now sees every fix. Standard OS region-monitoring geofence mode is unaffected (#297). -
3.7.504 Aug 2026Release notes
Open source →FIX: (Android + iOS) a geofence EXIT is now confirmed across
GEOFENCE_EXIT_CONFIRMATIONS(2) consecutive out-of-fence fixes rather than a single one. Consumer GNSS routinely emits an isolated "over-confident" fix — one that lands hundreds of metres outside while reporting a tight accuracy the accuracy-aware gate (#274) cannot see through; field logs from a vivo V2431 and a Samsung SM-G781B show a stationary office device jumping to 198–301 m out at 1.7–9 m reported accuracy and straight back. Such a fix is always transient, so requiring the device to be confidently outside for two consecutive fixes absorbs the glitch on every device, while a genuine departure — which stays outside — still fires exactly one EXIT, delayed only by one fix interval. This is the temporal complement to the spatial exit hysteresis (#268); the decision lives in the Rust core, so Android and iOS inherit it identically, and the pure-Dart evaluator mirrors it (#294). -
3.7.403 Aug 2026Release notes
Open source →FIX: (Android + iOS) in
geofenceModeHighAccuracy, a stationary device inside a geofence no longer emits a false ENTER on every resume/boot.startGeofences()callsclearHighAccuracyState(), which wipes the evaluator's in-memory inside-set, and it runs on everyready()/takeover ("Resuming geofence tracking on ready/takeover") and after boot/task-removal — so on an aggressive OEM it fires many times per hour. After each wipe the next fix satisfiesentered && !was_insideand the evaluator re-emits ENTER; on an attendance backend each becomes a punch-in/punch-out, and a field report showed ~9 auto IN/OUT pairs in a day while the employee never left the office. The exit hysteresis from #268 and the accuracy-aware EXIT from #274/#276 cannot help, because they govern the crossing math within one evaluator lifetime while the "already inside" memory is discarded on every resume — so each takeover looks like a legitimate first-ever ENTER.startGeofences()now takes anisResumeflag: a resume/boot preserves inside-state and only a fresh explicit start resets it (which still re-emits the initial-entry ENTER once). In addition,GeofenceManagerpersists a "known inside" set (SharedPreferences on Android, UserDefaults on iOS) that dedups high-accuracy ENTER/EXIT emissions and survives process death, so even a cold-start re-entry after an OEM kill is suppressed while a genuine departure and return still fire (#292). Finally,startGeofences()is now idempotent — calling it again while already tracking in geofence mode (the common "refresh fences on every app launch" pattern) is treated as a resume and preserves the inside-set, so only a genuine (re)start (first enable, or afterstop()) re-arms the initial-entry ENTER. The persisted set is also kept honest across the full geofence lifecycle: removing a geofence clears its inside-state (so a re-added id, or an id reused for a different location, can ENTER again), and on cold start the evaluator is re-seeded from the persisted set so a device that left a fence while the app was killed reports a real EXIT on the next fix instead of getting stuck inside and suppressing the next genuine ENTER. -
3.7.301 Aug 2026Release notes
Open source →FIX: (Android) minified release builds no longer fall back to the AOSP location stack on devices that have Google Play services.
TraceletServices.isGmsAvailableresolvedGoogleApiAvailabilityreflectively soplay-services-basecould stay a soft dependency, but R8 rewrites theClass.forNamestring literal to the renamed class while leaving thegetMethod("getInstance")argument untouched — so the class resolved, the method lookup threwNoSuchMethodException, and thecatchreported GMS as missing. Field logs show it verbatim:Exception in isGmsAvailable reflection check: v2.d.getInstance []on a Galaxy S23. Every minified build since 3.6.x was therefore running on rawLocationManagerwithGPS_PROVIDER+NETWORK_PROVIDERinterleaved, the deprecatedaddProximityAlert, and a no-op activity-recognition client — feeding coarse network fixes straight into the geofence evaluator, which the accuracy-aware EXIT gating from #274/#276 cannot defend against. The probe now distinguishes "GMS absent" from "the probe could not run": a probe failure falls back to an OS package-manager query that no shrinker can rename, and a-keeprule forGoogleApiAvailabilityships in both consumer ProGuard files so the precise reflective path keeps working in host apps.FIX: geofence ENTER/EXIT transitions are now logged, at
INFO, with the full decision trace on both Android and iOS.evaluateHighAccuracyProximityand the OS-transition handler previously logged nothing at any level, so a report of an occasional false EXIT produced a bug report with zero geofence content and had to be triaged from configuration alone. Each crossing now emits[geofence] EXIT <id> dist= radius= buffer= thr= margin= accRaw= accEff= exitAccuracyMax=, which is what separates a genuine departure from drift: a smallaccRawwith a largedistis an over-confident fix,clampApplied=trueshowsgeofenceExitAccuracyMaxbinding and weakening drift immunity relative to the-1default, andaccuracyInvalid=true gatingDisabled=trueflags a fix with no valid accuracy (negativehorizontalAccuracyon iOS,0.0on Android), which the evaluator treats as zero uncertainty. The line carries distance-from-centre rather than coordinates, so it is safe to paste into an issue. The OS/AOSP path logssource=osand states that it has no accuracy gating, andupdateProximitynow labels its line "not ENTER/EXIT" — it reports monitoring scope, and apps that readgeofencesChange.offas an exit will see phantom exits from a single far-drifting fix.FEAT:
TraceletBugReportgained a Geofence transitions (decision trace) section that lifts[geofence]lines out of the general log stream and scansgeofenceTraceLimit(2000) entries rather than the 500-entry log window. Crossings are rare while lifecycle chatter is not, so in a busy app the transitions were being pushed out of the exported window before anyone generated a report.FIX:
HealthCheck.motionPermissionis documented correctly and gained a typedHealthCheck.motionAuthorizationgetter. The field carries aMotionAuthorizationStatusindex (notDetermined,granted,deniedForever→ 0, 1, 2), but its doc comment described CoreMotion'sCMAuthorizationStatusscale, which orders the cases differently and has a separaterestrictedvalue. Callers that implemented the documented contract — including Tracelet Doctor — reported a granted permission as "Restricted". The new getter returns the typed value (nullwhen out of range) so the mapping cannot be misread.PERF: the native loggers no longer run a
DELETEafter every log write. Retention is 500-2000 rows, so pruning is now amortized every 50 writes on both platforms. -
3.7.201 Aug 2026Release notes
Open source →FIX: (smart motion) the accelerometer can now contribute to a stationary decision after a start that begins in MOVING. The Rust coordinator initialises
is_accel_moving = falseand ignores an unchanged flag, andstart()never seeded it, so the stop-timeout fired, reported stationary, and the coordinator emitted nothing — leaving the GPS-speed machine as the only input that could ever change the pace.start()now seeds the flag from the state it starts in, and also re-syncs the coordinator's tracking mode (it was previously only synced ininitialize(), from the persisted mode, so a session that ended stationary could leave the coordinator unable to ever switch again) (#288).FIX: (smart motion)
MotionDetectorno longer writesisMovingitself in smart mode. The accelerometer is one of two inputs there — the coordinator owns the decision — so claiming the transition locally leftgetState().isMovingdisagreeing with the lastonMotionChangeevent and with actual GPS behaviour whenever the coordinator decided to stay continuous (#288).FIX:
shakeThreshold,stillThresholdandstillSampleCountare no longer transmitted unless you set them, so each platform keeps its own tuned default. Dart's defaults are the Android values, and they were sent unconditionally by any app that configured any motion field: iOS converts m/s² to g, sostillThreshold: 0.4arrived as0.04 g— about four times stricter than the0.15 giOS default it was meant to keep — andstillSampleCount: 25dwelt ~2.5 s at iOS's 10 Hz instead of the intended ~5 s. Reading these properties still reports the documented Dart defaults (2.5/0.4/25), and setting a value — even one equal to the default — is honoured on both platforms, so this is not a breaking API change. iOS's own fallback forstillSampleCountwas also corrected from 30 to 50 (≈5 s at 10 Hz), matching the documented intent and Android's ~5 s dwell (#288).FIX: a still device now reaches STATIONARY on schedule instead of being stranded in MOVING. While the speed state machine counted down in SLOWING, a single GPS fix at or above
speedMovingThresholdcancelled the countdown and restarted the wholespeedStationaryDelaywindow. GPS speed is noisy on a stationary device — an isolated1.56 m/sblip amid a stream of0.00 m/sfixes was enough — so the pace could keep restarting its countdown indefinitely while the accelerometer had already reported sustained stillness. SLOWING now requires three consecutive above-threshold fixes before returning to MOVING (the same sustained-motion remedyMotionDetectorapplies to accelerometer noise) and the countdown keeps its original start time across a blip. Safe by construction: SLOWING is still continuous tracking, so confirming over a couple of fixes costs no location fidelity. Applied on Android and iOS (#288).FIX: the
tracelet_syncsink is now process-wide instead of one perFlutterEngine. Both native plugins created a newTraceletSyncSinkon every engine attach and never detached one, so any host that spawns secondary engines —workmanagercreates one per background task, plus headless engines and engine groups — accumulated sinks for the life of the process. Each sink owns its own concurrency guard (aCoroutineScope+Mutexon Android, aSyncCoordinatoractor on iOS), so those guards stopped serializing anything and a single persisted location fanned out into N blocking auto-syncs, each pinning one or two threads:OutOfMemoryError: pthread_create failed, heap exhaustion, duplicate points server-side and racingclearLocationsUpTocalls. The sink is now created once and reused by every later engine, and it is deliberately kept alive on detach so native/headless tracking keeps syncing after a short-lived engine goes away. On iOS the plugin also stopped subscribing the sink twice per engine (directly and through thesyncProviderdidSet) and gained adetachFromEngine(for:)hook (#286).FIX: (iOS) a superseded sync provider can no longer stay subscribed to the
LocationEngine.registerSinkwas a bare append with no dedupe and there was no way to remove a sink at all, so duplicate and stale sinks each fanned out anotherinsertLocationfor the same fix.registerSinknow dedupes by identity,unregisterSinkwas added (Android has had both since #204), andTraceletSdk.syncProvidercancels and unregisters the provider it replaces (#286). -
3.7.129 Jul 2026Release notes
Open source →FIX:
locationSourceandreducedAccuracyare no longer dropped from persisted and synced locations. Both fields are emitted on the liveonLocationevent, but the persist path only serializedaudit_*,batteryandextrasinto theroute_contextcolumn, so the classification was lost at write time — every DB-sourced read (getLocations,getPendingLocations) and the DB-sourced sync payload (setSyncBodyBuilder) reportedlocationSource: "unknown"/reducedAccuracy: false. This broke the documented guidance to filter historical/synced fixes bylocationSource == "gps". Both fields are now persisted as first-classroute_contextkeys (likeaudit_*) and promoted back to the top level byLocationMapperon read; because the PigeonTlLocationboundary has no dedicated fields for them, they are carried across it viaextras(like the live event) and unpacked byLocation.fromMap, so the live event and DB-sourced reads agree. Applied on Android and iOS (#280).FIX: (iOS) high-accuracy periodic fixes are no longer a single
requestLocation()one-shot, which frequently returned a stale cached or first-coarse fix before the GPS hardware converged (persisting a Wi-Fi/cell-level fix for a periodic tick). WhenperiodicDesiredAccuracyisDesiredAccuracy.high,performPeriodicFix()now routes through the same best-of-N sampling windowgetCurrentPositionalready uses (collectSamples→ most-accurate sample), bounded bylocationTimeout, so periodic fixes are GPS-quality. Non-high periodic accuracy keeps the cheaper single-shot path, and overlapping ticks are guarded against (#282). -
3.7.029 Jul 2026Release notes
Open source →FEAT(geofence): accuracy-aware geofence EXIT for high-accuracy mode. A circular geofence now only fires EXIT once the entire GPS error circle clears the fence (
distance - accuracy > radius + buffer), so a single high-drift, low-confidence fix no longer produces a false EXIT while a device is stationary inside a small geofence. ENTER stays accuracy-agnostic so arrivals still trigger promptly (#274).FEAT(geofence): new
GeofenceConfig.geofenceExitAccuracyMax(meters) to tune the accuracy-aware EXIT gating —-1full gating (default, most drift-resistant),0disables gating (fastest, most eager EXIT), andN > 0clamps the accuracy used in the exit test toNto bound the worst-case exit delay while still absorbing drift up toN. High-accuracy path only; no effect in standard OS region-monitoring mode (#276). -
3.6.1528 Jul 2026Release notes
Open source →FIX: (Android) geofence transitions and confirmed crash/fall deliveries could be silently dropped right after a cold boot. Since #260 moved the heavy
initialize()setup (Rust DB open,lateinitgeofenceManager/engines) onto a background thread,initialize()returns before those managers exist. #264 guarded theready()/bootstrapForBackground()paths, but the native broadcast entry points still raced init:GeofenceBroadcastReceiverreadgeofenceManagerimmediately afterinitialize()(so a cold-boot ENTER/EXIT — a trip start — was swallowed and lost), andCrashConfirmReceiverdelivered a confirmed impact onto not-yet-wired state. Both now funnel through a newawaitInit()gate that blocks until init completes (or reports failure/timeout) before touching those managers, so the transition/impact is delivered instead of dropped. iOS is unaffected (itsinitialize()is synchronous) (#271). -
3.6.1427 Jul 2026Release notes
Open source →FIX: geofence
ENTER/EXITflapping for a stationary device inside the radius (high-accuracy mode). The evaluator used a singledistance <= radiusthreshold for both entry and exit, so a motionless device whose GPS fixes jittered across the boundary emitted repeatedENTER/EXITevents. Exit now applies hysteresis — the deviceENTERs at the true radius but onlyEXITs once it is farther thanradius + max(radius * 0.1, 20 m)from the center — so boundary jitter no longer flips the state. Applied in both the pure-Dart evaluator (the active high-accuracy path) and the Rust core used by the native SDKs (#268).FEAT: (Android) add
Tracelet.requestTermination()to stop the GPS foreground service from a headless Dart isolate. When an FCM silent push runs a background task while the app is terminated,Tracelet.stop()is unavailable because it relies on Pigeon, which headless isolates cannot reach — so the foreground service kept polling and draining battery until the app was reopened. A newrequestTerminationhandler on thecom.tracelet/methodsMethodChannel (registered on the headlessFlutterEngine) callsTraceletSdk.stop(), letting background handlers shut tracking down cleanly (#267). -
3.6.1326 Jul 2026Release notes
Open source →FIX: (Android) prevent a runtime crash when
com.google.android.gms:play-services-locationresolves below 21.2.0. Tracelet's Android bytecode calls the interface-basedFusedLocationProviderClientandActivityRecognitionClientAPIs, which only became interfaces in play-services-location 21.2.0. When a host app resolved an older version (e.g. 19.0.0) transitively, those types were still concrete classes, so calling into them threwjava.lang.IncompatibleClassChangeError(crashing the periodic location worker and, after permission handling, the main thread). play-services-location stayscompileOnly, so the SDK still degrades gracefully to the AOSPLocationManagerwhen GMS is absent; a published Gradle dependency constraint now raises the resolved version to a compatible floor (>= 21.2.0) whenever the dependency is present, without adding it to the dependency graph (#263).FIX: (Android) prevent a boot/restart crash with
UninitializedPropertyAccessException: lateinit property geofenceManager has not been initialized. WithstartOnBoot: trueandstopOnTerminate: false,LocationService.startBootTracking()calledbootstrapForBackground()and then immediately accessed thegeofenceManager. Since 3.6.9,initialize()runs on a backgroundtracelet-initthread andbootstrapForBackground()did not wait for it, so on a cold boot thelateinitmanagers could still be unassigned — crashing the service inonStartCommandbefore the foreground notification was posted (a timing race most reliably seen on slower environments such as emulators).bootstrapForBackground()now blocks on the init latch and returns a success flag: it preserves the initialization exception (no longer mistaking a released latch for success) and verifies the Rust DB andgeofenceManagerare actually assigned.startBootTracking()defers gracefully without touching any manager, andPeriodicLocationWorkerreturnsResult.retry(), when initialization has not completed (#264).FIX: (Android)
Tracelet.addGeofence()no longer returnsfalsefor a geofence that was actually registered. When no device location is known yet,addGeofence()persists the record and callsregisterGeofence(), whose Google Play Services registration is asynchronous. The Flutter host calls this on the main thread, where the SDK correctly does not block on the registration callback — but it then returned the still-falseresult before the callback ran, so apps saw a bogus failure even thoughgetGeofences()listed the geofence. On the main thread the SDK now returnstrueonce the registration request has been scheduled without a synchronous error (off the main thread it still awaits the real callback result); genuine Play Services failures continue to be logged. iOS and web were unaffected (#265). -
3.6.1226 Jul 2026Release notes
Open source →FIX:
Tracelet.ready()no longer surfaces remote-config event registration failure as an uncaught async error. Since 3.6.10,ready()subscribes toremoteConfigEvents, which lazily registers the Pigeon event channel and firedrequestStateFlush()fire-and-forget. When the platform side was unreachable (e.g. a headlessflutter testwith no channels, or a temporarily detached engine), the rejected future became an uncaught async error routed to the zone error handler instead of one the caller'sawait ready(...)could catch — fatal for a ride-start path that wrappedready()in try/catch and still got torn down. The best-effort flush is now awaited inside a guarded helper that contains any failure, so it can never escape as an uncaught async error; event registration itself already succeeded, so nothing observable is lost and callers can always recover (#262). -
3.6.1125 Jul 2026Release notes
Open source →FIX: (iOS)
IosConfig.useSignificantChangesOnlyno longer keeps the persistent system location indicator on. On iOS 17+, enabling significant-change monitoring and callingTracelet.start()still showed an ongoing location indicator (Dynamic Island / status-bar pill) becausestart()opened aCLBackgroundActivitySessionwhenever the device was moving, even though continuous GPS was correctly skipped.CLBackgroundActivitySessionholds a background location activity alive and auto-shows the indicator, defeating the whole point of significant-change monitoring (low-power background location with no persistent indicator). The SDK now fully honors significant-changes-only mode — it neither opens aCLBackgroundActivitySessionnor starts continuous GPS (startUpdatingLocation), which independently light up the system location indicator — acrossstart(), the motion-detection pipeline's switch-to-continuous,changePacetransitions, and killed-state auto-resume, matching the existing behaviour of periodic mode and low-accuracy geofence-only mode. High-accuracy geofencing and the explicitIosConfig.useBackgroundActivitySessionopt-in are unaffected. The indicator may still blink briefly when a significant-change event is delivered, which is normal iOS behaviour. (#261) -
3.6.1024 Jul 2026Release notes
Open source →FIX: Remote configuration overrides (Enterprise
remoteConfigUrl) now propagate to the Dart layer. Remote config is fetched and applied entirely on the native side; previously the result never crossed back to Dart, soTracelet.activeConfig— and anything reading it, such astracelet_doctorand the Dart-side battery-budget engine — kept showing the last locally-set values (e.g. a remotely fetchedbatteryBudgetPerHourof1.0never appeared, while a localsetConfigvalue did). The native layer now emits anonRemoteConfigevent whenever it applies a remote override — both the freshly fetched config and the cached copy restored atready()— and the Dart layer folds it into the active config, re-initialising the Dart-side battery-budget engine. A newTracelet.onRemoteConfig(...)callback andTracelet.remoteConfigStreamlet apps react to server-driven configuration changes. -
3.6.924 Jul 2026Release notes
Open source →FIX: Remote config (and any runtime
setConfig()) now appliesbatteryBudgetPerHour. The battery-budget engine was only built duringready(), so a remote-config push such as{"geo":{"batteryBudgetPerHour":1.0}}delivered at runtime viasetConfig()was stored but never acted on — it only appeared to work after a cold restart (which applies the cached copy beforeready()builds the engine). The engine is now rebuilt whenbatteryBudgetPerHourchanges at runtime on both Android and iOS, and battery-budget sampling is started or stopped to match the live tracking state.FIX: iOS — all
Doubleconfiguration getters now read throughNSNumber, so integer-encoded values (e.g.1instead of1.0from a remote-config JSON endpoint, or a plain SwiftInt) coerce correctly instead of silently falling back to their defaults. This matches the existing Android coercion behaviour.FIX: Android —
initialize()now runs its heavy setup (opening the Rust database, whichfsyncs to disk) on a background thread instead of the caller's main thread, andready()waits for it to finish. Previously, when the system re-created a backgroundFlutterEngine(e.g.audio_service's media service after the app was killed),GeneratedPluginRegistrantre-attached the plugin and the diskfsyncran on that service's main thread, causing an ANR on databases grown large over days of tracking. Thanks to @dagovalsusa (#260). -
3.6.823 Jul 2026Release notes
Open source →FEAT: Expose
Tracelet.updateNotification(), a public API to refresh the active Android foreground-service notification after changing its configuration (#257). The foreground-service notification is configured throughForegroundServiceConfig(title, text, icon, color, actions, priority, ongoing state), but there was previously no public way to apply notification-only changes to an already-running service — a notification-onlysetConfig()did not repost the live notification, so new content only appeared after an unrelated service restart or foreground transition.updateNotification()now refreshes the active on-screen tracking indicator from the latest configuration without restarting the tracking pipeline. On Android theACTION_UPDATE_NOTIFICATIONservice path rebuilds and reposts the foreground-service notification (previously a no-op) when the service is promoted, and is a safe no-op when the service is not running. iOS has no foreground-service notification, soupdateNotification()instead refreshes the running Live Activity — when the app opted into one vialiveActivityConfig— from the latest config (the dynamic body; the title is immutable on a running activity), and is a safe no-op otherwise. Web is a no-op. -
3.6.723 Jul 2026Release notes
Open source →FIX: In
MotionDetectionMode.smart/.speed,setConfig()could restore a temporary stationary mode as the main tracking mode. Those modes run a single continuous motion-aware pipeline that temporarily flips the tracking mode to periodic/geofences while the device is stationary. A restart-sensitivesetConfig()captured that temporary tracking mode and rebuilt the pipeline via the standalonestartPeriodic()/startGeofences()paths, tearing down the motion-detection pipeline that switches back to continuous on movement — stranding tracking in a standalone stationary mode.setConfig()(and, on iOS,ready()'s resume path) now restarts the continuous motion-aware pipeline viastart(isResume: true)whenever the motion-detection mode is smart/speed, regardless of the temporary tracking mode; the pipeline re-enters the stationary sub-state on its own when still stationary. Fixed on both Android and iOS (#256). -
3.6.622 Jul 2026Release notes
Open source →FEAT: Added
Tracelet.getForegroundServiceHealth()— exposes the authoritative native foreground-service state (whether the service is running and promoted to the foreground, the last promotion result ofsuccess/deferred/failedwith its failure class and message, the notification id, and the last transition timestamp) alongside the desiredenabledstate. On Android 12+ a foreground-service start can be deferred or rejected by the OS even while tracking is enabled, soenabledalone is not proof that background tracking is operational; this lets apps build accurate tracking-health indicators, diagnostics, and recovery. iOS reports the desired state with null/false promotion fields (it has no foreground service), and web returns a minimal disabled map (#255).FIX: On Android, changing a restart-sensitive setting via
setConfig()while tracking with a foreground service could kill that service. The restart path called the fullstop()— which sendsACTION_STOPtoLocationService(stopForeground+stopSelf) — and immediately restarted the pipeline withACTION_START. On a fresh promotion theACTION_STOPhandler'sstopSelf()could win the race and destroy the service right afterACTION_STARTpromoted it, leaving no foreground service at all — the same race fixed forstartPeriodic()in #237.stop()now accepts apreserveForegroundServiceflag and thesetConfig()restart path keeps the service alive whenever the target mode still needs it, letting the idempotentACTION_STARTre-assert foreground with no gap; modes that do not use the service stop it cleanly with no follow-up start to race. iOS is unaffected (#254). -
3.6.521 Jul 2026Release notes
Open source →FIX: On Android a failed foreground promotion no longer leaves the service marked as a running foreground service.
LocationService.startForegroundWithNotification()catches astartForeground()failure and tears the service down (stopForeground+stopSelf+isRunning = false), but because the exception was swallowed, execution returned normally and every caller then setisForegroundService = trueunconditionally. The method now returns whether the promotion succeeded and all callers gateisForegroundServiceon that result, so a failed promotion leaves the flagfalse. iOS is unaffected — it has no foreground-service promotion that can fail after the fact (#253). -
3.6.420 Jul 2026Release notes
Open source →FIX: On iOS the heartbeat writer no longer persists a GPS fix that the normal dispatch already stored, so
getLocations()no longer returns byte-identical duplicate location rows (roughly half the points of a moving trip were duplicated on-device). The normal dispatch persists withevent="location"and the heartbeat timer re-tagged the same cached fix withevent="heartbeat"and inserted it again; the dedup guard only skipped repeats forevent="location", so the heartbeat write slipped through. The guard now shares one last-inserted-timestamp key across both writers. The Android guard is kept in parity (#252). -
3.6.319 Jul 2026Release notes
Open source →FIX:
destroyLocation(uuid)now deletes the record addressed by its public UUID on both Android and iOS. Previously both native SDKs parsed the UUID string as a numeric database id (toLongOrNull()/Int64(uuid)), so any real UUID failed to parse and the call returnedfalsewithout deleting anything — pending locations could never be acknowledged and the queue never drained. The UUID is now resolved to its row id before deletion, with the legacy numeric-id path kept for backward compatibility (#251).FIX:
IosConfig.activityTypeis now applied toCLLocationManageras configured on iOS. Two independent bugs previously made every value resolve to.otherNavigation: the Dart bridge mapped between two differently-ordered enums by raw index (so e.g.otherNavigationwas sent asfitness), and the native side stored the value as an Int but read it back as a String and always fell through to the default. Both sides now agree, soautomotiveNavigation/fitness/airbornetake effect (#250). -
3.6.218 Jul 2026Release notes
Open source →FEAT: Remote config (
remoteConfigUrl) is now fetched and applied natively on iOS and Android. Onready()the SDK fetches a JSON config map from your HTTPS endpoint, applies it over the local config (restarting the tracking pipeline when a tracking-relevant key changes), and refreshes it in the background on theremoteConfigRefreshIntervalcadence. The last successful response is cached to disk, so a restart resumes on the freshest known settings instantly and offline. Only HTTPS URLs are honored. Previously both platforms recognized the field but never fetched it — the native side silently fell back to the local config.FIX: Stop double-inserting stationary periodic fixes with the same uuid. The stationary periodic timer in
LocationServicenow passespersist=falsetogetCurrentPosition()so it stays the single writer of the enriched "periodic" record (and the single event dispatch). Fixes the "UNIQUE constraint failed: location_events.uuid" error that occurred every stationary tick (#248). -
3.6.117 Jul 2026Release notes
Open source →FIX: Explicit
GeofenceConfig(geofenceModeHighAccuracy: false)is now honored on aggressive OEMs (Samsung/Xiaomi/Huawei/OnePlus/Oppo/Vivo) instead of being silently forced totrue— which madestartGeofences()start the location engine and theLocationServiceforeground service with its persistent notification, the exact thing low-accuracy geofences-only mode exists to avoid (and which Google Play prohibits solely for geofencing from 2026-10-28). Consistent with the #243 fix, the configured value is authoritative on every device and the SDK logs a reliability warning instead (#247). -
3.6.016 Jul 2026Release notes
Open source →FEAT:
Tracelet.updateLocationProviderOptions()— temporarily overridedesiredAccuracy/distanceFilteron the running OS provider without a pipeline restart; ephemeral (cleared bystop()), persisted config untouched. Live on iOS (CLLocationManagerproperty update) and Android (callback-preserving fused re-subscription) (#241).FIX: Explicit
foregroundService.enabled: false/periodicUseForegroundService: falseare now honored on aggressive OEMs (Xiaomi/Huawei/Samsung/OnePlus/Oppo/Vivo) instead of being silently forced back on with the default foreground notification; the SDK logs a reliability warning instead. Leftover foreground services are also torn down when switching to a no-service periodic strategy, and sticky service restarts re-validate state/config before re-posting the notification (#243).FIX:
rejectMockLocationsnow guards every Android delivery path —getCurrentPosition()(including the last-known fallback),watchPosition(), and periodic fixes — not just continuous tracking (found auditing #243).FIX: iOS
buildLocationMaphardcodedactivity: {type: "unknown", confidence: -1}on every persisted/dispatched location, dropping the classified transport mode even withfusedClassifierAuthoritative: true. Per-pointactivitynow carries the effective mode and confidence (fused when authoritative, scaled 0–100; otherwise platform Activity Recognition), including on the dead-reckoning path, and Android pairs the authoritative fused type with the fused confidence instead of the unrelated AR confidence (#244).FIX: Fused transport modes are persisted in the Activity Recognition vocabulary on both platforms (
vehicle→in_vehicle,cycling→on_bicycle), and the DartLocation.activity.typeparser now accepts the native snake_case strings —in_vehicle/on_bicycle/on_footpreviously collapsed toActivityType.unknown(follow-up to #244).FIX:
activity.confidencenow survives the DB round-trip — newactivity_confidencecolumn in the location store (auto-migrated;-1for rows persisted before the column existed), stored on every insert including encrypted payloads, and returned bygetLocations()and the sync-interceptor sink instead of a hardcoded100(#245). -
3.5.713 Jul 2026Release notes
Open source →FIX: Build fails without AGP built-in Kotlin (AGP <9 / builtInKotlin=false) (#239).
-
3.5.609 Jul 2026Release notes
Open source →FIX: Custom sync body 400 Bad Request HTTP errors now gracefully return fallback results instead of propagating fatal exceptions in native Sync engines (#238).
-
3.5.507 Jul 2026Release notes
Open source →FIX: Ensure foreground service is properly started in periodic mode when configured (#237).
-
3.5.430 Jun 2026Release notes
Open source →FIX: Enrich geofence transition events with real coordinate metrics (accuracy/speed/heading/altitude) from the last GPS fix and attach the battery snapshot, instead of hardcoded zeros (#231). FIX: Propagate runtime
setConfigchanges to the active native tracking/sensor loops by performing a clean full-pipeline restart (location + motion/speed) when a tracking-relevant key changes (#230). FIX: Null-guard subsystems indestroyAll()so engine/Activity teardown never throws when the SDK was never initialized (fatalUnable to destroy activity) (#227). FIX: Android: standard geofence mode no longer starts a foreground service, complying with Google Play's policy (effective 2026-10-28) that prohibits using a foreground service solely for geofencing. Native geofences keep firing while the app is suspended/terminated; geofence-only apps can removeFOREGROUND_SERVICE_LOCATIONfrom their manifest. -
3.5.330 Jun 2026Release notes
Open source →FIX: Added explicit ProGuard keep rules for
TraceletStartupProviderin thetracelet_androidpackage to preventClassNotFoundExceptionon process start when aggressive shrinking (like R8 full mode) is used (#228). -
3.5.223 Jun 2026Release notes
Open source →FIX: Android continuous tracking no longer silently stops after a while on aggressive OEMs (Samsung One UI, etc.). The foreground-service wakelock used a fixed 10-minute auto-expiry and was never renewed, so once it lapsed the CPU could deep-sleep and FusedLocationProvider stopped delivering updates with no error or callback. The wakelock is now renewed for the lifetime of tracking (#222).
-
3.5.119 Jun 2026Release notes
Open source →FEAT: Crash detection now uses the device barometer as an extra confirmation clue — a serious crash or airbag deployment causes a quick cabin air-pressure change, which raises crash confidence on phones that have a pressure sensor. Phones without one simply skip this check, with no downside (#173). FEAT: Crashes are now corroborated by a sudden post-impact speed collapse — when the vehicle goes from fast to nearly stopped in the seconds right after the jolt, crash confidence is raised. It only ever adds confidence, never cancels a real crash (#181). FEAT: Falls are now corroborated by the classic free-fall → impact → stillness signature — a brief weightless drop followed by the body coming to rest raises fall confidence (#180). FEAT: Crash/fall confirmation is now process-death-safe — if the OS kills the app during the cancel countdown (phone thrown, vehicle at rest, Doze), the confirmed event is still delivered from a re-armed wake-up (#182). DOCS: Rewrote the Driving & Safety crash/fall confirmation section in plain, beginner-friendly language.
-
3.5.019 Jun 2026Release notes
Open source →FEAT: Crash-detection ML model promoted from beta to stable — the shipped model is trained on the CC0 / public-domain Smartphone IMU Road Accident Detection dataset, so it is cleared for commercial use in production apps (#183). FEAT: The on-device encrypted model cache now auto-re-downloads when a new model version is published (SHA-256 of the cached blob no longer matches the expected digest), so model upgrades roll out in the same session instead of falling back to the rule engine for a cycle. FEAT (example): Driving & Safety page now shows a live crash-model download/load status indicator, a "Crash (ML model)" debug inference path, a "Benign bump" demo, and a bench "Throw-test" mode. PERF: Per-window crash-model probability is now logged for on-device observability.
-
3.4.218 Jun 2026Release notes
Open source →- REFACTOR: reformat test files and sync body context for consistent code style. (5552f795)
-
3.4.117 Jun 2026 -
3.4.017 Jun 2026Release notes
Open source →- FEAT(ios): Live Activity for active tracking — a Lock Screen & Dynamic Island indicator backed by ActivityKit, layered over the standard background pipeline (no redundant second location stream) (#202).
- FIX(ios): resolve release-mode launch crash (
SIGTRAP) caused by a duplicate key in the default config, plus a Widget Extension availability gate that hid the Live Activity and could crash the extension on iOS < 18. - FIX: per-call extras passed to
getCurrentPosition(extras:)/getLastKnownLocation(extras:)are now forwarded to native and merged with the globalHttpConfig.extrasinto the synced payload — the platform layer previously dropped per-callextrasanddesiredAccuracy(#201). - FIX:
getCurrentPositionnow defaults to high accuracy and never silently runs at passive priority, which could fail an explicit one-shot request withLOCATION_FAILURE. - FIX(android): a single location batch is now uploaded exactly once — duplicate sync providers no longer fire
requestSyncBodytwice for the same batch, preventing duplicate server uploads and duplicate DB rows (#204). - REFACTOR: extract issues 185 and 198, fix iOS config mapping. (1d088e0d)
- FIX: resolve accuracy priority mappings in Android and iOS. (65f5127d)
-
3.3.416 Jun 2026 -
3.3.315 Jun 2026 -
3.3.215 Jun 2026Release notes
Open source →Release notes
Open source →-
FIX (Location data, Android/iOS): Several location-map fields surfaced as static/default values in the Dart layer because the native-map → platform-channel converters dropped or mis-keyed them (#175):
getCurrentPosition(extras:, desiredAccuracy:)were silently ignored on Android (never forwarded to the SDK) — now applied.battery.isChargingwas alwaysfalse— the converter readisCharginginstead of the native snake_caseis_charging.isMovingwas alwaysfalse— readisMovinginstead of nativeis_moving.
Converters now read the native keys (with camelCase fallback), and field-by-field regression tests over the converters were added on both platforms to prevent recurrence.
-
TUNE (Crash detection): Lowered the default
crashGThresholdfrom3.0 gto2.0 g. Validation against the large VZCrash field dataset showed the 3.0 g speed-gated rule missed ~48% of real crashes (median impact ~2.2 g) while the false-positive budget was small. Crash detection is opt-in with a cancel-countdown, so the default now favours recall — raise it if you see too many prompts. See #173. (Crash detection remains beta pending first-party field validation.)
-
-
3.3.114 Jun 2026Release notes
Open source →- FIX(crash): harden crash/fall detection (confirmation survival, threshold, debounce, sample rate). (0a17e804)
Release notes
Open source →- FIX (Crash detection, Android/iOS): Confirmed
crash/fallevents are no longer lost when tracking stops right after the impact (the common crash → vehicle-at-rest →stopTimeoutcase). The confirmation countdown now runs independently of tracking state and self-terminates when no candidate is pending (#169). - FIX (Crash detection): The effective crash g-threshold matched the documented value — the confidence gate previously raised a 3.0 g threshold to ~3.6 g, increasing false negatives (#170).
- FIX (Crash detection): A single crash (primary spike + bounce/secondary impacts) no longer raises multiple candidates; a refractory period debounces one event into one prompt (#171).
- IMPROVE (Crash detection, Android/iOS): When crash/fall detection is enabled, the accelerometer is sampled at a higher rate (Android
SENSOR_DELAY_GAME+ no batch latency; iOS 100 Hz) so short impact peaks (~50–150 ms) are actually captured instead of missed between motion-detection samples (#172). Roadmap for research-grade robustness (Δv, sensor fusion, free-fall signature, process-death survival): #173.
-
3.3.014 Jun 2026Release notes
Open source →- FIX (Audit, Android/iOS): The tamper-proof audit chain only covered locations that flowed through the foreground location dispatcher. Background/headless persists (periodic worker, location service, killed-state relaunch, geofence events) wrote location rows with no matching audit-trail link, so
getAuditProof()returnednullfor those records even with audit enabled. Audit links are now generated at the single persistence chokepoint, so every persisted location is chained regardless of source. Chain mutation is also now thread-safe. - FIX (Audit, iOS):
appendToChainno longer creates an audit row for records without auuid(it previously used an empty string). Such orphan rows had no retrievable location and madeverifyAuditTrail()report the whole chain as broken ("missing location record"). uuid-less records are now skipped on both platforms. The audit hash version was bumped (auto-resets any orphaned/incomplete chains from the prior logic on first launch). - FEAT (Battery, Android): Motion-gated wakelock — drop the OEM partial wakelock when stationary and re-assert it on movement, via
AndroidConfig.releaseWakelockWhenStationary(opt-in, default off; gated on the hardware significant-motion wake sensor) (#162). - FEAT (Driving & Safety): On-device driving-behavior telematics —
harsh_braking/harsh_acceleration/harsh_cornering/speedingviaTelematicsConfig+Tracelet.onDrivingEvent(opt-in, default off) (#163). - FEAT (Driving & Safety): On-device transport-mode classifier (still/walking/running/cycling/vehicle) fusing accelerometer + GPS via
ClassifierConfig+Tracelet.onModeChange(#164). - FEAT (Driving & Safety): Crash & fall detection with a cancel-countdown confirmation flow via
ImpactConfig+Tracelet.onImpactandTracelet.confirmImpact/Tracelet.cancelImpact(opt-in, default off) (#165). - All three features are default-off and side-channel — no change to existing tracking when disabled. See Driving & Safety.
- FIX (Audit, Android/iOS): The tamper-proof audit chain only covered locations that flowed through the foreground location dispatcher. Background/headless persists (periodic worker, location service, killed-state relaunch, geofence events) wrote location rows with no matching audit-trail link, so
-
3.2.1913 Jun 2026 -
3.2.1812 Jun 2026Release notes
Open source →- FIX (Native):
ready()/getState()now populateState.configwith the active configuration instead of leaving it permanentlynull(#147). - FEAT: Add
HttpConfig.syncIntervalfor interval-based sync — the documented repeating-timer cadence was missing from the Dart config and the Pigeon layer; the native interval timer now flushes the offline queue on this cadence (#149). - FIX (Native):
destroySyncedLocations()returns the real number of synced-and-pruned locations instead of a hardcoded0stub (#154). - FEAT: Expose the offline queue with
getPendingLocations()andgetPendingLocationCount()(#159). - FIX (Native): Honor the
useKalmanFilterconfig key so the Extended Kalman Filter is no longer silently disabled by a key mismatch (#148). - FIX (Native): Propagate the detected activity (walking / driving / still) into recorded locations — fixes a permanent
"activity": "unknown"(#155). - FIX (Native): Rebuild the native location processor when
ready()applies a new config, so settings such asdistanceFiltertake effect immediately instead of using stale defaults (#157). - FIX (Native):
getCount()honors time-bound queries instead of always returning the whole-database total (#152). - FIX: Guard the
AuditConfighash-algorithm mapping so configuringsha384/sha512no longer crashes with a fatalRangeErrorduringready()— unsupported variants fall back tosha256(#150). - FIX (Native): The HTTP sync payload now includes each point's motion state
is_moving(#151) and its triggerevent(location / motionchange / heartbeat / geofence) (#156) — both were previously omitted by the native sync record.
- FIX (Native):
-
3.2.1712 Jun 2026Release notes
Open source →- FIX (Native): Resolve iOS auto-sync thread starvation by offloading synchronous HTTP requests to a background DispatchQueue to prevent blocking Swift Concurrency pools (#146).
- CHORE (Docs): Fix Nextra changelog rendering bug and improve auto-translation glossary script for internationalization.
-
3.2.1612 Jun 2026Release notes
Open source →- FIX (Native): Resolve Android/iOS getting stuck in the moving state and never transitioning back to stationary, which kept continuous GPS active and drained the battery. The accelerometer stillness sampler now stays active during the stop-timeout countdown and requires sustained motion — rather than a single noisy or stale sample — to abort it (#142).
- FIX (Native): Background and post-reboot location captures are persisted (and therefore synced) again. Headless tracking (killed-state relaunch / boot) never calls
ready(), so an internal readiness guard silently dropped every captured location before it reached the database, leaving auto-sync with nothing to upload. - FIX (Android): The foreground-service notification now reliably appears when the app is backgrounded or terminated with
showNotificationOnPauseOnlyenabled. The app's own foreground service skewed foreground/background detection (and OS process-importance updates lag), so the pause-only notification was suppressed even though tracking and syncing continued.
-
3.2.1511 Jun 2026Release notes
Open source →- FIX (Native): Allow
getState()andstop()to be called beforeready()is invoked, correctly reporting persistent state and shutting down background services if the app was restarted from a killed state. - CHORE: Update dependencies and constraints.
- FIX: Resolve
MissingPluginExceptionand test timing issues withsetHasCustomSyncBodyBuilder.
- FIX (Native): Allow
-
3.2.1411 Jun 2026Release notes
Open source →- FIX(sync): keep method channel alive to avoid iOS timeout bugs when no builder is registered. (9a083478)
- FIX(sync): resolve issue 134 where custom sync body timeouts prevented background syncs. (7fa16fdf)
- FIX(sync): fix background auto-sync abortion when no custom builder is registered (Issue #134). (631542a1)
- DOCS: add official documentation URL to all package READMEs. (9eb6951e)
- DOCS: integrate nextra website and update pubspec URLs. (99b7fda8)
-
3.2.1310 Jun 2026Release notes
Open source →- FIX(android):
startOnBootnow resumes tracking after a reboot on devices where the OS refuses to start alocationforeground service fromBOOT_COMPLETED(e.g. Android 14). Previously tracking silently never resumed after a reboot; it now falls back to background WorkManager/alarm tracking. - FIX(android): HTTP sync now works headlessly after a reboot — background sync can refresh an expiring auth token and build a custom sync body without the app being opened. Previously the headless Dart sync bridge was only wired when a UI engine attached, so post-reboot sync used a stale token (or the wrong payload) until the app was launched.
- FIX(android):
-
3.2.1210 Jun 2026Release notes
Open source →- CHORE: Re-release to align the full federated package set and native SDKs to a single consistent version. The 3.2.11 release published with mismatched versions across some packages (a few resolved to 3.2.10). No functional code changes.
-
3.2.1109 Jun 2026 withdrawnRelease notes
Open source →- FIX: Custom sync-body builder now falls back to the headless engine on timeout (instead of aborting the sync) on both Android and iOS — fixes location sync stopping after a few minutes in the background when using
setSyncBodyBuilder(Issue #134).
- FIX: Custom sync-body builder now falls back to the headless engine on timeout (instead of aborting the sync) on both Android and iOS — fixes location sync stopping after a few minutes in the background when using