Claude refactoring

This commit is contained in:
Dimitris
2026-02-25 11:09:31 +01:00
parent 5a6165dff8
commit eb6d3e4ef7
14 changed files with 553 additions and 288 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 49
versionName = "0.2.0.49"
versionCode = 50
versionName = "0.2.0.50"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -4,7 +4,6 @@ import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

View File

@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -22,45 +22,83 @@ import com.kouros.data.R
import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.round
private const val MANEUVER_TYPE_EXIT_RIGHT = 45
private const val MANEUVER_TYPE_EXIT_LEFT = 46
private const val METERS_PER_KILOMETER = 1000.0
private const val DISTANCE_THRESHOLD_METERS = 1000
private val CardTopPadding = 60.dp
private val CardElevation = 6.dp
private val IconSize = 48.dp
private val ExitTextSize = 18.sp
private val PrimaryTextSize = 24.sp
private val SpacerWidth = 8.dp
private val CardPadding = 16.dp
private val ElementSpacing = 8.dp
@Composable
fun NavigationInfo(step: StepData?, nextStep: StepData?) {
if (step != null && step.instruction.isNotEmpty()) {
fun NavigationInfo(
step: StepData?,
nextStep: StepData?
) {
step?.takeIf { it.instruction.isNotEmpty() }?.let { currentStep ->
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 6.dp
), modifier = Modifier
.padding(top = 60.dp)
elevation = CardDefaults.cardElevation(defaultElevation = CardElevation),
modifier = Modifier
.padding(top = CardTopPadding)
.fillMaxWidth()
) {
Column {
Column(
modifier = Modifier.padding(CardPadding),
horizontalAlignment = Alignment.Start
) {
Icon(
painter = painterResource(step.icon),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
painter = painterResource(currentStep.icon),
contentDescription = stringResource(id = R.string.navigation_icon_description),
modifier = Modifier.size(IconSize),
tint = MaterialTheme.colorScheme.primary
)
if (step.currentManeuverType == 46
|| step.currentManeuverType == 45
) {
Text(text = "Exit ${step.exitNumber}", fontSize = 18.sp)
}
Row {
if (step.leftStepDistance < 1000) {
Text(text = "${step.leftStepDistance.toInt()} m", fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
} else {
Text(
text = "${(step.leftStepDistance / 1000).round(1)} km",
fontSize = 24.sp,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(
modifier = Modifier.padding(5.dp)
if (currentStep.isExitManeuver) {
Text(
text = stringResource(R.string.exit_number, currentStep.exitNumber),
fontSize = ExitTextSize,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(top = ElementSpacing)
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(top = ElementSpacing)
) {
DistanceText(distance = currentStep.leftStepDistance)
Spacer(modifier = Modifier.padding(horizontal = SpacerWidth))
Text(
text = currentStep.instruction,
fontSize = PrimaryTextSize,
color = MaterialTheme.colorScheme.primary
)
Text(text = step.instruction, fontSize = 24.sp, color = MaterialTheme.colorScheme.primary)
}
}
}
}
}
}
@Composable
private fun DistanceText(distance: Double) {
val formattedDistance = when {
distance < DISTANCE_THRESHOLD_METERS -> "${distance.toInt()} m"
else -> "${(distance / METERS_PER_KILOMETER).round(1)} km"
}
Text(
text = formattedDistance,
fontSize = PrimaryTextSize,
color = MaterialTheme.colorScheme.primary
)
}
private val StepData.isExitManeuver: Boolean
get() = currentManeuverType == MANEUVER_TYPE_EXIT_RIGHT ||
currentManeuverType == MANEUVER_TYPE_EXIT_LEFT

View File

@@ -0,0 +1,190 @@
package com.kouros.navigation.car
import android.location.Location
import androidx.car.app.CarContext
import androidx.car.app.connection.CarConnection
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarHardwareLocation
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.Speed
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.utils.getSettingsRepository
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Manages car hardware sensor listeners for navigation.
* Handles location, compass, and speed sensors from the car hardware.
*
* @param carContext The car context for accessing hardware services
* @param lifecycleOwner Owner of the lifecycle for coroutine management
* @param onLocationUpdate Callback for location updates
* @param onCompassUpdate Callback for compass/orientation updates
* @param onSpeedUpdate Callback for speed updates
*/
class CarSensorManager(
private val carContext: CarContext,
private val lifecycleOwner: LifecycleOwner,
private val onLocationUpdate: (Location) -> Unit,
private val onCompassUpdate: (Float) -> Unit,
private val onSpeedUpdate: (Float) -> Unit
) {
private val carHardwareManager: CarHardwareManager =
carContext.getCarService(CarHardwareManager::class.java)
private val settingsRepository = getSettingsRepository(carContext)
private var carConnection: Int = CarConnection.CONNECTION_TYPE_NOT_CONNECTED
private var isLocationSensorActive = false
private var isSpeedSensorActive = false
/**
* Car hardware location listener.
* Receives location data from the car's GPS system.
*/
private val carLocationListener: OnCarDataAvailableListener<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) {
val location = data.location.value
if (location != null) {
onLocationUpdate(location)
}
}
}
/**
* Car compass/orientation sensor listener.
* Updates orientation for map rotation.
*/
private val carCompassListener: OnCarDataAvailableListener<Compass?> =
OnCarDataAvailableListener { data ->
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
val orientation = data.orientations.value
if (!orientation.isNullOrEmpty()) {
onCompassUpdate(orientation[0])
}
}
}
/**
* Car speed sensor listener.
* Receives speed in meters per second from car hardware.
*/
private val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
val speed = data.displaySpeedMetersPerSecond.value
if (speed != null) {
onSpeedUpdate(speed)
}
}
}
init {
// Observe car location setting changes
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
settingsRepository.carLocationFlow.collectLatest { useCarLocation ->
if (useCarLocation) {
addLocationSensors()
} else {
removeLocationSensors()
}
}
}
}
}
/**
* Updates the car connection state and manages speed sensor accordingly.
*
* @param connectionState The current car connection type
*/
fun updateConnectionState(connectionState: Int) {
carConnection = connectionState
when (connectionState) {
CarConnection.CONNECTION_TYPE_NATIVE,
CarConnection.CONNECTION_TYPE_PROJECTION -> addSpeedSensor()
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> removeSpeedSensor()
}
}
/**
* Checks if car location sensors should be used based on settings.
*
* @return Flow of boolean indicating if car location should be used
*/
fun shouldUseCarLocation() = settingsRepository.carLocationFlow
/**
* Adds location and compass sensors if not already active.
*/
private fun addLocationSensors() {
if (isLocationSensorActive) return
val carSensors = carHardwareManager.carSensors
carSensors.addCompassListener(
CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carCompassListener
)
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST,
carContext.mainExecutor,
carLocationListener
)
isLocationSensorActive = true
}
/**
* Removes location and compass sensors.
*/
private fun removeLocationSensors() {
if (!isLocationSensorActive) return
val carSensors = carHardwareManager.carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
carSensors.removeCompassListener(carCompassListener)
isLocationSensorActive = false
}
/**
* Adds speed sensor if not already active.
*/
private fun addSpeedSensor() {
if (isSpeedSensorActive) return
if (carConnection == CarConnection.CONNECTION_TYPE_NATIVE ||
carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
val carInfo = carHardwareManager.carInfo
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
isSpeedSensorActive = true
}
}
/**
* Removes speed sensor.
*/
private fun removeSpeedSensor() {
if (!isSpeedSensorActive) return
val carInfo = carHardwareManager.carInfo
carInfo.removeSpeedListener(carSpeedListener)
isSpeedSensorActive = false
}
/**
* Cleans up all sensor listeners.
* Should be called when the session is destroyed.
*/
fun cleanup() {
removeLocationSensors()
removeSpeedSensor()
}
}

View File

@@ -0,0 +1,105 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.car.app.CarContext
import androidx.core.location.LocationListenerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Manages device GPS location updates for navigation.
* Coordinates with car hardware sensors to avoid duplicate location sources.
*
* @param carContext The car context for accessing system services
* @param lifecycleOwner Owner of the lifecycle for coroutine management
* @param shouldUseCarLocationFlow Flow indicating whether car location hardware should be used
* @param onLocationUpdate Callback invoked when location updates are received
* @param onInitialLocation Callback invoked with the last known location when starting
*/
class DeviceLocationManager(
private val carContext: CarContext,
private val lifecycleOwner: LifecycleOwner,
private val shouldUseCarLocationFlow: Flow<Boolean>,
private val onLocationUpdate: (Location) -> Unit,
private val onInitialLocation: (Location) -> Unit
) {
private val locationManager: LocationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var shouldUseDeviceLocation = true
private var isListening = false
/**
* Location listener that receives GPS updates from the device.
* Only processes location if car location hardware is not being used.
*/
private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
if (location != null && shouldUseDeviceLocation) {
onLocationUpdate(location)
}
}
init {
// Observe car location setting to toggle device location usage
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
shouldUseCarLocationFlow.collectLatest { useCarLocation ->
shouldUseDeviceLocation = !useCarLocation
}
}
}
}
/**
* Starts requesting location updates from device GPS.
* Provides initial location via callback and then starts continuous updates.
*
* @param minTimeMs Minimum time interval between updates in milliseconds (default: 500ms)
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 500, minDistanceM: Float = 5f) {
if (isListening) return
// Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) {
onInitialLocation(lastLocation)
onLocationUpdate(lastLocation)
}
// Start continuous location updates
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceM,
locationListener
)
isListening = true
}
/**
* Stops receiving location updates from device GPS.
* Should be called when the session is destroyed to prevent memory leaks.
*/
fun stopLocationUpdates() {
if (!isListening) return
locationManager.removeUpdates(locationListener)
isListening = false
}
/**
* Checks if location updates are currently active.
*/
fun isListeningForUpdates(): Boolean = isListening
}

View File

@@ -1,27 +1,16 @@
package com.kouros.navigation.car
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.location.Location
import android.location.LocationManager
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.connection.CarConnection
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarHardwareLocation
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.Speed
import androidx.core.location.LocationListenerCompat
import androidx.core.net.toUri
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -45,11 +34,8 @@ import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import android.Manifest.permission
/**
@@ -64,7 +50,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
val useContacts = false
// Model for managing route state and navigation logic for Android Auto
lateinit var routeModel: RouteCarModel;
lateinit var routeModel: RouteCarModel
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
@@ -72,41 +58,25 @@ class NavigationSession : Session(), NavigationScreen.Listener {
// 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() }
if (!useCarLocation) {
updateLocation(location!!)
}
}
// Manages car hardware sensors (location, compass, speed)
lateinit var carSensorManager: CarSensorManager
// Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManager
/**
* 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) {
}
override fun onResume(owner: LifecycleOwner) {
}
override fun onPause(owner: LifecycleOwner) {
}
override fun onStop(owner: LifecycleOwner) {
}
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
removeSensors()
Log.i(TAG, "In onDestroy()")
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener)
if (::carSensorManager.isInitialized) {
carSensorManager.cleanup()
}
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
Log.i(TAG, "NavigationSession destroyed")
}
}
@@ -116,48 +86,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
// 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<CarHardwareLocation?> =
OnCarDataAvailableListener { data ->
if (data.location.status == CarValue.STATUS_SUCCESS) {
val location = data.location.value
if (location != null) {
updateLocation(location)
}
}
}
/**
* Listener for car compass/orientation sensor.
* Updates the surface renderer with car orientation for map rotation.
*/
val carCompassListener: OnCarDataAvailableListener<Compass?> =
OnCarDataAvailableListener { data ->
if (data.orientations.status == CarValue.STATUS_SUCCESS) {
val orientation = data.orientations.value
if (orientation != null) {
surfaceRenderer.carOrientation = orientation[0]
}
}
}
/**
* Listener for car speed sensor updates.
* Receives speed in meters per second from car hardware.
*/
val carSpeedListener = OnCarDataAvailableListener<Speed> { data ->
if (data.displaySpeedMetersPerSecond.status == CarValue.STATUS_SUCCESS) {
val speed = data.displaySpeedMetersPerSecond.value
surfaceRenderer.updateCarSpeed(speed!!)
}
}
init {
val lifecycle: Lifecycle = lifecycle
lifecycle.addObserver(mLifeCycleObserver)
lifecycle.addObserver(lifecycleObserver)
}
/**
@@ -177,7 +107,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Initializes car hardware sensors if available.
*/
fun onPermissionGranted(permission : Boolean) {
addSensors(routeModel.navState.carConnection)
if (::carSensorManager.isInitialized) {
carSensorManager.updateConnectionState(routeModel.navState.carConnection)
}
}
/**
@@ -187,14 +119,16 @@ class NavigationSession : Session(), NavigationScreen.Listener {
*/
fun onConnectionStateUpdated(connectionState: Int) {
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
if (::carSensorManager.isInitialized) {
carSensorManager.updateConnectionState(connectionState)
}
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> {
ObjectBox.init(carContext)
navigationScreen.checkPermission("android.car.permission.CAR_SPEED")
} // Automotive OS
}
CarConnection.CONNECTION_TYPE_PROJECTION -> {
"Connected to Android Auto"
navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED")
}
}
@@ -206,8 +140,17 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* and returns appropriate starting screen.
*/
override fun onCreateScreen(intent: Intent): Screen {
setupViewModelStore()
initializeViewModels()
initializeManagers()
initializeScreen()
return checkPermissionsAndGetScreen()
}
// Create ViewModelStoreOwner to manage ViewModels across lifecycle
/**
* Sets up ViewModelStoreOwner and manages its lifecycle.
*/
private fun setupViewModelStore() {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
@@ -218,195 +161,180 @@ class NavigationSession : Session(), NavigationScreen.Listener {
viewModelStoreOwner.viewModelStore.clear()
}
}
}
// Initialize ViewModel with saved routing engine preference
/**
* Initializes ViewModels and observes their state changes.
*/
private fun initializeViewModels() {
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
routeModel = RouteCarModel()
// Monitor car connection state
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
}
// Initialize surface renderer for map display
/**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
// Create main navigation screen
navigationScreen =
NavigationScreen(carContext, surfaceRenderer, routeModel, this, navigationViewModel)
carSensorManager = CarSensorManager(
carContext = carContext,
lifecycleOwner = this,
onLocationUpdate = ::updateLocation,
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
)
// Check for required permissions before starting
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
&& !useContacts
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED)
) {
requestLocationUpdates()
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
navigationViewModel.loadRecentPlace(location, surfaceRenderer.carOrientation, carContext)
}
)
}
/**
* Creates the main navigation screen.
*/
private fun initializeScreen() {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
routeModel,
this,
navigationViewModel
)
}
/**
* Checks required permissions and returns appropriate screen.
* Shows permission request screen if needed, otherwise starts location updates.
*/
private fun checkPermissionsAndGetScreen(): Screen {
val hasLocationPermission = carContext.checkSelfPermission(permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val hasContactsPermission = !useContacts ||
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
return if (hasLocationPermission && hasContactsPermission) {
deviceLocationManager.startLocationUpdates()
navigationScreen
} else {
// If we do not have the location permission, show the request permission screen.
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(navigationScreen)
return RequestPermissionScreen(
carContext,
permissionCheckCallback = {
screenManager.pop()
},
)
}
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)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.addCompassListener(CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carCompassListener)
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST,
carContext.mainExecutor,
carLocationListener
)
}
if (connectionState == CarConnection.CONNECTION_TYPE_NATIVE
|| connectionState == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
showPermissionScreen()
}
}
/**
* Unregisters all car hardware sensor listeners.
* Called when session is being destroyed to prevent memory leaks.
* Shows the permission request screen.
*/
fun removeSensors() {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_NATIVE
|| routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.removeSpeedListener(carSpeedListener)
}
private fun showPermissionScreen(): Screen {
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(navigationScreen)
return RequestPermissionScreen(
carContext,
permissionCheckCallback = { screenManager.pop() }
)
}
/**
* 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)) {
val uri = ("http://" + intent.dataString).toUri()
val location = Location(LocationManager.GPS_PROVIDER)
screenManager.popToRoot()
screenManager.pushForResult(
SearchScreen(
carContext,
surfaceRenderer,
navigationViewModel
// TODO: Uri
)
) { obj: Any? ->
if (obj != null) {
}
}
// Handle Android Auto ACTION_NAVIGATE intent
if (CarContext.ACTION_NAVIGATE == intent.action) {
handleNavigateIntent(screenManager)
return
}
val uri = intent.data
if (uri != null && uriScheme == uri.scheme
&& uriHost == uri.schemeSpecificPart
) {
val top = screenManager.getTop()
when (uri.fragment) {
"DEEP_LINK_ACTION" -> if (top !is NavigationScreen) {
screenManager.popToRoot()
}
// Handle custom deep links
handleDeepLink(intent, screenManager)
}
else -> {}
}
/**
* Handles ACTION_NAVIGATE intent by showing search screen.
*/
private fun handleNavigateIntent(screenManager: ScreenManager) {
screenManager.popToRoot()
screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel)
) { result ->
// Handle search result if needed
}
}
/**
* Called when car configuration changes (e.g., day/night mode).
* Handles custom deep link URIs.
*/
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
println("Configuration: ${newConfiguration.isNightModeActive}")
super.onCarConfigurationChanged(newConfiguration)
}
private fun handleDeepLink(intent: Intent, screenManager: ScreenManager) {
val uri = intent.data ?: return
if (uri.scheme != uriScheme || uri.schemeSpecificPart != uriHost) return
/**
* 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 =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (location != null) {
navigationViewModel.loadRecentPlace(location = location, surfaceRenderer.carOrientation, carContext)
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 500,
/* minDistanceM= */ 5f,
mLocationListener
)
when (uri.fragment) {
"DEEP_LINK_ACTION" -> {
if (screenManager.getTop() !is NavigationScreen) {
screenManager.popToRoot()
}
}
}
}
/**
* 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) {
updateBearing(location)
if (routeModel.isNavigating()) {
handleNavigationLocation(location)
} else {
surfaceRenderer.updateLocation(location)
}
}
/**
* Updates route bearing if location has bearing information.
*/
private fun updateBearing(location: Location) {
if (location.hasBearing()) {
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
}
if (routeModel.isNavigating()) {
navigationScreen.updateTrip(location)
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 {
surfaceRenderer.updateLocation(location)
}
}
/**
* Handles location updates during active navigation.
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
navigationScreen.updateTrip(location)
if (routeModel.navState.arrived) return
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation)
when {
distance > MAXIMAL_ROUTE_DEVIATION -> {
navigationScreen.calculateNewRoute(routeModel.navState.destination)
}
distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation)
}
else -> {
surfaceRenderer.updateLocation(location)
}
} else {
surfaceRenderer.updateLocation(location)
}
}

View File

@@ -33,11 +33,9 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
@@ -561,7 +559,6 @@ class NavigationScreen(
}
fun checkPermission(permission: String) {
println("Car connection permission: $permission")
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission)

View File

@@ -84,7 +84,7 @@ class NavigationSettings(
)
.addItem(
buildRowForScreenTemplate(
PasswordSettings(carContext, navigationViewModel),
PasswordSettings(carContext),
R.string.tomtom_api_key
)
)

View File

@@ -15,8 +15,7 @@ import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class PasswordSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
private val carContext: CarContext
) : Screen(carContext) {
var errorMessage: String? = null
@@ -51,7 +50,6 @@ class PasswordSettings(
val pinSignInAction = Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setOnClickListener(ParkedOnlyOnClickListener.create {
println("Sign")
invalidate()
})
.build()

View File

@@ -0,0 +1,21 @@
package com.kouros.navigation.data
import android.location.Location
import com.kouros.navigation.model.IconMapper
import com.kouros.navigation.utils.location
// Immutable Data Class
data class NavigationState (
val route: Route = Route.Builder().buildEmpty(),
val iconMapper: IconMapper = IconMapper(),
val navigating: Boolean = false,
val arrived: Boolean = false,
val travelMessage: String = "",
val maneuverType: Int = 0,
val lastLocation: Location = location(0.0, 0.0),
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place(),
val carConnection: Int = 0,
)

View File

@@ -37,7 +37,7 @@ class TomTomRepository : NavigationRepository() {
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val url =
routeUrl + "${currentLocation.latitude},${currentLocation.longitude}:${location.latitude},${location.longitude}" +
"/json?vehicleHeading=90&sectionType=traffic&report=effectiveSettings&routeType=eco" +
"/json?sectionType=traffic&report=effectiveSettings&routeType=eco" +
"&traffic=true&avoid=unpavedRoads&travelMode=car" +
"&vehicleMaxSpeed=120&vehicleCommercial=false" +
"&instructionsType=text&language=en-GB&sectionType=lanes" +

View File

@@ -6,6 +6,7 @@ import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.NavigationState
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Route
import com.kouros.navigation.data.StepData
@@ -23,22 +24,6 @@ import kotlin.math.absoluteValue
open class RouteModel {
// Immutable Data Class
data class NavigationState(
val route: Route = Route.Builder().buildEmpty(),
val iconMapper: IconMapper = IconMapper(),
val navigating: Boolean = false,
val arrived: Boolean = false,
val travelMessage: String = "",
val maneuverType: Int = 0,
val lastLocation: Location = location(0.0, 0.0),
val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0,
val destination: Place = Place(),
val carConnection: Int = 0,
)
var navState = NavigationState()
val route: Route

View File

@@ -52,5 +52,7 @@
<string name="options">Optionen</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Verwende Auto Einstellungen</string>
<string name="exit_number">Ausfahrt nummer</string>
<string name="navigation_icon_description">Navigations Icon</string>
</resources>

View File

@@ -38,4 +38,6 @@
<string name="options">Options</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Use car settings</string>
<string name="exit_number">Exit number</string>
<string name="navigation_icon_description">Navigation icon</string>
</resources>