From 263b5b576dfd946c8f1c2f597feca90ec04d5b81 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Thu, 26 Mar 2026 17:04:52 +0100 Subject: [PATCH] Navigation Screen to Session, Remove NavigationService --- .../com/kouros/navigation/ui/MainActivity.kt | 40 ++ .../kouros/navigation/car/ClusterSession.kt | 7 +- .../navigation/car/NavigationCarAppService.kt | 3 +- .../car/NavigationNotificationService.kt | 6 +- .../navigation/car/NavigationSession.kt | 336 +++++++++++++++- .../kouros/navigation/car/SurfaceRenderer.kt | 20 +- .../car/screen/NavigationListener.kt | 4 +- .../navigation/car/screen/NavigationScreen.kt | 360 ++++-------------- .../car/screen/RoutePreviewScreen.kt | 1 + .../observers/NavigationObserverManager.kt | 19 +- .../java/com/kouros/navigation/data/Data.kt | 3 +- 11 files changed, 462 insertions(+), 337 deletions(-) diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index a88cbdc..e64a985 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -43,6 +43,7 @@ import com.google.android.gms.location.LocationServices import com.kouros.data.R import com.kouros.navigation.MainApplication.Companion.navigationViewModel import com.kouros.navigation.car.TextToSpeechManager +import com.kouros.navigation.car.navigation.NavigationService import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE import com.kouros.navigation.data.Constants.TAG @@ -79,6 +80,8 @@ import kotlin.time.Duration.Companion.seconds class MainActivity : ComponentActivity() { + var navigationService: NavigationService? = null + var isBound: Boolean = false val routeData = MutableLiveData("") val routeModel = RouteModel() @@ -99,6 +102,20 @@ class MainActivity : ComponentActivity() { } } + // Monitors the state of the connection to the navigation service. + private val serviceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder: NavigationService.LocalBinder = service as NavigationService.LocalBinder + navigationService = binder.service + isBound = true + } + + override fun onServiceDisconnected(name: ComponentName?) { + navigationService = null + isBound = false + } + } + val cameraPosition = MutableLiveData( CameraPosition( zoom = 15.0, target = Position(latitude = 48.1857475, longitude = 11.5793627) @@ -151,6 +168,27 @@ class MainActivity : ComponentActivity() { } } + override fun onStart() { + super.onStart() + Log.i(TAG, "In onStart()") + bindService( + Intent(this, NavigationService::class.java), + serviceConnection, + BIND_AUTO_CREATE + ) + requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1) + } + + override fun onStop() { + Log.i(TAG, "In onStop(). bound $isBound") + if (isBound) { + unbindService(serviceConnection) + isBound = false + navigationService = null + } + super.onStop() + } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun StartScreen( @@ -318,6 +356,7 @@ class MainActivity : ComponentActivity() { routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) routeModel.startNavigation(newRoute) routeData.value = routeModel.curRoute.routeGeoJson + navigationService?.startNavigation() } fun stopNavigation(closeSheet: () -> Unit) { closeSheet() @@ -325,6 +364,7 @@ class MainActivity : ComponentActivity() { getSettingsViewModel(applicationContext).onLastRouteChanged("") routeData.value = "" stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0) + navigationService?.stopNavigation() } fun textToSpeech() { diff --git a/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt index 6521b21..e6ce6d8 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt @@ -34,6 +34,7 @@ import com.kouros.data.R import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationScreen +import com.kouros.navigation.data.Place import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.launch @@ -67,7 +68,7 @@ internal class ClusterSession : Session(), NavigationListener { OnClickListener {}) .build() - mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) + mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner) // mNavigationScreen = // new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface); @@ -99,6 +100,10 @@ internal class ClusterSession : Session(), NavigationListener { override fun updateTrip(trip: Trip) { } + override fun navigateToPlace(place: Place) { + + } + companion object { val TAG: String = ClusterSession::class.java.getSimpleName() } 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 0378d3e..cc9cce1 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 @@ -14,7 +14,7 @@ import com.kouros.navigation.data.Constants.TAG class NavigationCarAppService : CarAppService() { - val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP = + val intentActionNavNotificationOpenApp = "com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP" val channelId: String = "NavigationSessionChannel" @@ -31,7 +31,6 @@ class NavigationCarAppService : CarAppService() { } override fun onCreateSession(sessionInfo: SessionInfo): Session { - Log.d(TAG, "Display Type: ${sessionInfo.displayType}") if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) { return ClusterSession() } else { diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt index 7a15cde..ac2437d 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationNotificationService.kt @@ -136,9 +136,9 @@ class NavigationNotificationService : Service() { // heads-up notification or the rail widget. val pendingIntent = CarPendingIntent.getCarApp( context, - NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(), + NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(), Intent( - NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP + NavigationCarAppService().intentActionNavNotificationOpenApp ).setComponent( ComponentName( context, @@ -146,7 +146,7 @@ class NavigationNotificationService : Service() { ) ).setData( NavigationCarAppService().createDeepLinkUri( - NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP + NavigationCarAppService().intentActionNavNotificationOpenApp ) ), 0 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 53a7e89..795b7a7 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 @@ -40,25 +40,38 @@ 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.Place import com.kouros.navigation.data.RouteEngine 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.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 /** @@ -67,7 +80,7 @@ import java.time.ZoneOffset * car hardware sensors, routing engine selection, and screen navigation. * Implements NavigationScreen.Listener for handling navigation events. */ -class NavigationSession : Session(), NavigationListener { +class NavigationSession : Session(), NavigationListener, NavigationObserverCallback { // Flag to enable/disable contact access feature val useContacts = false @@ -75,6 +88,8 @@ class NavigationSession : Session(), NavigationListener { // Model for managing route state and navigation logic for Android Auto lateinit var routeModel: RouteCarModel + var route = "" + // Main navigation screen displayed to the user lateinit var navigationScreen: NavigationScreen @@ -95,6 +110,15 @@ class NavigationSession : Session(), NavigationListener { val simulation = Simulation() + private var routingEngine = 0 + + private var showTraffic = false; + + var lastCameraSearch = 0 + + var speedCameras = listOf() + + var lastRouteDate: LocalDateTime = LocalDateTime.now() var navigationManagerStarted = false /** @@ -130,8 +154,22 @@ class NavigationSession : Session(), NavigationListener { var guidanceAudio = 0 + var lastTrafficDate: LocalDateTime = LocalDateTime.MIN + lateinit var observerManager: NavigationObserverManager + + val repository = getSettingsRepository(carContext) + + val settingsViewModel = getSettingsViewModel(carContext) + init { lifecycle.addObserver(lifecycleObserver) + repository.routingEngineFlow.asLiveData().observe(this, Observer { + routingEngine = it + }) + + repository.trafficFlow.asLiveData().observe(this, Observer { + showTraffic = it + }) } /** @@ -144,6 +182,8 @@ class NavigationSession : Session(), NavigationListener { RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository()) else -> NavigationViewModel(TomTomRepository()) } + observerManager = NavigationObserverManager(navigationViewModel, this) + observerManager.attachAllObservers(this) } /** @@ -166,11 +206,13 @@ class NavigationSession : Session(), NavigationListener { when (connectionState) { CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NATIVE -> { - navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION) + navigationViewModel.permissionGranted.value = + checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION) } CarConnection.CONNECTION_TYPE_PROJECTION -> { - navigationViewModel.permissionGranted.value = checkPermission(carContext, GMS_CAR_SPEED_PERMISSION) + navigationViewModel.permissionGranted.value = + checkPermission(carContext, GMS_CAR_SPEED_PERMISSION) } } } @@ -230,7 +272,7 @@ class NavigationSession : Session(), NavigationListener { autoDriveEnabled = true startNavigation() CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG) - .show() + .show() } override fun onStopNavigation() { @@ -242,7 +284,7 @@ class NavigationSession : Session(), NavigationListener { } } }) - surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) + surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner) carSensorManager = CarSensorManager( carContext = carContext, @@ -281,7 +323,6 @@ class NavigationSession : Session(), NavigationListener { navigationScreen = NavigationScreen( carContext, surfaceRenderer, - routeModel, this, navigationViewModel ) @@ -369,6 +410,11 @@ class NavigationSession : Session(), NavigationListener { * Handles route snapping, deviation detection for rerouting, and map updates. */ fun updateLocation(location: Location) { + val streetName = if (routeModel.isNavigating()) { + routeModel.currentStep().street + } else { + "" + } if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) { surfaceRenderer.updateCarSpeed(location.speed) } @@ -376,8 +422,8 @@ class NavigationSession : Session(), NavigationListener { if (routeModel.isNavigating()) { handleNavigationLocation(location) } else { - navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) - surfaceRenderer.updateLocation(location) + checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) + surfaceRenderer.updateLocation(location, streetName) } } @@ -395,11 +441,19 @@ class NavigationSession : Session(), NavigationListener { * Snaps location to route and checks for deviation requiring reroute. */ private fun handleNavigationLocation(location: Location) { - if (guidanceAudio == 1) { handleGuidanceAudio() } - navigationScreen.updateTrip(location) + val streetName = routeModel.currentStep().street + val currentDate = LocalDateTime.now(ZoneOffset.UTC) + checkTraffic(currentDate, location) + updateSpeedCamera(location) + checkRoute(currentDate, location) + routeModel.updateLocation(location, navigationViewModel) + checkArrival() + + updateTripNavigationScreen(location) + if (routeModel.navState.arrived) return val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val distance = location.distanceTo(snappedLocation) @@ -409,15 +463,81 @@ class NavigationSession : Session(), NavigationListener { } distance < MAXIMAL_SNAP_CORRECTION -> { - surfaceRenderer.updateLocation(snappedLocation) + surfaceRenderer.updateLocation(snappedLocation, streetName) } else -> { - surfaceRenderer.updateLocation(location) + surfaceRenderer.updateLocation(location, streetName) } } } + fun updateTripNavigationScreen(location: Location) { + val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, 0) + val travelEstimateStep = routeModel.travelEstimateStep(carContext, 0) + val steps = mutableListOf() + val street = if (routeModel.navState.destination.street != null) { + routeModel.navState.destination.street!! + } else { + // routeModel.navState.destination.name!! + "Street" + } + val destination = Destination.Builder() + .setName(street) + .setAddress(street) + .build() + val distance = + formattedDistance(0, routeModel.routeCalculator.leftStepDistance()) + steps.add(routeModel.currentStep(carContext)) + if (routeModel.navState.nextStep) { + steps.add(routeModel.nextStep(carContext = carContext)) + + } + navigationScreen.updateTrip( + isNavigating = routeModel.isNavigating(), + isRerouting = false, + hasArrived = routeModel.isArrival(), + destinationTravelEstimate = travelEstimateTrip, + stepTravelEstimate = travelEstimateStep, + destinations = mutableListOf(destination), + steps = steps, + nextStepRemainingDistance = 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) + updateTrip(tripBuilder.build()) + } + + /** + * Checks for arrival + */ + fun checkArrival() { + if (routeModel.isArrival() + && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE + ) { + stopNavigation() + settingsViewModel.onLastRouteChanged("") + routeModel.navState = routeModel.navState.copy(arrived = true) + surfaceRenderer.routeData.value = "" + } + } + /** * Stops active navigation and clears route state. * Called when user exits navigation or arrives at destination. @@ -430,6 +550,7 @@ class NavigationSession : Session(), NavigationListener { autoDriveEnabled = false } surfaceRenderer.routeData.value = "" + lastCameraSearch = 0 surfaceRenderer.viewStyle = ViewStyle.VIEW navigationScreen.navigationType = NavigationType.VIEW } @@ -470,7 +591,196 @@ class NavigationSession : Session(), NavigationListener { } } - companion object { + /** + * Handles the received route string. + * Starts navigation and invalidates the screen. + */ + override fun onRouteReceived(route: String) { + if (route.isNotEmpty()) { + this.route = route + if (routeModel.isNavigating()) { + updateRoute(route) + } else { + prepareRoute(route) + } + updateTripNavigationScreen(surfaceRenderer.lastLocation) + } + } + + /** + * Prepare route and start navigation + */ + private fun prepareRoute(route: String) { + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + routeModel.startNavigation(route) + if (routeModel.hasLegs()) { + settingsViewModel.onLastRouteChanged(route) + } + surfaceRenderer.setRouteData(routeModel.curRoute.routeGeoJson) + startNavigation() + updateTripNavigationScreen(surfaceRenderer.lastLocation) + //navigationScreen.updateTrip(surfaceRenderer.lastLocation) + } + + /** + * Update route and traffic data + */ + private fun updateRoute(route: String) { + val newRouteModel = RouteModel() + newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + newRouteModel.startNavigation(route) + routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay + //navigationScreen.updateTrip(surfaceRenderer.lastLocation) + updateTripNavigationScreen(surfaceRenderer.lastLocation) + } + + + override fun isNavigating(): Boolean = routeModel.isNavigating() + + /** + * 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.speedCamerasData.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) { + val preview = place.route //navigationViewModel.previewRoute.value + navigationViewModel.previewRoute.value = "" + val location = location(place.longitude, place.latitude) + navigationViewModel.saveRecent(carContext, place) + //currentNavigationLocation = location + if (preview.isEmpty()) { + navigationViewModel.loadRoute( + carContext, + surfaceRenderer.lastLocation, + location, + surfaceRenderer.carOrientation + ) + } else { + routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex) + onRouteReceived(preview) + } + routeModel.navState = routeModel.navState.copy(destination = place) + 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) + } + } + } + + /** + * Checks if a new route is needed based on the time since the last update. + */ + private fun checkRoute(currentDate: LocalDateTime, location: Location) { + val duration = Duration.between(currentDate, lastRouteDate) + val routeUpdate = routeModel.curRoute.summary.duration / 4 + if (duration.abs().seconds > routeUpdate) { + lastRouteDate = currentDate + val destination = location( + routeModel.navState.destination.longitude, + routeModel.navState.destination.latitude + ) + navigationViewModel.loadRoute( + carContext, + location, + destination, + surfaceRenderer.carOrientation + ) + } + } + + companion object { // URI host for deep linking var uriHost: String = "navigation" diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index ee8b1eb..c8e6647 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -66,7 +66,7 @@ import java.time.LocalDateTime class SurfaceRenderer( private var carContext: CarContext, private var lifecycle: Lifecycle, - private var routeModel: RouteCarModel, + //private var routeModel: RouteCarModel, private var viewModelStoreOwner: ViewModelStoreOwner ) : DefaultLifecycleObserver { @@ -362,13 +362,9 @@ class SurfaceRenderer( * Calculates appropriate bearing, zoom, and maintains view style. * Uses car orientation sensor if available, otherwise falls back to location bearing. */ - fun updateLocation(location: Location) { + fun updateLocation(location: Location, streetName : String) { synchronized(this) { - if (routeModel.isNavigating()) { - street.value = routeModel.currentStep().street - } else { - street.value = "" - } + street.value = streetName if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { val bearing = if (carOrientation == 999F) { if (location.hasBearing()) { @@ -402,8 +398,8 @@ class SurfaceRenderer( /** * Sets route data for active navigation and switches to VIEW mode. */ - fun setRouteData() { - routeData.value = routeModel.curRoute.routeGeoJson + fun setRouteData(routeGeoJson: String) { + routeData.value = routeGeoJson viewStyle = ViewStyle.VIEW } @@ -413,7 +409,7 @@ class SurfaceRenderer( fun activateNavigationView() { viewStyle = ViewStyle.VIEW tilt = TILT - updateLocation(lastLocation) + updateLocation(lastLocation, "") } /** @@ -481,11 +477,11 @@ class SurfaceRenderer( * Updates car location from the connected car system. * Only updates location when using OSRM routing engine. */ - fun updateCarLocation(location: Location) { + fun updateCarLocation(location: Location, streetName: String) { val repository = getSettingsRepository(carContext) val routingEngine = runBlocking { repository.routingEngineFlow.first() } if (routingEngine == RouteEngine.OSRM.ordinal) { - updateLocation(location) + updateLocation(location, streetName) } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt index 6784384..9783f03 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt @@ -1,7 +1,7 @@ package com.kouros.navigation.car.screen import androidx.car.app.navigation.model.Trip - +import com.kouros.navigation.data.Place /** A listener for navigation start and stop signals. */ @@ -14,4 +14,6 @@ interface NavigationListener { /** Updates trip information. */ fun updateTrip(trip: Trip) + + fun navigateToPlace(place: Place) } 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 0864432..64ef8c8 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 @@ -9,6 +9,7 @@ import androidx.car.app.Screen import androidx.car.app.model.Action import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.ActionStrip +import androidx.car.app.model.CarColor import androidx.car.app.model.CarIcon import androidx.car.app.model.Distance import androidx.car.app.model.Header @@ -21,7 +22,8 @@ import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.RoutingInfo -import androidx.car.app.navigation.model.Trip +import androidx.car.app.navigation.model.Step +import androidx.car.app.navigation.model.TravelEstimate import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -30,29 +32,16 @@ import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer -import com.kouros.navigation.car.navigation.RouteCarModel -import com.kouros.navigation.car.screen.observers.NavigationObserverCallback -import com.kouros.navigation.car.screen.observers.NavigationObserverManager import com.kouros.navigation.car.screen.settings.SettingsScreen import com.kouros.navigation.data.Constants -import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE -import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE import com.kouros.navigation.data.Place import com.kouros.navigation.data.ViewStyle -import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.model.NavigationViewModel -import com.kouros.navigation.model.RouteModel -import com.kouros.navigation.utils.GeoUtils -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.flow.first import kotlinx.coroutines.launch -import java.time.Duration -import java.time.LocalDateTime -import java.time.ZoneOffset -import kotlin.math.absoluteValue /** * Main screen for car navigation. @@ -61,10 +50,9 @@ import kotlin.math.absoluteValue open class NavigationScreen( carContext: CarContext, private var surfaceRenderer: SurfaceRenderer, - private var routeModel: RouteCarModel, private var listener: NavigationListener, private val navigationViewModel: NavigationViewModel -) : Screen(carContext), NavigationObserverCallback { +) : Screen(carContext) { var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) @@ -73,13 +61,6 @@ open class NavigationScreen( var recentPlace: Place = Place() var navigationType = NavigationType.VIEW - var lastTrafficDate: LocalDateTime = LocalDateTime.MIN - - var lastRouteDate: LocalDateTime = LocalDateTime.now() - var lastCameraSearch = 0 - var speedCameras = listOf() - val observerManager = NavigationObserverManager(navigationViewModel, this) - val repository = getSettingsRepository(carContext) val settingsViewModel = getSettingsViewModel(carContext) @@ -89,13 +70,31 @@ open class NavigationScreen( private var tripSuggestion = false private var tripSuggestionCalled = false - - private var routingEngine = 0 - - private var showTraffic = false; private var arrivalTimer: CountDownTimer? = null private var reRouteTimer: CountDownTimer? = null + private var isNavigating = false + private var isRerouting = false + private var hasArrived = false + + private lateinit var destinations: MutableList + + private lateinit var stepRemainingDistance: Distance + + private lateinit var destinationTravelEstimate: TravelEstimate + + private lateinit var stepTravelEstimate: TravelEstimate + + private var shouldShowNextStep = false + + private var shouldShowLanes = false + + private lateinit var steps: MutableList + + var junctionImage: CarIcon? = null + + var backGroundColor = CarColor.BLUE + val observerRecentPlaces = Observer> { newPlaces -> recentPlaces.addAll(newPlaces) if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { @@ -106,25 +105,17 @@ open class NavigationScreen( } init { - observerManager.attachAllObservers(this) lifecycleScope.launch { settingsViewModel.tripSuggestion.first() - settingsViewModel.routingEngine.first() } repository.distanceModeFlow.asLiveData().observe(this, Observer { distanceMode = it }) - repository.trafficFlow.asLiveData().observe(this, Observer { - showTraffic = it - }) repository.tripSuggestionFlow.asLiveData().observe(this, Observer { navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) tripSuggestion = it }) - repository.routingEngineFlow.asLiveData().observe(this, Observer { - routingEngine = it - }) lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStop(owner: LifecycleOwner) { arrivalTimer?.cancel() @@ -164,12 +155,11 @@ open class NavigationScreen( 0, { stopNavigation() }) ) - updateTrip() return NavigationTemplate.Builder() .setNavigationInfo( getRoutingInfo() ) - .setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode)) + .setDestinationTravelEstimate(destinationTravelEstimate) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip( mapActionStrip( @@ -185,7 +175,7 @@ open class NavigationScreen( ) }) ) - .setBackgroundColor(routeModel.backGroundColor()) + .setBackgroundColor(backGroundColor) .build() } @@ -194,7 +184,7 @@ open class NavigationScreen( */ private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { return NavigationTemplate.Builder() - .setBackgroundColor(routeModel.backGroundColor()) + .setBackgroundColor(backGroundColor) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip( mapActionStrip( @@ -221,7 +211,7 @@ open class NavigationScreen( arrivalTimer = object : CountDownTimer(8000, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { - routeModel.navState = routeModel.navState.copy(arrived = false) + // routeModel.navState = routeModel.navState.copy(arrived = false) navigationType = NavigationType.VIEW invalidate() } @@ -235,8 +225,8 @@ open class NavigationScreen( */ fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { var street = "" - if (routeModel.navState.destination.street != null) { - street = routeModel.navState.destination.street!! + if (destinations.first().address != null) { + street = destinations.first().address.toString() } return NavigationTemplate.Builder() .setNavigationInfo( @@ -255,7 +245,7 @@ open class NavigationScreen( ) .build() ) - .setBackgroundColor(routeModel.backGroundColor()) + // .setBackgroundColor(routeModel.backGroundColor()) .setActionStrip(actionStripBuilder.build()) .setMapActionStrip( mapActionStrip( @@ -299,7 +289,7 @@ open class NavigationScreen( createNavigateAction(it) ) .setOnClickListener { - navigateToPlace(it) + listener.navigateToPlace(it) } listBuilder.addItem( row.build() @@ -349,7 +339,7 @@ open class NavigationScreen( return NavigationTemplate.Builder() .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setActionStrip(actionStripBuilder.build()) - .setBackgroundColor(routeModel.backGroundColor()) + // .setBackgroundColor(routeModel.backGroundColor()) .build() } @@ -357,16 +347,13 @@ open class NavigationScreen( * Builds and returns RoutingInfo based on the current step and distance. */ fun getRoutingInfo(): RoutingInfo { - val distance = - formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance()) val routingInfo = RoutingInfo.Builder() .setCurrentStep( - routeModel.currentStep(carContext = carContext), - Distance.create(distance.first, distance.second) + steps.first(), + stepRemainingDistance ) - if (routeModel.navState.nextStep) { - val nextStep = routeModel.nextStep(carContext = carContext) - routingInfo.setNextStep(nextStep) + if (shouldShowNextStep && steps.size > 1) { + routingInfo.setNextStep(steps[1]) } return routingInfo.build() } @@ -391,7 +378,7 @@ open class NavigationScreen( ) ) { obj: Any? -> if (obj != null) { - navigateToPlace(place) + listener.navigateToPlace(place) } } } @@ -475,44 +462,18 @@ open class NavigationScreen( ) // result see observer } else { - navigateToPlace(place) + listener.navigateToPlace(place) } } } } - /** - * Loads a route to the specified place and sets it as the destination. - */ - fun navigateToPlace(place: Place) { - val preview = navigationViewModel.previewRoute.value - navigationViewModel.previewRoute.value = "" - val location = location(place.longitude, place.latitude) - navigationViewModel.saveRecent(carContext, place) - currentNavigationLocation = location - if (preview.isNullOrEmpty()) { - navigationViewModel.loadRoute( - carContext, - surfaceRenderer.lastLocation, - location, - surfaceRenderer.carOrientation - ) - } else { - routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex) - navigationViewModel.route.value = preview - } - routeModel.navState = routeModel.navState.copy(destination = place) - surfaceRenderer.activateNavigationView() - invalidate() - } - /** * Stops navigation, resets state, and notifies listeners. */ fun stopNavigation() { navigationType = NavigationType.VIEW listener.stopNavigation() - lastCameraSearch = 0 invalidate() } @@ -550,227 +511,40 @@ open class NavigationScreen( ) } + /** * Updates navigation state with the current location, checks for arrival, and traffic updates. */ - fun updateTrip(location: Location) { - val currentDate = LocalDateTime.now(ZoneOffset.UTC) - checkRoute(currentDate, location) - checkTraffic(currentDate, location) - - updateSpeedCamera(location) - - routeModel.updateLocation(location, navigationViewModel) - checkArrival() - - invalidate() - } - - /** - * Checks if a new route is needed based on the time since the last update. - */ - private fun checkRoute(currentDate: LocalDateTime, location: Location) { - val duration = Duration.between(currentDate, lastRouteDate) - val routeUpdate = routeModel.curRoute.summary.duration / 4 - if (duration.abs().seconds > routeUpdate) { - lastRouteDate = currentDate - val destination = location( - routeModel.navState.destination.longitude, - routeModel.navState.destination.latitude - ) - navigationViewModel.loadRoute( - carContext, - location, - destination, - surfaceRenderer.carOrientation - ) - } - } - - /** - * 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) - } - } - - /** - * Checks for arrival - */ - fun checkArrival() { - if (routeModel.isArrival() - && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE - ) { - listener.stopNavigation() - settingsViewModel.onLastRouteChanged("") - routeModel.navState = routeModel.navState.copy(arrived = true) - surfaceRenderer.routeData.value = "" - navigationType = NavigationType.ARRIVAL - invalidate() - } - } - - /** - * Updates the trip information and notifies the listener with a new Trip object. - * This includes destination name, address, travel estimate, and loading status. - */ - private fun updateTrip() { - if (routeModel.isNavigating() && !routeModel.navState.destination.name.isNullOrEmpty()) { - val tripBuilder = Trip.Builder() - val destination = Destination.Builder() - .setName(routeModel.navState.destination.name ?: "") - .setAddress(routeModel.navState.destination.street ?: "") - .build() - tripBuilder.addDestination( - destination, - routeModel.travelEstimateTrip(carContext, distanceMode) - ) - tripBuilder.setLoading(false) - tripBuilder.setCurrentRoad(routeModel.currentStep.street) - tripBuilder.addStep(routeModel.currentStep(carContext), routeModel.travelEstimateStep(carContext, distanceMode )) - listener.updateTrip(tripBuilder.build()) - } - } - - /** - * 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, + fun updateTrip( + isNavigating: Boolean, + isRerouting: Boolean, + hasArrived: Boolean, + destinations: MutableList, + steps: MutableList, + destinationTravelEstimate: TravelEstimate, + stepTravelEstimate: TravelEstimate, + nextStepRemainingDistance: Distance, + shouldShowNextStep: Boolean, + shouldShowLanes: Boolean, + junctionImage: CarIcon?, + backGroundColor: CarColor ) { - 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) - } - } - } + this.isNavigating = isNavigating + this.isRerouting = isRerouting + this.hasArrived = hasArrived + this.destinations = destinations + this.steps = steps + stepRemainingDistance = nextStepRemainingDistance + this.destinationTravelEstimate = destinationTravelEstimate + this.stepTravelEstimate = stepTravelEstimate + this.shouldShowNextStep = shouldShowNextStep + this.shouldShowLanes = shouldShowLanes + this.junctionImage = junctionImage + this.backGroundColor = backGroundColor - /** - * Handles the received route string. - * Starts navigation and invalidates the screen. - */ - override fun onRouteReceived(route: String) { - if (route.isNotEmpty()) { - if (routeModel.isNavigating()) { - updateRoute(route) - } else { - prepareRoute(route) - } - invalidate() - } - } - - /** - * Prepare route and start navigation - */ - private fun prepareRoute(route: String) { - routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) navigationType = NavigationType.NAVIGATION - routeModel.startNavigation(route) - if (routeModel.hasLegs()) { - settingsViewModel.onLastRouteChanged(route) - } - surfaceRenderer.setRouteData() - listener.startNavigation() - } - - /** - * Update route and traffic data - */ - private fun updateRoute(route: String) { - val newRouteModel = RouteModel() - newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine) - navigationType = NavigationType.NAVIGATION - newRouteModel.startNavigation(route) - routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay - } - - /** - * Checks if navigation is currently active. - */ - override fun isNavigating(): Boolean = routeModel.isNavigating() - - /** - * 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.speedCamerasData.value = speedData - } - - /** - * Handles received maximum speed data and updates the surface renderer. - */ - override fun onMaxSpeedReceived(speed: Int) { - surfaceRenderer.maxSpeed.value = speed - } - - /** - * Invalidates the screen. - */ - override fun invalidateScreen() { invalidate() } - } /** diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 25764ab..fbe820c 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -347,6 +347,7 @@ class RoutePreviewScreen( private fun onNavigate(index: Int) { destination.routeIndex = index + destination.route = navigationViewModel.previewRoute.value.toString() setResult(destination) finish() } 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 fc6e81e..d618d79 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.NavigationSession import com.kouros.navigation.model.NavigationViewModel /** @@ -17,18 +18,14 @@ class NavigationObserverManager( val speedCameraObserver = SpeedCameraObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback) - /** - * Attaches all observers to the ViewModel. - * Call this from NavigationScreen's init block or lifecycle method. - */ - fun attachAllObservers(screen: androidx.car.app.Screen) { - viewModel.route.observe(screen, routeObserver) - viewModel.traffic.observe(screen, trafficObserver) - viewModel.placeLocation.observe(screen, placeSearchObserver) - viewModel.speedCameras.observe(screen, speedCameraObserver) - viewModel.maxSpeed.observe(screen, maxSpeedObserver) + fun attachAllObservers(session: NavigationSession) { + viewModel.route.observe(session, routeObserver) + viewModel.traffic.observe(session, trafficObserver) + viewModel.placeLocation.observe(session, placeSearchObserver) + viewModel.speedCameras.observe(session, speedCameraObserver) + viewModel.maxSpeed.observe(session, maxSpeedObserver) } - + /** * Detaches all observers from the ViewModel. * Call this when the screen is being destroyed. diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 49fdd30..d900f25 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -44,7 +44,8 @@ data class Place( var distance: Float = 0F, //var avatar: Uri? = null, var lastDate: Long = 0, - var routeIndex: Int = 0 + var routeIndex: Int = 0, + var route: String = "", ) data class ContactData(