Android SDK Usage & Integration
In-SDK Geofencing is the recommended way to integrate: hand the SDK your stops and it starts and stops tracking automatically as the driver enters and exits each one. Prefer to drive geofence enter/exit yourself? See Manual Geofencing below.
In-SDK Geofencing
Hand the SDK your stops and it auto-starts and stops a tracking session as the driver enters and exits each one.
This flow has four touchpoints in your driver app (Initialize SDK is shared with the manual flow below):
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | Driver app launches | Initialize SDK |
| 2 | Start geofencing and tracking all stops | Start Geofencing |
| 3 | Driver confirms drop-off or takes proof of delivery | Mark Drop-off |
| 4 | Stop geofencing and tracking all stops | Stop Geofencing |
If the driver's route changes mid-shift, call Update Stops as well.
Requires location permission (plus Notifications on Android 13+ and Bluetooth on Android 12+). For background operation, also requires ACCESS_BACKGROUND_LOCATION ("Allow all the time") — always a separate, second request after foreground location is granted. Without it, geofence registration is skipped entirely (startRouteGeofencing logs a warning and no geofences are set). See Runtime Permissions below, or the Permissions page for buckets, states and the staged flow.
How It Works
- You pass the route as a
List<DeliveryStop>; the SDK runs a foreground service for its lifetime and geofences the nearest stops. - On ENTER, it auto-starts a session for that stop (same as
startDeliveryByAddressString). - You call
markDropoffwhen the driver completes the stop. - On EXIT, the session stops only if
markDropoffwas called. Otherwise it runs untiltimeoutSeconds, so a false EXIT can't cut a delivery short. - Every auto-start/stop emits a
GeofenceSessionEventfor your UI.
The SDK handles the rest automatically: it absorbs GPS jitter (enter/exit hysteresis + debounce), tightens location cadence near stops to balance precision and battery, and transparently manages more stops than Android's 100-geofence cap. These are tuned via remote config, not in your app.
API Reference
| Function | Description |
|---|---|
startRouteGeofencing(stops, options, callback) | Begins geofencing for stops (List<DeliveryStop>), tuned by options (RouteGeofenceOptions). Fails if stops is empty or the SDK isn't initialized/eligible (see Runtime Permissions) |
markDropoff(deliveryId, dropoffType) | Marks a stop's drop-off, gating its geofence EXIT stop. Shared with the manual flow (see Mark Drop-off) |
updateRouteStops(stops, callback) | Replaces the active stop set (List<DeliveryStop>) mid-route and re-diffs registration. A session for a removed stop is auto-stopped (tagged removed_from_route) |
stopRouteGeofencing() | Clears all geofences, stops active route sessions, ends the foreground service |
resumeRouteGeofencingIfNeeded() | Restores geofencing from persisted state. Call on every app launch; no-op if no route is active |
monitoredStops | Property. Returns the stops currently registered (List<DeliveryStop>, empty if no route is active) |
geofenceSessionEvents | Property. A hot Flow<GeofenceSessionEvent> of auto start/stop events (replays the latest to new subscribers) |
1. Initialize SDK
Initialize the SDK in your main Activity, typically in onCreate(), then set your API key once initialization succeeds.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.DoorstepPermission
import com.doorstepai.sdks.tracking.DoorstepPermissionState
class MainActivity : ComponentActivity() {
companion object {
private const val FIXED_API_TOKEN = SOME_API_KEY_REF // Reference from an ENV
}
private var permissionsGranted by mutableStateOf(false)
private var sdkInitialized by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkPermissionsAndInitialize()
setContent { /* Your Compose UI */ }
}
fun checkPermissionsAndInitialize() {
// checkPermission never prompts — see Runtime Permissions below.
val location = DoorstepAI.checkPermission(this, DoorstepPermission.LOCATION_WHEN_IN_USE)
permissionsGranted = location.state == DoorstepPermissionState.GRANTED
if (permissionsGranted) {
initializeSDK()
} else {
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_WHEN_IN_USE))
}
}
private fun initializeSDK() {
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = {
DoorstepAI.setAPIKey(FIXED_API_TOKEN)
sdkInitialized = true
},
onFailure = { error -> sdkInitialized = false }
)
}
}
}
init parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
context | Context | Yes | Application context |
notificationTitle | String? | No | Title on the foreground-service notification shown while tracking |
notificationText | String? | No | Description on that notification |
callback | (Result<Unit>) -> Unit | Yes | Initialization result callback |
setAPIKey parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key | String | n/a | JWT sent as Authorization: Bearer <key> |
shouldGetConfig | Boolean | true | When true, fetch config immediately. When false, defer until the first startDeliveryByX call |
// Defer config fetch until the first delivery starts:
DoorstepAI.setAPIKey(FIXED_API_TOKEN, shouldGetConfig = false)
Store your API key securely using BuildConfig fields, environment variables, or a secure configuration service. Never hardcode API keys in production builds.
2. Start Geofencing
Call startRouteGeofencing once you have the driver's stops for the shift. Pass every stop up front, not one at a time:
import com.doorstepai.sdks.tracking.DoorstepAI
DoorstepAI.startRouteGeofencing(
stops = myRouteStops, // List<DeliveryStop>
options = myGeofenceOptions // RouteGeofenceOptions(), defaults if omitted
) { result ->
result.fold(
onSuccess = { Log.i("DoorstepAI", "Geofencing started") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to start geofencing: ${error.message}") }
)
}
3. Mark Drop-off
Mark the drop-off when the driver takes a POD or confirms delivery in-app. markDropoff is a suspend function — call it from a coroutine scope. It returns no Result; failures are logged internally via SDKLogger.
The call is identical for both flows. With In-SDK Geofencing, marking drop-off also gates the automatic geofence EXIT stop for that delivery: the session keeps running until it's called (or the timeout backstop fires).
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
lifecycleScope.launch {
DoorstepAI.markDropoff(
deliveryId = "delivery_12345",
dropoffType = DropoffType.POD // or DropoffType.NON_POD
)
}
markDropoff parameters
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. The session you're marking |
dropoffType | DropoffType | POD (proof of delivery captured) or NON_POD |
Custom Events
Use newEvent to record a custom event on a delivery. It comes in both a callback and a suspend variant. Pass an optional timestamp to backdate the event; omit it and the event is recorded at "now".
// Callback variant
DoorstepAI.newEvent(
eventName = "taking_pod",
deliveryId = "delivery_12345"
) { result -> /* same fold as above */ }
// Suspend variant, recorded at an explicit time
lifecycleScope.launch {
DoorstepAI.newEvent(
eventName = "taking_pod",
deliveryId = "delivery_12345",
timestamp = 1720000000.0 // epoch seconds
)
}
newEvent parameters
| Parameter | Type | Description |
|---|---|---|
eventName | String | Required. The event to record |
deliveryId | String | Required. The session the event belongs to |
timestamp | Double? | Epoch seconds the event occurred. When provided, the event is recorded at that time instead of "now". Defaults to null (now) |
4. Stop Geofencing
Call stopRouteGeofencing when the shift ends (or the driver logs out). It clears every geofence, stops any sessions still running, and ends the foreground service:
DoorstepAI.stopRouteGeofencing()
Update Stops
Call updateRouteStops when the driver's route changes mid-shift (a stop added, removed, or reordered). It re-diffs against the currently monitored stops instead of tearing everything down, and auto-stops a session for any stop dropped from the list, tagged removed_from_route:
DoorstepAI.updateRouteStops(
stops = updatedRouteStops // List<DeliveryStop>
) { result ->
result.fold(
onSuccess = { Log.i("DoorstepAI", "Stops updated") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to update stops: ${error.message}") }
)
}
Resuming After Relaunch
Not part of the standard flow above, but worth adding for reliability: route state is persisted, so geofencing survives process death and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch, right after init succeeds, to restore it without re-supplying the stop list. It's a no-op if no route is active, so it's safe to call unconditionally:
private fun initializeSDK() {
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = {
DoorstepAI.setAPIKey(FIXED_API_TOKEN)
DoorstepAI.resumeRouteGeofencingIfNeeded()
},
onFailure = { error -> /* handle init failure */ }
)
}
}
Getting Monitored Stops
Read monitoredStops to get the stops currently registered, useful for rehydrating your UI on launch or confirming a route is active:
val stops = DoorstepAI.monitoredStops
Log.i("DoorstepAI", "${stops.size} stop(s) currently monitored")
Types
DeliveryStop
A single stop on the route. deliveryId is used as both the session's clientSessionId and the geofence request id: the same id you pass to markDropoff and stopDelivery.
@Serializable
data class DeliveryStop(
val deliveryId: String,
val address: String,
val latitude: Double,
val longitude: Double,
val radiusMeters: Double? = null,
val customerId: String? = null,
val driverId: String? = null
)
| Field | Type | Required | Description |
|---|---|---|---|
deliveryId | String | Yes | Your id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins) |
address | String | Yes | Passed verbatim to session creation |
latitude | Double | Yes | Stop latitude (geofence center) |
longitude | Double | Yes | Stop longitude (geofence center) |
radiusMeters | Double? | No | Per-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when null |
customerId | String? | No | Forwarded to session creation for correlation on your backend |
driverId | String? | No | Forwarded to session creation for correlation on your backend |
RouteGeofenceOptions
Tuning for the whole route. Every field has a default, so startRouteGeofencing(stops) { … } works with no options.
@Serializable
data class RouteGeofenceOptions(
val defaultRadiusMeters: Double = 250.0,
val timeoutSeconds: Double? = null,
val autoStopAfterDropoffSeconds: Double? = null,
val manualForeground: Boolean = false
)
| Field | Type | Default | Description |
|---|---|---|---|
defaultRadiusMeters | Double | 250.0 | Geofence radius for stops that don't set their own radiusMeters |
timeoutSeconds | Double? | null | Backstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. null uses remote config |
autoStopAfterDropoffSeconds | Double? | null | Forwarded to the post-dropoff auto-stop (see Auto-Stop After Dropoff) |
manualForeground | Boolean | false | When true, your app owns the foreground service and the SDK won't promote its own. See Manual Foreground Service |
GeofenceSessionEvent & GeofenceSessionEventType
Emitted on geofenceSessionEvents whenever the SDK auto-starts or auto-stops a session in response to a geofence transition.
data class GeofenceSessionEvent(
val deliveryId: String,
val type: GeofenceSessionEventType,
val reason: String,
val serverSessionId: String? = null
)
enum class GeofenceSessionEventType { STARTED, STOPPED }
| Field | Type | Description |
|---|---|---|
deliveryId | String | The stop's DeliveryStop.deliveryId |
type | GeofenceSessionEventType | STARTED (session auto-started on ENTER) or STOPPED (session auto-stopped) |
reason | String | What triggered it: geofence, distance, timeout, removed_from_route, route_cleared, or manual |
serverSessionId | String? | Server-assigned session id, populated on STARTED once session creation succeeds. null on STOPPED, and null on STARTED if the session was created in CSV-only mode (no server round-trip) |
Observing Geofencing Events
Collect geofenceSessionEvents to subscribe to auto start/stop events so your UI can reflect them in real time. It's a hot Flow<GeofenceSessionEvent> that replays the latest value to new subscribers:
lifecycleScope.launch {
DoorstepAI.geofenceSessionEvents.collect { event ->
Log.i("DoorstepAI", "Geofencing event: ${event.deliveryId} -> ${event.type} (${event.reason})")
}
}
Manual Geofencing
If you'd rather drive geofence enter/exit yourself instead of using In-SDK Geofencing above, wire up these four touchpoints:
The DoorstepAI SDK has four touchpoints in your driver app. Wire up each one and you're done. Everything else on this page is reference detail for those four calls.
| # | When this happens in the driver app | What you call |
|---|---|---|
| 1 | App launches | Initialize SDK |
| 2 | Driver enters the ≥250 m delivery geofence | Start Tracking |
| 3 | Driver takes POD or confirms drop-off in-app | Mark Drop-off |
| 4 | Driver exits the ≥250 m delivery geofence | Stop Tracking |
Tracking needs runtime location permission. The full permission flow (Compose launcher + DoorstepAIPermissionUtils) is documented under Runtime Permissions below.
1. Initialize SDK
Initialize the SDK the same way as described in Initialize SDK above. This step is identical for both flows.
2. Start Tracking
Call startDelivery… when the driver enters the delivery geofence — not once inside the building. Pass a unique deliveryId you can correlate on your side, and handle the Result in the callback (invalid key, denied permissions, etc.).
Pick whichever address format you have:
import com.doorstepai.sdks.tracking.AddressType
import com.doorstepai.sdks.tracking.LatLngObject
// By Google Place ID (fold once to show the callback shape)
DoorstepAI.startDeliveryByPlaceID(
placeID = "some_place_id",
deliveryId = "delivery_12345"
) { result ->
result.fold(
onSuccess = { message -> updateDeliveryStatus(DeliveryStatus.ACTIVE) },
onFailure = { error -> showErrorToUser(error.message) }
)
}
// By address components
val address = AddressType(
streetNumber = "123",
route = "Main Street",
subPremise = "Apt 4B",
locality = "San Francisco",
administrativeAreaLevel1 = "CA",
postalCode = "94102"
)
DoorstepAI.startDeliveryByAddressType(
address = address,
deliveryId = "delivery_12345"
) { result -> /* same fold as above */ }
// By single address string, with optional coordinates and knobs
DoorstepAI.startDeliveryByAddressString(
address = "123 Main St, Apt 4B, San Francisco, CA 94102",
deliveryId = "delivery_12345",
coordinates = LatLngObject(lat = 37.7749, lng = -122.4194),
timeoutSeconds = 1200.0
) { result -> /* same fold as above */ }
Start parameters
Every start method takes a deliveryId plus these optional knobs:
| Parameter | Type | Description |
|---|---|---|
deliveryId | String | Required. Unique per session; correlate it on your side |
timeoutSeconds | Double? | Auto-stops tracking after this duration, a backstop if the exit geofence is missed |
manualForeground | Boolean | When true, the SDK won't promote its TrackingService to the foreground; your app must already run its own. Defaults to false. See Foreground Service |
coordinates | LatLngObject? | (AddressType / address-string variants only) pairs a textual address with a lat/lng you resolved upstream |
customerId | String? | Optional customer identifier passed through to session creation, for correlation on your backend |
driverId | String? | Optional driver identifier passed through to session creation, for correlation on your backend |
Deprecated start methods
startDeliveryByPlusCode and startDeliveryByLatLng are deprecated. Use startDeliveryByAddressString / startDeliveryByAddressType with coordinates instead. They remain for backwards compatibility:
DoorstepAI.startDeliveryByPlusCode(
plusCode = "some_plus_code",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }
DoorstepAI.startDeliveryByLatLng(
latitude = 37.7749,
longitude = -122.4194,
subUnit = "Apt 4B",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }
The deprecated Plus Code and lat/lng overloads do not expose manualForeground. Use the supported
place/address starts when you need that option.
3. Mark Drop-off
Mark the drop-off the same way as described in Mark Drop-off above. This step is identical for both flows.
4. Stop Tracking
Call stopDelivery when the driver exits the delivery geofence, not inside the building:
try {
DoorstepAI.stopDelivery("delivery_12345")
} catch (e: Exception) {
Log.e("DoorstepAI", "Error stopping delivery: ${e.message}")
}
That covers the full lifecycle. The sections below are reference for the calls above.
Runtime Permissions
Tracking needs foreground location before initializing (or before the first start). Background collection and route geofencing additionally need background location, which Android 11+ auto-denies when it is bundled with the foreground ask — so it is always a separate, second request, made only after foreground location is granted. Background location is only half of it: collection also needs a running foreground service to survive the app being backgrounded. checkPermissions observes without prompting; requestPermissions asks for exactly the buckets you whitelist and nothing else.
Which prompts appear
| Permission | Prompt | When |
|---|---|---|
| Location (fine + coarse) | System location dialog (Precise/Approximate toggle on Android 12+) | When requested |
POST_NOTIFICATIONS | Notifications dialog | Android 13+ (API 33) |
BLUETOOTH_SCAN | "Nearby devices" dialog | Android 12+ (API 31) |
ACCESS_BACKGROUND_LOCATION | "Allow all the time" (a Settings screen on Android 11+) | Requested separately, after foreground location |
INTERNET, ACCESS_NETWORK_STATE, FOREGROUND_SERVICE, WAKE_LOCK, and the legacy BLUETOOTH/BLUETOOTH_ADMIN permissions are install-time (normal) permissions — they're granted automatically and never prompt, so they don't belong in a runtime request.
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.DoorstepPermission
import com.doorstepai.sdks.tracking.DoorstepPermissionState
// Observe — never prompts. LOCATION_ALWAYS reports WHEN_IN_USE_ONLY when only
// foreground location is held: background tracking and route geofencing won't work then.
fun backgroundReady(): Boolean {
val states = DoorstepAI.checkPermissions(this)
return states[DoorstepPermission.LOCATION_ALWAYS]?.state == DoorstepPermissionState.GRANTED
}
// Ask 1 — foreground location, e.g. from your "Start shift" button.
fun onStartShiftTapped() {
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_WHEN_IN_USE))
}
// Ask 2 — background location, from a LATER, separate user action (after your own
// "why background location" screen) and only once foreground location is granted.
fun onBackgroundRationaleAccepted() {
val foreground = DoorstepAI.checkPermission(this, DoorstepPermission.LOCATION_WHEN_IN_USE)
if (foreground.state != DoorstepPermissionState.GRANTED) return
DoorstepAI.requestPermissions(this, setOf(DoorstepPermission.LOCATION_ALWAYS))
}
Keep those two asks in separate call sites, not two lines in a row: issued in the same turn, the second one sees foreground location as not-yet-granted and comes back with LOCATION_ALWAYS in deferred instead of prompting. The staged permission flow shows the full four-stage version.
Forward every answer so the SDK can tell a re-askable DENIED from a Settings-only PERMANENTLY_DENIED:
override fun onRequestPermissionsResult(rc: Int, perms: Array<String>, results: IntArray) {
super.onRequestPermissionsResult(rc, perms, results)
DoorstepAI.notePermissionRequestResult(this, perms, results)
checkPermissionsAndInitialize() // re-read state, then initialize or ask for background next
}
Buckets, states, the staged permission flow and troubleshooting are on the Permissions page.
Never bundle LOCATION_ALWAYS into the same requestPermissions call as LOCATION_WHEN_IN_USE — on Android 11+ (API 30) the system silently ignores it. Request foreground location first, then request LOCATION_ALWAYS alone once it's granted.
Advanced APIs
Foreground Service
A running foreground service is what keeps collection alive once the driver leaves your app. Android freezes a backgrounded process that has no foreground service, and collection stops with it — background location permission alone does not prevent that. Getting the FGS up, and confirming it came up, is therefore part of a correct integration.
By default (manualForeground = false) the SDK owns this for you: startDelivery* launches its
TrackingService, which immediately promotes itself with a location-type foreground service and
posts the tracking notification. Title and text come from init; the
FOREGROUND_SERVICE and FOREGROUND_SERVICE_LOCATION permissions arrive through the SDK's
library manifest.
Confirm the service actually started
Promotion can be refused, and the SDK reports that rather than failing the call, so check it once during integration:
-
The tracking notification appears in the shade for the whole session. It is the user-visible proof.
POST_NOTIFICATIONSmust be granted for it to be shown. -
Ask the OS, which is the definitive check:
adb shell dumpsys activity services com.doorstepai.sdks.tracking.internal.TrackingService \
| grep -iE 'isForeground|fgRequired' -
Watch the log —
adb logcat -s TrackingService -s DoorstepAI. These lines mean it did not come up:Log line Cause Foreground start rejected by Android policyForegroundServiceStartNotAllowedException— on Android 12+ you cannot start an FGS from the background. Start the delivery while your app is in the foregroundFailed to promote service to foregroundAnything else, with the exception attached manualForeground=true but no host foreground service is currently runningYou opted out of the SDK's FGS but yours is not up — see below
On Android 12+ (API 31) an app in the background may not start a foreground service, and on API 29+
startDelivery* refuses outright — failing its callback with "Cannot start tracking while app
is backgrounded without background location permission" — if the app is backgrounded and
ACCESS_BACKGROUND_LOCATION is not granted. Start the session while the driver is looking at your
app; once the FGS is up, backgrounding is fine.
manualForeground = true — your app owns the service
Pass it when your app already runs a shift-long foreground service, so the driver does not get
two tracking notifications. The SDK then starts TrackingService with startService() and
never calls startForeground() — it relies entirely on yours to keep the process alive.
That makes it your obligation, and the SDK will not rescue you: if no host FGS is running it logs the warning above and continues, so tracking simply dies when the app is backgrounded.
// 1. Your foreground service must ALREADY be running — and promoted.
ContextCompat.startForegroundService(this, Intent(this, MyShiftService::class.java))
// (inside MyShiftService.onCreate: startForeground(id, notification, FOREGROUND_SERVICE_TYPE_LOCATION))
// 2. Then hand the delivery to the SDK.
DoorstepAI.startDeliveryByAddressString(
address = "123 Main St",
deliveryId = "delivery_12345",
manualForeground = true,
) { result -> /* … */ }
// 3. Keep your service alive for the whole session — stopping it stops collection.
Checklist for manualForeground = true:
- Your service is running and promoted with
startForeground()beforestartDelivery*. - It declares
android:foregroundServiceType="location"and your manifest keepsFOREGROUND_SERVICE_LOCATION— without the location type, Android withholds location while backgrounded even though the service lives. - It stays up until after
stopDelivery. - You verified the warning line above is absent from logcat.
The flag is persisted to SharedPreferences, so it survives TrackingService being recreated by
the OS in a fresh process.
For route geofencing the equivalent knob is
RouteGeofenceOptions.manualForeground. Leave it false and the SDK's
route service is the foreground service for the whole route, including the sessions it auto-starts
on ENTER; set it true and the same obligation above applies for as long as the route is active.
Auto-Stop After Dropoff
Pass autoStopAfterDropoffSeconds on startDeliveryByX(...). After the next markDropoff(...), the SDK schedules a Doze-safe AlarmManager alarm that fires stopDelivery(...) when the timer elapses.
- Host-supplied value always wins over remote config.
null→ falls back to remote config (MiscConfiguration.autoStopAfterDropoffSeconds).<= 0→ no auto-stop scheduled.
Observing Config Load
DoorstepAI.configFetched is a hot Flow<Long> that emits the wall-clock timestamp (ms) of every successful config landing. It replays the most recent value, so a late subscriber sees the current state immediately.
lifecycleScope.launch {
DoorstepAI.configFetched.collect { timestampMs ->
Log.i("DoorstepAI", "Config fetched at $timestampMs")
}
}
Best Practices
- Always handle the
Result.foldover success/failure on everystartDelivery*callback, and branch on error type (network, permission, etc.) to show the right message.markDropoff/newEvent's suspend variants have noResult, so wrap them intry/catchif you need to react to failures in your UI. - Update UI on the main thread. SDK callbacks may arrive on background threads, so wrap UI updates in
runOnUiThread { … }. - Let tracking run across lifecycle changes. The SDK keeps tracking through
onPause/onDestroy. Don't stop a delivery just because an Activity is destroyed. Only callstopDeliveryon geofence exit (or rely ontimeoutSecondsas a backstop).
Next Steps
- 🔐 Review Permissions: buckets, states, and staged prompts
- 💡 View Complete Examples: full implementation examples
- 🛠️ Troubleshooting Guide: common integration and runtime issues