Skip to content

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

ScenarioWhat happensGap
BackgroundedThe foreground service keeps running; nothing changes.None
Swiped awayonTaskRemoved restarts the service immediately (START_STICKY), unless stopOnTerminate says otherwise. Tracking never fully stops.None
OS-killedA 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-stoppedNothing 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
RebootBootReceiver 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 back
adb shell am force-stop # ← NOT this: force-stop is the one case nothing recovers from

Instead, swipe the app from recents and watch:

adb logcat | grep -i bgeo

For 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.