From 69b27d3b6cf224673af3889de99443c5d1b6faf1 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Sat, 4 Apr 2026 09:39:54 +0200 Subject: [PATCH] Service --- .../car/DeviceLocationManagerService.kt | 2 +- .../navigation/car/NavigationCarAppService.kt | 3 +- .../car/NavigationServiceSession.kt | 732 ++++++++++++++++++ .../navigation/car/NavigationSession.kt | 172 +--- .../car/navigation/NavigationService.kt | 189 +++-- .../navigation/car/screen/NavigationScreen.kt | 7 +- .../observers/NavigationObserverManager.kt | 3 +- 7 files changed, 927 insertions(+), 181 deletions(-) create mode 100644 common/car/src/main/java/com/kouros/navigation/car/NavigationServiceSession.kt diff --git a/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManagerService.kt b/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManagerService.kt index eebf5e1..d1a84c9 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManagerService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/DeviceLocationManagerService.kt @@ -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 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 1a3a671..d4eef9e 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 @@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() { return ClusterSession() } else { createNotificationChannel() - return NavigationSession() + //return NavigationSession() + return NavigationServiceSession() } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationServiceSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationServiceSession.kt new file mode 100644 index 0000000..458da7d --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationServiceSession.kt @@ -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() + + 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, + steps: MutableList, + 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) { + 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) { + speedCameras = cameras + val coordinates = mutableListOf>() + 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() + 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" + } +} 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 213c656..8b198f7 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 @@ -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?, - steps: MutableList?, - 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) } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt index 0f0416d..846b12b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationService.kt @@ -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?, - steps: MutableList?, - nextDestinationTravelEstimate: TravelEstimate?, - nextStepRemainingDistance: Distance?, + destinations: MutableList, + steps: MutableList, + 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().toMutableList(), +// steps = emptyList().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().toMutableList(), + steps = emptyList().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() + 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) + } } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index bb6cc1a..5a5121a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -56,7 +56,6 @@ open class NavigationScreen( ) : Screen(carContext) { var recentPlaces = mutableListOf() - var recentPlace: Place = Place() var navigationType = NavigationType.VIEW @@ -85,7 +84,7 @@ open class NavigationScreen( private var showAlternativeRoute = false val observerRecentPlaces = Observer> { 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, diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt index d618d79..da72bbd 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt @@ -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)