Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.bookovia.com/llms.txt

Use this file to discover all available pages before exploring further.

Bookovia Android SDK

The official Android SDK for the Bookovia Telematics API provides a native Kotlin implementation optimized for Android applications with offline support, background location tracking, and lifecycle-aware components.

Features

  • Native Kotlin - Built specifically for Android with Kotlin coroutines
  • Offline Support - Room database integration for offline data storage
  • Background Location - Continuous location tracking with foreground service
  • Lifecycle Aware - Automatic handling of Android lifecycle events
  • Coroutines Support - Modern async programming with Kotlin coroutines
  • Architecture Components - Integration with ViewModel, LiveData, and Room

Installation

Gradle (Module-level)

dependencies {
    implementation 'com.bookovia:android-sdk:2.1.0'
    
    // Optional: for offline support
    implementation 'com.bookovia:android-sdk-offline:2.1.0'
}

Maven

<dependency>
    <groupId>com.bookovia</groupId>
    <artifactId>android-sdk</artifactId>
    <version>2.1.0</version>
</dependency>
Requirements:
  • Android API level 21+ (Android 5.0)
  • Kotlin 1.8+
  • Compile SDK 34+

Permissions

Add required permissions to your AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Required for API communication -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Required for location tracking -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    <!-- For background location (Android 10+) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    
    <!-- For foreground service -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
    
    <application>
        <!-- Location tracking service -->
        <service
            android:name="com.bookovia.sdk.LocationTrackingService"
            android:foregroundServiceType="location"
            android:exported="false" />
    </application>
</manifest>

Quick Start

Initialize the SDK

import com.bookovia.sdk.BookoviaClient
import com.bookovia.sdk.BookoviaConfig

class MainActivity : AppCompatActivity() {
    private lateinit var bookovia: BookoviaClient
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // Initialize Bookovia client
        bookovia = BookoviaClient.Builder(this)
            .apiKey("bkv_live_your_api_key")
            .config(
                BookoviaConfig(
                    baseUrl = "https://api.bookovia.com/v1",
                    enableOfflineMode = true,
                    enableDebugLogging = BuildConfig.DEBUG
                )
            )
            .build()
    }
}

Start a Trip

import com.bookovia.sdk.models.*
import kotlinx.coroutines.*

class TripManager {
    private val bookovia = BookoviaClient.instance
    private var currentTrip: Trip? = null
    
    suspend fun startTrip(vehicleId: String, driverId: String? = null): Trip? {
        return try {
            val request = StartTripRequest(
                vehicleId = vehicleId,
                driverId = driverId,
                startLocation = Location(
                    latitude = 40.7128,
                    longitude = -74.0060
                ),
                metadata = mapOf(
                    "purpose" to "delivery",
                    "platform" to "android"
                )
            )
            
            val trip = bookovia.trips.start(request)
            currentTrip = trip
            
            // Start location tracking
            startLocationTracking(trip.tripId)
            
            trip
        } catch (e: Exception) {
            Log.e("TripManager", "Failed to start trip", e)
            null
        }
    }
    
    suspend fun stopTrip(): Trip? {
        return currentTrip?.let { trip ->
            try {
                stopLocationTracking()
                
                val stoppedTrip = bookovia.trips.stop(
                    tripId = trip.tripId,
                    request = StopTripRequest(
                        endLocation = getLastKnownLocation()
                    )
                )
                
                currentTrip = null
                stoppedTrip
            } catch (e: Exception) {
                Log.e("TripManager", "Failed to stop trip", e)
                null
            }
        }
    }
}

Location Tracking

import com.bookovia.sdk.location.LocationTracker
import com.bookovia.sdk.location.LocationUpdateListener

class LocationTrackingManager : LocationUpdateListener {
    private val bookovia = BookoviaClient.instance
    private val locationTracker = LocationTracker(context)
    private var currentTripId: String? = null
    
    fun startTracking(tripId: String) {
        currentTripId = tripId
        
        val config = LocationTrackingConfig(
            updateInterval = 30_000L, // 30 seconds
            fastestInterval = 10_000L, // 10 seconds
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY,
            enableBackgroundTracking = true
        )
        
        locationTracker.startTracking(config, this)
    }
    
    fun stopTracking() {
        locationTracker.stopTracking()
        currentTripId = null
    }
    
    override fun onLocationUpdate(location: AndroidLocation) {
        currentTripId?.let { tripId ->
            // Upload location to API
            GlobalScope.launch {
                try {
                    bookovia.locations.upload(
                        LocationUploadRequest(
                            tripId = tripId,
                            latitude = location.latitude,
                            longitude = location.longitude,
                            timestamp = Instant.ofEpochMilli(location.time),
                            speed = location.speed.toDouble(),
                            heading = location.bearing.toDouble(),
                            accuracy = location.accuracy.toDouble()
                        )
                    )
                } catch (e: Exception) {
                    Log.e("LocationTracking", "Failed to upload location", e)
                }
            }
        }
    }
    
    override fun onLocationError(error: LocationError) {
        Log.e("LocationTracking", "Location error: ${error.message}")
    }
}

API Reference

Trip Management

// Trip Query Methods (DataSyncManager)
class DataSyncManager {
    // User Trip Queries
    suspend fun getUserActiveTrips(userId: String): Map<String, Any?>
    suspend fun getUserTripHistory(userId: String, status: String? = null): Map<String, Any?>
    
    // Organization Trip Queries
    suspend fun getOrgActiveTrips(orgId: String): Map<String, Any?>
    suspend fun getOrgTripHistory(orgId: String, status: String? = null): Map<String, Any?>
    
    // Vehicle Trip Queries
    suspend fun getVehicleActiveTrips(vehicleId: String): Map<String, Any?>
    suspend fun getVehicleTripHistory(vehicleId: String, status: String? = null): Map<String, Any?>
}

// Usage
val tripService = bookovia.trips

// Start trip
val trip = tripService.start(
    StartTripRequest(
        vehicleId = "vehicle_123",
        driverId = "driver_456"
    )
)

// List trips
val trips = tripService.list(
    TripFilters(
        status = TripStatus.ACTIVE,
        limit = 50
    )
)

Location Services

interface LocationService {
    suspend fun upload(request: LocationUploadRequest)
    suspend fun batchUpload(request: BatchUploadRequest): BatchUploadResponse
    suspend fun getRoute(tripId: String): RouteResponse
    suspend fun findNearby(request: NearbySearchRequest): List<NearbyLocation>
}

// Batch upload locations
val locations = listOf(
    LocationPoint(
        latitude = 40.7128,
        longitude = -74.0060,
        timestamp = Instant.now(),
        speed = 35.0,
        heading = 180.0
    )
)

bookovia.locations.batchUpload(
    BatchUploadRequest(
        tripId = "trip_123",
        locations = locations
    )
)

Safety Analytics

interface SafetyService {
    suspend fun getScore(request: SafetyScoreRequest): SafetyScore
    suspend fun analyzeBehavior(request: BehaviorAnalysisRequest): BehaviorAnalysis
    suspend fun getHarshEvents(filters: EventFilters): EventListResponse
    suspend fun getCrashRisk(request: CrashRiskRequest): CrashRiskAssessment
}

// Get safety score
val score = bookovia.safety.getScore(
    SafetyScoreRequest(
        driverId = "driver_456",
        dateRange = DateRange(
            start = LocalDate.now().minusDays(30),
            end = LocalDate.now()
        )
    )
)

println("Safety Score: ${score.score}/100 (Grade: ${score.grade})")

Data Models

Core Models

@Parcelize
data class Trip(
    val tripId: String,
    val organizationId: String,
    val vehicleId: String,
    val driverId: String?,
    val status: TripStatus,
    val startTime: Instant,
    val endTime: Instant?,
    val startLocation: Location?,
    val endLocation: Location?,
    val analytics: TripAnalytics,
    val metadata: Map<String, Any>?,
    val createdAt: Instant,
    val updatedAt: Instant
) : Parcelable

@Parcelize
data class Location(
    val latitude: Double,
    val longitude: Double,
    val address: String? = null
) : Parcelable

@Parcelize
data class LocationPoint(
    val latitude: Double,
    val longitude: Double,
    val timestamp: Instant,
    val speed: Double,
    val heading: Double,
    val accuracy: Double? = null,
    val altitude: Double? = null
) : Parcelable

enum class TripStatus {
    ACTIVE, COMPLETED, PAUSED, CANCELLED
}

Request Models

data class StartTripRequest(
    val vehicleId: String,
    val driverId: String? = null,
    val startLocation: Location? = null,
    val metadata: Map<String, Any>? = null,
    val odometerReading: Int? = null,
    val fuelLevelPercent: Float? = null
)

data class StopTripRequest(
    val endLocation: Location? = null,
    val odometerReading: Int? = null,
    val fuelLevelPercent: Float? = null,
    val metadata: Map<String, Any>? = null
)

data class TripFilters(
    val vehicleId: String? = null,
    val driverId: String? = null,
    val status: TripStatus? = null,
    val startDate: LocalDate? = null,
    val endDate: LocalDate? = null,
    val limit: Int = 25,
    val offset: Int = 0
)

Architecture Components Integration

ViewModel Integration

class TripViewModel(
    private val bookovia: BookoviaClient,
    private val repository: TripRepository
) : ViewModel() {
    
    private val _currentTrip = MutableLiveData<Trip?>()
    val currentTrip: LiveData<Trip?> = _currentTrip
    
    private val _tripStatus = MutableLiveData<TripStatus>()
    val tripStatus: LiveData<TripStatus> = _tripStatus
    
    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error
    
    fun startTrip(vehicleId: String, driverId: String?) {
        viewModelScope.launch {
            try {
                val trip = bookovia.trips.start(
                    StartTripRequest(
                        vehicleId = vehicleId,
                        driverId = driverId
                    )
                )
                
                _currentTrip.value = trip
                _tripStatus.value = trip.status
                
                // Save to local database
                repository.saveTrip(trip)
                
            } catch (e: Exception) {
                _error.value = "Failed to start trip: ${e.message}"
            }
        }
    }
    
    fun stopCurrentTrip() {
        _currentTrip.value?.let { trip ->
            viewModelScope.launch {
                try {
                    val stoppedTrip = bookovia.trips.stop(trip.tripId)
                    _currentTrip.value = stoppedTrip
                    _tripStatus.value = stoppedTrip.status
                    
                    repository.updateTrip(stoppedTrip)
                    
                } catch (e: Exception) {
                    _error.value = "Failed to stop trip: ${e.message}"
                }
            }
        }
    }
}

Room Database Integration

@Entity(tableName = "trips")
data class TripEntity(
    @PrimaryKey val tripId: String,
    val vehicleId: String,
    val driverId: String?,
    val status: String,
    val startTime: Long,
    val endTime: Long?,
    val distanceKm: Double,
    val durationMinutes: Int,
    val safetyScore: Int,
    val synced: Boolean = false
)

@Dao
interface TripDao {
    @Query("SELECT * FROM trips WHERE synced = 0")
    suspend fun getUnsyncedTrips(): List<TripEntity>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertTrip(trip: TripEntity)
    
    @Update
    suspend fun updateTrip(trip: TripEntity)
    
    @Query("UPDATE trips SET synced = 1 WHERE tripId = :tripId")
    suspend fun markAsSynced(tripId: String)
}

@Database(
    entities = [TripEntity::class, LocationEntity::class],
    version = 1,
    exportSchema = false
)
abstract class BookoviaDatabase : RoomDatabase() {
    abstract fun tripDao(): TripDao
    abstract fun locationDao(): LocationDao
}

Offline Support

class OfflineTripManager(
    private val bookovia: BookoviaClient,
    private val database: BookoviaDatabase
) {
    
    suspend fun startTripOffline(vehicleId: String): String {
        val tripId = "offline_${System.currentTimeMillis()}"
        
        val trip = TripEntity(
            tripId = tripId,
            vehicleId = vehicleId,
            driverId = null,
            status = "active",
            startTime = System.currentTimeMillis(),
            endTime = null,
            distanceKm = 0.0,
            durationMinutes = 0,
            safetyScore = 100,
            synced = false
        )
        
        database.tripDao().insertTrip(trip)
        return tripId
    }
    
    suspend fun syncPendingTrips() {
        val unsyncedTrips = database.tripDao().getUnsyncedTrips()
        
        for (tripEntity in unsyncedTrips) {
            try {
                // Convert to API request and sync
                val request = tripEntity.toStartTripRequest()
                val syncedTrip = bookovia.trips.start(request)
                
                // Update local trip with server ID
                database.tripDao().updateTrip(
                    tripEntity.copy(
                        tripId = syncedTrip.tripId,
                        synced = true
                    )
                )
                
            } catch (e: Exception) {
                Log.e("OfflineSync", "Failed to sync trip ${tripEntity.tripId}", e)
            }
        }
    }
}

Background Services

Foreground Location Service

class LocationTrackingService : Service() {
    private val bookovia by lazy { BookoviaClient.instance }
    private val locationClient by lazy { LocationServices.getFusedLocationProviderClient(this) }
    
    private var currentTripId: String? = null
    
    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
    }
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_START_TRACKING -> {
                currentTripId = intent.getStringExtra(EXTRA_TRIP_ID)
                startForegroundTracking()
            }
            ACTION_STOP_TRACKING -> {
                stopForegroundTracking()
            }
        }
        return START_NOT_STICKY
    }
    
    private fun startForegroundTracking() {
        val notification = createTrackingNotification()
        startForeground(NOTIFICATION_ID, notification)
        
        val locationRequest = LocationRequest.Builder(30_000) // 30 seconds
            .setPriority(Priority.PRIORITY_HIGH_ACCURACY)
            .setMinUpdateIntervalMillis(10_000) // 10 seconds
            .build()
        
        try {
            locationClient.requestLocationUpdates(
                locationRequest,
                locationCallback,
                Looper.getMainLooper()
            )
        } catch (e: SecurityException) {
            Log.e("LocationService", "Location permission denied", e)
            stopSelf()
        }
    }
    
    private val locationCallback = object : LocationCallback() {
        override fun onLocationResult(locationResult: LocationResult) {
            currentTripId?.let { tripId ->
                locationResult.lastLocation?.let { location ->
                    uploadLocation(tripId, location)
                }
            }
        }
    }
    
    private fun uploadLocation(tripId: String, location: android.location.Location) {
        lifecycleScope.launch {
            try {
                bookovia.locations.upload(
                    LocationUploadRequest(
                        tripId = tripId,
                        latitude = location.latitude,
                        longitude = location.longitude,
                        timestamp = Instant.ofEpochMilli(location.time),
                        speed = location.speed.toDouble(),
                        heading = location.bearing.toDouble(),
                        accuracy = location.accuracy.toDouble()
                    )
                )
            } catch (e: Exception) {
                Log.e("LocationService", "Failed to upload location", e)
                // Store locally for later sync
                storeLocationOffline(tripId, location)
            }
        }
    }
    
    companion object {
        const val ACTION_START_TRACKING = "com.bookovia.START_TRACKING"
        const val ACTION_STOP_TRACKING = "com.bookovia.STOP_TRACKING"
        const val EXTRA_TRIP_ID = "trip_id"
        const val NOTIFICATION_ID = 1001
    }
}

Error Handling

import com.bookovia.sdk.exceptions.*

try {
    val trip = bookovia.trips.start(request)
} catch (e: AuthenticationException) {
    // Handle authentication error
    Log.e("Auth", "Invalid API key: ${e.message}")
    
} catch (e: ValidationException) {
    // Handle validation errors
    Log.e("Validation", "Invalid request: ${e.fieldErrors}")
    e.fieldErrors.forEach { error ->
        Log.e("Validation", "${error.field}: ${error.message}")
    }
    
} catch (e: RateLimitException) {
    // Handle rate limiting
    Log.w("RateLimit", "Rate limited. Retry after: ${e.retryAfterMs}ms")
    
} catch (e: NetworkException) {
    // Handle network errors
    Log.e("Network", "Network error: ${e.message}")
    // Maybe store request for later retry
    
} catch (e: BookoviaException) {
    // Handle other Bookovia API errors
    Log.e("Bookovia", "API error: ${e.message}")
    
} catch (e: Exception) {
    // Handle unexpected errors
    Log.e("Unexpected", "Unexpected error", e)
}

Testing

Unit Tests

@Test
class TripManagerTest {
    
    @Mock
    private lateinit var mockBookovia: BookoviaClient
    
    @Mock
    private lateinit var mockTripService: TripService
    
    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        whenever(mockBookovia.trips).thenReturn(mockTripService)
    }
    
    @Test
    fun `startTrip should return trip on success`() = runTest {
        // Given
        val request = StartTripRequest(vehicleId = "vehicle_123")
        val expectedTrip = createMockTrip("trip_123", "vehicle_123")
        
        whenever(mockTripService.start(request)).thenReturn(expectedTrip)
        
        val tripManager = TripManager(mockBookovia)
        
        // When
        val result = tripManager.startTrip("vehicle_123")
        
        // Then
        assertThat(result).isEqualTo(expectedTrip)
        verify(mockTripService).start(request)
    }
    
    @Test
    fun `startTrip should handle authentication error`() = runTest {
        // Given
        val request = StartTripRequest(vehicleId = "vehicle_123")
        
        whenever(mockTripService.start(request))
            .thenThrow(AuthenticationException("Invalid API key"))
        
        val tripManager = TripManager(mockBookovia)
        
        // When & Then
        assertThrows<AuthenticationException> {
            tripManager.startTrip("vehicle_123")
        }
    }
}

Instrumentation Tests

@RunWith(AndroidJUnit4::class)
class LocationTrackingTest {
    
    @get:Rule
    val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
        android.Manifest.permission.ACCESS_FINE_LOCATION
    )
    
    @Test
    fun testLocationTracking() {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val locationTracker = LocationTracker(context)
        
        val config = LocationTrackingConfig(
            updateInterval = 1000L,
            priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        )
        
        var locationReceived = false
        
        locationTracker.startTracking(config, object : LocationUpdateListener {
            override fun onLocationUpdate(location: android.location.Location) {
                locationReceived = true
                assertThat(location.latitude).isNotEqualTo(0.0)
                assertThat(location.longitude).isNotEqualTo(0.0)
            }
            
            override fun onLocationError(error: LocationError) {
                fail("Location error: ${error.message}")
            }
        })
        
        // Wait for location update
        Thread.sleep(5000)
        locationTracker.stopTracking()
        
        assertThat(locationReceived).isTrue()
    }
}

Example Implementation

Complete Trip Management Activity

class TripActivity : AppCompatActivity() {
    private lateinit var binding: ActivityTripBinding
    private lateinit var tripManager: TripManager
    private lateinit var locationManager: LocationTrackingManager
    
    private var currentTrip: Trip? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityTripBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        tripManager = TripManager()
        locationManager = LocationTrackingManager(this)
        
        setupUI()
        requestLocationPermissions()
    }
    
    private fun setupUI() {
        binding.btnStartTrip.setOnClickListener {
            startTrip()
        }
        
        binding.btnStopTrip.setOnClickListener {
            stopTrip()
        }
        
        updateUI()
    }
    
    private fun startTrip() {
        lifecycleScope.launch {
            try {
                binding.progressBar.isVisible = true
                
                val trip = tripManager.startTrip(
                    vehicleId = "vehicle_android_001",
                    driverId = "driver_123"
                )
                
                currentTrip = trip
                locationManager.startTracking(trip.tripId)
                
                updateUI()
                showMessage("Trip started: ${trip.tripId}")
                
            } catch (e: Exception) {
                showError("Failed to start trip: ${e.message}")
            } finally {
                binding.progressBar.isVisible = false
            }
        }
    }
    
    private fun stopTrip() {
        currentTrip?.let { trip ->
            lifecycleScope.launch {
                try {
                    binding.progressBar.isVisible = true
                    
                    locationManager.stopTracking()
                    val stoppedTrip = tripManager.stopTrip()
                    
                    currentTrip = null
                    updateUI()
                    
                    showMessage("Trip completed. Distance: ${stoppedTrip?.analytics?.distanceKm} km")
                    
                } catch (e: Exception) {
                    showError("Failed to stop trip: ${e.message}")
                } finally {
                    binding.progressBar.isVisible = false
                }
            }
        }
    }
    
    private fun updateUI() {
        val isActive = currentTrip?.status == TripStatus.ACTIVE
        
        binding.btnStartTrip.isEnabled = !isActive
        binding.btnStopTrip.isEnabled = isActive
        
        binding.tvTripStatus.text = if (isActive) {
            "Trip Active: ${currentTrip?.tripId}"
        } else {
            "No active trip"
        }
    }
    
    private fun requestLocationPermissions() {
        if (ContextCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION),
                LOCATION_PERMISSION_REQUEST
            )
        }
    }
    
    private fun showMessage(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
    
    private fun showError(error: String) {
        Toast.makeText(this, error, Toast.LENGTH_LONG).show()
    }
    
    companion object {
        private const val LOCATION_PERMISSION_REQUEST = 1001
    }
}

Support


Ready to build Android telematics apps? Check out our quickstart guide and API reference for more examples.