Android SDK Examples & Troubleshooting
Complete implementation examples and solutions to common integration challenges.
Quickstart
The full lifecycle, stripped of UI:
import androidx.lifecycle.lifecycleScope
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.DropoffType
import kotlinx.coroutines.launch
// Once at app startup
DoorstepAI.init(
context = this,
notificationTitle = "Tracking...",
notificationText = "Tracking your delivery"
) { result ->
result.fold(
onSuccess = { DoorstepAI.setAPIKey(API_KEY) },
onFailure = { /* handle init failure */ }
)
}
// Per delivery, driven by your geofence
DoorstepAI.startDeliveryByAddressString(
address = "123 Main St, Apt 4B, San Francisco, CA 94102",
deliveryId = "delivery_12345",
coordinates = LatLngObject(lat = 37.7749, lng = -122.4194)
) { /* result handler */ }
// markDropoff is a suspend function; call it from a coroutine scope
lifecycleScope.launch {
DoorstepAI.markDropoff(
deliveryId = "delivery_12345",
dropoffType = DropoffType.POD
)
}
DoorstepAI.stopDelivery("delivery_12345")
Route Geofencing Quickstart
Minimal usage of the in-SDK route geofencing API. See Using the SDK → In-SDK Route Geofencing for the full API reference:
import androidx.lifecycle.lifecycleScope
import com.doorstepai.sdks.tracking.DeliveryStop
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.RouteGeofenceOptions
import kotlinx.coroutines.launch
// Once at app startup (after resuming any persisted state)
DoorstepAI.resumeRouteGeofencingIfNeeded()
// Build the day's route
val todaysStops = listOf(
DeliveryStop(
deliveryId = "delivery_12345",
address = "123 Main St, Apt 4B, San Francisco, CA 94102",
latitude = 37.7749,
longitude = -122.4194
),
DeliveryStop(
deliveryId = "delivery_67890",
address = "500 Market St, San Francisco, CA 94105",
latitude = 37.7899,
longitude = -122.4001,
radiusMeters = 150.0 // optional per-stop override
)
)
// Hand the SDK the day's route (options are optional; defaults shown)
DoorstepAI.startRouteGeofencing(
stops = todaysStops,
options = RouteGeofenceOptions(defaultRadiusMeters = 250.0)
) { result ->
result.fold(
onSuccess = { Log.i("DoorstepAI", "Route geofencing started") },
onFailure = { error -> Log.e("DoorstepAI", "Failed to start route geofencing: ${error.message}") }
)
}
// Observe session start/stop events to drive UI
lifecycleScope.launch {
DoorstepAI.geofenceSessionEvents.collect { event ->
Log.i("DoorstepAI", "Route event: ${event.deliveryId} -> ${event.type} (${event.reason})")
}
}
// Driver takes a POD at a stop; gates the auto-stop on geofence EXIT
lifecycleScope.launch {
DoorstepAI.markDropoff(deliveryId = "delivery_12345", dropoffType = DropoffType.POD)
}
// Route changes mid-day (stop added/removed): pass the full updated stop list
DoorstepAI.updateRouteStops(stops = todaysStops.drop(1)) { /* result handler */ }
// End of route
DoorstepAI.stopRouteGeofencing()
Full test-app component
The component below wires those calls to a Jetpack Compose UI for manual testing:
Show full component
package com.doorstepai.doorstepsdktestapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.doorstepai.doorstepsdktestapp.ui.theme.DoorstepSDKTestAppTheme
import com.doorstepai.sdks.tracking.DoorstepAI
import com.doorstepai.sdks.tracking.AddressType
import com.doorstepai.sdks.tracking.DropoffType
import kotlinx.coroutines.launch
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
// DoorstepAIPermissionUtils is omitted here for brevity. See the full class in
// [Using the SDK → Runtime Permissions](/Android/using#runtime-permissions) and copy it into your app.
class MainActivity : ComponentActivity() {
companion object {
private const val FIXED_API_TOKEN = "api_key" // Replace with your actual API token
}
private var permissionsGranted by mutableStateOf(false)
private var sdkInitialized by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
checkPermissionsAndInitialize()
setContent {
DoorstepSDKTestAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
DoorstepSDKTestScreen(
context = this,
permissionsGranted = permissionsGranted,
sdkInitialized = sdkInitialized,
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
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)
sdkInitialized = true
} catch (e: Exception) {
sdkInitialized = false
}
},
onFailure = { error ->
sdkInitialized = false
}
)
}
} catch (e: Exception) {
sdkInitialized = false
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DoorstepSDKTestScreen(
context: MainActivity,
permissionsGranted: Boolean,
sdkInitialized: Boolean,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
var statusText by remember { mutableStateOf("Checking permissions...") }
var currentDeliveryId by remember { mutableStateOf("") }
var placeId by remember { mutableStateOf("") }
var eventName by remember { mutableStateOf("") }
var isDeliveryActive by remember { mutableStateOf(false) }
var activeDeliveryType by remember { mutableStateOf("") }
// Address fields
var streetNumber by remember { mutableStateOf("") }
var route by remember { mutableStateOf("") }
var subPremise by remember { mutableStateOf("") }
var locality by remember { mutableStateOf("") }
var administrativeArea by remember { mutableStateOf("") }
var postalCode by remember { mutableStateOf("") }
// 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"
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "doorstep.ai DropOff SDK Test App",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = when {
!permissionsGranted -> MaterialTheme.colorScheme.errorContainer
!sdkInitialized -> MaterialTheme.colorScheme.tertiaryContainer
else -> MaterialTheme.colorScheme.primaryContainer
}
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = statusText,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
if (!permissionsGranted) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = DoorstepAIPermissionUtils.getPermissionRationale(),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
if (permissionsGranted && sdkInitialized) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Delivery ID",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
OutlinedTextField(
value = currentDeliveryId,
onValueChange = { currentDeliveryId = it },
label = { Text("Enter Delivery ID") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
}
}
Spacer(modifier = Modifier.height(16.dp))
if (isDeliveryActive) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Active Delivery",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Type: $activeDeliveryType",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "ID: $currentDeliveryId",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
try {
DoorstepAI.stopDelivery(currentDeliveryId)
statusText = "Delivery stopped for: $currentDeliveryId"
isDeliveryActive = false
activeDeliveryType = ""
} catch (e: Exception) {
statusText = "Error stopping delivery: ${e.message}"
}
},
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
) {
Text("Stop Active Delivery")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
if (!isDeliveryActive) {
Text(
text = "Start New Delivery",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Start)
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = placeId,
onValueChange = { placeId = it },
label = { Text("Place ID") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Button(
onClick = {
if (currentDeliveryId.isNotEmpty() && placeId.isNotEmpty()) {
try {
DoorstepAI.startDeliveryByPlaceID(placeId, currentDeliveryId) { result ->
result.fold(
onSuccess = { message ->
statusText = message
isDeliveryActive = true
activeDeliveryType = "Place ID"
},
onFailure = { error ->
statusText = "Failed to start delivery: ${error.message}"
}
)
}
} catch (e: Exception) {
statusText = "Error starting delivery: ${e.message}"
}
} else {
statusText = "Please enter both Delivery ID and Place ID"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = currentDeliveryId.isNotEmpty() && placeId.isNotEmpty()
) {
Text("Start Delivery by Place ID")
}
Button(
onClick = {
if (currentDeliveryId.isNotEmpty()) {
// markDropoff is a suspend function; dispatch it on the Compose coroutine scope
scope.launch {
try {
DoorstepAI.markDropoff(currentDeliveryId, DropoffType.POD)
statusText = "Marked dropoff: $currentDeliveryId"
} catch (e: Exception) {
statusText = "Error marking dropoff: ${e.message}"
}
}
} else {
statusText = "Please enter a Delivery ID"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = currentDeliveryId.isNotEmpty()
) {
Text("Mark Dropoff (POD)")
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Address Components",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.align(Alignment.Start)
)
OutlinedTextField(
value = streetNumber,
onValueChange = { streetNumber = it },
label = { Text("Street Number") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = route,
onValueChange = { route = it },
label = { Text("Route") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = subPremise,
onValueChange = { subPremise = it },
label = { Text("Sub Premise (Apt/Suite)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = locality,
onValueChange = { locality = it },
label = { Text("Locality (City)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = administrativeArea,
onValueChange = { administrativeArea = it },
label = { Text("Administrative Area (State)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
OutlinedTextField(
value = postalCode,
onValueChange = { postalCode = it },
label = { Text("Postal Code") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Button(
onClick = {
if (currentDeliveryId.isNotEmpty() && streetNumber.isNotEmpty() && route.isNotEmpty()) {
try {
val address = AddressType(
streetNumber = streetNumber,
route = route,
subPremise = subPremise,
locality = locality,
administrativeAreaLevel1 = administrativeArea,
postalCode = postalCode
)
DoorstepAI.startDeliveryByAddressType(address, currentDeliveryId) { result ->
result.fold(
onSuccess = { message ->
statusText = message
isDeliveryActive = true
activeDeliveryType = "Address"
},
onFailure = { error ->
statusText = "Failed to start delivery: ${error.message}"
}
)
}
} catch (e: Exception) {
statusText = "Error starting delivery: ${e.message}"
}
} else {
statusText = "Please enter Delivery ID, Street Number, and Route"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = currentDeliveryId.isNotEmpty() && streetNumber.isNotEmpty() && route.isNotEmpty()
) {
Text("Start Delivery by Address")
}
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Events",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(Alignment.Start)
)
OutlinedTextField(
value = eventName,
onValueChange = { eventName = it },
label = { Text("Event Name") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
// newEvent records a custom event on a delivery; pass an optional timestamp to backdate it (see Using the SDK).
Button(
onClick = {
if (currentDeliveryId.isNotEmpty() && eventName.isNotEmpty()) {
try {
DoorstepAI.newEvent(eventName, currentDeliveryId) { result ->
result.fold(
onSuccess = { message ->
statusText = "Event created: $message"
},
onFailure = { error ->
statusText = "Failed to create event: ${error.message}"
}
)
}
} catch (e: Exception) {
statusText = "Error creating event: ${e.message}"
}
} else {
statusText = "Please enter both Delivery ID and Event Name"
}
},
modifier = Modifier.fillMaxWidth(),
enabled = currentDeliveryId.isNotEmpty() && eventName.isNotEmpty()
) {
Text("Create Event")
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
Troubleshooting
1. SDK Initialization Failures
Problem: SDK fails to initialize with callback errors.
Solution: Use context.applicationContext (not an Activity context) when calling DoorstepAI.init, and check the onFailure branch for the underlying error before setting the API key. Most failures trace back to an invalid/placeholder API key or a missing manifest permission, so verify both first.
2. Runtime Permission Issues
Problem: Location or activity recognition permissions are denied.
Solution: If shouldShowRequestPermissionRationale returns true for ACCESS_FINE_LOCATION, show an explanation dialog before re-requesting. Android suppresses the system prompt after a prior denial unless the user understands why it's needed. Request only the permissions from DoorstepAIPermissionUtils.getMissingPermissions(), not the full list, to avoid re-prompting for permissions already granted.
3. Network Connectivity Issues
Problem: SDK fails to communicate with servers.
Solution: Check ConnectivityManager.getNetworkCapabilities() for an active network before assuming an SDK bug. Most "SDK can't reach servers" reports are device connectivity issues (airplane mode, captive Wi-Fi portals, VPN misconfiguration), not SDK problems.
Testing Checklist
Before releasing your Android integration:
- SDK initializes successfully in Activity
- API key is valid and securely stored
- All required permissions are declared in AndroidManifest.xml
- Runtime permissions are properly requested and handled
- Notification permission is granted (Android 13+)
- Error handling is implemented for all SDK methods
- Delivery IDs are unique and meaningful