From 5a6165dff83d543d7018fc1576034278c3212abf Mon Sep 17 00:00:00 2001 From: Dimitris Date: Wed, 25 Feb 2026 09:48:39 +0100 Subject: [PATCH] SheetContent Simulation --- app/build.gradle.kts | 1 + .../kouros/navigation/model/MockLocation.kt | 3 +- .../com/kouros/navigation/model/Simulation.kt | 132 +++++++++++++++++ .../com/kouros/navigation/ui/MainActivity.kt | 133 +++--------------- .../kouros/navigation/ui/NavigationSheet.kt | 4 +- .../com/kouros/navigation/ui/SearchSheet.kt | 15 +- .../navigation/car/NavigationCarAppService.kt | 16 --- .../navigation/car/NavigationSession.kt | 94 ++++++++++++- .../kouros/navigation/car/SurfaceRenderer.kt | 118 +++++++++++++++- .../navigation/car/screen/SearchScreen.kt | 1 - .../navigation/data/osrm/OsrmRepository.kt | 2 +- .../data/valhalla/ValhallaRepository.kt | 2 +- .../navigation/model/NavigationViewModel.kt | 10 +- 13 files changed, 384 insertions(+), 147 deletions(-) create mode 100644 app/src/main/java/com/kouros/navigation/model/Simulation.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 047b3c5..d016153 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,7 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.foundation) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt index 1a8e55a..d50a136 100644 --- a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt +++ b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt @@ -98,4 +98,5 @@ class MockLocation (private var locationManager: LocationManager) { } } -} \ No newline at end of file +} + diff --git a/app/src/main/java/com/kouros/navigation/model/Simulation.kt b/app/src/main/java/com/kouros/navigation/model/Simulation.kt new file mode 100644 index 0000000..a674c09 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/model/Simulation.kt @@ -0,0 +1,132 @@ +package com.kouros.navigation.model + +import android.content.Context +import com.kouros.data.R +import com.kouros.navigation.MainApplication.Companion.navigationViewModel +import com.kouros.navigation.utils.location +import io.ticofab.androidgpxparser.parser.GPXParser +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.delay +import kotlinx.coroutines.launch +import org.joda.time.DateTime +import kotlin.collections.forEach + +fun simulate(routeModel: RouteModel, mock: MockLocation) { + 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) + Thread.sleep(1000) + } + } + lastLocation = curLocation + } + } +} + +fun test(applicationContext: Context, routeModel: RouteModel) { + for ((index, step) in routeModel.curLeg.steps.withIndex()) { + for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { + routeModel.updateLocation( + applicationContext, + location(waypoint[0], waypoint[1]), navigationViewModel + ) + val step = routeModel.currentStep() + val nextStep = routeModel.nextStep() + println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}") + } + } +} + +fun testSingle(applicationContext: Context, routeModel: RouteModel, mock: MockLocation) { + testSingleUpdate( + applicationContext, + 48.185976, + 11.578463, + routeModel, + mock + ) // Silcherstr. 23-13 + testSingleUpdate( + applicationContext, + 48.186712, + 11.578574, + routeModel, + mock + ) // Silcherstr. 27-33 + testSingleUpdate( + applicationContext, + 48.186899, + 11.580480, + routeModel, + mock + ) // Schmalkadenerstr. 24-28 +} + +fun testSingleUpdate( + applicationContext: Context, + latitude: Double, + longitude: Double, + routeModel: RouteModel, + mock: MockLocation +) { + if (1 == 1) { + mock.setMockLocation(latitude, longitude, 0F) + } else { + routeModel.updateLocation( + applicationContext, + location(longitude, latitude), navigationViewModel + ) + } + val step = routeModel.currentStep() + val nextStep = routeModel.nextStep() + Thread.sleep(1_000) +} + +fun gpx(context: Context, mock: MockLocation) { + CoroutineScope(Dispatchers.IO).launch { + var lastLocation = location(0.0, 0.0) + val parser = GPXParser() + val input = context.resources.openRawResource(R.raw.vh) + val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread + parsedGpx?.let { + val tracks = parsedGpx.tracks + tracks.forEach { tr -> + val segments: MutableList? = tr.trackSegments + segments!!.forEach { seg -> + var lastTime = DateTime.now() + seg!!.trackPoints.forEach { p -> + val curLocation = location(p.longitude, p.latitude) + val ext = p.extensions + val speed: Double? + if (ext != null) { + speed = ext.speed + mock.curSpeed = speed.toFloat() + } + + val duration = p.time.millis - lastTime.millis + val bearing = lastLocation.bearingTo(curLocation) + println("Bearing $bearing") + mock.setMockLocation(p.latitude, p.longitude, bearing) + if (duration > 0) { + delay(duration / 5) + } + lastTime = p.time + lastLocation = curLocation + } + } + } + } + } +} + +enum class SimulationType { + SIMULATE, TEST, GPX, TEST_SINGLE +} 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 bb2e34c..75fa00c 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -1,10 +1,8 @@ package com.kouros.navigation.ui -import NavigationSheet import android.Manifest import android.annotation.SuppressLint import android.app.AppOpsManager -import android.content.Context import android.location.LocationManager import android.os.Bundle import android.os.Process @@ -18,6 +16,7 @@ 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 @@ -56,6 +55,11 @@ import com.kouros.navigation.data.StepData import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.MockLocation 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.test +import com.kouros.navigation.model.testSingle import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.navigation.AppNavGraph @@ -64,14 +68,7 @@ import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location -import io.ticofab.androidgpxparser.parser.GPXParser -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.delay import kotlinx.coroutines.launch -import org.joda.time.DateTime import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.Location @@ -86,7 +83,7 @@ class MainActivity : ComponentActivity() { val routeModel = RouteModel() var tilt = 50.0 val useMock = false - val type = 3 // 1 simulate 2 test 3 gpx 4 testSingle + val type = SimulationType.GPX val stepData: MutableLiveData by lazy { MutableLiveData() @@ -94,6 +91,7 @@ class MainActivity : ComponentActivity() { val nextStepData: MutableLiveData by lazy { MutableLiveData() } + var lastLocation = location(0.0, 0.0) val observer = Observer { newRoute -> if (newRoute.isNotEmpty()) { @@ -101,13 +99,12 @@ class MainActivity : ComponentActivity() { routeData.value = routeModel.curRoute.routeGeoJson if (useMock) { when (type) { - 1 -> simulate() - 2 -> test() - 3 -> gpx( - context = applicationContext + SimulationType.SIMULATE -> simulate(routeModel, mock) + SimulationType.TEST -> test(applicationContext, routeModel) + SimulationType.GPX -> gpx( + context = applicationContext, mock ) - - 4 -> testSingle() + SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock) } } } @@ -187,11 +184,12 @@ class MainActivity : ComponentActivity() { val appViewModel: AppViewModel = appViewModel() val darkMode by appViewModel.darkMode.collectAsState() - val sheetPeekHeight = 250.dp + val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1) val scaffoldState = rememberBottomSheetScaffoldState() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val sheetPeekHeight = 250.dp val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) } val locationProvider = rememberDefaultLocationProvider( @@ -209,7 +207,7 @@ class MainActivity : ComponentActivity() { fun closeSheet() { scope.launch { scaffoldState.bottomSheetState.partialExpand() - sheetPeekHeightState.value = sheetPeekHeight + sheetPeekHeightState.value = 50.dp } } NavigationTheme(useDarkTheme = darkMode == 1) { @@ -217,7 +215,7 @@ class MainActivity : ComponentActivity() { snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - scaffoldState = scaffoldState, + scaffoldState = scaffoldState, sheetPeekHeight = sheetPeekHeightState.value, sheetContent = { SheetContent(latitude, step, nextStep) { closeSheet() } @@ -363,7 +361,10 @@ class MainActivity : ComponentActivity() { } fun simulateNavigation() { - simulate() + simulate( + routeModel = routeModel, + mock = mock + ) } private fun checkMockLocationEnabled() { @@ -385,95 +386,5 @@ class MainActivity : ComponentActivity() { e.printStackTrace() } } +} - fun simulate() { - 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) - Thread.sleep(1000) - } - } - lastLocation = curLocation - } - } - } - - fun test() { - for ((index, step) in routeModel.curLeg.steps.withIndex()) { - //if (index in 3..3) { - for ((windex, waypoint) in step.maneuver.waypoints.withIndex()) { - routeModel.updateLocation( - applicationContext, - location(waypoint[0], waypoint[1]), navigationViewModel - ) - val step = routeModel.currentStep() - val nextStep = routeModel.nextStep() - println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}") - } - //} - } - } - - fun testSingle() { - testSingleUpdate(48.185976, 11.578463) // Silcherstr. 23-13 - testSingleUpdate(48.186712, 11.578574) // Silcherstr. 27-33 - testSingleUpdate(48.186899, 11.580480) // Schmalkadenerstr. 24-28 - } - - fun testSingleUpdate(latitude: Double, longitude: Double) { - if (1 == 1) { - mock.setMockLocation(latitude, longitude, 0F) - } else { - routeModel.updateLocation( - applicationContext, - location(longitude, latitude), navigationViewModel - ) - } - val step = routeModel.currentStep() - val nextStep = routeModel.nextStep() - Thread.sleep(1_000) - } - - fun gpx(context: Context) { - CoroutineScope(Dispatchers.IO).launch { - var lastLocation = location(0.0, 0.0) - val parser = GPXParser() - val input = context.resources.openRawResource(R.raw.vh) - val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread - parsedGpx?.let { - val tracks = parsedGpx.tracks - tracks.forEach { tr -> - val segments: MutableList? = tr.trackSegments - segments!!.forEach { seg -> - var lastTime = DateTime.now() - seg!!.trackPoints.forEach { p -> - val curLocation = location(p.longitude, p.latitude) - val ext = p.extensions - val speed: Double? - if (ext != null) { - speed = ext.speed - mock.curSpeed = speed.toFloat() - } - - val duration = p.time.millis - lastTime.millis - val bearing = lastLocation.bearingTo(curLocation) - println("Bearing $bearing") - mock.setMockLocation(p.latitude, p.longitude, bearing) - if (duration > 0) { - delay(duration / 5) - } - lastTime = p.time - lastLocation = curLocation - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt index 193981a..3fb73bb 100755 --- a/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/NavigationSheet.kt @@ -1,3 +1,5 @@ +package com.kouros.navigation.ui + import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,9 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.drawable.IconCompat import com.kouros.data.R -import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.StepData import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.formatDateTime diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt index 4051558..eeb8f11 100644 --- a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt @@ -61,6 +61,7 @@ fun SearchSheet( .fillMaxWidth() .wrapContentHeight() ) { + // Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) SearchBar( textFieldState = textFieldState, searchPlaces = emptyList(), @@ -71,7 +72,7 @@ fun SearchSheet( closeSheet = { closeSheet() } ) - //Home(applicationContext, viewModel, location, closeSheet = { closeSheet() }) + if (recentPlaces.value != null) { val items = listOf(recentPlaces) if (items.isNotEmpty()) { @@ -142,7 +143,7 @@ fun SearchBar( SearchBarDefaults.InputField( leadingIcon = { Icon( - painter = painterResource(id = R.drawable.search_48px), + painter = painterResource(id = R.drawable.speed_camera_24px), "Search", modifier = Modifier.size(24.dp, 24.dp), ) @@ -166,8 +167,8 @@ fun SearchBar( RecentPlaces(searchPlaces, viewModel, context, location, closeSheet) } if (searchResults.isNotEmpty()) { - Text("Search places") - SearchPlaces(searchResults, viewModel, context, location, closeSheet) + Text("Search places") + SearchPlaces(searchResults, viewModel, context, location, closeSheet) } } } @@ -186,7 +187,7 @@ private fun SearchPlaces( ) { val color = remember { PlaceColor } LazyColumn( - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { if (searchResults.isNotEmpty()) { @@ -199,7 +200,7 @@ private fun SearchPlaces( modifier = Modifier.size(24.dp, 24.dp), ) ListItem( - headlineContent = { Text("${place.address.road} ${place.address.postcode}") }, + headlineContent = { Text(place.displayName) }, modifier = Modifier .clickable { val pl = Place( @@ -235,7 +236,7 @@ private fun RecentPlaces( ) { val color = remember { PlaceColor } LazyColumn( - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { items(recentPlaces, key = { it.id }) { place -> diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt index ae9ce95..e66897d 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.kouros.navigation.car import android.annotation.SuppressLint 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 280810c..2ac4cd5 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 @@ -52,16 +52,30 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import android.Manifest.permission +/** + * Main session for Android Auto/Automotive OS navigation. + * Manages the lifecycle of the navigation session, including location updates, + * car hardware sensors, routing engine selection, and screen navigation. + * Implements NavigationScreen.Listener for handling navigation events. + */ class NavigationSession : Session(), NavigationScreen.Listener { + // Flag to enable/disable contact access feature val useContacts = false + // Model for managing route state and navigation logic for Android Auto lateinit var routeModel: RouteCarModel; + // Main navigation screen displayed to the user lateinit var navigationScreen: NavigationScreen + // Handles map surface rendering on the car display lateinit var surfaceRenderer: SurfaceRenderer + /** + * Location listener that receives GPS updates from the device. + * Only processes location if car location hardware is not being used. + */ var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? -> val repository = getSettingsRepository(carContext) val useCarLocation = runBlocking { repository.carLocationFlow.first() } @@ -70,6 +84,10 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Lifecycle observer for managing session lifecycle events. + * Cleans up resources when the session is destroyed. + */ private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { } @@ -92,9 +110,16 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + // ViewModel for navigation data and business logic lateinit var navigationViewModel: NavigationViewModel + // Store for ViewModels to survive configuration changes lateinit var viewModelStoreOwner : ViewModelStoreOwner + + /** + * Listener for car hardware location updates. + * Receives location data from the car's GPS system. + */ val carLocationListener: OnCarDataAvailableListener = OnCarDataAvailableListener { data -> if (data.location.status == CarValue.STATUS_SUCCESS) { @@ -105,6 +130,10 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Listener for car compass/orientation sensor. + * Updates the surface renderer with car orientation for map rotation. + */ val carCompassListener: OnCarDataAvailableListener = OnCarDataAvailableListener { data -> if (data.orientations.status == CarValue.STATUS_SUCCESS) { @@ -115,17 +144,26 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Listener for car speed sensor updates. + * Receives speed in meters per second from car hardware. + */ val carSpeedListener = OnCarDataAvailableListener { data -> if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) { val speed = data.displaySpeedMetersPerSecond.value surfaceRenderer.updateCarSpeed(speed!!) } } + init { val lifecycle: Lifecycle = lifecycle lifecycle.addObserver(mLifeCycleObserver) } + /** + * Called when routing engine preference changes. + * Creates appropriate repository based on user selection. + */ fun onRoutingEngineStateUpdated(routeEngine : Int) { navigationViewModel = when (routeEngine) { RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository()) @@ -134,10 +172,19 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Called when location permission is granted. + * Initializes car hardware sensors if available. + */ fun onPermissionGranted(permission : Boolean) { addSensors(routeModel.navState.carConnection) } + /** + * Called when car connection state changes. + * Handles different connection types: Not Connected, Automotive OS Native, Android Auto Projection. + * Requests appropriate car speed permissions based on connection type. + */ fun onConnectionStateUpdated(connectionState: Int) { routeModel.navState = routeModel.navState.copy(carConnection = connectionState) when (connectionState) { @@ -153,8 +200,14 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Creates the initial screen for the session. + * Sets up ViewModel store, initializes components, checks permissions, + * and returns appropriate starting screen. + */ override fun onCreateScreen(intent: Intent): Screen { + // Create ViewModelStoreOwner to manage ViewModels across lifecycle viewModelStoreOwner = object : ViewModelStoreOwner { override val viewModelStore = ViewModelStore() } @@ -166,6 +219,7 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + // Initialize ViewModel with saved routing engine preference navigationViewModel = getViewModel(carContext) navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated) @@ -174,13 +228,17 @@ class NavigationSession : Session(), NavigationScreen.Listener { routeModel = RouteCarModel() + // Monitor car connection state CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated) + // Initialize surface renderer for map display surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) + // Create main navigation screen navigationScreen = NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel) + // Check for required permissions before starting if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && !useContacts @@ -207,6 +265,11 @@ class NavigationSession : Session(), NavigationScreen.Listener { return navigationScreen } + /** + * Registers listeners for car hardware sensors. + * Only adds location and compass sensors if useCarLocation setting is enabled. + * Speed sensor is added for both native and projection connections. + */ fun addSensors(connectionState: Int) { val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo val repository = getSettingsRepository(carContext) @@ -228,6 +291,10 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Unregisters all car hardware sensor listeners. + * Called when session is being destroyed to prevent memory leaks. + */ fun removeSensors() { val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo val repository = getSettingsRepository(carContext) @@ -242,6 +309,10 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Handles new intents, primarily for navigation deep links from other apps. + * Supports ACTION_NAVIGATE for starting navigation to a specific location. + */ override fun onNewIntent(intent: Intent) { val screenManager = carContext.getCarService(ScreenManager::class.java) if ((CarContext.ACTION_NAVIGATE == intent.action)) { @@ -278,11 +349,18 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Called when car configuration changes (e.g., day/night mode). + */ override fun onCarConfigurationChanged(newConfiguration: Configuration) { println("Configuration: ${newConfiguration.isNightModeActive}") super.onCarConfigurationChanged(newConfiguration) } + /** + * Requests GPS location updates from the device. + * Updates with last known location and starts listening for updates every 500ms or 5 meters. + */ @SuppressLint("MissingPermission") fun requestLocationUpdates() { val locationManager = @@ -300,6 +378,12 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Updates navigation state with new location. + * Handles route snapping, deviation detection for rerouting, and map updates. + * Snaps location to nearest point on route if within threshold. + * Triggers reroute calculation if deviated too far from route. + */ fun updateLocation(location: Location) { if (location.hasBearing()) { routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing) @@ -309,10 +393,12 @@ class NavigationSession : Session(), NavigationScreen.Listener { if (!routeModel.navState.arrived) { val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val distance = location.distanceTo(snapedLocation) + // Check if user has deviated too far from route if (distance > MAXIMAL_ROUTE_DEVIATION) { navigationScreen.calculateNewRoute(routeModel.navState.destination) return } + // Snap to route if close enough, otherwise use raw location if (distance < MAXIMAL_SNAP_CORRECTION) { surfaceRenderer.updateLocation(snapedLocation) } else { @@ -324,14 +410,20 @@ class NavigationSession : Session(), NavigationScreen.Listener { } } + /** + * Stops active navigation and clears route state. + * Called when user exits navigation or arrives at destination. + */ override fun stopNavigation(context: CarContext) { routeModel.stopNavigation(context) } companion object { + // URI host for deep linking var uriHost: String = "navigation" + // URI scheme for deep linking var uriScheme: String = "samples" } -} \ No newline at end of file +} diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 3fadc2a..0f9464b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -51,53 +51,100 @@ import org.maplibre.compose.style.BaseStyle import org.maplibre.spatialk.geojson.Position +/** + * Handles map rendering for Android Auto using a virtual display. + * Creates a VirtualDisplay to render Compose UI onto the car's surface. + * Manages camera position, zoom, tilt, and navigation state for the map view. + */ class SurfaceRenderer( private var carContext: CarContext, lifecycle: Lifecycle, private var routeModel: RouteCarModel, private var viewModelStoreOwner: ViewModelStoreOwner ) : DefaultLifecycleObserver { + // Last known location for bearing calculations var lastLocation = location(0.0, 0.0) + // Car orientation sensor value (999F means no valid orientation) var carOrientation = 999F + + // Current camera position state for the map private val cameraPosition = MutableLiveData( CameraPosition( zoom = 15.0, target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude) ) ) + + // Visible area of the map surface (can change based on UI elements) private var visibleArea = MutableLiveData( Rect(0, 0, 0, 0) ) + + // Stable area that won't change during scrolling var stableArea = Rect() + + // Surface dimensions var width = 0 var height = 0 + + // Last bearing for smooth transitions var lastBearing = 0.0 + + // LiveData for route GeoJSON data val routeData = MutableLiveData("") + // Traffic incident data (incident ID to GeoJSON mapping) val trafficData = MutableLiveData(emptyMap()) + + // Speed camera locations as GeoJSON val speedCamerasData = MutableLiveData("") + + // Current speed in km/h val speed = MutableLiveData(0F) + // Speed limit for current road val maxSpeed = MutableLiveData(0) + + // Current view mode (navigation, preview, etc.) var viewStyle = ViewStyle.VIEW + + // Center location for route preview lateinit var centerLocation: Location + + // Route distance for calculating preview zoom var previewDistance = 0.0 + + // Compose view for rendering the map lateinit var mapView: ComposeView + + // Camera tilt angle (default 55 degrees for navigation) var tilt = 55.0 + // Map base style (day/night) val style: MutableLiveData by lazy { MutableLiveData() } + /** + * SurfaceCallback implementation for handling the Android Auto surface lifecycle. + * Creates and manages the VirtualDisplay and Presentation for rendering Compose content. + */ val mSurfaceCallback: SurfaceCallback = object : SurfaceCallback { + // Custom lifecycle owner for the virtual display lateinit var lifecycleOwner: CustomLifecycleOwner + // Virtual display for rendering the map lateinit var virtualDisplay: VirtualDisplay + // Presentation that hosts the Compose view lateinit var presentation: Presentation + /** + * Called when the surface becomes available. + * Creates VirtualDisplay, initializes lifecycle, and sets up Compose rendering. + */ override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) { synchronized(this@SurfaceRenderer) { Log.i(TAG, "Surface available $surfaceContainer") @@ -135,18 +182,29 @@ class SurfaceRenderer( } } + /** + * Called when the visible area changes (e.g., due to UI elements appearing). + */ override fun onVisibleAreaChanged(newVisibleArea: Rect) { synchronized(this@SurfaceRenderer) { visibleArea.value = newVisibleArea } } + /** + * Called when the stable area changes. + * Stable area is guaranteed not to change during scroll events. + */ override fun onStableAreaChanged(newStableArea: Rect) { synchronized(this@SurfaceRenderer) { stableArea = newStableArea } } + /** + * Called when the surface is being destroyed. + * Cleans up resources and notifies lifecycle owner. + */ override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) { synchronized(this@SurfaceRenderer) { Log.i(TAG, "SurfaceRenderer destroyed") @@ -159,11 +217,17 @@ class SurfaceRenderer( } } + /** + * Called when user scrolls the map (not currently implemented). + */ override fun onScroll(distanceX: Float, distanceY: Float) { synchronized(this@SurfaceRenderer) { } } + /** + * Called when user scales (zooms) the map (not currently implemented). + */ override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) { } @@ -179,6 +243,10 @@ class SurfaceRenderer( } + /** + * Composable function that renders the map and navigation UI. + * Observes various LiveData sources and updates the map accordingly. + */ @Composable fun MapView() { @@ -204,6 +272,10 @@ class SurfaceRenderer( ShowPosition(cameraState, position, paddingValues) } + /** + * Composable that handles camera animations and navigation overlays. + * Displays speed indicator and navigation images during active navigation. + */ @Composable fun ShowPosition( cameraState: CameraState, @@ -244,7 +316,10 @@ class SurfaceRenderer( .setSurfaceCallback(mSurfaceCallback) } - /** Handles the map zoom-in and zoom-out events. */ + /** + * Handles the map zoom-in and zoom-out events. + * Switches to PAN_VIEW mode and updates camera zoom level. + */ fun handleScale(zoomSign: Int) { synchronized(this) { if (viewStyle == ViewStyle.VIEW) { @@ -264,6 +339,11 @@ class SurfaceRenderer( } } + /** + * Updates the camera position based on current location. + * Calculates appropriate bearing, zoom, and maintains view style. + * Uses car orientation sensor if available, otherwise falls back to location bearing. + */ fun updateLocation(location: Location) { synchronized(this) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { @@ -296,6 +376,10 @@ class SurfaceRenderer( } } + /** + * Updates camera position with new bearing, zoom, and target. + * Posts update to LiveData for UI observation. + */ private fun updateCameraPosition(bearing: Double, zoom: Double, target: Position) { synchronized(this) { cameraPosition.postValue( @@ -310,15 +394,25 @@ class SurfaceRenderer( } } + /** + * Sets route data for active navigation and switches to VIEW mode. + */ fun setRouteData() { routeData.value = routeModel.curRoute.routeGeoJson viewStyle = ViewStyle.VIEW } + /** + * Updates traffic incident data on the map. + */ fun setTrafficData(traffic: Map ) { trafficData.value = traffic as MutableMap? } + /** + * Sets up route preview mode with overview camera position. + * Calculates appropriate zoom based on route distance. + */ fun setPreviewRouteData(routeModel: RouteModel) { viewStyle = ViewStyle.PREVIEW with(routeModel) { @@ -333,6 +427,9 @@ class SurfaceRenderer( ) } + /** + * Displays a specific location (e.g., amenity/POI) on the map. + */ fun setCategories(location: Location, route: String) { synchronized(this) { viewStyle = ViewStyle.AMENITY_VIEW @@ -345,6 +442,10 @@ class SurfaceRenderer( } } + /** + * Updates car location from the connected car system. + * Only updates location when using OSRM routing engine. + */ fun updateCarLocation(location: Location) { val repository = getSettingsRepository(carContext) val routingEngine = runBlocking { repository.routingEngineFlow.first() } @@ -353,10 +454,16 @@ class SurfaceRenderer( } } + /** + * Updates current speed for display. + */ fun updateCarSpeed(newSpeed: Float) { speed.value = newSpeed } + /** + * Centers the map on a specific category/POI location. + */ fun setCategoryLocation(location: Location, category: String) { viewStyle = ViewStyle.AMENITY_VIEW cameraPosition.postValue( @@ -373,7 +480,14 @@ class SurfaceRenderer( } +/** + * Enum representing different map view modes. + * - VIEW: Active navigation mode with follow-car camera + * - PREVIEW: Route overview before starting navigation + * - PAN_VIEW: User-controlled map panning + * - AMENITY_VIEW: Displaying POI/amenity locations + */ enum class ViewStyle { VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW -} \ No newline at end of file +} diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt index 5c60089..1634678 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt @@ -57,7 +57,6 @@ class SearchScreen( .setNoItemsMessage("No search results to show") if (!isSearchComplete) { categories.forEach { - it.name itemListBuilder.addItem( Row.Builder() .setTitle(it.name) diff --git a/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt index 9ef717e..a847822 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt @@ -34,6 +34,6 @@ class OsrmRepository : NavigationRepository() { location: Location, carOrientation: Float ): String { - TODO("Not yet implemented") + return "" } } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaRepository.kt index aac6645..ae26cb4 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/valhalla/ValhallaRepository.kt @@ -53,6 +53,6 @@ class ValhallaRepository : NavigationRepository() { location: Location, carOrientation: Float ): String { - TODO("Not yet implemented") + return "" } } \ No newline at end of file diff --git a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt index 697cb48..d751b48 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt @@ -250,10 +250,12 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo currentLocation, carOrientation ) - val trafficData = rebuildTraffic(data) - traffic.postValue( - trafficData - ) + if (data.isNotEmpty()) { + val trafficData = rebuildTraffic(data) + traffic.postValue( + trafficData + ) + } } catch (e: Exception) { e.printStackTrace() }