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

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# README.md
## Introduction
## Simulation
adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 60
versionName = "0.2.0.60"
versionCode = 61
versionName = "0.2.0.61"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -1,6 +1,7 @@
package com.kouros.navigation.model
import android.content.Context
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.utils.location
@@ -9,18 +10,20 @@ import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import kotlin.collections.forEach
var simulationJob: Job? = null
fun simulate(routeModel: RouteModel, mock: MockLocation) {
CoroutineScope(Dispatchers.IO).launch {
simulationJob?.cancel()
simulationJob = CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) {
val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) {
val bearing = lastLocation.bearingTo(curLocation)
mock.setMockLocation(waypoint[1], waypoint[0], bearing)

View File

@@ -13,36 +13,30 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
@@ -52,7 +46,6 @@ import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
@@ -63,6 +56,7 @@ import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SimulationType
import com.kouros.navigation.model.gpx
import com.kouros.navigation.model.simulate
import com.kouros.navigation.model.simulationJob
import com.kouros.navigation.model.test
import com.kouros.navigation.model.testSingle
import com.kouros.navigation.ui.app.AppViewModel
@@ -92,6 +86,7 @@ class MainActivity : ComponentActivity() {
val routeModel = RouteModel()
var tilt = TILT
val useMock = false
val type = SimulationType.SIMULATE
val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData()
@@ -108,11 +103,8 @@ class MainActivity : ComponentActivity() {
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(newRoute)
if (routeModel.hasLegs()) {
getSettingsViewModel(applicationContext).onLastRouteChanged(newRoute)
}
routeData.value = routeModel.curRoute.routeGeoJson
checkMock()
// checkMock()
}
}
@@ -143,6 +135,13 @@ class MainActivity : ComponentActivity() {
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
override fun onDestroy() {
if (simulationJob != null) {
simulationJob?.cancel()
}
super.onDestroy()
}
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -371,6 +370,7 @@ class MainActivity : ComponentActivity() {
routeModel.stopNavigation()
getSettingsViewModel(applicationContext).onLastRouteChanged("")
if (useMock) {
simulationJob?.cancel()
mock.setMockLocation(latitude, longitude, 0F)
}
routeData.value = ""

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.
@@ -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)
@@ -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,29 +622,36 @@ 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)
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
) {
stopNavigation()
getSettingsViewModel(carContext).onLastRouteChanged("")
listener.stopNavigation()
settingsViewModel.onLastRouteChanged("")
navState = navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
}
invalidate()
}
/**
* Updates the trip information and notifies the listener with a new Trip object.

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 {

View File

@@ -1,9 +1,9 @@
[versions]
agp = "9.0.1"
agp = "9.1.0"
androidGpxParser = "2.3.1"
androidSdkTurf = "6.0.1"
datastore = "1.2.0"
gradle = "9.0.1"
gradle = "9.1.0"
koinAndroid = "4.1.1"
koinAndroidxCompose = "4.1.1"
koinComposeViewmodel = "4.1.1"
@@ -21,7 +21,7 @@ material = "1.13.0"
carApp = "1.7.0"
androidx-car = "1.7.0"
materialIconsExtended = "1.7.8"
mockitoCore = "5.21.0"
mockitoCore = "5.22.0"
mockitoKotlin = "6.2.3"
rules = "1.7.0"
runner = "1.7.0"