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.
Mobile Integration Guide
This comprehensive guide covers implementing Bookovia telematics in mobile applications across all major platforms, including location tracking, offline support, background processing, and real-time data synchronization.Platform Overview
Bookovia supports native and cross-platform mobile development:| Platform | SDK | Language | Key Features |
|---|---|---|---|
| iOS | Native iOS SDK | Swift | Core Location, Background App Refresh, Push Notifications |
| Android | Native Android SDK | Kotlin/Java | Foreground Services, WorkManager, FCM |
| React Native | RN SDK | TypeScript/JavaScript | Native modules, Background tasks |
| Flutter | Flutter SDK | Dart | Platform channels, Isolates |
Getting Started
Prerequisites
Before integrating Bookovia in your mobile app:- API Keys - Get your API keys from Bookovia Dashboard
- Platform Setup - Configure development environment for your target platform
- Permissions - Understand required location and background permissions
- Testing Devices - Physical devices recommended for location testing
Installation
- iOS (Swift)
- Android (Kotlin)
- React Native
- Flutter
CocoaPods InstallationSwift Package Manager
# Podfile
pod 'BookoviaTelematics', '~> 1.0.0'
pod install
dependencies: [
.package(url: "https://github.com/bookovia/ios-sdk.git", from: "1.0.0")
]
Gradle SetupMaven Repository
// app/build.gradle
dependencies {
implementation 'com.bookovia:telematics-android:1.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
}
// project/build.gradle
allprojects {
repositories {
maven { url 'https://maven.bookovia.com/releases' }
}
}
NPM InstallationYarn Installation
npm install @bookovia/react-native-sdk
# iOS additional setup
cd ios && pod install
yarn add @bookovia/react-native-sdk
cd ios && pod install
pubspec.yaml
dependencies:
bookovia_telematics: ^1.0.0
geolocator: ^9.0.0
permission_handler: ^10.0.0
flutter pub get
Permissions Setup
Location Permissions
- iOS
- Android
- React Native
- Flutter
Info.plist ConfigurationRequest Permissions
<dict>
<!-- Always required -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to track your trips and provide safety analytics.</string>
<!-- For background tracking -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs background location access to track your trips even when the app is not in use.</string>
<!-- Background modes -->
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>background-fetch</string>
</array>
</dict>
import CoreLocation
import BookoviaTelematics
class LocationManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private let bookovia = BookoviaSDK.shared
func requestPermissions() {
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
}
func locationManager(_ manager: CLLocationManager,
didChangeAuthorization status: CLAuthorizationStatus) {
switch status {
case .authorizedWhenInUse:
// Request always authorization for background tracking
locationManager.requestAlwaysAuthorization()
case .authorizedAlways:
// Start background tracking
bookovia.startBackgroundTracking()
case .denied:
// Handle denied permissions
showPermissionDialog()
default:
break
}
}
}
AndroidManifest.xmlRuntime Permission Requests
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Background location (Android 10+) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Foreground service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Network access -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application>
<!-- Foreground service declaration -->
<service
android:name="com.bookovia.telematics.LocationTrackingService"
android:foregroundServiceType="location"
android:exported="false" />
</application>
</manifest>
import android.Manifest
import androidx.activity.result.contract.ActivityResultContracts
import com.bookovia.telematics.BookoviaSDK
class MainActivity : AppCompatActivity() {
private val bookovia = BookoviaSDK.getInstance(this)
private val locationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
when {
permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true -> {
// Fine location granted
requestBackgroundPermission()
}
permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true -> {
// Coarse location granted
requestBackgroundPermission()
}
else -> {
// No location permissions granted
showPermissionRationale()
}
}
}
private val backgroundLocationLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
bookovia.startBackgroundTracking()
}
}
private fun requestLocationPermissions() {
locationPermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
private fun requestBackgroundPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
backgroundLocationLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
} else {
bookovia.startBackgroundTracking()
}
}
}
Permission Setup
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
import { BookoviaSDK } from '@bookovia/react-native-sdk';
const requestLocationPermissions = async () => {
let permission;
if (Platform.OS === 'ios') {
permission = PERMISSIONS.IOS.LOCATION_WHEN_IN_USE;
} else {
permission = PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION;
}
const result = await request(permission);
switch (result) {
case RESULTS.GRANTED:
// Request background permission
await requestBackgroundPermission();
break;
case RESULTS.DENIED:
// Show rationale dialog
showPermissionDialog();
break;
case RESULTS.BLOCKED:
// Direct to settings
showSettingsDialog();
break;
}
};
const requestBackgroundPermission = async () => {
let bgPermission;
if (Platform.OS === 'ios') {
bgPermission = PERMISSIONS.IOS.LOCATION_ALWAYS;
} else if (Platform.Version >= 29) {
bgPermission = PERMISSIONS.ANDROID.ACCESS_BACKGROUND_LOCATION;
}
if (bgPermission) {
const result = await request(bgPermission);
if (result === RESULTS.GRANTED) {
BookoviaSDK.startBackgroundTracking();
}
}
};
Permission Setup
import 'package:permission_handler/permission_handler.dart';
import 'package:bookovia_telematics/bookovia_telematics.dart';
class PermissionManager {
static Future<bool> requestLocationPermissions() async {
// Request location when in use first
PermissionStatus status = await Permission.locationWhenInUse.request();
if (status.isGranted) {
// Request always location for background tracking
status = await Permission.locationAlways.request();
if (status.isGranted) {
await BookoviaTelematics.startBackgroundTracking();
return true;
}
}
return false;
}
static Future<void> showPermissionDialog() async {
// Show custom dialog explaining why permissions are needed
// Direct user to app settings if permissions are permanently denied
if (await Permission.location.isPermanentlyDenied) {
openAppSettings();
}
}
}
Basic Integration
SDK Initialization
- iOS
- Android
- React Native
- Flutter
import BookoviaTelematics
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize Bookovia SDK
BookoviaSDK.shared.configure(
apiKey: "bkv_live_your_api_key",
environment: .production // or .sandbox
)
// Set delegate for callbacks
BookoviaSDK.shared.delegate = self
return true
}
}
// MARK: - BookoviaSDKDelegate
extension AppDelegate: BookoviaSDKDelegate {
func bookovia(_ sdk: BookoviaSDK, didStartTrip trip: Trip) {
print("Trip started: \(trip.id)")
}
func bookovia(_ sdk: BookoviaSDK, didUpdateLocation location: LocationPoint) {
// Handle location updates
}
func bookovia(_ sdk: BookoviaSDK, didDetectSafetyEvent event: SafetyEvent) {
// Handle safety events
if event.severity == .critical {
showSafetyAlert(event)
}
}
}
import com.bookovia.telematics.BookoviaSDK
import com.bookovia.telematics.BookoviaConfig
import com.bookovia.telematics.callbacks.BookoviaCallback
class BookoviaApplication : Application() {
override fun onCreate() {
super.onCreate()
val config = BookoviaConfig.Builder()
.apiKey("bkv_live_your_api_key")
.environment(BookoviaConfig.Environment.PRODUCTION)
.enableDebugLogging(BuildConfig.DEBUG)
.build()
BookoviaSDK.initialize(this, config)
// Set global callback
BookoviaSDK.getInstance(this).setCallback(object : BookoviaCallback {
override fun onTripStarted(trip: Trip) {
Log.d("Bookovia", "Trip started: ${trip.id}")
}
override fun onLocationUpdate(location: LocationPoint) {
// Handle location updates
}
override fun onSafetyEvent(event: SafetyEvent) {
if (event.severity == SafetyEvent.Severity.CRITICAL) {
showSafetyNotification(event)
}
}
override fun onError(error: BookoviaError) {
Log.e("Bookovia", "SDK Error: ${error.message}")
}
})
}
}
import React, { useEffect } from 'react';
import { BookoviaSDK } from '@bookovia/react-native-sdk';
const App = () => {
useEffect(() => {
// Initialize SDK
BookoviaSDK.initialize({
apiKey: 'bkv_live_your_api_key',
environment: 'production', // or 'sandbox'
enableDebugLogging: __DEV__,
});
// Set up event listeners
const tripListener = BookoviaSDK.addTripListener((trip) => {
console.log('Trip started:', trip.id);
});
const locationListener = BookoviaSDK.addLocationListener((location) => {
// Handle location updates
console.log('Location update:', location);
});
const safetyListener = BookoviaSDK.addSafetyEventListener((event) => {
if (event.severity === 'critical') {
showSafetyAlert(event);
}
});
// Cleanup listeners
return () => {
tripListener.remove();
locationListener.remove();
safetyListener.remove();
};
}, []);
return (
<YourAppComponent />
);
};
import 'package:bookovia_telematics/bookovia_telematics.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
StreamSubscription<Trip>? _tripSubscription;
StreamSubscription<LocationPoint>? _locationSubscription;
StreamSubscription<SafetyEvent>? _safetySubscription;
@override
void initState() {
super.initState();
_initializeBooKovia();
}
void _initializeBooKovia() async {
await BookoviaTelematics.initialize(
apiKey: 'bkv_live_your_api_key',
environment: BookoviaEnvironment.production,
enableDebugLogging: kDebugMode,
);
// Set up event streams
_tripSubscription = BookoviaTelematics.tripStream.listen((trip) {
print('Trip started: ${trip.id}');
});
_locationSubscription = BookoviaTelematics.locationStream.listen((location) {
// Handle location updates
print('Location: ${location.latitude}, ${location.longitude}');
});
_safetySubscription = BookoviaTelematics.safetyEventStream.listen((event) {
if (event.severity == SafetySeverity.critical) {
_showSafetyAlert(event);
}
});
}
@override
void dispose() {
_tripSubscription?.cancel();
_locationSubscription?.cancel();
_safetySubscription?.cancel();
super.dispose();
}
}
Trip Management
Starting and Stopping Trips
- iOS
- Android
- React Native
- Flutter
class TripManager {
private let bookovia = BookoviaSDK.shared
func startTrip() async {
do {
let trip = try await bookovia.startTrip(
metadata: [
"driver_name": "John Doe",
"vehicle_id": "vehicle_123",
"purpose": "delivery"
]
)
print("Trip started: \(trip.id)")
// Update UI
DispatchQueue.main.async {
self.updateTripUI(trip: trip)
}
} catch {
print("Failed to start trip: \(error)")
showErrorAlert(error)
}
}
func stopTrip() async {
do {
let summary = try await bookovia.stopTrip()
print("Trip completed: \(summary.distance) km in \(summary.duration) minutes")
// Show trip summary
DispatchQueue.main.async {
self.showTripSummary(summary)
}
} catch {
print("Failed to stop trip: \(error)")
}
}
}
class TripManager(private val context: Context) {
private val bookovia = BookoviaSDK.getInstance(context)
fun startTrip() {
val metadata = mapOf(
"driver_name" to "John Doe",
"vehicle_id" to "vehicle_123",
"purpose" to "delivery"
)
bookovia.startTrip(metadata, object : TripCallback {
override fun onSuccess(trip: Trip) {
Log.d("TripManager", "Trip started: ${trip.id}")
// Update UI on main thread
(context as? Activity)?.runOnUiThread {
updateTripUI(trip)
}
}
override fun onError(error: BookoviaError) {
Log.e("TripManager", "Failed to start trip: ${error.message}")
showErrorDialog(error)
}
})
}
suspend fun startTripAsync(): Trip = suspendCoroutine { continuation ->
bookovia.startTrip(emptyMap(), object : TripCallback {
override fun onSuccess(trip: Trip) {
continuation.resume(trip)
}
override fun onError(error: BookoviaError) {
continuation.resumeWithException(error)
}
})
}
fun stopTrip() {
bookovia.stopTrip(object : TripSummaryCallback {
override fun onSuccess(summary: TripSummary) {
Log.d("TripManager", "Trip completed: ${summary.distance} km")
showTripSummary(summary)
}
override fun onError(error: BookoviaError) {
Log.e("TripManager", "Failed to stop trip: ${error.message}")
}
})
}
}
import { BookoviaSDK } from '@bookovia/react-native-sdk';
class TripManager {
static async startTrip(metadata = {}) {
try {
const trip = await BookoviaSDK.startTrip({
metadata: {
driver_name: 'John Doe',
vehicle_id: 'vehicle_123',
purpose: 'delivery',
...metadata
}
});
console.log('Trip started:', trip.id);
return trip;
} catch (error) {
console.error('Failed to start trip:', error);
throw error;
}
}
static async stopTrip() {
try {
const summary = await BookoviaSDK.stopTrip();
console.log(`Trip completed: ${summary.distance} km in ${summary.duration} minutes`);
return summary;
} catch (error) {
console.error('Failed to stop trip:', error);
throw error;
}
}
static async getCurrentTrip() {
try {
return await BookoviaSDK.getCurrentTrip();
} catch (error) {
return null; // No active trip
}
}
}
// React Hook for trip management
import { useState, useEffect } from 'react';
export const useTrip = () => {
const [currentTrip, setCurrentTrip] = useState(null);
const [isTracking, setIsTracking] = useState(false);
useEffect(() => {
// Check for existing trip on mount
TripManager.getCurrentTrip().then(setCurrentTrip);
// Listen for trip events
const tripListener = BookoviaSDK.addTripListener((trip) => {
setCurrentTrip(trip);
setIsTracking(trip.status === 'active');
});
return () => tripListener.remove();
}, []);
const startTrip = async (metadata) => {
const trip = await TripManager.startTrip(metadata);
setCurrentTrip(trip);
setIsTracking(true);
return trip;
};
const stopTrip = async () => {
const summary = await TripManager.stopTrip();
setCurrentTrip(null);
setIsTracking(false);
return summary;
};
return { currentTrip, isTracking, startTrip, stopTrip };
};
import 'package:bookovia_telematics/bookovia_telematics.dart';
class TripManager {
static Future<Trip> startTrip({Map<String, dynamic>? metadata}) async {
try {
final trip = await BookoviaTelematics.startTrip(
metadata: {
'driver_name': 'John Doe',
'vehicle_id': 'vehicle_123',
'purpose': 'delivery',
...?metadata
},
);
print('Trip started: ${trip.id}');
return trip;
} catch (error) {
print('Failed to start trip: $error');
rethrow;
}
}
static Future<TripSummary> stopTrip() async {
try {
final summary = await BookoviaTelematics.stopTrip();
print('Trip completed: ${summary.distance} km in ${summary.duration} minutes');
return summary;
} catch (error) {
print('Failed to stop trip: $error');
rethrow;
}
}
static Future<Trip?> getCurrentTrip() async {
try {
return await BookoviaTelematics.getCurrentTrip();
} catch (error) {
return null; // No active trip
}
}
}
// Flutter provider for trip state management
class TripProvider extends ChangeNotifier {
Trip? _currentTrip;
bool _isTracking = false;
StreamSubscription<Trip>? _tripSubscription;
Trip? get currentTrip => _currentTrip;
bool get isTracking => _isTracking;
TripProvider() {
_initializeTrip();
}
void _initializeTrip() async {
// Check for existing trip
_currentTrip = await TripManager.getCurrentTrip();
_isTracking = _currentTrip?.status == TripStatus.active;
notifyListeners();
// Listen for trip events
_tripSubscription = BookoviaTelematics.tripStream.listen((trip) {
_currentTrip = trip;
_isTracking = trip.status == TripStatus.active;
notifyListeners();
});
}
Future<Trip> startTrip([Map<String, dynamic>? metadata]) async {
final trip = await TripManager.startTrip(metadata: metadata);
_currentTrip = trip;
_isTracking = true;
notifyListeners();
return trip;
}
Future<TripSummary> stopTrip() async {
final summary = await TripManager.stopTrip();
_currentTrip = null;
_isTracking = false;
notifyListeners();
return summary;
}
@override
void dispose() {
_tripSubscription?.cancel();
super.dispose();
}
}
Background Processing
iOS Background Configuration
import BackgroundTasks
class BackgroundTaskManager {
static let shared = BackgroundTaskManager()
private let backgroundTaskIdentifier = "com.yourapp.location-sync"
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: backgroundTaskIdentifier,
using: nil
) { task in
self.handleLocationSync(task: task as! BGAppRefreshTask)
}
}
func scheduleBackgroundSync() {
let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)
request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 minutes
try? BGTaskScheduler.shared.submit(request)
}
private func handleLocationSync(task: BGAppRefreshTask) {
scheduleBackgroundSync() // Schedule next sync
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
// Sync cached location data
BookoviaSDK.shared.syncCachedData { success in
task.setTaskCompleted(success: success)
}
}
}
Android Foreground Service
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.bookovia.telematics.BookoviaSDK
class LocationTrackingService : Service() {
companion object {
private const val NOTIFICATION_ID = 1
private const val CHANNEL_ID = "location_tracking"
}
private lateinit var bookovia: BookoviaSDK
override fun onCreate() {
super.onCreate()
bookovia = BookoviaSDK.getInstance(this)
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
// Start location tracking
bookovia.startLocationTracking()
return START_STICKY // Restart if killed
}
override fun onDestroy() {
super.onDestroy()
bookovia.stopLocationTracking()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"Location Tracking",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Tracking your location for trip analysis"
setSound(null, null)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Trip in Progress")
.setContentText("Tracking your location")
.setSmallIcon(R.drawable.ic_location)
.setOngoing(true)
.setSilent(true)
.build()
}
}
Offline Support
Data Caching and Sync
- iOS
- Android
- React Native
- Flutter
import CoreData
class OfflineDataManager {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "BookoviaCache")
container.loadPersistentStores { _, error in
if let error = error {
fatalError("Core Data error: \(error)")
}
}
return container
}()
func cacheLocationData(_ locations: [LocationPoint]) {
let context = persistentContainer.viewContext
for location in locations {
let cachedLocation = CachedLocation(context: context)
cachedLocation.latitude = location.latitude
cachedLocation.longitude = location.longitude
cachedLocation.timestamp = location.timestamp
cachedLocation.tripId = location.tripId
cachedLocation.synced = false
}
try? context.save()
}
func syncCachedData() async {
let context = persistentContainer.newBackgroundContext()
await context.perform {
let request: NSFetchRequest<CachedLocation> = CachedLocation.fetchRequest()
request.predicate = NSPredicate(format: "synced == NO")
request.fetchLimit = 100 // Sync in batches
guard let unsyncedLocations = try? context.fetch(request) else { return }
let locationData = unsyncedLocations.map { cached in
LocationPoint(
latitude: cached.latitude,
longitude: cached.longitude,
timestamp: cached.timestamp!,
tripId: cached.tripId!
)
}
// Upload to Bookovia API
Task {
do {
try await BookoviaSDK.shared.uploadLocationBatch(locationData)
// Mark as synced
await context.perform {
unsyncedLocations.forEach { $0.synced = true }
try? context.save()
}
} catch {
print("Sync failed: \(error)")
}
}
}
}
}
import androidx.room.*
import androidx.work.*
import java.util.concurrent.TimeUnit
@Entity(tableName = "cached_locations")
data class CachedLocation(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val latitude: Double,
val longitude: Double,
val timestamp: Long,
val tripId: String,
val synced: Boolean = false
)
@Dao
interface LocationDao {
@Insert
suspend fun insertLocations(locations: List<CachedLocation>)
@Query("SELECT * FROM cached_locations WHERE synced = 0 LIMIT 100")
suspend fun getUnsyncedLocations(): List<CachedLocation>
@Query("UPDATE cached_locations SET synced = 1 WHERE id IN (:ids)")
suspend fun markAsSynced(ids: List<Long>)
@Query("DELETE FROM cached_locations WHERE synced = 1 AND timestamp < :cutoff")
suspend fun cleanupOldData(cutoff: Long)
}
class OfflineDataManager(private val context: Context) {
private val bookovia = BookoviaSDK.getInstance(context)
private val database = BookoviaDatabase.getInstance(context)
suspend fun cacheLocationData(locations: List<LocationPoint>) {
val cachedLocations = locations.map { location ->
CachedLocation(
latitude = location.latitude,
longitude = location.longitude,
timestamp = location.timestamp,
tripId = location.tripId
)
}
database.locationDao().insertLocations(cachedLocations)
}
fun scheduleSyncWork() {
val syncWorkRequest = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(
"location_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncWorkRequest
)
}
}
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val database = BookoviaDatabase.getInstance(applicationContext)
val unsyncedLocations = database.locationDao().getUnsyncedLocations()
if (unsyncedLocations.isNotEmpty()) {
val locationData = unsyncedLocations.map { cached ->
LocationPoint(
latitude = cached.latitude,
longitude = cached.longitude,
timestamp = cached.timestamp,
tripId = cached.tripId
)
}
BookoviaSDK.getInstance(applicationContext).uploadLocationBatch(locationData)
// Mark as synced
database.locationDao().markAsSynced(unsyncedLocations.map { it.id })
}
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import { BookoviaSDK } from '@bookovia/react-native-sdk';
class OfflineDataManager {
static CACHE_KEY = 'bookovia_cached_locations';
static MAX_CACHE_SIZE = 1000;
static async cacheLocationData(locations) {
try {
const existingData = await this.getCachedLocations();
const newData = [...existingData, ...locations.map(loc => ({
...loc,
cached_at: Date.now(),
synced: false
}))];
// Keep only latest data if cache is too large
const trimmedData = newData.slice(-this.MAX_CACHE_SIZE);
await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(trimmedData));
} catch (error) {
console.error('Failed to cache location data:', error);
}
}
static async getCachedLocations() {
try {
const data = await AsyncStorage.getItem(this.CACHE_KEY);
return data ? JSON.parse(data) : [];
} catch (error) {
return [];
}
}
static async syncCachedData() {
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
console.log('No network connection, skipping sync');
return;
}
try {
const cachedData = await this.getCachedLocations();
const unsyncedData = cachedData.filter(item => !item.synced);
if (unsyncedData.length === 0) {
return;
}
// Sync in batches of 50
const batchSize = 50;
for (let i = 0; i < unsyncedData.length; i += batchSize) {
const batch = unsyncedData.slice(i, i + batchSize);
try {
await BookoviaSDK.uploadLocationBatch(batch);
// Mark batch as synced
const updatedData = cachedData.map(item => {
if (batch.find(b => b.timestamp === item.timestamp)) {
return { ...item, synced: true };
}
return item;
});
await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(updatedData));
} catch (batchError) {
console.error('Batch sync failed:', batchError);
break; // Stop syncing on error
}
}
} catch (error) {
console.error('Sync failed:', error);
}
}
static startPeriodicSync() {
// Sync every 5 minutes when app is active
return setInterval(() => {
this.syncCachedData();
}, 5 * 60 * 1000);
}
static async cleanupOldData() {
try {
const cachedData = await this.getCachedLocations();
const cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000); // 7 days
const cleanData = cachedData.filter(item =>
!item.synced || item.cached_at > cutoff
);
await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(cleanData));
} catch (error) {
console.error('Cleanup failed:', error);
}
}
}
// React Hook for offline support
export const useOfflineSync = () => {
const [isOnline, setIsOnline] = useState(true);
const [pendingSync, setPendingSync] = useState(0);
useEffect(() => {
const unsubscribe = NetInfo.addEventListener(state => {
setIsOnline(state.isConnected);
if (state.isConnected) {
// Auto-sync when coming back online
OfflineDataManager.syncCachedData();
}
});
// Periodic sync interval
const syncInterval = OfflineDataManager.startPeriodicSync();
return () => {
unsubscribe();
clearInterval(syncInterval);
};
}, []);
const manualSync = async () => {
await OfflineDataManager.syncCachedData();
};
return { isOnline, pendingSync, manualSync };
};
import 'package:sqflite/sqflite.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:bookovia_telematics/bookovia_telematics.dart';
class OfflineDataManager {
static Database? _database;
static const String tableName = 'cached_locations';
static Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
static Future<Database> _initDatabase() async {
final db = await openDatabase(
'bookovia_cache.db',
version: 1,
onCreate: (db, version) {
return db.execute('''
CREATE TABLE $tableName (
id INTEGER PRIMARY KEY AUTOINCREMENT,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
timestamp INTEGER NOT NULL,
trip_id TEXT NOT NULL,
synced INTEGER NOT NULL DEFAULT 0
)
''');
},
);
return db;
}
static Future<void> cacheLocationData(List<LocationPoint> locations) async {
final db = await database;
final batch = db.batch();
for (final location in locations) {
batch.insert(tableName, {
'latitude': location.latitude,
'longitude': location.longitude,
'timestamp': location.timestamp.millisecondsSinceEpoch,
'trip_id': location.tripId,
'synced': 0,
});
}
await batch.commit(noResult: true);
}
static Future<List<Map<String, dynamic>>> getUnsyncedLocations() async {
final db = await database;
return await db.query(
tableName,
where: 'synced = ?',
whereArgs: [0],
limit: 100,
orderBy: 'timestamp ASC',
);
}
static Future<void> markAsSynced(List<int> ids) async {
final db = await database;
final batch = db.batch();
for (final id in ids) {
batch.update(
tableName,
{'synced': 1},
where: 'id = ?',
whereArgs: [id],
);
}
await batch.commit(noResult: true);
}
static Future<void> syncCachedData() async {
final connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
print('No network connection, skipping sync');
return;
}
try {
final unsyncedData = await getUnsyncedLocations();
if (unsyncedData.isEmpty) return;
final locationData = unsyncedData.map((row) => LocationPoint(
latitude: row['latitude'],
longitude: row['longitude'],
timestamp: DateTime.fromMillisecondsSinceEpoch(row['timestamp']),
tripId: row['trip_id'],
)).toList();
await BookoviaTelematics.uploadLocationBatch(locationData);
// Mark as synced
final ids = unsyncedData.map((row) => row['id'] as int).toList();
await markAsSynced(ids);
print('Synced ${locationData.length} cached locations');
} catch (error) {
print('Sync failed: $error');
}
}
static Future<void> startPeriodicSync() async {
Timer.periodic(Duration(minutes: 5), (timer) {
syncCachedData();
});
// Also sync when connectivity changes
Connectivity().onConnectivityChanged.listen((result) {
if (result != ConnectivityResult.none) {
syncCachedData();
}
});
}
static Future<void> cleanupOldData() async {
final db = await database;
final cutoff = DateTime.now()
.subtract(Duration(days: 7))
.millisecondsSinceEpoch;
await db.delete(
tableName,
where: 'synced = 1 AND timestamp < ?',
whereArgs: [cutoff],
);
}
}
Real-time Features
WebSocket Integration
- iOS
- Android
- React Native
- Flutter
import Foundation
import Starscream
class BookoviaWebSocket: WebSocketDelegate {
private var socket: WebSocket?
private let apiKey: String
init(apiKey: String) {
self.apiKey = apiKey
}
func connect() {
guard let url = URL(string: "wss://api.bookovia.com/v1/streaming") else { return }
var request = URLRequest(url: url)
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
socket = WebSocket(request: request)
socket?.delegate = self
socket?.connect()
}
func subscribe(to channels: [String], filters: [String: Any] = [:]) {
let subscription = [
"type": "subscribe",
"channels": channels,
"filters": filters
]
if let data = try? JSONSerialization.data(withJSONObject: subscription),
let string = String(data: data, encoding: .utf8) {
socket?.write(string: string)
}
}
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected:
print("WebSocket connected")
authenticateAndSubscribe()
case .disconnected(let reason, let code):
print("WebSocket disconnected: \(reason) (\(code))")
attemptReconnect()
case .text(let text):
handleMessage(text)
case .error(let error):
print("WebSocket error: \(error)")
default:
break
}
}
private func authenticateAndSubscribe() {
// Subscribe to real-time location updates
subscribe(to: ["locations", "safety_events", "trip_updates"])
}
private func handleMessage(_ text: String) {
guard let data = text.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let type = json["type"] as? String else { return }
switch type {
case "location_update":
handleLocationUpdate(json)
case "safety_event":
handleSafetyEvent(json)
case "trip_update":
handleTripUpdate(json)
default:
break
}
}
}
import okhttp3.*
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class BookoviaWebSocket(private val apiKey: String) : WebSocketListener() {
private var webSocket: WebSocket? = null
private val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
fun connect() {
val request = Request.Builder()
.url("wss://api.bookovia.com/v1/streaming")
.addHeader("Authorization", "Bearer $apiKey")
.build()
webSocket = client.newWebSocket(request, this)
}
fun subscribe(channels: List<String>, filters: Map<String, Any> = emptyMap()) {
val subscription = JSONObject().apply {
put("type", "subscribe")
put("channels", channels)
put("filters", JSONObject(filters))
}
webSocket?.send(subscription.toString())
}
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.d("WebSocket", "Connected")
authenticateAndSubscribe()
}
override fun onMessage(webSocket: WebSocket, text: String) {
try {
val json = JSONObject(text)
val type = json.getString("type")
when (type) {
"location_update" -> handleLocationUpdate(json)
"safety_event" -> handleSafetyEvent(json)
"trip_update" -> handleTripUpdate(json)
}
} catch (e: Exception) {
Log.e("WebSocket", "Message parsing error", e)
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d("WebSocket", "Disconnected: $reason ($code)")
attemptReconnect()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e("WebSocket", "Connection failed", t)
attemptReconnect()
}
private fun authenticateAndSubscribe() {
subscribe(listOf("locations", "safety_events", "trip_updates"))
}
private fun attemptReconnect() {
// Implement exponential backoff reconnection
Handler(Looper.getMainLooper()).postDelayed({
connect()
}, 5000)
}
}
import { BookoviaWebSocket } from '@bookovia/react-native-sdk';
class RealtimeManager {
constructor(apiKey) {
this.apiKey = apiKey;
this.websocket = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
}
connect() {
this.websocket = new BookoviaWebSocket({
apiKey: this.apiKey,
url: 'wss://api.bookovia.com/v1/streaming',
onOpen: () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.authenticateAndSubscribe();
},
onMessage: (data) => {
this.handleMessage(data);
},
onClose: () => {
console.log('WebSocket disconnected');
this.isConnected = false;
this.attemptReconnect();
},
onError: (error) => {
console.error('WebSocket error:', error);
}
});
this.websocket.connect();
}
subscribe(channels, filters = {}) {
if (this.isConnected) {
this.websocket.send({
type: 'subscribe',
channels,
filters
});
}
}
handleMessage(data) {
switch (data.type) {
case 'location_update':
this.handleLocationUpdate(data);
break;
case 'safety_event':
this.handleSafetyEvent(data);
break;
case 'trip_update':
this.handleTripUpdate(data);
break;
}
}
authenticateAndSubscribe() {
this.subscribe(['locations', 'safety_events', 'trip_updates']);
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
setTimeout(() => {
this.connect();
}, delay);
}
}
disconnect() {
if (this.websocket) {
this.websocket.close();
this.websocket = null;
this.isConnected = false;
}
}
}
// React Hook for real-time data
export const useRealtimeData = (apiKey) => {
const [realtimeData, setRealtimeData] = useState({
locations: [],
safetyEvents: [],
tripUpdates: []
});
const [connectionStatus, setConnectionStatus] = useState('disconnected');
useEffect(() => {
const manager = new RealtimeManager(apiKey);
manager.handleLocationUpdate = (data) => {
setRealtimeData(prev => ({
...prev,
locations: [...prev.locations.slice(-99), data] // Keep last 100
}));
};
manager.handleSafetyEvent = (data) => {
setRealtimeData(prev => ({
...prev,
safetyEvents: [...prev.safetyEvents.slice(-49), data] // Keep last 50
}));
};
manager.connect();
return () => manager.disconnect();
}, [apiKey]);
return { realtimeData, connectionStatus };
};
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:bookovia_telematics/bookovia_telematics.dart';
class BookoviaWebSocket {
final String apiKey;
WebSocketChannel? _channel;
StreamController<Map<String, dynamic>>? _messageController;
Timer? _reconnectTimer;
int _reconnectAttempts = 0;
static const int maxReconnectAttempts = 10;
BookoviaWebSocket({required this.apiKey});
Stream<Map<String, dynamic>> get messageStream =>
_messageController?.stream ?? Stream.empty();
Future<void> connect() async {
try {
_messageController ??= StreamController<Map<String, dynamic>>.broadcast();
_channel = WebSocketChannel.connect(
Uri.parse('wss://api.bookovia.com/v1/streaming'),
protocols: null,
);
// Send authentication
_channel!.sink.add(jsonEncode({
'type': 'auth',
'api_key': apiKey,
}));
// Listen for messages
_channel!.stream.listen(
(message) {
try {
final data = jsonDecode(message) as Map<String, dynamic>;
_messageController!.add(data);
_handleMessage(data);
} catch (e) {
print('Message parsing error: $e');
}
},
onDone: () {
print('WebSocket connection closed');
_attemptReconnect();
},
onError: (error) {
print('WebSocket error: $error');
_attemptReconnect();
},
);
_reconnectAttempts = 0;
print('WebSocket connected');
// Subscribe to channels after connection
await _authenticateAndSubscribe();
} catch (e) {
print('WebSocket connection failed: $e');
_attemptReconnect();
}
}
void subscribe(List<String> channels, {Map<String, dynamic>? filters}) {
final subscription = {
'type': 'subscribe',
'channels': channels,
if (filters != null) 'filters': filters,
};
_channel?.sink.add(jsonEncode(subscription));
}
void _handleMessage(Map<String, dynamic> data) {
final type = data['type'] as String?;
switch (type) {
case 'location_update':
_handleLocationUpdate(data);
break;
case 'safety_event':
_handleSafetyEvent(data);
break;
case 'trip_update':
_handleTripUpdate(data);
break;
}
}
Future<void> _authenticateAndSubscribe() async {
subscribe(['locations', 'safety_events', 'trip_updates']);
}
void _attemptReconnect() {
if (_reconnectAttempts < maxReconnectAttempts) {
_reconnectAttempts++;
final delay = Duration(
milliseconds: math.min(1000 * math.pow(2, _reconnectAttempts).toInt(), 30000)
);
_reconnectTimer = Timer(delay, () {
connect();
});
}
}
void disconnect() {
_reconnectTimer?.cancel();
_channel?.sink.close();
_messageController?.close();
_channel = null;
_messageController = null;
}
}
// Provider for real-time data
class RealtimeProvider extends ChangeNotifier {
final BookoviaWebSocket _websocket;
StreamSubscription? _messageSubscription;
List<LocationPoint> _locations = [];
List<SafetyEvent> _safetyEvents = [];
List<TripUpdate> _tripUpdates = [];
List<LocationPoint> get locations => _locations;
List<SafetyEvent> get safetyEvents => _safetyEvents;
List<TripUpdate> get tripUpdates => _tripUpdates;
RealtimeProvider({required String apiKey})
: _websocket = BookoviaWebSocket(apiKey: apiKey) {
_initializeWebSocket();
}
void _initializeWebSocket() {
_websocket.connect();
_messageSubscription = _websocket.messageStream.listen((data) {
_handleRealtimeData(data);
notifyListeners();
});
}
void _handleRealtimeData(Map<String, dynamic> data) {
switch (data['type']) {
case 'location_update':
final location = LocationPoint.fromJson(data);
_locations = [..._locations.take(99), location]; // Keep last 100
break;
case 'safety_event':
final event = SafetyEvent.fromJson(data);
_safetyEvents = [..._safetyEvents.take(49), event]; // Keep last 50
break;
case 'trip_update':
final update = TripUpdate.fromJson(data);
_tripUpdates = [..._tripUpdates.take(29), update]; // Keep last 30
break;
}
}
@override
void dispose() {
_messageSubscription?.cancel();
_websocket.disconnect();
super.dispose();
}
}
UI Implementation
Trip Controls Component
- iOS (SwiftUI)
- Android (Jetpack Compose)
- React Native
- Flutter
import SwiftUI
import BookoviaTelematics
struct TripControlsView: View {
@StateObject private var tripManager = TripManager()
@State private var showingTripSummary = false
var body: some View {
VStack(spacing: 20) {
// Trip Status
tripStatusView
// Control Buttons
controlButtonsView
// Live Stats
if tripManager.isTracking {
liveStatsView
}
}
.padding()
.sheet(isPresented: $showingTripSummary) {
TripSummaryView(summary: tripManager.lastTripSummary)
}
}
private var tripStatusView: some View {
HStack {
Circle()
.fill(tripManager.isTracking ? Color.green : Color.gray)
.frame(width: 12, height: 12)
Text(tripManager.isTracking ? "Trip Active" : "No Active Trip")
.font(.headline)
Spacer()
if tripManager.isTracking {
Text(tripManager.elapsedTime)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
private var controlButtonsView: some View {
HStack(spacing: 16) {
Button(action: {
if tripManager.isTracking {
Task {
await tripManager.stopTrip()
showingTripSummary = true
}
} else {
Task {
await tripManager.startTrip()
}
}
}) {
HStack {
Image(systemName: tripManager.isTracking ? "stop.fill" : "play.fill")
Text(tripManager.isTracking ? "Stop Trip" : "Start Trip")
}
.frame(maxWidth: .infinity)
.padding()
.background(tripManager.isTracking ? Color.red : Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(tripManager.isLoading)
}
}
private var liveStatsView: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
StatCard(title: "Distance", value: "\(tripManager.currentDistance, specifier: "%.1f") km")
StatCard(title: "Speed", value: "\(tripManager.currentSpeed, specifier: "%.0f") km/h")
StatCard(title: "Safety Score", value: "\(tripManager.safetyScore, specifier: "%.0f")")
}
}
}
struct StatCard: View {
let title: String
let value: String
var body: some View {
VStack {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.headline)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(8)
}
}
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun TripControlsScreen(
tripViewModel: TripViewModel = viewModel()
) {
val uiState by tripViewModel.uiState.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
// Trip Status
TripStatusCard(
isTracking = uiState.isTracking,
elapsedTime = uiState.elapsedTime
)
// Control Button
TripControlButton(
isTracking = uiState.isTracking,
isLoading = uiState.isLoading,
onStartTrip = { tripViewModel.startTrip() },
onStopTrip = { tripViewModel.stopTrip() }
)
// Live Stats
if (uiState.isTracking) {
LiveStatsGrid(
distance = uiState.currentDistance,
speed = uiState.currentSpeed,
safetyScore = uiState.safetyScore
)
}
}
// Trip Summary Dialog
uiState.tripSummary?.let { summary ->
TripSummaryDialog(
summary = summary,
onDismiss = { tripViewModel.clearTripSummary() }
)
}
}
@Composable
fun TripStatusCard(
isTracking: Boolean,
elapsedTime: String
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(12.dp)
.background(
color = if (isTracking) Color.Green else Color.Gray,
shape = RoundedCornerShape(50)
)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = if (isTracking) "Trip Active" else "No Active Trip",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.weight(1f))
if (isTracking) {
Text(
text = elapsedTime,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
fun TripControlButton(
isTracking: Boolean,
isLoading: Boolean,
onStartTrip: () -> Unit,
onStopTrip: () -> Unit
) {
Button(
onClick = if (isTracking) onStopTrip else onStartTrip,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = !isLoading,
colors = ButtonDefaults.buttonColors(
containerColor = if (isTracking)
MaterialTheme.colorScheme.error
else
MaterialTheme.colorScheme.primary
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary
)
} else {
Text(if (isTracking) "Stop Trip" else "Start Trip")
}
}
}
@Composable
fun LiveStatsGrid(
distance: Double,
speed: Double,
safetyScore: Double
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
StatCard(
title = "Distance",
value = "${distance.format(1)} km",
modifier = Modifier.weight(1f)
)
StatCard(
title = "Speed",
value = "${speed.format(0)} km/h",
modifier = Modifier.weight(1f)
)
StatCard(
title = "Safety Score",
value = safetyScore.format(0),
modifier = Modifier.weight(1f)
)
}
}
@Composable
fun StatCard(
title: String,
value: String,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = value,
style = MaterialTheme.typography.headlineSmall
)
}
}
}
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
Alert
} from 'react-native';
import { useTrip } from '../hooks/useTrip';
import { useLiveStats } from '../hooks/useLiveStats';
const TripControlsScreen = () => {
const { currentTrip, isTracking, startTrip, stopTrip } = useTrip();
const { distance, speed, safetyScore, elapsedTime } = useLiveStats(currentTrip?.id);
const [isLoading, setIsLoading] = useState(false);
const handleTripControl = async () => {
setIsLoading(true);
try {
if (isTracking) {
const summary = await stopTrip();
showTripSummary(summary);
} else {
await startTrip({
driver_name: 'John Doe',
vehicle_id: 'vehicle_123'
});
}
} catch (error) {
Alert.alert('Error', error.message);
} finally {
setIsLoading(false);
}
};
const showTripSummary = (summary) => {
Alert.alert(
'Trip Complete',
`Distance: ${summary.distance} km\nDuration: ${summary.duration} minutes\nSafety Score: ${summary.safetyScore}`,
[{ text: 'OK' }]
);
};
return (
<View style={styles.container}>
{/* Trip Status */}
<View style={styles.statusCard}>
<View style={styles.statusRow}>
<View style={[
styles.statusIndicator,
{ backgroundColor: isTracking ? '#00C851' : '#757575' }
]} />
<Text style={styles.statusText}>
{isTracking ? 'Trip Active' : 'No Active Trip'}
</Text>
{isTracking && (
<Text style={styles.elapsedTime}>{elapsedTime}</Text>
)}
</View>
</View>
{/* Control Button */}
<TouchableOpacity
style={[
styles.controlButton,
{ backgroundColor: isTracking ? '#ff4444' : '#007AFF' }
]}
onPress={handleTripControl}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="white" />
) : (
<Text style={styles.controlButtonText}>
{isTracking ? 'Stop Trip' : 'Start Trip'}
</Text>
)}
</TouchableOpacity>
{/* Live Stats */}
{isTracking && (
<View style={styles.statsContainer}>
<StatCard title="Distance" value={`${distance?.toFixed(1) || '0.0'} km`} />
<StatCard title="Speed" value={`${Math.round(speed || 0)} km/h`} />
<StatCard title="Safety Score" value={Math.round(safetyScore || 0).toString()} />
</View>
)}
</View>
);
};
const StatCard = ({ title, value }) => (
<View style={styles.statCard}>
<Text style={styles.statTitle}>{title}</Text>
<Text style={styles.statValue}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#f5f5f5',
},
statusCard: {
backgroundColor: 'white',
borderRadius: 8,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.22,
shadowRadius: 2.22,
},
statusRow: {
flexDirection: 'row',
alignItems: 'center',
},
statusIndicator: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 12,
},
statusText: {
fontSize: 18,
fontWeight: '600',
flex: 1,
},
elapsedTime: {
fontSize: 14,
color: '#757575',
},
controlButton: {
height: 56,
borderRadius: 8,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 20,
},
controlButtonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
},
statCard: {
flex: 1,
backgroundColor: 'white',
borderRadius: 8,
padding: 12,
marginHorizontal: 4,
alignItems: 'center',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.22,
shadowRadius: 2.22,
},
statTitle: {
fontSize: 12,
color: '#757575',
marginBottom: 4,
},
statValue: {
fontSize: 18,
fontWeight: '600',
},
});
export default TripControlsScreen;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:bookovia_telematics/bookovia_telematics.dart';
class TripControlsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Trip Controls'),
),
body: Consumer<TripProvider>(
builder: (context, tripProvider, child) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
// Trip Status Card
_TripStatusCard(
isTracking: tripProvider.isTracking,
elapsedTime: tripProvider.elapsedTime,
),
SizedBox(height: 20),
// Control Button
_TripControlButton(
isTracking: tripProvider.isTracking,
isLoading: tripProvider.isLoading,
onPressed: () => _handleTripControl(context, tripProvider),
),
SizedBox(height: 20),
// Live Stats
if (tripProvider.isTracking) ...[
_LiveStatsGrid(
distance: tripProvider.currentDistance,
speed: tripProvider.currentSpeed,
safetyScore: tripProvider.safetyScore,
),
],
],
),
);
},
),
);
}
void _handleTripControl(BuildContext context, TripProvider tripProvider) async {
try {
if (tripProvider.isTracking) {
final summary = await tripProvider.stopTrip();
_showTripSummary(context, summary);
} else {
await tripProvider.startTrip({
'driver_name': 'John Doe',
'vehicle_id': 'vehicle_123',
});
}
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $error')),
);
}
}
void _showTripSummary(BuildContext context, TripSummary summary) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Trip Complete'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Distance: ${summary.distance.toStringAsFixed(1)} km'),
Text('Duration: ${summary.duration} minutes'),
Text('Safety Score: ${summary.safetyScore.round()}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
}
class _TripStatusCard extends StatelessWidget {
final bool isTracking;
final String elapsedTime;
const _TripStatusCard({
required this.isTracking,
required this.elapsedTime,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: isTracking ? Colors.green : Colors.grey,
shape: BoxShape.circle,
),
),
SizedBox(width: 12),
Expanded(
child: Text(
isTracking ? 'Trip Active' : 'No Active Trip',
style: Theme.of(context).textTheme.headlineSmall,
),
),
if (isTracking)
Text(
elapsedTime,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
);
}
}
class _TripControlButton extends StatelessWidget {
final bool isTracking;
final bool isLoading;
final VoidCallback onPressed;
const _TripControlButton({
required this.isTracking,
required this.isLoading,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: isTracking ? Colors.red : Colors.blue,
foregroundColor: Colors.white,
),
child: isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(isTracking ? 'Stop Trip' : 'Start Trip'),
),
);
}
}
class _LiveStatsGrid extends StatelessWidget {
final double distance;
final double speed;
final double safetyScore;
const _LiveStatsGrid({
required this.distance,
required this.speed,
required this.safetyScore,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: _StatCard(
title: 'Distance',
value: '${distance.toStringAsFixed(1)} km',
),
),
SizedBox(width: 12),
Expanded(
child: _StatCard(
title: 'Speed',
value: '${speed.round()} km/h',
),
),
SizedBox(width: 12),
Expanded(
child: _StatCard(
title: 'Safety Score',
value: safetyScore.round().toString(),
),
),
],
);
}
}
class _StatCard extends StatelessWidget {
final String title;
final String value;
const _StatCard({
required this.title,
required this.value,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: EdgeInsets.all(12.0),
child: Column(
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
),
),
SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
),
);
}
}
Testing and Debugging
Development Testing
- iOS
- Android
- React Native
- Flutter
import XCTest
import BookoviaTelematics
class BookoviaSDKTests: XCTestCase {
var sdk: BookoviaSDK!
override func setUpWithError() throws {
sdk = BookoviaSDK.shared
sdk.configure(
apiKey: "bkv_test_your_test_api_key",
environment: .sandbox
)
}
func testTripStartStop() async throws {
// Start trip
let trip = try await sdk.startTrip(metadata: ["test": "true"])
XCTAssertNotNil(trip.id)
XCTAssertEqual(trip.status, .active)
// Wait a moment
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
// Stop trip
let summary = try await sdk.stopTrip()
XCTAssertGreaterThan(summary.duration, 0)
XCTAssertEqual(summary.tripId, trip.id)
}
func testLocationUpload() async throws {
let locations = [
LocationPoint(
latitude: 40.7128,
longitude: -74.0060,
timestamp: Date(),
accuracy: 5.0
)
]
try await sdk.uploadLocationBatch(locations)
// Test passes if no exception is thrown
}
func testOfflineMode() async throws {
// Simulate offline mode
sdk.setOfflineMode(true)
let trip = try await sdk.startTrip()
let locations = [
LocationPoint(
latitude: 40.7128,
longitude: -74.0060,
timestamp: Date()
)
]
// Should cache data instead of uploading
try await sdk.uploadLocationBatch(locations)
// Go back online
sdk.setOfflineMode(false)
// Should sync cached data
try await sdk.syncCachedData()
}
}
// Simulator testing with mock locations
class LocationSimulator {
static func simulateTrip() {
guard let sdk = BookoviaSDK.shared else { return }
let route = [
(40.7128, -74.0060), // Start
(40.7138, -74.0050), // Point 1
(40.7148, -74.0040), // Point 2
(40.7158, -74.0030), // End
]
Task {
let trip = try await sdk.startTrip()
for (index, coordinate) in route.enumerated() {
let location = LocationPoint(
latitude: coordinate.0,
longitude: coordinate.1,
timestamp: Date(),
speed: 30.0 + Double(index) * 5.0
)
try await sdk.uploadLocation(location)
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
}
try await sdk.stopTrip()
}
}
}
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.bookovia.telematics.BookoviaSDK
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BookoviaSDKTest {
private lateinit var sdk: BookoviaSDK
@Before
fun setup() {
val config = BookoviaConfig.Builder()
.apiKey("bkv_test_your_test_api_key")
.environment(BookoviaConfig.Environment.SANDBOX)
.enableDebugLogging(true)
.build()
sdk = BookoviaSDK.getInstance(context, config)
}
@Test
fun testTripLifecycle() = runTest {
// Start trip
val trip = sdk.startTripAsync()
assertNotNull(trip.id)
assertEquals(TripStatus.ACTIVE, trip.status)
// Upload some location data
val locations = listOf(
LocationPoint(
latitude = 40.7128,
longitude = -74.0060,
timestamp = System.currentTimeMillis(),
accuracy = 5f
)
)
sdk.uploadLocationBatchAsync(locations)
// Stop trip
val summary = sdk.stopTripAsync()
assertNotNull(summary.tripId)
assertTrue(summary.duration > 0)
}
@Test
fun testOfflineCaching() = runTest {
// Enable offline mode
sdk.setOfflineMode(true)
val locations = generateTestLocations(10)
sdk.uploadLocationBatchAsync(locations)
// Verify data is cached
val cachedCount = sdk.getCachedLocationCount()
assertEquals(10, cachedCount)
// Go back online and sync
sdk.setOfflineMode(false)
sdk.syncCachedDataAsync()
// Verify cache is cleared after sync
assertEquals(0, sdk.getCachedLocationCount())
}
private fun generateTestLocations(count: Int): List<LocationPoint> {
return (0 until count).map { i ->
LocationPoint(
latitude = 40.7128 + (i * 0.0001),
longitude = -74.0060 + (i * 0.0001),
timestamp = System.currentTimeMillis() + (i * 1000),
speed = 30f + (i * 2f)
)
}
}
}
// Location simulation for testing
class LocationSimulator(private val context: Context) {
fun simulateRoute(route: List<Pair<Double, Double>>) {
val sdk = BookoviaSDK.getInstance(context)
GlobalScope.launch {
val trip = sdk.startTripAsync()
route.forEachIndexed { index, (lat, lng) ->
val location = LocationPoint(
latitude = lat,
longitude = lng,
timestamp = System.currentTimeMillis(),
speed = 25f + (index * 3f),
accuracy = 3f + Random.nextFloat() * 2f
)
sdk.uploadLocationAsync(location)
delay(2000) // Wait 2 seconds between points
}
sdk.stopTripAsync()
}
}
}
import { BookoviaSDK } from '@bookovia/react-native-sdk';
import { describe, it, expect, beforeAll } from '@jest/globals';
describe('BookoviaSDK Integration Tests', () => {
beforeAll(async () => {
await BookoviaSDK.initialize({
apiKey: 'bkv_test_your_test_api_key',
environment: 'sandbox',
enableDebugLogging: true,
});
});
it('should start and stop a trip successfully', async () => {
// Start trip
const trip = await BookoviaSDK.startTrip({
metadata: { test: 'true' }
});
expect(trip.id).toBeDefined();
expect(trip.status).toBe('active');
// Upload test location
await BookoviaSDK.uploadLocation({
latitude: 40.7128,
longitude: -74.0060,
timestamp: new Date().toISOString(),
accuracy: 5,
});
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Stop trip
const summary = await BookoviaSDK.stopTrip();
expect(summary.tripId).toBe(trip.id);
expect(summary.duration).toBeGreaterThan(0);
});
it('should handle offline mode correctly', async () => {
// Enable offline mode
await BookoviaSDK.setOfflineMode(true);
// Upload location data (should be cached)
const locations = generateTestLocations(5);
await BookoviaSDK.uploadLocationBatch(locations);
// Check cached data
const cachedCount = await BookoviaSDK.getCachedLocationCount();
expect(cachedCount).toBe(5);
// Go back online
await BookoviaSDK.setOfflineMode(false);
// Sync should clear cache
await BookoviaSDK.syncCachedData();
const remainingCached = await BookoviaSDK.getCachedLocationCount();
expect(remainingCached).toBe(0);
});
it('should handle safety event detection', (done) => {
const listener = BookoviaSDK.addSafetyEventListener((event) => {
expect(event.type).toBeDefined();
expect(event.severity).toBeDefined();
listener.remove();
done();
});
// Simulate harsh braking event
BookoviaSDK.simulateSafetyEvent({
type: 'harsh_braking',
severity: 'medium',
location: {
latitude: 40.7128,
longitude: -74.0060,
}
});
});
});
// Test utilities
const generateTestLocations = (count) => {
return Array.from({ length: count }, (_, i) => ({
latitude: 40.7128 + (i * 0.0001),
longitude: -74.0060 + (i * 0.0001),
timestamp: new Date(Date.now() + i * 1000).toISOString(),
speed: 30 + (i * 2),
accuracy: 3 + Math.random() * 2,
}));
};
// Location simulation for development
export class LocationSimulator {
static async simulateRoute(route) {
const trip = await BookoviaSDK.startTrip();
for (let i = 0; i < route.length; i++) {
const [lat, lng] = route[i];
await BookoviaSDK.uploadLocation({
latitude: lat,
longitude: lng,
timestamp: new Date().toISOString(),
speed: 25 + (i * 3),
accuracy: 3 + Math.random() * 2,
});
// Wait 2 seconds between points
await new Promise(resolve => setTimeout(resolve, 2000));
}
return await BookoviaSDK.stopTrip();
}
static generateNYCRoute() {
return [
[40.7589, -73.9851], // Times Square
[40.7505, -73.9934], // Herald Square
[40.7411, -74.0040], // Washington Square
[40.7282, -74.0776], // Hudson Yards
[40.7128, -74.0060], // One World Trade
];
}
}
import 'package:flutter_test/flutter_test.dart';
import 'package:bookovia_telematics/bookovia_telematics.dart';
void main() {
group('BookoviaTelematics Integration Tests', () {
setUpAll(() async {
await BookoviaTelematics.initialize(
apiKey: 'bkv_test_your_test_api_key',
environment: BookoviaEnvironment.sandbox,
enableDebugLogging: true,
);
});
testWidgets('should start and stop trip successfully', (tester) async {
// Start trip
final trip = await BookoviaTelematics.startTrip(
metadata: {'test': 'true'},
);
expect(trip.id, isNotNull);
expect(trip.status, equals(TripStatus.active));
// Upload test location
await BookoviaTelematics.uploadLocation(LocationPoint(
latitude: 40.7128,
longitude: -74.0060,
timestamp: DateTime.now(),
accuracy: 5.0,
));
// Wait
await tester.pump(Duration(seconds: 2));
// Stop trip
final summary = await BookoviaTelematics.stopTrip();
expect(summary.tripId, equals(trip.id));
expect(summary.duration, greaterThan(Duration.zero));
});
test('should handle offline caching', () async {
// Enable offline mode
await BookoviaTelematics.setOfflineMode(true);
// Generate test locations
final locations = List.generate(5, (i) => LocationPoint(
latitude: 40.7128 + (i * 0.0001),
longitude: -74.0060 + (i * 0.0001),
timestamp: DateTime.now().add(Duration(seconds: i)),
speed: 30.0 + (i * 2),
accuracy: 3.0 + (i * 0.5),
));
// Upload (should cache)
await BookoviaTelematics.uploadLocationBatch(locations);
// Check cached count
final cachedCount = await BookoviaTelematics.getCachedLocationCount();
expect(cachedCount, equals(5));
// Go online and sync
await BookoviaTelematics.setOfflineMode(false);
await BookoviaTelematics.syncCachedData();
// Verify cache cleared
final remainingCached = await BookoviaTelematics.getCachedLocationCount();
expect(remainingCached, equals(0));
});
test('should detect safety events', () async {
final completer = Completer<SafetyEvent>();
// Listen for safety events
final subscription = BookoviaTelematics.safetyEventStream.listen((event) {
completer.complete(event);
});
// Simulate harsh braking
await BookoviaTelematics.simulateSafetyEvent(
SafetyEvent(
type: SafetyEventType.harshBraking,
severity: SafetySeverity.medium,
location: LocationPoint(
latitude: 40.7128,
longitude: -74.0060,
timestamp: DateTime.now(),
),
),
);
final event = await completer.future;
expect(event.type, equals(SafetyEventType.harshBraking));
expect(event.severity, equals(SafetySeverity.medium));
await subscription.cancel();
});
});
}
// Location simulation for development testing
class LocationSimulator {
static Future<TripSummary> simulateRoute(List<List<double>> route) async {
final trip = await BookoviaTelematics.startTrip();
for (int i = 0; i < route.length; i++) {
final coordinate = route[i];
final location = LocationPoint(
latitude: coordinate[0],
longitude: coordinate[1],
timestamp: DateTime.now(),
speed: 25.0 + (i * 3),
accuracy: 3.0 + (Random().nextDouble() * 2),
);
await BookoviaTelematics.uploadLocation(location);
// Wait 2 seconds between points
await Future.delayed(Duration(seconds: 2));
}
return await BookoviaTelematics.stopTrip();
}
static List<List<double>> generateTestRoute() {
return [
[40.7589, -73.9851], // Times Square
[40.7505, -73.9934], // Herald Square
[40.7411, -74.0040], // Washington Square
[40.7282, -74.0776], // Hudson Yards
[40.7128, -74.0060], // One World Trade
];
}
static Stream<LocationPoint> generateLocationStream({
required List<List<double>> route,
Duration interval = const Duration(seconds: 1),
}) async* {
for (int i = 0; i < route.length; i++) {
final coordinate = route[i];
yield LocationPoint(
latitude: coordinate[0],
longitude: coordinate[1],
timestamp: DateTime.now(),
speed: 20.0 + (i * 2.5),
heading: _calculateHeading(i > 0 ? route[i-1] : coordinate, coordinate),
accuracy: 2.0 + (Random().nextDouble() * 3),
);
await Future.delayed(interval);
}
}
static double _calculateHeading(List<double> from, List<double> to) {
// Simple bearing calculation
final lat1 = from[0] * (math.pi / 180);
final lat2 = to[0] * (math.pi / 180);
final dLng = (to[1] - from[1]) * (math.pi / 180);
final y = math.sin(dLng) * math.cos(lat2);
final x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dLng);
return (math.atan2(y, x) * 180 / math.pi + 360) % 360;
}
}
Best Practices
Performance Optimization
- Battery Efficiency - Use adaptive location frequency based on movement
- Network Usage - Batch uploads and compress data when possible
- Memory Management - Clean up listeners and subscriptions properly
- Background Processing - Handle app lifecycle changes gracefully
Security Considerations
- API Key Security - Never hardcode API keys in source code
- Data Encryption - Use secure storage for cached location data
- User Privacy - Request minimal necessary permissions
- Data Anonymization - Consider privacy settings for sensitive data
Error Handling
- Network Failures - Implement robust offline caching
- Permission Denials - Provide clear rationale and fallbacks
- SDK Errors - Log errors for debugging but handle gracefully
- Data Validation - Validate location data before upload
Testing Strategy
- Unit Tests - Test SDK integration and data flow
- Integration Tests - Test end-to-end trip scenarios
- Device Testing - Test on multiple devices and OS versions
- Performance Testing - Monitor battery and memory usage
Next Steps
Web Dashboard
Build real-time fleet monitoring dashboards with React and Vue.js
SDK Documentation
Explore detailed SDK documentation for your platform
API Reference
Browse comprehensive API documentation and endpoints
Fleet Management
Learn advanced fleet management and optimization techniques
Ready to integrate Bookovia in your mobile app? Choose your platform SDK and follow the quickstart guide to get up and running in minutes.