My app's GPS tracking service kept running after an FCM silent push triggered auto-punchout. The app was killed. The background handler ran, punched the user out, and tried to stop the GPS service. The service ignored it. Battery drained until the user manually opened the app.
The problem wasn't in my code. It was in Tracelet, the GPS tracking library I depend on.
Why Tracelet.stop() doesn't work from a headless isolate
Tracelet is a Flutter GPS tracking library by Ikolvi. My app (Zontra, a field-workforce tracking platform) uses it for all location capture. Tracelet.stop() works fine when called from the main isolate. It uses Pigeon-generated bindings to talk to the native Android service.
Pigeon doesn't work from headless isolates. When Firebase Cloud Messaging delivers a silent push to a killed app, Android spins up a headless Dart isolate with no Flutter engine attached. The Pigeon channel isn't registered there. Tracelet.stop() throws MissingPluginException and the GPS foreground service continues running.
I'd been working around this by telling the backend to not send silent pushes when tracking was active. That's fragile. The actual fix needed to happen in Tracelet's native layer.
The patch
I filed issue #267 and opened PR #266. Three files, 68 lines added.
On the Kotlin side, I added a "requestTermination" handler to HeadlessTaskService.kt's existing method channel (com.tracelet/methods). This channel already handles setDynamicHeaders and setSyncBodyResponse from headless isolates, so the infrastructure was there. The handler calls TraceletSdk.getInstance(context).stop() wrapped in try/catch.
On the Dart side, I added Tracelet.requestTermination() as a static method that invokes the headless MethodChannel directly. On iOS it catches MissingPluginException and returns false (iOS headless architecture is different and out of scope for this PR).
// In FCM background handler or other headless isolate: final stopped = await Tracelet.requestTermination(); // true if native service acknowledged, false on iOS or failure GalacticTitan (Tracelet maintainer) merged it.
Contributing upstream to your own dependency
This is a different dynamic than contributing to Flutter or a package you don't use. I ship Zontra to production. The GPS service running after punchout is a real support ticket from a real field worker whose phone died at 4pm. I have skin in this fix working.
That also means I have to be careful about the scope. I could have added a bunch of Zontra-specific things while I had the Tracelet source open. I didn't. The PR does one thing: expose a termination path that works from headless isolates. The issue explains why. The fix is general enough that any app using Tracelet with FCM background handlers benefits.
I left the analog stick Y-axis question (from the gamepads PR, different repo, same principle) as a reminder: fix what you can verify, flag what you can't, and don't bundle unrelated changes because you happen to be in the neighborhood.
