Skip to main content

Android SDK Usage & Integration

Learn how to initialize the DoorstepAI SDK and integrate delivery tracking into your Android application.

SDK Initialization

Activity-Level Initialization

Initialize the SDK in your main Activity, typically in onCreate():

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() {
try {
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = {
try {
DoorstepAI.setAPIKey(FIXED_API_TOKEN)
println("✅ DoorstepAI SDK initialized successfully + API token set")
sdkInitialized = true
} catch (e: Exception) {
println("❌ Failed to set API token: ${e.message}")
sdkInitialized = false
}
},
onFailure = { error ->
println("❌ SDK initialization failed: ${error.message}")
sdkInitialized = false
}
)
}
} catch (e: Exception) {
println("❌ Error during SDK initialization: ${e.message}")
sdkInitialized = 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.

Initialization Parameters

ParameterTypeDescriptionRequired
contextContextApplication contextRequired
notificationTitleString?Title for tracking notificationsRequired
notificationTextString?Description for tracking notificationsRequired
callback(Result<Unit>) -> UnitInitialization result callback⚠️ Recommended

setAPIKey Parameters

ParameterTypeDefaultDescription
keyStringJWT 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)
Notification Customization

The notification title and text are used for the foreground service notification that appears when the SDK is actively tracking deliveries. These parameters are required for proper SDK initialization.

Core Functionality

Delivery IDs

Delivery IDs are designed to be unique identifiers for each delivery session. Use meaningful, unique identifiers that help you track and manage your deliveries effectively.

Starting Delivery Tracking

The SDK provides multiple methods to start delivery tracking based on different address formats.

Every start method accepts these optional knobs:

  • timeoutSeconds: Double? — auto-stops tracking after the given duration, as a backstop in case an exit geofence is missed.
  • manualForeground: Boolean — when true, the SDK does not promote its internal TrackingService to the foreground; the host app must already be running its own foreground service to keep the process alive. Defaults to false.
  • autoStopAfterDropoffSeconds: Double? — auto-stops tracking the given number of seconds after markDropoff is called. If null, the SDK falls back to the value in remote config; if 0 or negative, no auto-stop is scheduled. The timer is backed by AlarmManager.setAndAllowWhileIdle, so it survives Doze without SCHEDULE_EXACT_ALARM.

The AddressType and address-string variants additionally accept coordinates: LatLngObject? so you can pair a textual address with the lat/lng you already resolved upstream.

1. Start by Place ID

DoorstepAI.startDeliveryByPlaceID(
placeID = "some_place_id",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0, // optional
manualForeground = false, // optional
autoStopAfterDropoffSeconds = 600.0 // optional
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DoorstepAI", "✅ Delivery started successfully: $message")
// Handle successful start
updateDeliveryStatus(DeliveryStatus.ACTIVE)
},
onFailure = { error ->
Log.e("DoorstepAI", "❌ Failed to start delivery", error)
// Handle error
showErrorToUser(error.message)
}
)
}
Geofence Start Requirement

Execute startDelivery... when the driver enters the delivery geofence around the target building. Do not start inside the building.

2. Start by Address Components

import com.doorstepai.sdks.tracking.AddressType
import com.doorstepai.sdks.tracking.LatLngObject

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",
coordinates = LatLngObject(lat = 37.7749, lng = -122.4194), // optional
timeoutSeconds = 1200.0, // optional
manualForeground = false, // optional
autoStopAfterDropoffSeconds = 600.0 // optional
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DoorstepAI", "✅ Address delivery started: $message")
},
onFailure = { error ->
Log.e("DoorstepAI", "❌ Address delivery failed", error)
}
)
}

3. Start by Address String

DoorstepAI.startDeliveryByAddressString(
address = "123 Main St, Apt 4B, San Francisco, CA 94102",
deliveryId = "delivery_12345",
coordinates = LatLngObject(lat = 37.7749, lng = -122.4194), // optional
timeoutSeconds = 1200.0, // optional
manualForeground = false, // optional
autoStopAfterDropoffSeconds = 600.0 // optional
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DoorstepAI", "✅ Address string delivery started: $message")
},
onFailure = { error ->
Log.e("DoorstepAI", "❌ Address string delivery failed", error)
}
)
}

Deprecated: Start by Plus Code

Deprecated

startDeliveryByPlusCode is deprecated. Use startDeliveryByAddressString (or startDeliveryByAddressType) and pass coordinates if you have them.

// Deprecated — kept for backwards compatibility.
DoorstepAI.startDeliveryByPlusCode(
plusCode = "some_plus_code",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }

Deprecated: Start by Lat/Lng

Deprecated

startDeliveryByLatLng is deprecated. Use startDeliveryByAddressString or startDeliveryByAddressType with the coordinates parameter instead.

// Deprecated — kept for backwards compatibility.
DoorstepAI.startDeliveryByLatLng(
latitude = 37.7749,
longitude = -122.4194,
subUnit = "Apt 4B",
deliveryId = "delivery_12345",
timeoutSeconds = 1200.0
) { /* result handler */ }
  • Use both an enter and exit geofence to bound the delivery session.
  • A radius of 250m or larger typically yields the best results.

Delivery Events

Track important delivery milestones using event reporting:

Mark Dropoff (POD or non-POD)

DoorstepAI.markDropoff(
deliveryId = "delivery_12345",
dropoffType = DropoffType.POD // or DropoffType.NON_POD
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DoorstepAI", "✅ Dropoff marked")
},
onFailure = { error ->
Log.e("DoorstepAI", "❌ Dropoff mark failed: ${error.message}")
}
)
}
// NOTE: The previous methods of marking a dropoff still work as expected.
DoorstepAI.newEvent(
eventName = "taking_pod",
deliveryId = "delivery_12345"
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DoorstepAI", "Event sent")
},
onFailure = { error ->
Log.e("DoorstepAI", "Event send failed: ${error.message}")
}
)
}

Stopping Delivery Tracking

Stop tracking when the delivery is complete:

// Stop delivery tracking
try {
DoorstepAI.stopDelivery("delivery_12345")
Log.i("DoorstepAI", "🛑 Delivery tracking stopped")
} catch (e: Exception) {
Log.e("DoorstepAI", "❌ Error stopping delivery: ${e.message}")
}
Geofence Stop Requirement

Execute stopDelivery(...) when the driver exits the delivery geofence surrounding the building. Do not stop inside the building.

Address Types Reference

AddressType Data Class

data class AddressType(
val streetNumber: String, // "123"
val route: String, // "Main Street"
val subPremise: String, // "Apt 4B", "Unit 2"
val locality: String, // "San Francisco"
val administrativeAreaLevel1: String, // "CA", "California"
val postalCode: String // "94102"
)

LatLngObject

Pairs a textual address with the lat/lng you already resolved upstream. Pass it as the coordinates argument on startDeliveryByAddressType or startDeliveryByAddressString.

@Serializable
data class LatLngObject(
val lat: Double,
val lng: Double
)

Usage Examples

// Minimal address
val simpleAddress = AddressType(
streetNumber = "456",
route = "Oak Avenue",
subPremise = null,
locality = "Oakland",
administrativeAreaLevel1 = "CA",
postalCode = "94610"
)

// Complete address with apartment
val detailedAddress = AddressType(
streetNumber = "789",
route = "Pine Street",
subPremise = "Suite 100",
locality = "Berkeley",
administrativeAreaLevel1 = "California",
postalCode = "94704"
)

Runtime Permissions

Permission Request with Jetpack Compose

Use the permission launcher in your Compose UI:

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
) {
var statusText by remember { mutableStateOf("Checking permissions...") }

// Permission launcher - automatically trigger when needed
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.all { it.value }
if (allGranted) {
statusText = "All permissions granted! Initializing SDK..."
context.checkPermissionsAndInitialize()
} else {
val deniedPermissions = permissions.filter { !it.value }.keys
statusText = "Some permissions were denied: ${deniedPermissions.joinToString(", ")}"
}
}

LaunchedEffect(permissionsGranted) {
if (!permissionsGranted) {
val missingPermissions = DoorstepAIPermissionUtils.getMissingPermissions(context)
if (missingPermissions.isNotEmpty()) {
permissionLauncher.launch(missingPermissions.toTypedArray())
}
}
}

LaunchedEffect(permissionsGranted, sdkInitialized) {
statusText = when {
!permissionsGranted -> "Requesting permissions for DoorstepAI SDK..."
!sdkInitialized -> "Initializing DoorstepAI SDK..."
else -> "DoorstepAI SDK ready"
}
}

// Your Compose UI here
}

Permission Helper Class

Use the provided DoorstepAIPermissionUtils class to manage permissions:

// Check if all permissions are granted
if (DoorstepAIPermissionUtils.hasAllPermissions(context)) {
// Initialize SDK
initializeSDK()
} else {
// Request missing permissions
val missingPermissions = DoorstepAIPermissionUtils.getMissingPermissions(context)
// Launch permission request
}

// Get permission rationale for users
val rationale = DoorstepAIPermissionUtils.getPermissionRationale()

Advanced APIs

Retrying GNSS After Permission Grants

If the user grants location permissions after init, retry GNSS callback registration so raw GNSS data starts flowing without an SDK reinit:

// Call after the user grants ACCESS_FINE_LOCATION (or related) at runtime.
DoorstepAI.retryGnssCallbacks()

Manual Foreground Service

Set manualForeground = true on startDeliveryByX(...) when your app already runs a foreground service that keeps the process alive — for example, a delivery-status FGS in your driver app. The SDK will start its TrackingService as a plain (non-foreground) service rather than promoting it to the foreground itself, 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 also persisted to SharedPreferences so the SDK observes it correctly if TrackingService is recreated by the OS in a fresh process.

Auto-Stop After Dropoff

Pass autoStopAfterDropoffSeconds on the startDeliveryByX(...) call. After the next markDropoff(...) for that session, the SDK schedules a Doze-safe AlarmManager alarm that fires stopDelivery(...) automatically when the timer elapses.

  • Host-supplied value always wins over remote config.
  • null → fall back to remote config (MiscConfiguration.autoStopAfterDropoffSeconds).
  • <= 0 → no auto-stop scheduled.

Dev Mode

Validate the API token and flip DoorstepAI.devMode to true if the token has dev privileges:

lifecycleScope.launch {
val enabled = DoorstepAI.enableDevMode()
if (enabled) {
Log.i("DoorstepAI", "✅ Dev mode active")
}
}

// Probe without changing state:
val canDev = DoorstepAI.validateDevModeAccess()

// Disable explicitly:
DoorstepAI.disableDevMode()

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

1. Error Handling

Always implement comprehensive error handling for SDK methods:

fun startDeliveryWithErrorHandling(placeID: String, deliveryId: String) {
DoorstepAI.startDeliveryByPlaceID(
placeID = placeID,
deliveryId = deliveryId
) { result ->
result.fold(
onSuccess = { message ->
Log.i("DeliveryTracker", "Success: $message")

// Update UI on main thread
runOnUiThread {
updateDeliveryStatus(DeliveryStatus.ACTIVE)
showSuccessMessage("Delivery tracking started")
}
},
onFailure = { error ->
Log.e("DeliveryTracker", "Error starting delivery", error)

// Handle specific error types
runOnUiThread {
when {
error.message?.contains("network", ignoreCase = true) == true -> {
showErrorMessage("Network error. Please check your connection.")
}
error.message?.contains("permission", ignoreCase = true) == true -> {
showErrorMessage("Location permission required.")
}
else -> {
showErrorMessage("Failed to start delivery tracking.")
}
}
}
}
)
}
}

2. Lifecycle Management

Handle Android lifecycle appropriately:

class DeliveryTrackingActivity : ComponentActivity() {
private var currentDeliveryId: String? = null

override fun onResume() {
super.onResume()
// SDK automatically handles activity lifecycle
// No additional action required
}

override fun onPause() {
super.onPause()
// SDK continues tracking in background
Log.d("DeliveryTracker", "Activity paused - tracking continues")
}

override fun onDestroy() {
super.onDestroy()
// Only stop delivery if this is the final activity
if (isFinishing && currentDeliveryId != null) {
// Consider if delivery should be stopped here
// Usually you want tracking to continue even if activity is destroyed
}
}
}

3. Thread Safety

Always update UI on the main thread:

private fun handleDeliveryCallback(result: Result<String>) {
// SDK callbacks may come from background threads
runOnUiThread {
result.fold(
onSuccess = { message ->
// Safe to update UI here
statusTextView.text = "Delivery Active: $message"
progressBar.visibility = View.GONE
},
onFailure = { error ->
// Safe to update UI here
statusTextView.text = "Error: ${error.message}"
progressBar.visibility = View.GONE
}
)
}
}

Next Steps

Now that you understand the core SDK functionality:

  1. 💡 View Complete Examples - See full implementation examples
  2. 🛠️ Troubleshooting Guide - Solve common integration and runtime issues
info

Need help with implementation? Check out our complete examples with error handling, permission management, and best practices.

:::