Auto Drive Simulation

This commit is contained in:
Dimitris
2026-03-05 13:42:51 +01:00
parent e582c1e0dc
commit 8c103a1f96
12 changed files with 145 additions and 50 deletions

View File

@@ -4,7 +4,6 @@ import android.Manifest.permission
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.speech.tts.TextToSpeech
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.Screen
@@ -21,9 +20,10 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.asLiveData
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
@@ -42,8 +42,6 @@ import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import org.maplibre.compose.expressions.dsl.step
import java.util.Locale
/**
@@ -76,6 +74,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var textToSpeechManager: TextToSpeechManager
var autoDriveEnabled = false
val simulation = Simulation()
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
@@ -91,9 +92,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
Log.i(TAG, "NavigationSession destroyed")
}
}
@@ -200,19 +201,19 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
navigationManager = carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
// Called when the app should simulate navigation (e.g., for testing)
// Implement your simulation logic here
Log.d("CarApp", "Auto Drive Enabled")
autoDriveEnabled = true
}
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
Log.d("CarApp", "Stop Navigation Requested")
// Stop turn-by-turn logic and clean up
routeModel.stopNavigation()
autoDriveEnabled = false
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
@@ -341,6 +342,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Handles route snapping, deviation detection for rerouting, and map updates.
*/
fun updateLocation(location: Location) {
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION ) {
surfaceRenderer.updateCarSpeed(location.speed)
}
updateBearing(location)
if (routeModel.isNavigating()) {
handleNavigationLocation(location)
@@ -364,7 +368,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
*/
private fun handleNavigationLocation(location: Location) {
if (guidanceAudio == 1) {
handleGuidanceAudio()
handleGuidanceAudio()
}
navigationScreen.updateTrip(location)
if (routeModel.navState.arrived) return
@@ -374,9 +378,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
distance > MAXIMAL_ROUTE_DEVIATION -> {
navigationScreen.calculateNewRoute(routeModel.navState.destination)
}
distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation)
}
else -> {
surfaceRenderer.updateLocation(location)
}
@@ -390,6 +396,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
override fun stopNavigation() {
routeModel.stopNavigation()
navigationManager.navigationEnded()
if (autoDriveEnabled) {
simulation.stopSimulation()
autoDriveEnabled = false
}
}
/**
@@ -398,6 +408,13 @@ class NavigationSession : Session(), NavigationScreen.Listener {
*/
override fun startNavigation() {
navigationManager.navigationStarted()
if (autoDriveEnabled) {
simulation.startSimulation(
routeModel, lifecycle.coroutineScope
) { location ->
updateLocation(location)
}
}
}
override fun updateTrip(trip: Trip) {
@@ -407,7 +424,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
/**
* Handle guidance audio
* Called when user wants to hear the step by step instructions
* Called when user wants to hear the step-by-step instructions
*/
private fun handleGuidanceAudio() {
val currentStep = routeModel.route.currentStep()

View File

@@ -172,7 +172,7 @@ class RouteCarModel : RouteModel() {
}
fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) {
carContext.getCarService<AppManager?>(AppManager::class.java)
carContext.getCarService(AppManager::class.java)
.showAlert(
createAlert(
carContext,

View File

@@ -0,0 +1,50 @@
package com.kouros.navigation.car.navigation
import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class Simulation {
private var simulationJob: Job? = null
fun startSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 2 seconds)
delay(500)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
}
fun stopSimulation() {
simulationJob?.cancel()
}
}

View File

@@ -36,9 +36,12 @@ import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
@@ -89,13 +92,16 @@ class NavigationScreen(
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
var distanceMode = 0
init {
observerManager.attachAllObservers(this)
lifecycleScope.launch {
getSettingsViewModel(carContext).routingEngine.first()
getSettingsViewModel(carContext).recentPlaces.first()
settingsViewModel.routingEngine.first()
settingsViewModel.recentPlaces.first()
distanceMode = repository.distanceModeFlow.first()
}
}
@@ -111,7 +117,7 @@ class NavigationScreen(
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
getSettingsViewModel(carContext).onLastRouteChanged(route)
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData()
listener.startNavigation()
@@ -616,30 +622,37 @@ class NavigationScreen(
fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC)
val duration = Duration.between(current, lastTrafficDate)
if (duration.abs().seconds > 360) {
if (duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
updateSpeedCamera(location)
with(routeModel) {
updateLocation( location, navigationViewModel)
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
getSettingsViewModel(carContext).onLastRouteChanged("")
navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
checkArrival()
}
invalidate()
}
/**
* Checks for arrival
*/
private fun RouteCarModel.checkArrival() {
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
listener.stopNavigation()
settingsViewModel.onLastRouteChanged("")
navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
}
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.

View File

@@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.2.21"
alias(libs.plugins.kotlin.kapt)
//id("com.google.protobuf") version "0.9.6"
}
android {

View File

@@ -125,12 +125,13 @@ object Constants {
const val MAXIMAL_ROUTE_DEVIATION = 80.0
const val DESTINATION_ARRIVAL_DISTANCE = 40.0
const val DESTINATION_ARRIVAL_DISTANCE = 20.0
const val NEAREST_LOCATION_DISTANCE = 10F
const val MAXIMUM_LOCATION_DISTANCE = 100000F
const val TRAFFIC_UPDATE = 300
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"

View File

@@ -21,6 +21,9 @@ private const val tomtomFields =
const val useAsset = false
const val useAssetTraffic = false
class TomTomRepository : NavigationRepository() {
override fun getRoute(
context: Context,
@@ -63,7 +66,7 @@ class TomTomRepository : NavigationRepository() {
val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAsset) {
return if (useAssetTraffic) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)
trafficJson.bufferedReader().use { it.readText() }
} else {