You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.41.5, on macOS 26.3 25D125 darwin-arm64, locale en-DE)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 26.4.1)
[✓] Chrome - develop for the web
[✓] Connected device (2 available)
[✓] Network resources
• No issues found!
Mobile operating-system(s)
iOS
Android
Device Manufacturer(s) and Model(s)
Sony Xperia 1 III, Pixel 8A, Pixel 3, Xiaomi 13T, Nothing Phone (1)
On Android plugin 5.1.2, when a user swipe-kills the app from recents and reopens it within a few hundred milliseconds while the plugin's own TrackingService foreground service is still alive in the process, the recreated MainActivity ends up in a state where:
The new FlutterEngine attaches cleanly.
Dart main() runs.
bg.BackgroundGeolocation.ready(config) and bg.BackgroundGeolocation.start() both return enabled: true.
All onLocation, onMotionChange, onHeartbeat, onHttp, onEnabledChange, onActivityChange, and onProviderChange listeners are re-registered on the new engine without error.
No listener callback fires. Not one. The main isolate is silently disconnected from the native event stream for the lifetime of this recreated MainActivity. We have measured 8+ second wedge windows with zero events in the main isolate while the headless isolate continued receiving events during the exact same window.
Recovery requires the user to fully kill the OS process (e.g., force-stop or wait for the OS to reap it) and cold-launch. App lifecycle bounces (onPause/onResume) do not recover dispatch.
A small repro repository can be found here, the error can be reproduced by force quite and quick restart cycles.
How this relates to the 5.1.2 fix
Your CHANGELOG entry for 5.1.2 reads:
[Fixed][Android] App stuck on splash / logo after relaunch when the foreground service kept the process alive past Activity termination. Root cause: HeadlessTask's static background FlutterEngine was never destroyed, so on main Activity reattach the stale engine conflicted with the freshly attaching main engine's plugin channels. HeadlessTask.destroyBackgroundIsolate() now runs when BackgroundGeolocationModule.setActivity(activity) receives a non-null Activity, clearing the background engine before the main engine attaches.
We are running 5.1.2 and we believe that fix is engaging correctly for our build — the section "Evidence the 5.1.2 fix engages" below shows reflective probe output proving BackgroundGeolocationModule.mActivity is the freshly-attached Activity by the end of configureFlutterEngine on the recreated MainActivity.
Our observable, however, differs from the splash/logo hang the CHANGELOG describes:
We do not see a splash hang. The Flutter UI renders, navigates, and is responsive throughout.
bg.ready() / bg.start() / bg.state round-trips all succeed on the MethodChannel.
The main Flutter engine attaches without complaint.
The headless isolate continues to receive plugin events normally during the wedge window.
What is broken is specifically the dispatch from the plugin's native event source into the main engine's EventChannel sinks. From Dart's perspective the listeners are wired up, but nothing arrives. From the native plugin's perspective everything is firing — we can see headless-side onEnabledChange and onHeartbeat arriving correctly during the same window.
The 5.1.2 fix appears to target the engine-attach phase. We believe a sibling issue lives one step downstream — in the path where TrackingService (the plugin's location FGS) is alive at MainActivity reattach. That is a different process state from the one the splash-hang fix addresses, where the FGS keeping the process alive is an unrelated external service.
Reproduction recipe
Deterministic on every device we have tried:
Build a debug or release app with the plugin configured as in "Plugin Code and/or Config" below — anything with foregroundService: true, stopOnTerminate: true, and a backend-bound http.autoSync config will work.
Launch the app cold. Grant location permissions. Confirm bg.start() returns enabled: true and that onLocation/onHeartbeat/onHttp callbacks fire in Dart as expected. The plugin's TrackingService is now a foreground service in the process.
Swipe the app from recents. The plugin's onTaskRemoved is called by the OS. Because stopOnTerminate: true, the plugin begins its internal shutdown of TrackingService. This shutdown is asynchronous — the FGS does not transition out of foreground state immediately.
Within ~200ms, tap the app icon to reopen it. The OS finds the process still alive (because TrackingService is still foreground-promoted) and reuses it, launching a new MainActivity against the existing process. A new FlutterEngine is constructed.
Observe: MainActivity.onCreate runs. configureFlutterEngine runs and registers the plugin. MainActivity.onResume runs. bg.ready() and bg.start() succeed. Listener registrations succeed.
Wait. No onLocation, onMotionChange, onHeartbeat, onHttp, or onEnabledChange arrives in the main isolate. We have measured 8+ second windows of complete silence while confirming via separate logging that the native plugin is still emitting events to its headless registrants.
The wedge persists for the lifetime of this recreated MainActivity. App-lifecycle cycles within the session do not recover. The user must full-kill (force stop) and cold-launch to recover.
The race window is narrow but consistent. If the user waits long enough (typically >2-3 seconds) between swipe-kill and reopen, TrackingService finishes its async stopOnTerminate teardown and is no longer foreground-promoted at reattach, and the wedge does not occur. The wedge specifically requires TrackingService to still be foreground-promoted at the moment the recreated MainActivity attaches.
What we have ruled out
Not our bg.stop() calls. We have audited and gated every Dart-side bg.stop(). The only stop between the healthy pre-swipe session and the wedged post-reopen session is the plugin's own onTaskRemoved handler.
Not a listener-registration mistake on our side. We register listeners exactly once per Flutter engine, before calling bg.ready(), following the "wire your speakers once" model from issue Duplicated onLocation listener and getting same location multiples times #826. The re-registration on the new engine follows the same path that works on a healthy cold-start.
Not a permission regression. The Activity has the same permissions across the swipe — we log ContextCompat.checkSelfPermission and they read as granted both pre-swipe and post-reattach.
Not the headless isolate stealing events. The headless isolate keeps receiving events during the wedge — we have headless-side logs of enabledchange and heartbeat arriving from inside the wedge window. So the plugin's native event firing is alive; only the main-engine dispatch is dead.
Not the 5.1.2 fix failing to engage. Reflective probe (below) reads BackgroundGeolocationModule.mActivity at three lifecycle milestones on the recreated MainActivity and confirms mActivity matches the currently-attaching Activity at every probe site.
Evidence the 5.1.2 fix engages
We built a small reflection probe in Kotlin that reads BackgroundGeolocationModule.mActivity via Class.forName(...).getDeclaredField("mActivity") and logs whether the plugin's stored Activity reference matches the currently-attaching MainActivity. We call it at three sites in the MainActivity lifecycle: post-onCreate, post-configureFlutterEngine, and post-onResume.
Probe shape (Kotlin, abbreviated):
object BgPluginProbe {
privateconstvalMODULE_CLASS="com.transistorsoft.flutter.backgroundgeolocation.BackgroundGeolocationModule"funprobe(activity:Activity, site:String) {
val moduleClass =Class.forName(MODULE_CLASS)
val instance = moduleClass.getMethod("getInstance").invoke(null)
val field = moduleClass.getDeclaredField("mActivity").apply { isAccessible =true }
val pluginActivity = field.get(instance) asActivity?val matchesCurrent =
pluginActivity !=null&& pluginActivity.hashCode() == activity.hashCode()
// logs { site, pluginActivityHash, currentActivityHash, matchesCurrent, pluginActivityClass }
}
}
On every wedge-producing reproduction, the probe reports matchesCurrent: true at all three sites on the recreated MainActivity. That means BackgroundGeolocationModule.setActivity(activity) is being called with a non-null Activity, which per the 5.1.2 CHANGELOG is the trigger for HeadlessTask.destroyBackgroundIsolate(). So the 5.1.2 cleanup is running. The wedge happens anyway.
A second probe reads the running-service state for our UID via ActivityManager.getRunningServices(...) filtered by Process.myUid(). It confirms TrackingService.foreground == true at the configureFlutterEngine.done site on every wedge-producing reproduction. So the FGS is foreground-promoted at exactly the moment the new MainActivity is being wired to the new engine.
The race we cannot win
There is a ~20ms window between the plugin's onTaskRemoved-driven internal stop kicking off and the user's tap-to-reopen attaching a new MainActivity to the still-alive process. TrackingService.stopForeground(...) does not synchronously complete by the time the new Activity starts wiring up. From the OS perspective the process is still pinned by a foreground service, so the recreated Activity is given the same process and the new FlutterEngine attaches to a plugin instance whose native side is mid-teardown of its previous tracking session.
We cannot debounce reopen from user space — the user is the one tapping the icon, and on a fast device the gap is unavoidable. We also cannot work around it from our Dart code, because by the time we observe enabled: true from bg.start(), the dispatch wiring is already broken and listener re-registration on the new engine does not heal it.
What we hypothesize (with low confidence — you know the internals)
Given the 5.1.2 fix targeted the case where the headless engine had a stale FlutterEngine, the analogous problem we may be hitting is on the TrackingService-side EventChannel.EventSink wiring. When the plugin tore down listeners for the previous Activity's engine on onTaskRemoved and is mid-rebuild against the new Activity's engine, the EventSinks plumbed inside TrackingService (or wherever the plugin's native event source dispatches to plugin channels) may be either:
Holding a stale sink reference that was nulled but not replaced when the previous Activity died,
Or rebuilt against the new engine but not actually re-bound to the native event source because the source's "bound" flag was not cleared during the partial teardown.
This is informed speculation only — we cannot read the plugin's internal dispatch table from Dart, and the source we have access to in pub-cache is the Dart wrapper; the dispatch logic lives in the closed-source tslocationmanager AAR.
We are not asking for a specific implementation. We are asking whether the 5.1.2 fix's cleanup pass can be extended (or another pass added) to cover the TrackingService-still-alive-at-reattach path that our observability shows is the trigger.
What we would value from you
Confirmation or correction of the hypothesis that this is a sibling issue in the same region as the 5.1.2 fix.
Whether BackgroundGeolocationModule.setActivity(activity) (or any plugin API we can call from Dart) currently has a path to force a re-bind of the native event dispatch against the new engine after a TrackingService-alive reattach.
Guidance on whether stopOnTerminate: false + enableHeadless: true is the only safe configuration when the app's process can be pinned across swipe-kill by a foreground service. The documentation does not explicitly address the FGS-pinned-but-stopOnTerminate-true configuration.
Thank you for the plugin — the 5.1.2 fix already closed a related case for us (no more splash hang) and we appreciate the investment in this region. We have done what diagnostic work we can from outside the AAR; the rest needs your eyes on the internals.
Plugin Code and/or Config
Default-mode configuration where the regression reproduces most consistently. We have four config modes (default, helping, attention, alarm); the wedge has been reproduced indefault and attention modes. Thefactory below is the default mode and is the one we run during normal app use.
factorySnBgConfig.defaultConfig({
requiredString locationAuthorizationRequest,
requiredbool disableMotionActivityUpdates,
}) =>SnBgConfig._(
locationAuthorizationRequest: locationAuthorizationRequest,
desiredAccuracy: bg.DesiredAccuracy.high,
extras:<String, dynamic>{"alarmMode":"IDLE"},
disableMotionActivityUpdates: disableMotionActivityUpdates,
heartbeatInterval:10.0,
disableElasticity:false,
disableStopDetection:false,
startOnBoot:false,
distanceFilter:null,
locationUpdateInterval:null,
stopOnTerminate:true,
stopOnStationary:true,
stopDetectionDelay:0.0,
preventSuspend:false,
);
// Materialized into the plugin's compound Config:
bg.Config(
foregroundService:true,
reset:true,
geolocation: bg.GeoConfig(
desiredAccuracy: bg.DesiredAccuracy.high,
distanceFilter:null,
stationaryRadius:0,
disableElasticity:false,
locationAuthorizationRequest:"WhenInUse",
disableLocationAuthorizationAlert:true,
allowIdenticalLocations:true,
fastestLocationUpdateInterval:0,
locationUpdateInterval:null,
),
http: bg.HttpConfig(
autoSync:true,
autoSyncThreshold:1,
headers:<String, dynamic>{
"request-type":"ZONES_WITHIN",
"PhoneStatus-Origin":"location-change",
// Authorization: Bearer <redacted>
},
rootProperty:".",
// url: <redacted backend endpoint>
),
logger: bg.LoggerConfig(
logLevel: bg.LogLevel.off,
logMaxDays:1,
),
app: bg.AppConfig(
enableHeadless:true,
heartbeatInterval:10.0,
startOnBoot:false,
stopOnTerminate:true,
preventSuspend:false,
),
activity: bg.ActivityConfig(
activityRecognitionInterval:0.0,
minimumActivityRecognitionConfidence:0,
disableStopDetection:false,
stopOnStationary:true,
disableMotionActivityUpdates:false,
stopDetectionDelay:0.0,
),
persistence: bg.PersistenceConfig(
maxDaysToPersist:1,
maxRecordsToPersist:1,
extras:<String, dynamic>{"alarmMode":"IDLE"},
),
)
Headless task is registered at the bottom of `main()` per the maintainer's canonical example and is log-only — it never calls `bg.ready()` or any `bg.onX(...)` from inside the headless isolate, per the rule from issue #677.@pragma('vm:entry-point')Future<void> userAppHeadlessTask(bg.HeadlessEvent event) async { // Log-only. No plugin reconfiguration. No listener calls. LifecycleLog.headlessEventFired(event.name, event.event);}void main() async { WidgetsFlutterBinding.ensureInitialized(); // ... Sentry init, runApp scheduled inside SentryFlutter.init(appRunner: ...) ... bg.BackgroundGeolocation.registerHeadlessTask(userAppHeadlessTask);}Listener installation is register-once per Flutter engine, called synchronously at the top of `LocationService.build()` before any `await`, before `bg.ready()`:void _installListenersOnce() { if (_listenersInstalled) return; _listenersInstalled = true; bg.BackgroundGeolocation.onLocation(_forward._onLocation); bg.BackgroundGeolocation.onMotionChange(_forward._onMotionChange); bg.BackgroundGeolocation.onHeartbeat(_forward._onHeartbeat); bg.BackgroundGeolocation.onHttp(_forward._onHttp); bg.BackgroundGeolocation.onEnabledChange(_forward._onEnabledChange); bg.BackgroundGeolocation.onActivityChange(_forward._onActivityChange); bg.BackgroundGeolocation.onProviderChange(_forward._onProviderChange);}
Relevant log output
Curated trace from one captured wedge. Timestamps are wall-clock from `adb logcat`, correlated with Dart-side logs piped through the same clock. All log tags from our app's code are prefixed with the originating class (`MainActivity`, `BgPluginProbe`, `TaskTerminationService`, `InitBG`, `LocationService`, `Headless`). All `bg.*` and `[TSLocationManager]` lines are from the plugin itself.### Phase 1 — Healthy pre-swipe session ends`MainActivity` is paused, then destroyed. `TaskTerminationService.onTaskRemoved` fires shortly after (Android delivers `onTaskRemoved` to started services in the task being removed).11:55:50.472 I MainActivity lifecycle.onPause {hasFocus=false, isFinishing=false, isChangingConfigurations=false}11:55:50.671 I MainActivity lifecycle.onDestroy {isFinishing=true, isChangingConfigurations=false, activityCreateCount=1}11:55:50.701 I [TSLocationManager] onActivityDestroyed: de.safenow.user.MainActivity11:55:50.722 I [TSLocationManager] [BackgroundGeolocationModule setActivity] activity: null11:55:50.731 D [HeadlessTask] destroyBackgroundIsolate: skipped — no active background engine11:55:50.848 I TaskTerminationService onTaskRemoved {servicePid=14207, processStartElapsedMs=2823}11:55:50.852 I [TSLocationManager] - Stopping tracking, onTaskRemoved11:55:50.857 I [TSLocationManager] [TrackingService stopSelf] — will stopForeground once shutdown completes11:55:50.860 I Headless event.fired {"name":"enabledchange","event":false}### Phase 2 — User reopens 20ms after onTaskRemovedThe OS finds the process still alive (TrackingService still foreground-promoted, mid-teardown) and reuses it. New `MainActivity` instance is created against the same PID.11:55:50.868 I AppSession session.start {trigger=launcher, pid=14207}11:55:51.343 I MainActivity lifecycle.onCreate {activityCreateCount=2, processStartElapsedMs=2823, processAgeMs=3171, pid=14207}11:55:51.349 I BgPluginProbe mActivity.read {site=onCreate.done, pluginActivityHash=171845632, currentActivityHash=171845632, matchesCurrent=true, pluginActivityClass=de.safenow.user.MainActivity}11:55:51.351 I BgPluginProbe fgs.state {site=onCreate.done, ourServiceCount=3, trackingServiceForeground=true, trackingServiceStarted=true, locationRequestServiceForeground=null, altBeaconServiceForeground=true, alarmServiceForeground=null, allServiceClasses=[com.transistorsoft.locationmanager.service.TrackingService, org.altbeacon.beacon.service.BeaconService, de.safenow.user.TaskTerminationService]}11:55:51.420 I [TSLocationManager] [BackgroundGeolocationModule setActivity] activity: de.safenow.user.MainActivity@17184563211:55:51.422 D [HeadlessTask] destroyBackgroundIsolate: invoked from setActivity(non-null)11:55:51.470 I MainActivity configureFlutterEngine.start11:55:51.488 I MainActivity configureFlutterEngine.done11:55:51.490 I BgPluginProbe mActivity.read {site=configureFlutterEngine.done, pluginActivityHash=171845632, currentActivityHash=171845632, matchesCurrent=true, pluginActivityClass=de.safenow.user.MainActivity}11:55:51.492 I BgPluginProbe fgs.state {site=configureFlutterEngine.done, ourServiceCount=3, trackingServiceForeground=true, trackingServiceStarted=true, altBeaconServiceForeground=true, allServiceClasses=[com.transistorsoft.locationmanager.service.TrackingService, org.altbeacon.beacon.service.BeaconService, de.safenow.user.TaskTerminationService]}11:55:51.520 I MainActivity lifecycle.onResume {activityCreateCount=2}11:55:51.522 I BgPluginProbe mActivity.read {site=onResume.done, pluginActivityHash=171845632, currentActivityHash=171845632, matchesCurrent=true, pluginActivityClass=de.safenow.user.MainActivity}Key observations from Phase 2:- `BgPluginProbe.mActivity.read` reports `matchesCurrent: true` at all three lifecycle sites. **The 5.1.2 fix engages.**- `BgPluginProbe.fgs.state` reports `trackingServiceForeground: true` at `configureFlutterEngine.done`. **TrackingService is alive and foreground-promoted while the new engine is being wired.**- `processAgeMs=3171` and the matching PID (`14207`) across the swipe confirm same-process reattach.### Phase 3 — Dart initialization on the new engine`bg.ready()` returns the same state the plugin had pre-swipe (stopped, because `onTaskRemoved` stopped it). `bg.start()` re-enables tracking and returns `enabled: true`. All listeners re-registered. From Dart, everything looks normal.11:55:51.530 I InitBG enter11:55:51.534 I LocationService listenersInstalledOnce11:55:51.610 I InitBG ready.returned {enabled:false, isMoving:false, trackingMode:1, schedulerEnabled:false}11:55:51.612 I InitBG start.calling11:55:51.655 I InitBG start.returned {enabled:true}11:55:51.657 I InitBG start.stateProbe {enabled:true, isMoving:false, trackingMode:1}### Phase 4 — The wedgeFor the next 8+ seconds, the main isolate receives zero events. Meanwhile, the headless isolate continues to receive plugin events from the same native source. The plugin's native side is firing; only the main-engine dispatch is dead.
11:55:51.700 I Headless event.fired {"name":"providerchange","event":{"enabled":true,"status":3,"network":true,"gps":true}}
11:55:53.842 I Headless event.fired {"name":"heartbeat","event":{"location":{"coords":{"latitude":...}}}}
11:55:56.118 I [TSLocationManager] [c.t.l.l.TSLocationManager onLocationResult] Location: ...accuracy=12.0
11:55:56.119 I Headless event.fired {"name":"heartbeat","event":{"location":{...}}}
11:55:58.401 I Headless event.fired {"name":"enabledchange","event":true}
11:55:59.652 I LocationService wedge.detected {"silentForMs":8000, "lastMainIsolateEventAt":null, "stateProbe":{"enabled":true,"isMoving":false}}
Note the gap: between `11:55:51.655` (`bg.start()` returned `enabled:true`) and `11:55:59.652` (`wedge.detected`), the main isolate received **zero**`onLocation`/`onHeartbeat`/`onHttp`/`onEnabledChange`/`onMotionChange`/`onProviderChange` callbacks, while the headless isolate received at least one `providerchange`, two `heartbeat`, and one `enabledchange`. The native side was alive throughout. Only the main-engine dispatch was wedged.
### Phase 5 — Persistence
The wedge persists for the remainder of this `MainActivity`. Subsequent `onPause`/`onResume` cycles within the session do not heal it. `bg.state` continues to return`enabled: true`. `bg.getCurrentPosition()` eventually returns (we have seen 9-15s latencies, vs sub-500ms on a healthy session). Listener callbacks never resume.Recovery requires the user to force-stop the app from Settings (or for the OS to reap the process) and cold-launch.### Reference: a healthy session for comparisonFor the same code path on a cold start where TrackingService was not pre-alive (process was launched fresh — no FGS pinning), listeners fire within milliseconds of `start.returned`:12:03:04.110 I MainActivity lifecycle.onCreate {activityCreateCount=1, processStartElapsedMs=98, processAgeMs=412, pid=15201}12:03:04.115 I BgPluginProbe fgs.state {site=onCreate.done, ourServiceCount=0, trackingServiceForeground=null, allServiceClasses=[]}12:03:04.260 I BgPluginProbe mActivity.read {site=configureFlutterEngine.done, matchesCurrent=true}12:03:04.300 I InitBG start.returned {enabled:true}12:03:04.318 I LocationService onProviderChange.received {enabled:true}12:03:04.450 I LocationService onLocation.received {coords:{latitude:...}, accuracy:12.3, age_ms:130}12:03:04.610 I LocationService onHttp.received {status:200}The only material difference between the wedged 11:55 trace and the healthy 12:03 trace is that `trackingServiceForeground` is `true` at engine-attach in the former and `null` (service not present) in the latter. Same code path otherwise, same `Config`, same Activity class. The presence of an already-foreground-promoted `TrackingService` at the moment `BackgroundGeolocationModule.setActivity(activity)` is called with the new Activity is the differentiator.### Headless-isolate evidenceFor completeness, here is what the headless isolate sees during the same wedge window as Phase 4, captured by `bg.BackgroundGeolocation.registerHeadlessTask(userAppHeadlessTask)`:11:55:51.701 I Headless event.fired {"name":"providerchange"}11:55:53.843 I Headless event.fired {"name":"heartbeat"}11:55:56.120 I Headless event.fired {"name":"heartbeat"}11:55:58.402 I Headless event.fired {"name":"enabledchange"}These events are emitted by the plugin's native event source. The main isolate, attached to the recreated MainActivity, received none of them.
Required Reading
Plugin Version
5.1.2
Flutter Doctor
Mobile operating-system(s)
Device Manufacturer(s) and Model(s)
Sony Xperia 1 III, Pixel 8A, Pixel 3, Xiaomi 13T, Nothing Phone (1)
Device operating-systems(s)
Android 12, Android 13, Android 15, Android 17 Beta
What happened?
Short version
On Android plugin 5.1.2, when a user swipe-kills the app from recents and reopens it within a few hundred milliseconds while the plugin's own
TrackingServiceforeground service is still alive in the process, the recreatedMainActivityends up in a state where:FlutterEngineattaches cleanly.main()runs.bg.BackgroundGeolocation.ready(config)andbg.BackgroundGeolocation.start()both returnenabled: true.onLocation,onMotionChange,onHeartbeat,onHttp,onEnabledChange,onActivityChange, andonProviderChangelisteners are re-registered on the new engine without error.MainActivity. We have measured 8+ second wedge windows with zero events in the main isolate while the headless isolate continued receiving events during the exact same window.Recovery requires the user to fully kill the OS process (e.g., force-stop or wait for the OS to reap it) and cold-launch. App lifecycle bounces (
onPause/onResume) do not recover dispatch.A small repro repository can be found here, the error can be reproduced by force quite and quick restart cycles.
How this relates to the 5.1.2 fix
Your CHANGELOG entry for 5.1.2 reads:
We are running 5.1.2 and we believe that fix is engaging correctly for our build — the section "Evidence the 5.1.2 fix engages" below shows reflective probe output proving
BackgroundGeolocationModule.mActivityis the freshly-attached Activity by the end ofconfigureFlutterEngineon the recreated MainActivity.Our observable, however, differs from the splash/logo hang the CHANGELOG describes:
bg.ready()/bg.start()/bg.stateround-trips all succeed on the MethodChannel.What is broken is specifically the dispatch from the plugin's native event source into the main engine's
EventChannelsinks. From Dart's perspective the listeners are wired up, but nothing arrives. From the native plugin's perspective everything is firing — we can see headless-sideonEnabledChangeandonHeartbeatarriving correctly during the same window.The 5.1.2 fix appears to target the engine-attach phase. We believe a sibling issue lives one step downstream — in the path where
TrackingService(the plugin's location FGS) is alive at MainActivity reattach. That is a different process state from the one the splash-hang fix addresses, where the FGS keeping the process alive is an unrelated external service.Reproduction recipe
Deterministic on every device we have tried:
foregroundService: true,stopOnTerminate: true, and a backend-boundhttp.autoSyncconfig will work.bg.start()returnsenabled: trueand thatonLocation/onHeartbeat/onHttpcallbacks fire in Dart as expected. The plugin'sTrackingServiceis now a foreground service in the process.onTaskRemovedis called by the OS. BecausestopOnTerminate: true, the plugin begins its internal shutdown ofTrackingService. This shutdown is asynchronous — the FGS does not transition out of foreground state immediately.TrackingServiceis still foreground-promoted) and reuses it, launching a newMainActivityagainst the existing process. A newFlutterEngineis constructed.MainActivity.onCreateruns.configureFlutterEngineruns and registers the plugin.MainActivity.onResumeruns.bg.ready()andbg.start()succeed. Listener registrations succeed.onLocation,onMotionChange,onHeartbeat,onHttp, oronEnabledChangearrives in the main isolate. We have measured 8+ second windows of complete silence while confirming via separate logging that the native plugin is still emitting events to its headless registrants.MainActivity. App-lifecycle cycles within the session do not recover. The user must full-kill (force stop) and cold-launch to recover.The race window is narrow but consistent. If the user waits long enough (typically >2-3 seconds) between swipe-kill and reopen,
TrackingServicefinishes its async stopOnTerminate teardown and is no longer foreground-promoted at reattach, and the wedge does not occur. The wedge specifically requiresTrackingServiceto still be foreground-promoted at the moment the recreatedMainActivityattaches.What we have ruled out
bg.stop()calls. We have audited and gated every Dart-sidebg.stop(). The only stop between the healthy pre-swipe session and the wedged post-reopen session is the plugin's ownonTaskRemovedhandler.bg.ready(), following the "wire your speakers once" model from issue Duplicated onLocation listener and getting same location multiples times #826. The re-registration on the new engine follows the same path that works on a healthy cold-start.ContextCompat.checkSelfPermissionand they read as granted both pre-swipe and post-reattach.enabledchangeandheartbeatarriving from inside the wedge window. So the plugin's native event firing is alive; only the main-engine dispatch is dead.BackgroundGeolocationModule.mActivityat three lifecycle milestones on the recreatedMainActivityand confirmsmActivitymatches the currently-attaching Activity at every probe site.Evidence the 5.1.2 fix engages
We built a small reflection probe in Kotlin that reads
BackgroundGeolocationModule.mActivityviaClass.forName(...).getDeclaredField("mActivity")and logs whether the plugin's stored Activity reference matches the currently-attachingMainActivity. We call it at three sites in the MainActivity lifecycle: post-onCreate, post-configureFlutterEngine, and post-onResume.Probe shape (Kotlin, abbreviated):
On every wedge-producing reproduction, the probe reports
matchesCurrent: trueat all three sites on the recreatedMainActivity. That meansBackgroundGeolocationModule.setActivity(activity)is being called with a non-nullActivity, which per the 5.1.2 CHANGELOG is the trigger forHeadlessTask.destroyBackgroundIsolate(). So the 5.1.2 cleanup is running. The wedge happens anyway.A second probe reads the running-service state for our UID via
ActivityManager.getRunningServices(...)filtered byProcess.myUid(). It confirmsTrackingService.foreground == trueat theconfigureFlutterEngine.donesite on every wedge-producing reproduction. So the FGS is foreground-promoted at exactly the moment the newMainActivityis being wired to the new engine.The race we cannot win
There is a ~20ms window between the plugin's
onTaskRemoved-driven internal stop kicking off and the user's tap-to-reopen attaching a newMainActivityto the still-alive process.TrackingService.stopForeground(...)does not synchronously complete by the time the new Activity starts wiring up. From the OS perspective the process is still pinned by a foreground service, so the recreated Activity is given the same process and the newFlutterEngineattaches to a plugin instance whose native side is mid-teardown of its previous tracking session.We cannot debounce reopen from user space — the user is the one tapping the icon, and on a fast device the gap is unavoidable. We also cannot work around it from our Dart code, because by the time we observe
enabled: truefrombg.start(), the dispatch wiring is already broken and listener re-registration on the new engine does not heal it.What we hypothesize (with low confidence — you know the internals)
Given the 5.1.2 fix targeted the case where the headless engine had a stale
FlutterEngine, the analogous problem we may be hitting is on the TrackingService-sideEventChannel.EventSinkwiring. When the plugin tore down listeners for the previous Activity's engine ononTaskRemovedand is mid-rebuild against the new Activity's engine, the EventSinks plumbed insideTrackingService(or wherever the plugin's native event source dispatches to plugin channels) may be either:This is informed speculation only — we cannot read the plugin's internal dispatch table from Dart, and the source we have access to in
pub-cacheis the Dart wrapper; the dispatch logic lives in the closed-sourcetslocationmanagerAAR.We are not asking for a specific implementation. We are asking whether the 5.1.2 fix's cleanup pass can be extended (or another pass added) to cover the
TrackingService-still-alive-at-reattach path that our observability shows is the trigger.What we would value from you
BackgroundGeolocationModule.setActivity(activity)(or any plugin API we can call from Dart) currently has a path to force a re-bind of the native event dispatch against the new engine after a TrackingService-alive reattach.stopOnTerminate: false+enableHeadless: trueis the only safe configuration when the app's process can be pinned across swipe-kill by a foreground service. The documentation does not explicitly address the FGS-pinned-but-stopOnTerminate-true configuration.Thank you for the plugin — the 5.1.2 fix already closed a related case for us (no more splash hang) and we appreciate the investment in this region. We have done what diagnostic work we can from outside the AAR; the rest needs your eyes on the internals.
Plugin Code and/or Config
Relevant log output