Tracelet 中的新增功能
及时了解 Tracelet 生态系统的最新功能、错误修复和性能改进。
tracelet
v3.8.83.8.8
FIX: (Android) getCurrentPosition() now wakes the provider for a fresh fix instead of retrying a call that can answer from cache forever. The 1.8.7 fix for #46 said getCurrentPosition(samples: 1) had moved to requestLocationUpdates() so it would wake GPS and return a fresh position; the code never did — every sample came from a one-shot getCurrentLocation(), retried every 800 ms, which can return null or a cached fix without engaging the hardware, and on the reporting device did exactly that until the timeout. Samples are now collected from two sources side by side for the bounded window: a continuous request, which is the only API that actually wakes the provider, and the one-shot loop, kept because it is the path that answers on budget devices whose battery optimisation throttles continuous requests for an app without a foreground service. The first genuine fix from either counts, a fix both deliver is counted once so samples still means distinct fixes, and every exit — enough samples, timeout, permission failure — removes the continuous request so a one-shot operation never leaves the provider running (#416).
FIX: (Android) requestLocationAuthorization() no longer hangs forever when the Activity is detached while the permission dialog is showing. The plugin completes the pending reply only from onRequestPermissionsResult, delivered through the listener it registers on the ActivityPluginBinding; onDetachedFromActivity() removed that listener and, since the 2.0.0 lifecycle rewrite, did nothing about the reply — so a dialog answered after a detach left the Future unresolved, and the next call short-circuited on the stale pending callback and returned the current status synchronously, which is why it appeared to work on the second try. The plugin now completes the reply with the current status on a full detach, before the activity is dropped so DENIED can still be told from NOT_DETERMINED, and leaves it alone on the config-change detach where the listener is re-added on reattach and the result still arrives (#415).
FIX: (iOS) continuous tracking no longer shows the blue location bar on an Always-authorized app that opted out of it. IosConfig.useBackgroundActivitySession is documented as opt-in and defaults to false, but only LocationEngine.start() ever read it: every transition to moving went through a path that checked useSignificantChangesOnly and nothing else, and opened a CLBackgroundActivitySession — which shows the system indicator for as long as it exists, regardless of showsBackgroundLocationIndicator. So the exact configuration the #210 resolution told users to adopt to hide the indicator showed it for the length of every trip, and it went away on its own some minutes after the device parked, which is what made it look like an OS quirk rather than the SDK. Under Always the session buys nothing — allowsBackgroundLocationUpdates and the location background mode already keep the stream delivering — so it now opens only with the opt-in. The one exception is When-In-Use authorization, where the session is what lets the stream survive suspension and the OS shows the indicator regardless; there it still opens, so no app that relied on it loses tracking. The in-app-evaluated geofence posture, which had the same unconditional start, goes through the same gate (#423).
FIX: (Android, iOS) Tracelet.ready() no longer throws Bad state: tracelet_core's codegen version (2.12.0) should be the same as runtime version (2.13.0) on a fresh install. The bindings were generated by flutter_rust_bridge 2.12.0 but the runtime was constrained as ^2.12.0, and flutter_rust_bridge checks the two match exactly at init. Since 2.13.0 was published on 23 August every new pub get resolved it, so every new integration of 3.8.7 crashed at ready(). The bindings are now generated by 2.13.0 and the runtime is pinned to the same minor, >=2.13.0 <2.14.0, so pub cannot resolve a runtime the generated code rejects; CI installs the codegen at that exact version too, instead of whatever is latest, so the two cannot drift again (#421).
FIX: (Android, iOS) a device that has been sitting still no longer discards the movement that should wake it. When the GPS-speed machine reports stationary and the accelerometer disagrees, the SDK settles it with the last resolved speed — near zero means the accelerometer is reading hand tremor on a still device, and it is overruled. That reading had no freshness check, while the pace machine beside it has refused fixes older than ten seconds since 3.8.7, so the two disagreed about a number neither could vouch for. A parked device stops its stream and the reading freezes at whatever it last resolved — 0.0057 m/s in the reported trace, well under the tremor threshold — and the stationary schedule produces no fresh fix by construction, which is exactly when the frozen value is most likely to be wrong. So a walk started minutes later, with the app in the background, was overruled by a speed measured before the device stopped, and the session never left stationary. Reopening the app produced a fresh fix and unwedged it, which is why this was reported as "the location indicator only appears while the app is in the foreground". An old reading is now unknown in the same sense a missing one already was — and unknown leaves the accelerometer standing rather than siding against it — with the decline recorded on the always-on channel (#404, #344, #318).
FIX: (Doctor) a bug report no longer says "all systems healthy" on a device whose OS has blocked background tracking. The blockers — the Restricted battery state, the RESTRICTED standby bucket, a foreground service denied the location capability, and a promotion the OS refused — are listed in the health verdict at the top of the report rather than only in a section further down that a reader has already been told to skip (#406, #405).
FIX: (Android) a foreground-service promotion the OS refuses is no longer reported as a successful one. In the "Restricted" battery state Android refuses the promotion and says so only in its own log — Service.startForeground() not allowed due to bg restriction — without throwing, so the call returned normally and the SDK recorded success. A device in that state showed isForeground=false types=00000000 caps=------ — demoted, with no location capability, GPS switched off about a second after every registration — while the report read Promoted to foreground: true / Last promotion result: success for a session that could not record anything in the background. A health snapshot that reads green while the OS has switched tracking off sends every investigation into the SDK instead of at the restriction, so the refusal is now reported as its own outcome and announced on the always-on channel (#406, #405).
FIX: (Android, iOS) stopping tracking no longer leaves a countdown that restarts it. The GPS-speed machine's stationary countdown is a scheduled callback, and stop() cleared the running flag without cancelling it — while the countdown's own guard tested the motion state, not whether the session was still alive. A session stopped part-way through slowing down therefore transitioned to stationary minutes later, and that transition is not inert: it switches the coordinator into the stationary schedule, which rewrites the tracking mode and the pace and arms a repeating location timer. A field report shows the stop at 18:43:09 and the countdown expiring at 18:45:59, leaving a stopped app reporting Tracking mode: periodic. Separately, because the platform's "stop location updates" call is asynchronous, a fix already in the provider's delivery pipeline still arrived afterwards and was run through the whole pipeline — enriched, geocoded, added to the odometer and written to the database the user had just stopped filling. Such a fix is now dropped, and the drop is recorded once on the always-on channel (#412).
FIX: (iOS) a geofence too small for the OS to resolve is now evaluated when the session starts stationary. Such a fence is decided from the location stream rather than by CoreLocation, and a stationary start deliberately runs no stream — so a fence stored before start() was never evaluated at all, staying dead until the device happened to move. The re-alignment that covers a fence added mid-session could not cover this one: it returns early unless ownership changed, and start-up had already set the flag from the stored fences a moment earlier, so the flag was right and nothing ever opened the stream it implied. Android has opened that stream since the original fix; iOS never did. The stream is opened and then judged like any other a stationary session inherits — parked back to the stationary schedule when both motion inputs agree, which still feeds the evaluator, rather than held open at full rate (#412, #357).
FIX: (Android) a fix arriving while continuous updates are stopped is recorded on the always-on lifecycle channel. A parked session was still recording roughly a fix a second for the whole stationary window, with no continuous updates starting entry between the park and the next wake-up — so the callback the park removed cannot have been the source, and the trace had no way to say what was. The entry names the caller, the engine identity and whether a periodic tick or a position watch is outstanding, which separates a second registration on one engine from a second engine in the same process. It is emitted once per stopped period rather than per fix. The stream start/stop entries carry the same engine identity plus a count of how many engines in the process hold a live stream, which is the only reading that separates a second engine from a second registration on one (#412, #318).
FIX: (Android) walking for a while and then standing still no longer leaves the location indicator lit for the rest of the session. Stopping after a brief move parked correctly, which is what made this look intermittent: the difference is that a fix taken seconds after a real walk still reads well above the near-zero speed the coordinator treats as hand tremor, so it declines to overrule the accelerometer and waits for the accelerometer to stand down on its own — and on Android two separate faults meant it never could. The stop countdown was armed on the single sample where the stillness streak crossed its threshold, and a walking activity update cancels that countdown without resetting the streak; since the streak only grows, it could never cross again. And the accelerometer's own "am I already stationary?" check read the shared isMoving flag, which in smart mode is the combined answer and which the GPS-speed machine writes the moment its own countdown expires — so the speed machine going stationary first silenced the accelerometer's edge before it was ever delivered. With both inputs mute the combined answer stayed "moving" with nothing left to change it, and full-rate GPS ran on a phone sitting still (#412).
FIX: (Android, iOS) a session that is not moving no longer holds a full-rate GPS stream open for the rest of its life. The SMART coordinator holds a posture — is the engine streaming, or is it on a stationary schedule — and the core only emits the stop action when it believes that posture is continuous. That posture was read from the committed pace alone, so every route that puts a stream up behind the coordinator wrote "parked" over a live engine, and no later stationary decision produced anything: a ready() takeover with isMoving = false over a running stream (#409); a stationary start, which opens a stream it otherwise would not because a fence too small for the OS to resolve has to be decided from one (#357); a fence added to a parked session from the map; and an OS wake-up near an in-app fence (#414). On Android the park itself writes trackingMode = periodic, so after the first park the reconcile answered "already stationary" without looking at the engine at all — and that is the state a real session spends most of its life in. Reported devices held 2-second fixes at full accuracy for six unbroken minutes, and for whole sessions from Start to Stop, every fix is_moving: false, lying on a desk with the location indicator lit: the pace was right the whole time, only the stream was never told. The posture is now the OR of the committed pace and the engine's live state in every mode, and the coordinator is reconciled once the engine has settled rather than from a prediction made before it started — so a stationary session that opened or inherited a stream is parked onto the stationary schedule, where an in-app fence is still evaluated at the stationary cadence rather than at full rate, and a moving one keeps streaming (#412, #409, #414, #357).
FIX: (Android, iOS) two calls to start() with the same configuration no longer give two different paces. start() seeds the GPS-speed machine with the last speed the process resolved — which exists for a resume, where feeding a fabricated 0 m/s stood a still-walking session down. The seed can only ever push the machine up, though, so a fresh start() that had just committed motion.isMoving: false came back moving on the strength of a stale reading. The first start() of a process has no fix to seed with and stays stationary as asked; the second seeds from the fix the first one acquired and comes up moving. The seed now applies only where the pace was inherited — a resume, or a start that begins moving — and a fresh start keeps the pace it was given (#344).
FIX: (Android, iOS) turning logging up no longer destroys the evidence. The always-on lifecycle trace bypasses logLevel because it is the record of what the background and killed-state paths did — but it was then pruned by row count alongside everything else, and at logLevel: verbose that cap is 2000 rows against a sensor trace emitting several lines a second. A field report's "Session lifecycle" section came back holding a single line after an hour of tracking: the entries were not missing, they had been evicted, and the retention window had collapsed from the configured logMaxDays to a couple of minutes. The row cap is now applied per channel, so the chatty one can no longer crowd out the one the report is written to carry (#318).
FIX: (Android) being near a geofence no longer tells the SDK the device is moving. A fence too small for the OS to resolve is registered inflated and decided in-app, so the transition the OS fires is a hint that you are near it — and claiming that hint ran through the same switch a real motion wake-up does, which writes isMoving = true, cancels the stationary schedule, and opens a full-rate stream. The reported device had parked itself correctly one line earlier (accelMoving=false speedMoving=false) and then streamed 2-second GPS with the indicator lit for the rest of the session, reporting Is moving: true on a desk. The pace was inherited from there: every later resume read it, forced the speed machine back to MOVING, and left the coordinator with no stationary decision to make. The wake-up now borrows the stream instead — the pace is left exactly as the motion sensors set it, and the loan is bounded: when the window closes the coordinator re-judges the state it is in, parking again if both inputs still say stationary and leaving the stream alone if the device really did move (#414). Two supporting corrections ride with it: a session resuming into a live process now takes the coordinator's speed input from the pace machine rather than from the intent — start() returns early when the machine is already running, so on that path the machine keeps whatever state it had, which may be slowing or stationary — and the posture reconcile is recorded on the always-on lifecycle channel, because it is the entry that answers "why is GPS still running" and a released build was discarding it at the default logLevel (#414, #318).
FIX: (Android, iOS) the parked-device GPS drain had two further causes, and on iOS a report could not show either. When the GPS-speed machine reports stationary while the accelerometer still reports motion, the SDK settles the disagreement with the last resolved speed: near zero means the accelerometer is reading hand tremor on a still device, and it is overruled. That override is itself a motion transition, and whenever the speed input was already stationary it is the one carrying the whole stop decision — it was computed and then discarded, and the speed transition behind it then changed nothing, so the stream was never told. The speed input reaches that stale state on its own: it lives for the whole process, and only a session starting stationary re-asserted it, so any session that parked handed the next started-moving one a value it could never clear — leaving that session's continuous GPS hanging on the accelerometer alone. On iOS the transition was also invisible: the motion pipeline parks by stopping location updates without ending the session, which never runs the code that records location stream: continuous updates stopping, so a released app's report could not say when — or whether — GPS was parked, the one line an Android report has always carried. And iOS decided whether a stream was live from a flag that stays true on a parked engine, which could wedge the same belief in the opposite direction and swallow the next wake-up.
FIX: (Android) boot-mode tracking no longer builds a second LocationEngine beside a live session engine. Task removal tears down the Flutter engine but deliberately keeps the SDK, so the session engine, its motion detector and its heartbeat all survive — and boot mode used to start a full second set beside them. Two streams then ran in parallel with separate fix caches, and a stationary switch stopped only the engine its own coordinator held: the reported device had the boot engine parked on a 5.7-minute-stale fix while the other streamed at 2 s. Boot mode exists for a process with no session left to do the work — a cold boot, or a sticky restart after process death — and it now checks for one before starting anything (#410).
FIX: (Android) background tracking could stop dead while every diagnostic said it was healthy, and the SDK now names the three reasons. Android 12+ decides whether a foreground service may use location when the service record is created — at startService(), not at startForeground() — and keeps that verdict for the life of the record. A service started from a background trigger (a reboot, an alarm, a geofence, a sticky restart) therefore posts its notification, reports itself promoted, carries FOREGROUND_SERVICE_TYPE_LOCATION, and is handed no location at all: GPS never starts, the status-bar location indicator never appears, and not one fix arrives until the app is reopened. getForegroundServiceHealth() gains serviceStartedInForeground and locationCapabilityLikelyDenied, which separate "promoted" from "promoted and able to track" — a distinction nothing in the API could previously express (#405). It also gains backgroundRestricted, standbyBucket and standbyBucketName: the "Restricted" battery state is independent of the battery-optimization exemption the health check already reported, and stronger — it blocks the promotion outright — so a device could report isIgnoringBatteryOptimizations: true and refuse to run in the background at the same time (#406). And the stall watchdog runs on its own timer instead of waiting for a fix to arrive, so a stream delivering nothing — the case where the SDK was completely blind — announces itself after 45s with the request it is complaining about, and announces its recovery. Silence and rejection are reported as different faults because they are: one means the pipeline is alive and mis-tuned, the other means the OS has stopped talking to the app. That watchdog is on both platforms — the fix-driven structure it replaces was never Android-specific, and a silent stream on iOS was equally invisible (#407). All of it is on the always-on lifecycle channel, so a release build reports it. Not yet fixed: keeping the service record out of the background in the first place, and recovering once it is there.
FEAT: (Android, iOS) trips have an identity, and the records made during them carry it. The SDK mints a UUIDv4 when a trip starts, keeps it for the trip's lifetime, and discards it at the end, so an id is never handed to a second journey. It is written into the new trip_id column on locations and driving events at insert time, which is what makes an offline backlog correct: a row recorded during one trip still uploads as that trip's even when a different trip is running by the time the flush happens, where resolving it at sync time would silently reassign the whole queue. The default sync payload gains a trip_id field on each location and telematics record — present as an explicit null outside a trip, so the key can be relied on — and nothing else is renamed or moved. Tracelet.onTripStart is new: a trip previously only became observable once it had ended, so an app that wanted to group records by trip had to re-derive the boundary from onMotionChange and would drift from the SDK's own detection. Tracelet.currentTripId reports the trip in force right now, so app-side data can be tagged with the same key. TripEvent gains tripId plus absolute startedAt/endedAt bounds — duration alone could not place a trip on a timeline. Additive throughout: the new fields are nullable and read null for trips and rows from before this version (#402).
tracelet_doctor
v3.8.83.8.8
FIX: the health-check verdict accounts for the conditions that stop background tracking outright, so a report can no longer open with "all systems healthy" while the section below it says the OS has switched tracking off. HealthCheck.warnings is computed from the health map alone and the background restrictions live in the foreground-service snapshot — a different platform call — so the two sections could contradict each other in the same report, and the summary is what gets read first. The Restricted battery state, the RESTRICTED standby bucket, a location-blind promotion and a promotion the OS refused now appear as 🔴 blockers ahead of the ordinary warnings, and they survive a failed health call rather than disappearing with it. Both sections now share one snapshot instead of making the same platform call twice (#406, #405).
FIX: the four log-derived sections share one read instead of fetching the same rows over the platform channel four times, and the lifecycle and geofence scans default to 5000 entries rather than 2000. 2000 was also the SDK's row cap at logLevel: verbose, so the scan could see the whole table and no more; now that the cap is applied per channel, the always-on LIFECYCLE rows are kept behind up to a full cap of level-based chatter and a 2000-row scan would stop short of exactly the entries these sections exist to surface (#318).
FIX: the report states a verdict on the foreground service instead of leaving it to be inferred from six rows that all say true. A service that is running, promoted, carrying FOREGROUND_SERVICE_TYPE_LOCATION and denied location by the OS rendered as an entirely green table, which is how a report covering 52 seconds of recording nothing read as healthy. The foreground-service section gains Started in foreground, Background restricted and Standby bucket, and the in-app card no longer shows "Healthy" for either state (#405, #406). The Location stream health section picks up the new silence and resume entries, and its empty case no longer claims "the stream has been accepting fixes" — it had asserted exactly that over a window in which it accepted none. An absence of markers is now reported as an absence of markers (#407).
tracelet_sync
v3.8.83.8.8
FEAT: SyncLocationRecord carries tripId, emitted into the default payload as trip_id on each location — taken from the stored row rather than the trip running at flush time (#402).
tracelet_supabase
v3.8.83.8.8
Version alignment with tracelet 3.8.8.
tracelet_firebase
v3.8.83.8.8
Version alignment with tracelet 3.8.8.
tracelet_android
v3.8.83.8.8
FIX: the pinned native SDK's getCurrentPosition() registers a continuous location request alongside the one-shot retries, so a device whose getCurrentLocation() answers null or from cache no longer spins to the timeout; the request is removed on every exit (#416).
FIX: onDetachedFromActivity() completes a pending permission reply via clearPendingPermissionCallback() before dropping the activity, so requestLocationAuthorization() no longer hangs when the Activity detaches under the dialog; the config-change detach is unchanged, since its result still arrives after reattach (#415).
FIX: the pinned native SDK stops a stale near-zero speed vetoing an accelerometer wake, so a backgrounded device leaves stationary mode when it actually moves; no Dart or plugin change (#404).
FIX: the pinned native SDK reports a foreground-service promotion the OS refused as refused rather than success; no Dart or plugin change (#406).
FIX: the pinned native SDK cancels the speed machine's stationary countdown on stop and drops fixes delivered after the session ended; no Dart or plugin change (#412).
FIX: the pinned native SDK re-arms the accelerometer's stop countdown after a walk and stops the GPS-speed machine silencing the accelerometer's stationary edge; no Dart or plugin change (#412).
FIX: the pinned native SDK parks the stream it opened or inherited when the motion pipeline parks — including after the session has already parked once — keeps a fresh start()'s committed pace, and stops verbose chatter evicting the lifecycle trace; no Dart or plugin change (#409, #412, #414).
FIX: the pinned native SDK keeps a fence wake-up from committing a moving pace; no Dart or plugin change (#414).
FEAT: the pinned native SDK mints and stamps a trip id; no Dart or plugin change (#402).
tracelet_ios
v3.8.83.8.8
FIX: the pinned native SDK opens CLBackgroundActivitySession on the continuous path only with useBackgroundActivitySession: true (or under When-In-Use authorization, where it is what keeps the stream alive), so an Always-authorized app that opted out no longer shows the blue location bar for every trip (#423).
FIX: the pinned native SDK announces a location stream that is delivering nothing, so a dead stream is no longer reported as a healthy one; no Dart or plugin change (#407).
FIX: the pinned native SDK stops a stale near-zero speed vetoing an accelerometer wake, so a backgrounded device leaves stationary mode when it actually moves; no Dart or plugin change (#404).
FIX: the pinned native SDK cancels the speed machine's stationary countdown on stop; no Dart or plugin change (#412).
FIX: the pinned native SDK opens the evaluator's location stream for an in-app-evaluated fence on a stationary start; no Dart or plugin change (#412, #357).
FIX: the pinned native SDK keeps a fresh start()'s committed pace against the last-known-speed seed, and stops verbose chatter evicting the lifecycle trace; no Dart or plugin change (#344, #318).
FIX: the pinned native SDK keeps a fence wake-up from committing a moving pace; no Dart or plugin change (#414).
FIX: the pinned native SDK stops the stream it inherited when the motion pipeline parks; no Dart or plugin change (#409).
FEAT: the pinned native SDK mints and stamps a trip id; no Dart or plugin change (#402).