Compare commits

8 Commits
Author SHA1 Message Date
Dimitris b99ebfd36f Service 2026-04-04 13:16:15 +02:00
Dimitris 69b27d3b6c Service 2026-04-04 09:39:54 +02:00
Dimitris 8b886c36b1 Service 2026-04-03 13:23:26 +02:00
Dimitris 2ce079a7c1 Service 2026-04-03 12:30:39 +02:00
Dimitris 8af2d3ad0b Service 2026-04-03 10:00:00 +02:00
Dimitris 1d67b3cc06 Arrival Issue 2026-04-03 09:59:32 +02:00
Dimitris a4227c80d3 NavigationService 2026-04-03 09:57:57 +02:00
Dimitris 757c4c8d8d NavigationService 2026-04-03 09:57:52 +02:00
14 changed files with 1336 additions and 31 deletions
+6
View File
@@ -45,6 +45,12 @@
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:exported="true"> android:exported="true">
</service> </service>
<service
android:name=".car.navigation.NavigationService"
android:enabled="true"
android:foregroundServiceType="location"
android:exported="true">
</service>
</application> </application>
</manifest> </manifest>
@@ -0,0 +1,97 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.location.LocationManager
import androidx.car.app.CarContext
import androidx.core.location.LocationListenerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.car.navigation.NavigationService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/**
* Manages device GPS location updates for navigation.
* Coordinates with car hardware sensors to avoid duplicate location sources.
*
* @param carContext The car context for accessing system services
* @param serviceOwner Owner of the lifecycle for coroutine management
* @param shouldUseCarLocationFlow Flow indicating whether car location hardware should be used
* @param onLocationUpdate Callback invoked when location updates are received
* @param onInitialLocation Callback invoked with the last known location when starting
*/
class DeviceLocationManagerService(
private val carContext: CarContext,
private val onLocationUpdate: (Location) -> Unit,
private val onInitialLocation: (Location) -> Unit
) {
private val locationManager: LocationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var shouldUseDeviceLocation = true
private var isListening = false
/**
* Location listener that receives GPS updates from the device.
* Only processes location if car location hardware is not being used.
*/
private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
if (shouldUseDeviceLocation) {
onLocationUpdate(location)
}
}
init {
}
/**
* Starts requesting location updates from device GPS.
* Provides initial location via callback and then starts continuous updates.
*
* @param minTimeMs Minimum time interval between updates in milliseconds (default: 500ms)
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5.0f) {
if (isListening) return
// Get and deliver last known location first
val lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
if (lastLocation != null) {
onInitialLocation(lastLocation)
onLocationUpdate(lastLocation)
}
// Start continuous location updates
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceM,
locationListener
)
isListening = true
}
/**
* Stops receiving location updates from device GPS.
* Should be called when the session is destroyed to prevent memory leaks.
*/
fun stopLocationUpdates() {
if (!isListening) return
locationManager.removeUpdates(locationListener)
isListening = false
}
/**
* Checks if location updates are currently active.
*/
fun isListeningForUpdates(): Boolean = isListening
}
@@ -35,7 +35,8 @@ class NavigationCarAppService : CarAppService() {
return ClusterSession() return ClusterSession()
} else { } else {
createNotificationChannel() createNotificationChannel()
return NavigationSession() //return NavigationSession()
return NavigationServiceSession()
} }
} }
@@ -0,0 +1,724 @@
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.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.lifecycleScope
import com.kouros.navigation.car.navigation.NavigationService
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.GMS_CAR_SPEED_PERMISSION
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.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.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
* Main session for Android Auto/Automotive OS navigation.
* Manages the lifecycle of the navigation session, including location updates,
* car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events.
*/
class NavigationServiceSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
// Handles map surface rendering on the car display
lateinit var surfaceRenderer: SurfaceRenderer
// Manages car hardware sensors (location, compass, speed)
lateinit var carSensorManager: CarSensorManager
var initialLocation = true;
lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var recentPlaces = mutableListOf<Place>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var destination = Place()
var notificationActive = false
var navigationService: NavigationService? = null
val serviceListener: NavigationService.Listener = object : NavigationService.Listener {
override fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
) {
navigationScreen.updateTrip(
isNavigating = isNavigating,
isRerouting = isRerouting,
hasArrived = hasArrived,
destinationTravelEstimate = destinationTravelEstimate,
stepTravelEstimate = stepTravelEstimate,
destinations = destinations,
steps = steps,
stepRemainingDistance = stepRemainingDistance,
shouldShowNextStep = shouldShowNextStep,
shouldShowLanes = shouldShowLanes,
junctionImage = junctionImage,
backGroundColor = backGroundColor
)
}
override fun updateServiceLocation(location: Location) {
if (initialLocation) {
navigationViewModel.loadRecentPlaces(
carContext,
location,
surfaceRenderer.carOrientation,
)
initialLocation = false
}
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, recentPlaces)
) { 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, destination)
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
Log.d(TAG, "stopNavigation")
navigationService!!.stopNavigation()
surfaceRenderer.navigation = false
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
Log.d(TAG, "end stopNavigation")
}
override fun updateTrip(trip: Trip) {
}
/**
* Recalculates a route for the specified place.
*/
override fun recalcRoute(destination: Place) {
val destination = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
listOf(destination),
surfaceRenderer.carOrientation
)
}
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
Log.d(TAG, "onRouteReceived")
if (route.isNotEmpty()) {
prepareRoute(route)
}
}
override fun isNavigating(): Boolean {
return navigationService!!.isNavigating()
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
this.route = route
startNavigation()
surfaceRenderer.setRouteData(navigationService!!.routeModel.curRoute.routeGeoJson)
}
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
if (traffic.isNotEmpty()) {
surfaceRenderer.setTrafficData(traffic)
}
}
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place)
}
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
cameras.forEach {
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCameraData.value = speedData
}
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed
}
override fun onRecentPlacesReceived(places: List<Place>) {
Log.d(TAG, "onRecentPlacesReceived ${places.size}")
recentPlaces = places.toMutableList()
navigationScreen.recentPlaces = places.toMutableList()
navigationScreen.invalidate()
}
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)
destination = place
// routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
//routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
// routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"
// URI scheme for deep linking
var uriScheme: String = "samples"
}
}
@@ -1,6 +1,5 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.Manifest
import android.Manifest.permission import android.Manifest.permission
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -10,7 +9,6 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager import androidx.car.app.navigation.NavigationManager
@@ -68,7 +66,6 @@ import kotlinx.coroutines.launch
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.collections.listOf
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -755,6 +752,9 @@ class NavigationSession : CarSession(), NavigationListener, NavigationObserverCa
surfaceRenderer.maxSpeed.value = speed surfaceRenderer.maxSpeed.value = speed
} }
override fun onRecentPlacesReceived(places: List<Place>) {
}
override fun invalidateScreen() { override fun invalidateScreen() {
navigationScreen.invalidate() navigationScreen.invalidate()
} }
@@ -372,6 +372,7 @@ class SurfaceRenderer(
* Uses car orientation sensor if available, otherwise falls back to location bearing. * Uses car orientation sensor if available, otherwise falls back to location bearing.
*/ */
fun updateLocation(location: Location, streetName: String) { fun updateLocation(location: Location, streetName: String) {
Log.d(TAG, "updateLocation Surface $location $streetName")
synchronized(this) { synchronized(this) {
street.value = streetName street.value = streetName
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
@@ -0,0 +1,349 @@
package com.kouros.navigation.car.navigation
import android.Manifest
import android.app.NotificationChannel
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.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.model.Distance.UNIT_METERS
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.Observer
import androidx.lifecycle.asLiveData
import com.kouros.navigation.car.DeviceLocationManagerService
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.data.Place
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 com.kouros.navigation.utils.location
import kotlin.collections.copy
import kotlin.compareTo
class NavigationService : Service() {
val TAG: String = "NavigationService"
val DEEP_LINK_ACTION: String = ("com.kouros.navigation.car.navigation"
+ ".NavigationDeepLinkAction")
val channelId: String = "NavigationServiceChannel"
/** The identifier for the navigation notification displayed for the foreground service. */
val NAV_NOTIFICATION_ID: Int = 87356325
/** The identifier for the non-navigation notifications, such as a traffic accident warning. */
val NOTIFICATION_ID: Int = 71653346
// Constants for location broadcast
val PACKAGE_NAME: String =
"androidx.car.app.sample.navigation.common.nav.navigationservice"
val EXTRA_STARTED_FROM_NOTIFICATION: String = PACKAGE_NAME + ".started_from_notification"
val CANCEL_ACTION: String = "CANCEL"
private var notificationManager: NotificationManager? = null
private var carContext: CarContext? = null
var autoDriveEnabled = false
val simulation = Simulation()
private lateinit var listener: Listener
// Model for managing route state and navigation logic for Android Auto
var routeModel = RouteCarModel()
// Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManagerService
var currentLocation = location(0.0, 0.0)
lateinit var navigationViewModel: NavigationViewModel
private lateinit var navigationManager: NavigationManager
private var navigationManagerInitialized = false
var binder: IBinder = LocalBinder()
/** A listener for the navigation state changes. */
interface Listener {
/** Callback called when the navigation state changes. */
fun navigationStateChanged(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
)
fun updateServiceLocation(location: Location)
}
/**
* Class used for the client Binder. Since this service runs in the same process as its clients,
* we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
val service: NavigationService
get() = this@NavigationService
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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 {
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() {
val serviceChannel = NotificationChannel(
"CHANNEL_ID",
"Location Service Channel",
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
/** Sets the [CarContext] to use while the service is connected. */
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun setCarContext(
carContext: CarContext,
listener: Listener
) {
Log.d(TAG, "in setCarContext")
this.carContext = carContext
navigationViewModel = getViewModel(carContext)
this.listener = listener
deviceLocationManager = DeviceLocationManagerService(
carContext = carContext,
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
updateLocation(location)
}
)
deviceLocationManager.startLocationUpdates()
navigationManagerInitialized = true
navigationManager =
carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
Log.d(TAG, "onAutoDriveEnabled")
// Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
simulation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
}
private fun simulation() {
simulation.gpxSimulation {
listener.updateServiceLocation(it)
}
}
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()
}
}
})
// Uncomment if navigating
// mNavigationManager.navigationStarted();
}
/** Clears the currently used {@link CarContext}. */
fun clearCarContext() {
Log.i(TAG, "clearContext");
carContext = null;
navigationManager.clearNavigationManagerCallback();
}
/** Starts navigation. */
fun startNavigation(route: String, destination: Place) {
Log.i(TAG, "Starting Navigation")
startService(Intent(applicationContext, NavigationService::class.java))
routeModel.navState = routeModel.navState.copy(destination = destination)
routeModel.navState = routeModel.navState.copy(routingEngine = 2)
routeModel.startNavigation(route)
if (routeModel.isNavigating()) {
routeModel.updateLocation(currentLocation, navigationViewModel)
listener.navigationStateChanged(
isNavigating = true,
isRerouting = false,
hasArrived = false,
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
}
}
/** Starts navigation. */
fun stopNavigation() {
if (autoDriveEnabled) {
autoDriveEnabled = false
}
if (navigationManagerInitialized)
navigationManager.navigationEnded()
listener.navigationStateChanged(
isNavigating = false,
isRerouting = false,
hasArrived = false,
destinations = emptyList<Destination>().toMutableList(),
steps = emptyList<Step>().toMutableList(),
destinationTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepTravelEstimate = routeModel.travelEstimate(carContext!!, 0.0, 0),
stepRemainingDistance = Distance.create(0.0, UNIT_METERS),
shouldShowNextStep = false,
shouldShowLanes = false,
junctionImage = null,
backGroundColor = CarColor.BLUE
)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation")
currentLocation = location
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
}
listener.navigationStateChanged(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinations = mutableListOf(routeModel.getDestination()),
destinationTravelEstimate = routeModel.getTravelEstimateTrip(carContext!!),
stepTravelEstimate = routeModel.getTravelEstimateStep(carContext!!),
steps = routeModel.getSteps(carContext!!),
stepRemainingDistance = routeModel.getDistance(),
shouldShowNextStep = false,
shouldShowLanes = false,
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(
routeModel.getDestination(),
routeModel.getTravelEstimateTrip(carContext!!)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.getDestination().name.toString())
tripBuilder.addStep(routeModel.getSteps(carContext!!).first(), routeModel.getTravelEstimateStep(carContext!!))
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)
}
}
}
@@ -18,6 +18,7 @@ import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.DurationSpan import androidx.car.app.model.DurationSpan
import androidx.car.app.model.ForegroundCarColorSpan import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Lane import androidx.car.app.navigation.model.Lane
import androidx.car.app.navigation.model.LaneDirection import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
@@ -99,7 +100,6 @@ class RouteCarModel : RouteModel() {
} }
fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate { fun travelEstimate(carContext: CarContext, timeLeft: Double, distanceMode: Int): TravelEstimate {
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance()) val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
@@ -134,6 +134,37 @@ class RouteCarModel : RouteModel() {
} }
return travelBuilder.build() return travelBuilder.build()
} }
fun getSteps(carContext: CarContext): MutableList<Step> {
val steps = mutableListOf<Step>()
steps.add(currentStep(carContext))
if (navState.nextStep) {
steps.add(nextStep(carContext = carContext))
}
return steps
}
fun getDistance(): Distance {
val distance =
formattedDistance(0, routeCalculator.leftStepDistance())
return Distance.create(distance.first, distance.second)
}
fun getTravelEstimateTrip(carContext: CarContext): TravelEstimate {
return travelEstimateTrip(carContext, 0)
}
fun getTravelEstimateStep(carContext: CarContext): TravelEstimate {
return travelEstimateStep(carContext, 0)
}
fun getDestination(): Destination {
return Destination.Builder()
.setName(navState.destination.name)
.setAddress(navState.destination.street)
.build()
}
private fun createDelay(delay: Int): CarText { private fun createDelay(delay: Int): CarText {
val delayBuilder = SpannableStringBuilder() val delayBuilder = SpannableStringBuilder()
delayBuilder.append( delayBuilder.append(
@@ -3,8 +3,10 @@ package com.kouros.navigation.car.navigation
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.SystemClock import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.data.BuildConfig import com.kouros.data.BuildConfig
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx import io.ticofab.androidgpxparser.parser.domain.Gpx
@@ -27,8 +29,9 @@ class Simulation {
) { ) {
if (routeModel.navState.route.isRouteValid()) { if (routeModel.navState.route.isRouteValid()) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
//gpxSimulation(routeModel, lifecycleScope, updateLocation) gpxSimulation(routeModel, lifecycleScope, updateLocation)
currentSimulation(routeModel, lifecycleScope, updateLocation) //gpxSimulation(updateLocation)
//currentSimulation(routeModel, lifecycleScope, updateLocation)
} else { } else {
currentSimulation(routeModel, lifecycleScope, updateLocation) currentSimulation(routeModel, lifecycleScope, updateLocation)
} }
@@ -66,7 +69,7 @@ class Simulation {
lastLocation = fakeLocation lastLocation = fakeLocation
} }
} }
// routeModel.stopNavigation() // routeModel.stopNavigation()
} }
} }
@@ -87,7 +90,7 @@ class Simulation {
simulationJob?.join() simulationJob?.join()
} }
simulationJob?.cancel() simulationJob?.cancel()
simulationJob =lifecycleScope.launch() { simulationJob = lifecycleScope.launch() {
var lastLocation = Location(LocationManager.FUSED_PROVIDER) var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f var curBearing = 0f
val parser = GPXParser() val parser = GPXParser()
@@ -118,7 +121,7 @@ class Simulation {
updateLocation(fakeLocation) updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second) // Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) { if (duration > 100) {
// delay(duration / 4) // delay(duration / 4)
} }
delay(500) delay(500)
lastTime = p.time lastTime = p.time
@@ -134,4 +137,62 @@ class Simulation {
fun stopSimulation() { fun stopSimulation() {
simulationJob?.cancel() simulationJob?.cancel()
} }
fun gpxSimulation(
updateLocation: (Location) -> Unit
) {
Runnable {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
val parser = GPXParser()
val parsedGpx: Gpx? =
parser.parse(route.byteInputStream())
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val ext = p.extensions
var curSpeed = 0F
if (ext != null) {
curSpeed = ext.speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = p.latitude
longitude = p.longitude
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = curSpeed
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) {
// delay(duration / 4)
}
Thread.sleep(2000)
lastTime = p.time
lastLocation = fakeLocation
}
}
}
}
}.run()
}
} }
@@ -56,7 +56,6 @@ open class NavigationScreen(
) : Screen(carContext) { ) : Screen(carContext) {
var recentPlaces = mutableListOf<Place>() var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place() var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
@@ -85,6 +84,7 @@ open class NavigationScreen(
private var showAlternativeRoute = false private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
Log.d(TAG, "NavigationScreen 4")
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
tripSuggestionCalled = true tripSuggestionCalled = true
@@ -99,9 +99,11 @@ open class NavigationScreen(
} }
repository.tripSuggestionFlow.asLiveData().observe(this, Observer { repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
Log.d(TAG, "NavigationScreen 3")
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it tripSuggestion = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer { repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it showAlternativeRoute = it
}) })
@@ -117,6 +119,7 @@ open class NavigationScreen(
* Returns the appropriate template based on the current navigation state. * Returns the appropriate template based on the current navigation state.
*/ */
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
Log.d(TAG, "NavigationScreen 2")
val actionStripBuilder = createActionStripBuilder({ val actionStripBuilder = createActionStripBuilder({
createAction( createAction(
carContext, carContext,
@@ -151,10 +154,11 @@ open class NavigationScreen(
.setDestinationTravelEstimate(destinationTravelEstimate) .setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip(carContext, mapActionStrip(
carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
Action.Builder() Action.Builder()
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24)) .setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
.setFlags(0) .setFlags(0)
.setOnClickListener { .setOnClickListener {
@@ -172,17 +176,18 @@ open class NavigationScreen(
* Creates and returns a template for the default view state. * Creates and returns a template for the default view state.
*/ */
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
val mapActionStrip = mapActionStrip(carContext, val mapActionStrip = mapActionStrip(
surfaceRenderer.viewStyle, carContext,
{ zoomPlus() }, { zoomMinus() }, { surfaceRenderer.viewStyle,
createAction( { zoomPlus() }, { zoomMinus() }, {
carContext = carContext, R.drawable.ic_recenter_24, createAction(
0, carContext = carContext, R.drawable.ic_recenter_24,
onClickAction = { 0,
surfaceRenderer.viewStyle = ViewStyle.VIEW onClickAction = {
invalidate() surfaceRenderer.viewStyle = ViewStyle.VIEW
}) invalidate()
}) })
})
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
@@ -238,7 +243,8 @@ open class NavigationScreen(
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip(carContext, mapActionStrip(
carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( createAction(
@@ -299,7 +305,8 @@ open class NavigationScreen(
val builder = MapWithContentTemplate.Builder() val builder = MapWithContentTemplate.Builder()
.setContentTemplate(contentTemplate) .setContentTemplate(contentTemplate)
.setActionStrip( .setActionStrip(
mapActionStrip(carContext, mapActionStrip(
carContext,
ViewStyle.VIEW, ViewStyle.VIEW,
{ settingsAction() }, { settingsAction() },
{ {
@@ -338,10 +345,12 @@ open class NavigationScreen(
*/ */
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
val routingInfo = RoutingInfo.Builder() val routingInfo = RoutingInfo.Builder()
.setCurrentStep( if (steps.isNotEmpty()) {
routingInfo.setCurrentStep(
steps.first(), steps.first(),
stepRemainingDistance stepRemainingDistance
) )
}
if (shouldShowNextStep && steps.size > 1) { if (shouldShowNextStep && steps.size > 1) {
routingInfo.setNextStep(steps[1]) routingInfo.setNextStep(steps[1])
} }
@@ -489,7 +498,6 @@ open class NavigationScreen(
} }
/** /**
* Updates navigation state with the current location, checks for arrival, and traffic updates. * Updates navigation state with the current location, checks for arrival, and traffic updates.
*/ */
@@ -519,7 +527,6 @@ open class NavigationScreen(
this.shouldShowLanes = shouldShowLanes this.shouldShowLanes = shouldShowLanes
this.junctionImage = junctionImage this.junctionImage = junctionImage
this.backGroundColor = backGroundColor this.backGroundColor = backGroundColor
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
invalidate() invalidate()
} }
@@ -1,7 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
/** /**
@@ -26,6 +25,8 @@ interface NavigationObserverCallback {
/** Called when max speed is updated */ /** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int) fun onMaxSpeedReceived(speed: Int)
fun onRecentPlacesReceived(places: List<Place>)
/** Called to request UI invalidation/refresh */ /** Called to request UI invalidation/refresh */
fun invalidateScreen() fun invalidateScreen()
@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.CarSession
import com.kouros.navigation.car.NavigationSession import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
@@ -18,12 +19,18 @@ class NavigationObserverManager(
val speedCameraObserver = SpeedCameraObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback)
fun attachAllObservers(session: NavigationSession) { val recentPlacesObserver = RecentPlacesObserver(callback)
fun attachAllObservers(session: CarSession) {
viewModel.route.observe(session, routeObserver) viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(session, trafficObserver) viewModel.traffic.observe(session, trafficObserver)
viewModel.placeLocation.observe(session, placeSearchObserver) viewModel.placeLocation.observe(session, placeSearchObserver)
viewModel.speedCameras.observe(session, speedCameraObserver) viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.maxSpeed.observe(session, maxSpeedObserver) viewModel.maxSpeed.observe(session, maxSpeedObserver)
viewModel.recentPlaces.observe(session, recentPlacesObserver)
} }
/** /**
@@ -0,0 +1,18 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Place
/**
* Observer for route updates. Triggers navigation start when a non-empty route is received.
*/
class RecentPlacesObserver(
private val callback: NavigationObserverCallback
) : Observer<List<Place>> {
override fun onChanged(value: List<Place>) {
if (value.isNotEmpty()) {
callback.onRecentPlacesReceived(value)
}
}
}
@@ -3,6 +3,7 @@ package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore //import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -10,6 +11,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.NavigationRepository import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Places import com.kouros.navigation.data.Places