Boot & killed-app behaviour
BGeo’s contract is that tracking and upload happen inside the engine, and
your code is optional at runtime. Once
start() has run, the foreground
service keeps recording, filtering, persisting to SQLite and uploading whether
or not a single line of your Kotlin is executing. Your onLocation listeners
are observers on top of that pipeline, not part of it.
“Killed” is not one state. Each of these has a different story:
- Backgrounded — the app is merely not in the foreground. Nothing is dead.
- Swiped away (task removed) — the user removed the app from the recent-apps list.
- OS-killed (memory eviction) — the system reclaimed the process, with no user action. The common real-world case is an overnight eviction.
- Force-stopped — the user pressed “Force stop” in Settings, or an OEM battery manager did the equivalent.
- Device reboot.
The matrix
| Scenario | What happens | Gap |
|---|---|---|
| Backgrounded | The foreground service keeps running; nothing changes. | None |
| Swiped away | onTaskRemoved restarts the service immediately (START_STICKY), unless stopOnTerminate says otherwise. Tracking never fully stops. | None |
| OS-killed | A foreground service is not normally evicted, but if the process dies anyway, Android restarts it and Application.onCreate runs — where attach() resumes tracking. | Seconds |
| Force-stopped | Nothing restarts. Android blocks every restart path — broadcasts, geofences, alarms — until a user launches the app again. This is by design in Android and cannot be worked around. | Until next launch |
| Reboot | BootReceiver restarts the service on BOOT_COMPLETED/LOCKED_BOOT_COMPLETED when tracking was enabled and startOnBoot is true. With false, nothing survives until the app is opened. | Until boot completes |
Why there is no headless task here
The React Native and Flutter SDKs both require you to register a headless task, because their runtime dies with the app: a JS context or a Dart isolate has to be spun up again before any of your code can react to a background event.
A native Android app has no such gap. Android restarts your process for
boot, geofence and service events and calls Application.onCreate every
time — so attach() there is the entire mechanism. By the time an event is
delivered, your listeners are alive again, in the same process, with no
special entry point and no @pragma annotations.
This is the main reason the native SDK is simpler to integrate than either
cross-platform binding, and the reason attach() is not optional. If you put
it in an Activity, every one of the scenarios above quietly stops delivering
events to your code — while the engine keeps tracking, so the server data
looks fine and only your app is blind.
stopOnTerminate
stopOnTerminate (default
false) decides whether tracking should stop when the app is swiped away.
With false, onTaskRemoved restarts the service and tracking continues —
this is what you want for a tracker whose whole job is to keep running.
With true, the service tears tracking down synchronously in onTaskRemoved,
and the user has to reopen your app and call start() again.
startOnBoot
startOnBoot (default false)
decides whether tracking resumes after a reboot, if it was enabled when the
device went down.
With true, BootReceiver restarts the service on BOOT_COMPLETED — and
also on LOCKED_BOOT_COMPLETED, so tracking resumes on an encrypted device
before the user has unlocked it for the first time.
With false, nothing survives the reboot: geofences, the activity-recognition
PendingIntents and the foreground service all die with the process, and
nothing comes back until the app is next opened.
Verifying this yourself
Do not trust an emulator for any of it — Doze, OEM battery managers and boot broadcasts all behave differently on real hardware. On a device:
# Swipe-away: watch the service come backadb shell am force-stop # ← NOT this: force-stop is the one case nothing recovers fromInstead, swipe the app from recents and watch:
adb logcat | grep -i bgeoFor reboot, enable startOnBoot, start tracking, adb reboot, and confirm
tracking resumes without opening the app. For the eviction case, background
the app and leave the device alone overnight; the fix stream continuing in the
morning is the result you are looking for.
getState() after any of these tells you what the engine thinks: enabled is
the persisted intent, and lastAcceptedFixAge (seconds, null until the first
fix) tells you whether it has actually produced anything since coming back.
See Logging & debugging.