Skip to main content

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 appWhat you call
1App launchesInitialize SDK
2Driver enters the ≥250 m delivery geofenceStart Tracking
3Driver takes POD or confirms drop-off in-appMark Drop-off
4Driver exits the ≥250 m delivery geofenceStop Tracking
Permissions

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.

MainActivity.kt
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

ParameterTypeRequiredDescription
contextContextYesApplication context
notificationTitleString?NoTitle on the foreground-service notification shown while tracking
notificationTextString?NoDescription on that notification
callback(Result<Unit>) -> UnitNoInitialization result callback (recommended)

setAPIKey parameters

ParameterTypeDefaultDescription
keyStringn/aJWT sent as Authorization: Bearer <key>
shouldGetConfigBooleantrueWhen 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)
API Key Security

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:

ParameterTypeDescription
deliveryIdStringRequired. Unique per session; correlate it on your side
timeoutSecondsDouble?Auto-stops tracking after this duration, a backstop if the exit geofence is missed
autoStopAfterDropoffSecondsDouble?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)
manualForegroundBooleanWhen 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
coordinatesLatLngObject?(AddressType / address-string variants only) pairs a textual address with a lat/lng you resolved upstream
customerIdString?Optional customer identifier passed through to session creation, for correlation on your backend
driverIdString?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

ParameterTypeDescription
deliveryIdStringRequired. The session you're marking
dropoffTypeDropoffTypePOD (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

ParameterTypeDescription
eventNameStringRequired. The event to record
deliveryIdStringRequired. The session the event belongs to
timestampDouble?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

MainActivity.kt
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:

DoorstepAIPermissionUtils.kt
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.

Permissions

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

  1. You pass the route as a List<DeliveryStop>; the SDK runs a foreground service for its lifetime and geofences the nearest stops.
  2. On ENTER, it auto-starts a session for that stop (same as startDeliveryByAddressString).
  3. You call markDropoff when the driver completes the stop.
  4. On EXIT, the session stops only if markDropoff was called — otherwise it runs until timeoutSeconds, so a false EXIT can't cut a delivery short.
  5. Every auto-start/stop emits a GeofenceSessionEvent for 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

FunctionDescription
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
)
FieldTypeRequiredDescription
deliveryIdStringYesYour id for the stop. Pass the same id to markDropoff/stopDelivery. Duplicates are de-duped (last wins)
addressStringYesPassed verbatim to session creation
latitudeDoubleYesStop latitude (geofence center)
longitudeDoubleYesStop longitude (geofence center)
radiusMetersDouble?NoPer-stop geofence radius. Falls back to RouteGeofenceOptions.defaultRadiusMeters / remote config when null
customerIdString?NoForwarded to session creation for correlation on your backend
driverIdString?NoForwarded 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
)
FieldTypeDefaultDescription
defaultRadiusMetersDouble250.0Geofence radius for stops that don't set their own radiusMeters
timeoutSecondsDouble?nullBackstop for each auto-started session: it stops this long after starting even if no EXIT/dropoff arrives. null uses remote config
autoStopAfterDropoffSecondsDouble?nullForwarded to the post-dropoff auto-stop (see Auto-Stop After Dropoff)
manualForegroundBooleanfalseWhen 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 }
FieldTypeDescription
deliveryIdStringThe stop's DeliveryStop.deliveryId
typeGeofenceSessionEventTypeSTARTED (session auto-started on ENTER) or STOPPED (session auto-stopped)
reasonStringWhat 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. fold over success/failure on every startDelivery* callback, and branch on error type (network, permission, etc.) to show the right message. markDropoff/newEvent's suspend variants have no Result, so wrap them in try/catch if 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 call stopDelivery on geofence exit (or rely on timeoutSeconds as a backstop).

Next Steps

  1. 💡 View Complete Examples: full implementation examples
  2. 🛠️ Troubleshooting Guide: common integration and runtime issues