From 8c103a1f968260f743b56c8c4c0aab0fc5e1ee57 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Thu, 5 Mar 2026 13:42:51 +0100 Subject: [PATCH] Auto Drive Simulation --- README.md | 7 +++ app/build.gradle.kts | 4 +- .../com/kouros/navigation/model/Simulation.kt | 7 ++- .../com/kouros/navigation/ui/MainActivity.kt | 22 ++++---- .../navigation/car/NavigationSession.kt | 41 ++++++++++----- .../car/navigation/RouteCarModel.kt | 2 +- .../navigation/car/navigation/Simulation.kt | 50 +++++++++++++++++++ .../navigation/car/screen/NavigationScreen.kt | 47 ++++++++++------- common/data/build.gradle.kts | 1 + .../java/com/kouros/navigation/data/Data.kt | 3 +- .../data/tomtom/TomTomRepository.kt | 5 +- gradle/libs.versions.toml | 6 +-- 12 files changed, 145 insertions(+), 50 deletions(-) create mode 100644 README.md create mode 100644 common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt diff --git a/README.md b/README.md new file mode 100644 index 0000000..02cace7 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# README.md + +## Introduction + +## Simulation + +adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d3c625c..4b2020f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/main/java/com/kouros/navigation/model/Simulation.kt b/app/src/main/java/com/kouros/navigation/model/Simulation.kt index 54fea0d..46b0f7a 100644 --- a/app/src/main/java/com/kouros/navigation/model/Simulation.kt +++ b/app/src/main/java/com/kouros/navigation/model/Simulation.kt @@ -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) diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index 7a80189..f677b9a 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -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 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 = "" diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index f47f95e..8f52809 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -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() diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index 4c7115c..4ae6c26 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -172,7 +172,7 @@ class RouteCarModel : RouteModel() { } fun showSpeedCamera(carContext: CarContext, distance: Double, maxSpeed: String) { - carContext.getCarService(AppManager::class.java) + carContext.getCarService(AppManager::class.java) .showAlert( createAlert( carContext, diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt new file mode 100644 index 0000000..c0c317f --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -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() + } +} \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index ff5522e..3086926 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -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. diff --git a/common/data/build.gradle.kts b/common/data/build.gradle.kts index c377cf9..019543e 100644 --- a/common/data/build.gradle.kts +++ b/common/data/build.gradle.kts @@ -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 { diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 067a1a0..5da03c7 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -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" diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 600459a..5ca9bf8 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -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 { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4de3d16..cf0457a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"