This commit is contained in:
Dimitris
2026-04-04 09:39:54 +02:00
parent 8b886c36b1
commit 69b27d3b6c
7 changed files with 927 additions and 181 deletions
@@ -59,7 +59,7 @@ class DeviceLocationManagerService(
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 1.0f) {
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5.0f) {
if (isListening) return
// Get and deliver last known location first
@@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() {
return ClusterSession()
} else {
createNotificationChannel()
return NavigationSession()
//return NavigationSession()
return NavigationServiceSession()
}
}
@@ -0,0 +1,732 @@
package com.kouros.navigation.car
import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.graphics.Color
import android.location.Location
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
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.NavigationService
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
* 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 NavigationServiceSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
// Handles map surface rendering on the car display
lateinit var surfaceRenderer: SurfaceRenderer
// Manages car hardware sensors (location, compass, speed)
lateinit var carSensorManager: CarSensorManager
var initialLocation = true;
lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var notificationActive = false
var navigationService: NavigationService? = null
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate?,
stepTravelEstimate: TravelEstimate?,
stepRemainingDistance: Distance?,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
) {
navigationScreen.updateTrip(
isNavigating = isNavigating,
isRerouting = isRerouting,
hasArrived = hasArrived,
destinationTravelEstimate = destinationTravelEstimate!!,
stepTravelEstimate = stepTravelEstimate!!,
destinations = destinations,
steps = steps,
stepRemainingDistance = stepRemainingDistance!!,
shouldShowNextStep = shouldShowNextStep,
shouldShowLanes = shouldShowLanes,
junctionImage = junctionImage,
backGroundColor = backGroundColor
)
}
override fun updateServiceLocation(location: Location) {
if (initialLocation) {
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
initialLocation = false
navigationViewModel.loadRoute(
carContext,
location,
listOf(homeHohenwaldeck),
surfaceRenderer.carOrientation
)
}
updateLocation(location)
}
}
// Monitors the state of the connection to the Navigation service.
val serviceConnection: ServiceConnection = object : ServiceConnection {
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.d("NavigationService", "In onServiceConnected() Session component:$service")
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
navigationService = binder.service
navigationService!!.setCarContext(carContext, serviceListener)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d("NavigationService", "In onServiceDisconnected() Session component: $name")
// Unhook map models here
navigationService!!.clearCarContext()
navigationService = null
}
}
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Log.i(TAG, "In onStart() Session")
carContext
.bindService(
Intent(carContext, NavigationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
carContext.unbindService(serviceConnection)
navigationService = null
}
override fun onDestroy(owner: LifecycleOwner) {
if (::carSensorManager.isInitialized) {
carSensorManager.cleanup()
}
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed")
}
}
// ViewModel for navigation data and business logic
lateinit var navigationViewModel: NavigationViewModel
// Store for ViewModels to survive configuration changes
lateinit var viewModelStoreOwner: ViewModelStoreOwner
var lastStepIndex = -1
var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
lateinit var repository: SettingsRepository
lateinit var settingsViewModel: SettingsViewModel
var carConnection: Int = 0
init {
lifecycle.addObserver(lifecycleObserver)
}
/**
* Called when routing engine preference changes.
* Creates appropriate repository based on user selection.
*/
fun onRoutingEngineStateUpdated(routeEngine: Int) {
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
}
}
/**
* Called when location permission is granted.
* Initializes car hardware sensors if available.
*/
fun onPermissionGranted(permission: Boolean) {
if (::carSensorManager.isInitialized && permission) {
carSensorManager.updateConnectionState(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) {
carConnection = connectionState
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
}
CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationViewModel.permissionGranted.value =
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
}
}
}
/**
* Creates the initial screen for the session.
* Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen.
*/
override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore()
initializeManagers()
initializeViewModels()
initializeScreen()
return checkPermissionsAndGetScreen()
}
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
* Sets up ViewModelStoreOwner and manages its lifecycle.
*/
private fun setupViewModelStore() {
viewModelStoreOwner = object : ViewModelStoreOwner {
override val viewModelStore = ViewModelStore()
}
lifecycleScope.launch {
try {
awaitCancellation()
} finally {
viewModelStoreOwner.viewModelStore.clear()
}
}
}
/**
* Initializes ViewModels and observes their state changes.
*/
private fun initializeViewModels() {
navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
}
/**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager(
carContext = carContext,
lifecycleOwner = this,
onLocationUpdate = ::updateLocation,
onCompassUpdate = { orientation -> surfaceRenderer.carOrientation = orientation },
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
)
textToSpeechManager = TextToSpeechManager(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
notificationManager = NotificationManager(carContext, this)
}
/**
* Creates the main navigation screen.
*/
private fun initializeScreen() {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
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) {
navigationScreen
} else {
showPermissionScreen()
}
}
/**
* Shows the permission request screen.
*/
private fun showPermissionScreen(): Screen {
val screenManager = carContext.getCarService(ScreenManager::class.java)
screenManager.push(navigationScreen)
return RequestPermissionScreen(
carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
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)
// Handle Android Auto ACTION_NAVIGATE intent
if (CarContext.ACTION_NAVIGATE == intent.action) {
handleNavigateIntent(screenManager)
return
}
// Handle custom deep links
handleDeepLink(intent, screenManager)
}
/**
* Handles ACTION_NAVIGATE intent by showing search screen.
*/
private fun handleNavigateIntent(screenManager: ScreenManager) {
screenManager.popToRoot()
screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel, mutableListOf())
) { result ->
// Handle search result if needed
}
}
/**
* Handles custom deep link URIs.
*/
private fun handleDeepLink(intent: Intent, screenManager: ScreenManager) {
val uri = intent.data ?: return
if (uri.scheme != uriScheme || uri.schemeSpecificPart != uriHost) return
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.
*/
fun updateLocation(location: Location) {
if (carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed)
}
updateBearing(location)
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
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)
}
}
/**
* Start navigation process.
* Called when user starts navigation
*/
override fun startNavigation() {
Log.d(TAG, "startNavigation")
navigationService!!.startNavigation(route)
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
navigationService!!.stopNavigation()
surfaceRenderer.navigation = false
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
Log.d(TAG, "end stopNavigation")
}
override fun updateTrip(trip: Trip) {
}
/**
* Recalculates a route for the specified place.
*/
override fun recalcRoute(destination: Place) {
val destination = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
listOf(destination),
surfaceRenderer.carOrientation
)
}
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) {
prepareRoute(route)
}
}
override fun isNavigating(): Boolean {
return navigationService!!.isNavigating()
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
this.route = route
startNavigation()
surfaceRenderer.setRouteData(navigationService!!.routeModel.curRoute.routeGeoJson)
}
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
override fun invalidateScreen() {
navigationScreen.invalidate()
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
override fun navigateToPlace(place: Place) {
Log.d(TAG, "navigateToPlace ${place.street}")
var prevDestination = Place()
if (surfaceRenderer.navigation) {
prevDestination = place
stopNavigation()
}
val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else {
listOf(location(place.longitude, place.latitude))
}
navigationViewModel.saveRecent(carContext, place)
// routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
//routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
// routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"
// URI scheme for deep linking
var uriScheme: String = "samples"
}
}
@@ -1,28 +1,20 @@
package com.kouros.navigation.car
import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.location.Location
import android.os.IBinder
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.CarToast
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.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
@@ -33,7 +25,6 @@ import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.asLiveData
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationService
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationListener
@@ -104,12 +95,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
lateinit var carSensorManager: CarSensorManager
// Manages device GPS location updates
val useDeviceLocationManager = false
lateinit var deviceLocationManager: DeviceLocationManager
var initialLocation = true;
// lateinit var navigationManager: NavigationManager
lateinit var navigationManager: NavigationManager
lateinit var textToSpeechManager: TextToSpeechManager
@@ -135,73 +123,12 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
var notificationActive = false
var navigationService: NavigationService? = null
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination?>?,
steps: MutableList<Step>?,
nextDestinationTravelEstimate: TravelEstimate?,
nextStepRemainingDistance: Distance?,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?
) {
}
override fun updateServiceLocation(location: Location) {
if (initialLocation) {
Log.d(TAG, "RecentPlaces $location")
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
initialLocation = false
}
updateLocation(location)
}
}
// Monitors the state of the connection to the Navigation service.
val serviceConnection: ServiceConnection = object : ServiceConnection {
@RequiresPermission(allOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION])
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
Log.i(TAG, "In onServiceConnected() Session component:$service")
val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder
navigationService = binder.service
navigationService!!.setCarContext(carContext, serviceListener)
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.i(TAG, "In onServiceDisconnected() component: $name")
// Unhook map models here
navigationService!!.clearCarContext()
navigationService = null
}
}
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
Log.i(TAG, "In onStart() Session")
carContext
.bindService(
Intent(carContext, NavigationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
override fun onPause(owner: LifecycleOwner) {
Log.d(TAG, "NavigationSession paused")
super.onPause(owner)
@@ -212,16 +139,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
super.onResume(owner)
}
override fun onStop(owner: LifecycleOwner) {
Log.i(TAG, "In onStop()")
carContext.unbindService(serviceConnection)
navigationService = null
}
override fun onDestroy(owner: LifecycleOwner) {
// if (::navigationManager.isInitialized) {
// navigationManager.clearNavigationManagerCallback()
// }
if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback()
}
if (::carSensorManager.isInitialized) {
carSensorManager.cleanup()
}
@@ -268,7 +189,6 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Creates appropriate repository based on user selection.
*/
fun onRoutingEngineStateUpdated(routeEngine: Int) {
Log.d(TAG, "onRoutingEngineStateUpdated $routeEngine")
if (!::navigationViewModel.isInitialized || routeEngine != routingEngine) {
navigationViewModel = when (routeEngine) {
RouteEngine.VALHALLA.ordinal -> NavigationViewModel(ValhallaRepository())
@@ -379,26 +299,26 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* 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)
// //deviceLocationManager.stopLocationUpdates()
// autoDriveEnabled = true
// startNavigation()
// CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
// .show()
// }
//
// override fun onStopNavigation() {
// // Called when the user stops navigation in the car screen
// // Stop turn-by-turn logic and clean up
// stopNavigation()
// if (autoDriveEnabled) {
// //deviceLocationManager.startLocationUpdates()
// }
// }
// })
navigationManager = carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
// Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
startNavigation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
}
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up
stopNavigation()
if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates()
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager(
@@ -409,21 +329,19 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
onSpeedUpdate = { speed -> surfaceRenderer.updateCarSpeed(speed) }
)
if (useDeviceLocationManager) {
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
}
)
}
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
}
)
textToSpeechManager = TextToSpeechManager(carContext)
@@ -457,8 +375,7 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
carContext.checkSelfPermission(permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
return if (hasLocationPermission && hasContactsPermission) {
if (useDeviceLocationManager)
deviceLocationManager.startLocationUpdates()
deviceLocationManager.startLocationUpdates()
navigationScreen
} else {
showPermissionScreen()
@@ -680,13 +597,10 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
* Called when user starts navigation
*/
override fun startNavigation() {
if (useDeviceLocationManager)
deviceLocationManager.stopLocationUpdates()
Log.d(TAG, "startNavigation")
navigationService!!.startNavigation()
surfaceRenderer.navigation = true
surfaceRenderer.viewStyle = ViewStyle.VIEW
// navigationManager.navigationStarted()
navigationManager.navigationStarted()
navigationManagerStarted = true
if (autoDriveEnabled) {
simulation.startSimulation(
@@ -705,12 +619,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
if (useDeviceLocationManager)
deviceLocationManager.startLocationUpdates()
navigationService!!.stopNavigation()
surfaceRenderer.navigation = false
routeModel.stopNavigation()
//navigationManager.navigationEnded()
navigationManager.navigationEnded()
if (autoDriveEnabled) {
simulation.stopSimulation()
autoDriveEnabled = false
@@ -721,12 +632,11 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
Log.d(TAG, "end stopNavigation")
}
override fun updateTrip(trip: Trip) {
if (navigationManagerStarted) {
//navigationManager.updateTrip(trip)
navigationManager.updateTrip(trip)
}
}
@@ -6,14 +6,14 @@ import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.location.LocationManager
import android.os.Binder
import android.os.IBinder
import android.os.SystemClock
import android.text.TextUtils
import android.util.Log
import androidx.annotation.RequiresPermission
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager
@@ -21,12 +21,24 @@ import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.navigation.car.DeviceLocationManagerService
import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import org.joda.time.DateTime
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import kotlin.collections.copy
import kotlin.compareTo
class NavigationService : Service() {
@@ -69,6 +81,8 @@ class NavigationService : Service() {
// Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManagerService
lateinit var navigationViewModel: NavigationViewModel
private lateinit var navigationManager: NavigationManager
private var navigationManagerInitialized = false
var binder: IBinder = LocalBinder()
@@ -81,16 +95,20 @@ class NavigationService : Service() {
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination?>?,
steps: MutableList<Step>?,
nextDestinationTravelEstimate: TravelEstimate?,
nextStepRemainingDistance: Distance?,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate?,
stepTravelEstimate: TravelEstimate?,
stepRemainingDistance: Distance?,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?
junctionImage: CarIcon?,
backGroundColor: CarColor
)
fun updateServiceLocation(location: Location)
}
/**
@@ -107,26 +125,32 @@ class NavigationService : Service() {
return START_STICKY
}
override fun onCreate() {
Log.i(TAG, "In onCreate()");
createNotificationChannel();
}
override fun onBind(p0: Intent?): IBinder {
Log.d(TAG, "in onBind")
return binder
}
override fun onUnbind(intent: Intent): Boolean {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
Log.d(TAG, "in UnBind")
if (!routeModel.isNavigating()) {
Log.d(TAG, "Stopping location updates")
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
}
return true
}
override fun onDestroy() {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
Log.i(TAG, "In onDestroy()");
}
private fun createNotificationChannel() {
@@ -148,12 +172,12 @@ class NavigationService : Service() {
) {
Log.d(TAG, "in setCarContext")
this.carContext = carContext
navigationViewModel = getViewModel(carContext)
this.listener = listener
deviceLocationManager = DeviceLocationManagerService(
carContext = carContext,
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
Log.d(TAG, "Initial location: $location")
updateLocation(location)
}
)
@@ -170,7 +194,6 @@ class NavigationService : Service() {
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
simulation()
//startNavigation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
}
@@ -190,9 +213,6 @@ class NavigationService : Service() {
}
}
})
// Uncomment if navigating
// mNavigationManager.navigationStarted();
}
@@ -205,25 +225,27 @@ class NavigationService : Service() {
}
/** Starts navigation. */
fun startNavigation() {
fun startNavigation(route: String) {
Log.i(TAG, "Starting Navigation")
startService(Intent(applicationContext, NavigationService::class.java))
Log.i(TAG, "Starting foreground service")
listener.navigationStateChanged(
isNavigating = false,
isRerouting = true,
hasArrived = false,
destinations = null,
steps = null,
nextDestinationTravelEstimate = null,
nextStepRemainingDistance = null,
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null
)
routeModel.navState = routeModel.navState.copy(routingEngine = 2)
routeModel.startNavigation(route)
if (routeModel.isNavigating()) {
// listener.navigationStateChanged(
// isNavigating = true,
// isRerouting = true,
// hasArrived = false,
// destinations = emptyList<Destination>().toMutableList(),
// steps = emptyList<Step>().toMutableList(),
// destinationTravelEstimate = null,
// stepTravelEstimate = null,
// stepRemainingDistance = null,
// shouldShowNextStep = false,
// shouldShowLanes = false,
// junctionImage = null,
// backGroundColor = CarColor.BLUE
// )
}
}
/** Starts navigation. */
@@ -234,22 +256,101 @@ class NavigationService : Service() {
if (navigationManagerInitialized)
navigationManager.navigationEnded()
listener.navigationStateChanged(
false,
isNavigating = false,
isRerouting = false,
hasArrived = false,
destinations = null,
steps = null,
nextDestinationTravelEstimate = null,
nextStepRemainingDistance = null,
destinations = emptyList<Destination>().toMutableList(),
steps = emptyList<Step>().toMutableList(),
destinationTravelEstimate = null,
stepTravelEstimate = null,
stepRemainingDistance = null,
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = CarColor.BLUE
)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
fun updateLocation(location: Location) {
listener.updateServiceLocation(location)
Log.d(TAG, "updateLocation")
if (routeModel.isNavigating()) {
routeModel.updateLocation(location, navigationViewModel)
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
listener.updateServiceLocation(snappedLocation)
checkArrival()
updateNavigationScreen( 0)
} else {
listener.updateServiceLocation(location)
}
}
fun isNavigating(): Boolean {
return routeModel.isNavigating()
}
fun updateNavigationScreen(distanceMode: Int) {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()
) {
return
}
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext!!, distanceMode)
val travelEstimateStep = routeModel.travelEstimateStep(carContext!!, distanceMode)
val steps = mutableListOf<Step>()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name)
.setAddress(routeModel.navState.destination.street)
.build()
val distance =
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
steps.add(routeModel.currentStep(carContext!!))
if (routeModel.navState.nextStep) {
steps.add(routeModel.nextStep(carContext = carContext!!))
}
listener.navigationStateChanged(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinationTravelEstimate = travelEstimateTrip,
stepTravelEstimate = travelEstimateStep,
destinations = mutableListOf(destination),
steps = steps,
stepRemainingDistance = Distance.create(distance.first, distance.second),
shouldShowNextStep = false,
shouldShowLanes = true,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
destination,
travelEstimateTrip
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(destination.name.toString())
tripBuilder.addStep(steps.first(), travelEstimateStep)
navigationManager.updateTrip(tripBuilder.build())
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
routeModel.navState = routeModel.navState.copy(arrived = true)
}
}
}
@@ -56,7 +56,6 @@ open class NavigationScreen(
) : Screen(carContext) {
var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW
@@ -85,7 +84,7 @@ open class NavigationScreen(
private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
Log.d(TAG, "RecentPlaces $newPlaces")
Log.d(TAG, "NavigationScreen 4")
recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
tripSuggestionCalled = true
@@ -100,9 +99,11 @@ open class NavigationScreen(
}
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
Log.d(TAG, "NavigationScreen 3")
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it
})
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
@@ -118,7 +119,7 @@ open class NavigationScreen(
* Returns the appropriate template based on the current navigation state.
*/
override fun onGetTemplate(): Template {
Log.d(TAG, "NavigationScreen $navigationType")
Log.d(TAG, "NavigationScreen 2")
val actionStripBuilder = createActionStripBuilder({
createAction(
carContext,
@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.CarSession
import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel
@@ -18,7 +19,7 @@ class NavigationObserverManager(
val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback)
fun attachAllObservers(session: NavigationSession) {
fun attachAllObservers(session: CarSession) {
viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(session, trafficObserver)
viewModel.placeLocation.observe(session, placeSearchObserver)