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>
- Android API level 21+ (Android 5.0)
- Kotlin 1.8+
- Compile SDK 34+
Permissions
Add required permissions to yourAndroidManifest.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
- GitHub: github.com/bookovia/android-sdk
- Maven Repository: Maven Central
- Issues: GitHub Issues
- Email: android-support@bookovia.com
Ready to build Android telematics apps? Check out our quickstart guide and API reference for more examples.