Android SDK Usage & Integration
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 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
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() {
if (DoorstepAIPermissionUtils.hasAllPermissions(this)) {
permissionsGranted = true
initializeSDK()
} else {
permissionsGranted = false
}
}
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 | No | Initialization result callback (recommended) |
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 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 |
autoStopAfterDropoffSeconds | Double? | Auto-stops this many seconds after markDropoff. null falls back to remote config; <= 0 schedules no auto-stop. Doze-safe (backed by AlarmManager.setAndAllowWhileIdle, no SCHEDULE_EXACT_ALARM needed) |
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 Manual 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 */ }
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.
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 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 requires runtime location permission. Request it before initializing (or before the first start).
Permission request with Jetpack Compose
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@Composable
fun DoorstepSDKTestScreen(
context: MainActivity,
permissionsGranted: Boolean,
sdkInitialized: Boolean,
modifier: Modifier = Modifier
) {
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.all { it.value }
if (allGranted) {
context.checkPermissionsAndInitialize()
}
}
LaunchedEffect(permissionsGranted) {
if (!permissionsGranted) {
val missingPermissions = DoorstepAIPermissionUtils.getMissingPermissions(context)
if (missingPermissions.isNotEmpty()) {
permissionLauncher.launch(missingPermissions.toTypedArray())
}
}
}
}
Permission helper
Copy this helper into your app to request the permissions the SDK needs:
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
/**
* DoorstepAI SDK Permission Utilities
*
* This class provides utilities for handling permissions required by the DoorstepAI SDK.
* This is needed as certain permissions need runtime approval
* Copy this entire class into your app to handle SDK permissions.
*/
object DoorstepAIPermissionUtils {
fun getRequiredPermissions(): List<String> {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_NETWORK_STATE
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
permissions.add(Manifest.permission.ACTIVITY_RECOGNITION)
permissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
permissions.add(Manifest.permission.FOREGROUND_SERVICE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
return permissions
}
fun hasAllPermissions(context: Context): Boolean {
return getRequiredPermissions().all { permission ->
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
fun getMissingPermissions(context: Context): List<String> {
return getRequiredPermissions().filter { permission ->
ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED
}
}
/**
* Get permission rationale text for showing to users
* @return User-friendly explanation of why permissions are needed
*/
fun getPermissionRationale(): String {
return "The DoorstepAI SDK needs these permissions to provide delivery waypoints"
}
}
Usage:
if (DoorstepAIPermissionUtils.hasAllPermissions(context)) {
initializeSDK()
} else {
val missingPermissions = DoorstepAIPermissionUtils.getMissingPermissions(context)
// Launch permission request with the missing permissions
}
// Get permission rationale to show users
val rationale = DoorstepAIPermissionUtils.getPermissionRationale()
Advanced APIs
Manual Foreground Service
Set manualForeground = true on startDeliveryByX(...) when your app already runs a foreground service that keeps the process alive. The SDK then starts its TrackingService as a plain (non-foreground) service instead of promoting its own, avoiding double FGS notifications.
// Before calling startDelivery, ensure your host FGS is running.
startService(Intent(this, MyHostForegroundService::class.java))
DoorstepAI.startDeliveryByAddressType(
address = address,
deliveryId = "delivery_12345",
manualForeground = true
) { /* ... */ }
The flag is persisted to SharedPreferences, so it survives TrackingService being recreated by the OS in a fresh process.
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")
}
}
In-SDK Route Geofencing
An opt-in alternative to running your own geofencing: hand the SDK the whole route and it auto-starts and stops a tracking session as the driver enters and exits each stop.
Requires location permission. For background operation, also requires ACCESS_BACKGROUND_LOCATION ("Allow all the time") — without it, geofences only fire while the app is foregrounded (startRouteGeofencing still succeeds and logs a warning).
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.
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", "Route geofencing started") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to start route geofencing: ${error.message}") }
)
}
API reference
| Function | Description |
|---|---|
startRouteGeofencing(stops, options, callback) | Begins geofencing for stops. Fails if stops is empty or the SDK isn't initialized/eligible (see Runtime Permissions) |
updateRouteStops(stops, callback) | Replaces the active stop set 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 |
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
)
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 |
Observing route events
geofenceSessionEvents is a hot Flow<GeofenceSessionEvent> of session start/stop events (replays the latest to new subscribers).
lifecycleScope.launch {
DoorstepAI.geofenceSessionEvents.collect { event ->
Log.i("DoorstepAI", "Route event: ${event.deliveryId} -> ${event.type} (${event.reason})")
}
}
monitoredStops returns the stops currently registered (empty if no route is active).
Lifecycle & persistence
Route state is persisted, so geofencing survives process death and OS restarts. Call resumeRouteGeofencingIfNeeded() on every app launch to restore it — no-op if no route is active.
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 routes larger than Android's 100-geofence cap. These are tuned via remote config, not in your app.
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
- 💡 View Complete Examples: full implementation examples
- 🛠️ Troubleshooting Guide: common integration and runtime issues