Compare commits

..

7 Commits

Author SHA1 Message Date
Dimitris
8ca450cd10 Camerat Duration 3 seconds 2026-03-28 13:43:36 +01:00
Dimitris
94d6d6d311 Overlay icons with paint 2026-03-28 13:37:05 +01:00
Dimitris
d81d33df43 Arrival Issue 2026-03-28 12:41:12 +01:00
Dimitris
5317a14fb3 Arrival Issue 2026-03-28 11:17:51 +01:00
Dimitris
90010d91b7 Notification 2026-03-27 13:40:14 +01:00
Dimitris
2348d3b633 Duration Map 2026-03-27 07:17:53 +01:00
Dimitris
263b5b576d Navigation Screen to Session, Remove NavigationService 2026-03-26 17:16:04 +01:00
18 changed files with 662 additions and 459 deletions

View File

@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 83
versionName = "0.2.0.83"
versionCode = 87
versionName = "0.2.0.87"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -41,8 +41,7 @@
</intent-filter>
</activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:name=".car.NavigationNotificationService"
android:foregroundServiceType="location"
android:exported="true">
</service>

View File

@@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.car.permission.CAR_SPEED"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature
android:name="android.hardware.type.automotive"
@@ -71,8 +72,7 @@
android:value="true" />
</activity>
<service
android:name="com.kouros.navigation.car.navigation.NavigationService"
android:enabled="true"
android:name=".car.NavigationNotificationService"
android:foregroundServiceType="location"
android:exported="true">
</service>

View File

@@ -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()
}

View File

@@ -67,7 +67,7 @@ class DeviceLocationManager(
* @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/
@SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 500, minDistanceM: Float = 5f) {
fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
if (isListening) return
// Get and deliver last known location first

View File

@@ -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 {

View File

@@ -9,6 +9,7 @@ import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.util.Log
import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent
@@ -16,6 +17,7 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.TAG
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
@@ -44,17 +46,12 @@ class NavigationNotificationService : Service() {
Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val message = intent.getStringExtra("EXTRA_MESSAGE") ?: "Navigating..."
initNotifications(this)
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, mNotificationCount).build()
)
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
val notification = getNavigationNotification(this, message)
// This updates the existing notification if the service is already running
CarNotificationManager.from(this).notify(NAV_NOTIFICATION_ID, notification)
startForeground(NAV_NOTIFICATION_ID, notification.build())
return START_NOT_STICKY
}
@@ -71,11 +68,12 @@ class NavigationNotificationService : Service() {
*/
internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
Log.d(TAG, "Notification handleMessage: $msg")
if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount)
getNavigationNotification(context, "Nachricht")
)
mNotificationCount++
mHandler.sendMessageDelayed(
@@ -96,6 +94,12 @@ class NavigationNotificationService : Service() {
val mOnlyAlertOnce: Boolean
)
fun startForeground(message: String) {
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, message).build()
)
}
companion object {
private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
@@ -124,21 +128,40 @@ class NavigationNotificationService : Service() {
CarNotificationManager.from(context).createNotificationChannel(navChannel)
}
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, message: String): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val distance = formatter.format((10) * 0.1) + "km"
return DirectionInfo(
message,
distance,
R.drawable.navigation_48px,
false
)
}
/** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification(
context: Context, notificationCount: Int
context: Context
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, notificationCount)
val directionInfo = getDirectionInfo(context, "Test")
// Set an intent to open the car app. The app receives this intent when the user taps the
// 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 +169,7 @@ class NavigationNotificationService : Service() {
)
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
NavigationCarAppService().intentActionNavNotificationOpenApp
)
),
0
@@ -179,54 +202,59 @@ class NavigationNotificationService : Service() {
)
}
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val repeatingCount = notificationCount % 35
if (repeatingCount in 0..<10) {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.stop_action_title),
distance,
R.drawable.arrow_back_24px,
repeatingCount > 0
fun getNavigationNotification(
context: Context, message: String
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, message)
// Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp(
context,
NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
Intent(
NavigationCarAppService().intentActionNavNotificationOpenApp
).setComponent(
ComponentName(
context,
NavigationCarAppService()::class.java
)
} else if (repeatingCount in 10..<20) {
// Distance decreases from 5km to 0.5km
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().intentActionNavNotificationOpenApp
)
} else if (repeatingCount in 20..<25) {
// Distance decreases from 200m to 40m
val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.navigation_48px, /* onlyAlertOnce= */
repeatingCount > 20
),
0
)
} else {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.charging_station),
distance,
R.drawable.local_gas_station_24,
repeatingCount > 25
return builder
// This title, text, and icon will be shown in both phone and car screen. These
// values can
// be overridden in the extender below, to customize notifications in the car
// screen.
.setContentTitle(directionInfo.mTitle)
.setContentText(directionInfo.mDistance)
.setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
// CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
// navigating on
// the background.
// These values cannot be overridden in the extender.
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
// car screen. This value cannot be overridden in the extender.
.setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
// The extender also allows various customizations, such as showing different title
// or icon on the car screen.
.extend(
CarAppExtender.Builder()
.setContentIntent(pendingIntent)
.build()
)
}
}
}
}

View File

@@ -1,13 +1,9 @@
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.car.app.CarContext
import androidx.car.app.CarToast
@@ -15,13 +11,11 @@ import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
@@ -40,25 +34,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 +74,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 +82,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
@@ -91,18 +100,44 @@ class NavigationSession : Session(), NavigationListener {
lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
var autoDriveEnabled = false
val simulation = Simulation()
private var routingEngine = 0
private var showTraffic = false;
private var distanceMode = 0
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var navigationManagerStarted = false
var notificationActive = false
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
*/
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
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 onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback()
@@ -116,6 +151,13 @@ class NavigationSession : Session(), NavigationListener {
if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed")
}
}
@@ -130,8 +172,26 @@ 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
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
@@ -144,6 +204,8 @@ class NavigationSession : Session(), NavigationListener {
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
}
/**
@@ -166,11 +228,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)
}
}
}
@@ -242,7 +306,7 @@ class NavigationSession : Session(), NavigationListener {
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
carSensorManager = CarSensorManager(
carContext = carContext,
@@ -272,6 +336,7 @@ class NavigationSession : Session(), NavigationListener {
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
notificationManager = NotificationManager(carContext, this)
}
/**
@@ -281,7 +346,6 @@ class NavigationSession : Session(), NavigationListener {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
routeModel,
this,
navigationViewModel
)
@@ -369,6 +433,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 +445,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,43 +464,113 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
routeModel.updateLocation(location, navigationViewModel)
if (routeModel.navState.arrived) return
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
navigationScreen.updateTrip(location)
if (routeModel.navState.arrived) return
val streetName = routeModel.currentStep().street
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
snapLocation(location, streetName)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
checkRoute(currentDate, location)
updateNavigationScreen()
checkArrival()
}
/**
* Updates the surface renderer with snapped location and street name.
* Checks if maximal route deviation is exceeded and reroutes if needed.
*/
private fun snapLocation(location: Location, streetName: String) {
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation)
when {
distance > MAXIMAL_ROUTE_DEVIATION -> {
stopNavigation()
navigationScreen.calculateNewRoute(routeModel.navState.destination)
}
distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation)
surfaceRenderer.updateLocation(snappedLocation, streetName)
}
else -> {
surfaceRenderer.updateLocation(location)
surfaceRenderer.updateLocation(location, streetName)
}
}
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
* Updates the navigation screen with new trip information.
*/
override fun stopNavigation() {
routeModel.stopNavigation()
navigationManager.navigationEnded()
if (autoDriveEnabled) {
simulation.stopSimulation()
autoDriveEnabled = false
fun updateNavigationScreen() {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()) {
return
}
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
val travelEstimateStep = routeModel.travelEstimateStep(carContext, distanceMode)
val steps = mutableListOf<Step>()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name)
.setAddress(routeModel.navState.destination.street)
.build()
val distance =
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
steps.add(routeModel.currentStep(carContext))
if (routeModel.navState.nextStep) {
steps.add(routeModel.nextStep(carContext = carContext))
}
navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinationTravelEstimate = travelEstimateTrip,
stepTravelEstimate = travelEstimateStep,
destinations = mutableListOf(destination),
steps = steps,
stepRemainingDistance = Distance.create(distance.first, distance.second),
shouldShowNextStep = false,
shouldShowLanes = true,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
destination,
travelEstimateTrip
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(destination.name.toString())
tripBuilder.addStep(steps.first(), travelEstimateStep)
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 = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
navigationScreen.navigationType = NavigationType.ARRIVAL
invalidateScreen()
}
}
/**
@@ -449,6 +588,27 @@ class NavigationSession : Session(), NavigationListener {
updateLocation(location)
}
}
if (notificationActive)
notificationManager.startNotificationService()
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
*/
override fun stopNavigation() {
routeModel.stopNavigation()
navigationManager.navigationEnded()
if (autoDriveEnabled) {
simulation.stopSimulation()
autoDriveEnabled = false
}
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
if (notificationActive)
notificationManager.stopNotificationService()
}
override fun updateTrip(trip: Trip) {
@@ -467,6 +627,195 @@ class NavigationSession : Session(), NavigationListener {
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index
if (notificationActive) {
notificationManager.sendMessage(stepData.message)
}
}
}
/**
* 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)
}
updateNavigationScreen()
}
}
/**
* 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()
updateNavigationScreen()
}
/**
* 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
updateNavigationScreen()
}
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* 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.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 = ""
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
routeModel.navState = routeModel.navState.copy(destination = place)
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<Elements>()
speedCameras.forEach {
val plLocation =
location(longitude = it.lon, latitude = it.lat)
val distance = plLocation.distanceTo(location)
it.distance = distance.toDouble()
updatedCameras.add(it)
}
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) {
try {
camera.tags.direction!!.toFloat()
} catch (e: Exception) {
0F
}
} else {
location.bearingTo(location(camera.lon, camera.lat)).absoluteValue
}
if (camera.distance < 80) {
if ((bearingSpeedCamera - bearingRoute.absoluteValue).absoluteValue < 15.0) {
routeModel.showSpeedCamera(carContext, camera.distance, camera.tags.maxspeed)
}
}
}
/**
* 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
)
}
}

View File

@@ -0,0 +1,62 @@
package com.kouros.navigation.car
import android.content.Intent
import android.location.Location
import android.os.Message
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.hardware.CarHardwareManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.kouros.navigation.data.Constants.TAG
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class NotificationManager(
private val carContext: CarContext,
private val lifecycleOwner: LifecycleOwner,
) {
private var notificationServiceStarted = false
private var serviceStarted = false
init {
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
}
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.DESTROYED) {
if (notificationServiceStarted) {
stopNotificationService()
}
}
}
}
fun startNotificationService() {
val intent = Intent(carContext, NavigationNotificationService::class.java)
carContext.startForegroundService(intent)
notificationServiceStarted = true
}
fun stopNotificationService() {
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
notificationServiceStarted = false
}
fun sendMessage(message: String) {
val intent = Intent(carContext, NavigationNotificationService::class.java).apply {
putExtra("EXTRA_MESSAGE", message)
}
carContext.startForegroundService(intent)
}
}

View File

@@ -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)
}
}

View File

@@ -28,6 +28,7 @@ class Simulation {
if (routeModel.navState.route.isRouteValid()) {
if (BuildConfig.DEBUG) {
gpxSimulation(routeModel, lifecycleScope, updateLocation)
//currentSimulation(routeModel, lifecycleScope, updateLocation)
} else {
currentSimulation(routeModel, lifecycleScope, updateLocation)
}
@@ -46,7 +47,8 @@ class Simulation {
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
for ((index, point) in points.withIndex()) {
if (index >= 0) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
@@ -63,7 +65,8 @@ class Simulation {
delay(1000)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
// routeModel.stopNavigation()
}
}
@@ -115,8 +118,9 @@ class Simulation {
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) {
delay(duration / 4)
// delay(duration / 4)
}
delay(500)
lastTime = p.time
lastLocation = fakeLocation
}

View File

@@ -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)
}

View File

@@ -1,7 +1,5 @@
package com.kouros.navigation.car.screen
import android.location.Location
import android.location.LocationManager
import android.os.CountDownTimer
import android.os.Handler
import androidx.car.app.CarContext
@@ -9,6 +7,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 +20,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 +30,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,41 +48,37 @@ 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 {
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
) : Screen(carContext) {
var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
private var distanceMode = 0
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<Destination>
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<Step>
private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
@@ -106,25 +89,14 @@ 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 +136,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 +156,7 @@ open class NavigationScreen(
)
})
)
.setBackgroundColor(routeModel.backGroundColor())
.setBackgroundColor(backGroundColor)
.build()
}
@@ -194,7 +165,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 +192,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 +206,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 +226,7 @@ open class NavigationScreen(
)
.build()
)
.setBackgroundColor(routeModel.backGroundColor())
// .setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -294,12 +265,12 @@ open class NavigationScreen(
val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder()
.setTitle(it.name!!)
.setTitle(it.name)
.addAction(
createNavigateAction(it)
)
.setOnClickListener {
navigateToPlace(it)
listener.navigateToPlace(it)
}
listBuilder.addItem(
row.build()
@@ -349,7 +320,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 +328,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 +359,7 @@ open class NavigationScreen(
)
) { obj: Any? ->
if (obj != null) {
navigateToPlace(place)
listener.navigateToPlace(place)
}
}
}
@@ -471,48 +439,22 @@ open class NavigationScreen(
if (place.longitude == 0.0) {
navigationViewModel.findAddress(
"${obj.city} ${obj.street}},",
currentNavigationLocation
surfaceRenderer.lastLocation
)
// 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()
}
@@ -520,7 +462,6 @@ open class NavigationScreen(
* Initiates recalculation for a new route to the destination.
*/
fun calculateNewRoute(destination: Place) {
stopNavigation()
navigationType = NavigationType.REROUTE
invalidate()
val mainThreadHandler = Handler(carContext.mainLooper)
@@ -553,224 +494,36 @@ 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
fun updateTrip(
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
) {
listener.stopNavigation()
settingsViewModel.onLastRouteChanged("")
routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
}
this.isNavigating = isNavigating
this.isRerouting = isRerouting
this.hasArrived = hasArrived
this.destinations = destinations
this.steps = steps
this.stepRemainingDistance = stepRemainingDistance
this.destinationTravelEstimate = destinationTravelEstimate
this.stepTravelEstimate = stepTravelEstimate
this.shouldShowNextStep = shouldShowNextStep
this.shouldShowLanes = shouldShowLanes
this.junctionImage = junctionImage
this.backGroundColor = backGroundColor
/**
* 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,
) {
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)
}
}
}
/**
* 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<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.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()
}
}
/**

View File

@@ -347,6 +347,7 @@ class RoutePreviewScreen(
private fun onNavigate(index: Int) {
destination.routeIndex = index
destination.route = navigationViewModel.previewRoute.value.toString()
setResult(destination)
finish()
}

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel
/**
@@ -17,16 +18,12 @@ 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)
}
/**

View File

@@ -34,17 +34,18 @@ data class Places(
@Serializable
data class Place(
var id: Long = 0,
var name: String? = null,
var category: String? = null,
var name: String = "",
var category: String = "",
var latitude: Double = 0.0,
var longitude: Double = 0.0,
var postalCode: String? = null,
var city: String? = null,
var street: String? = null,
var postalCode: String = "",
var city: String = "",
var street: String = "",
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(

View File

@@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import androidx.annotation.DrawableRes
import androidx.car.app.model.CarIcon
import androidx.car.app.navigation.model.LaneDirection
@@ -181,13 +182,17 @@ class IconMapper {
bitmaps.first().height,
bitmaps.first().config!!
)
val paint = Paint().apply {
color = android.graphics.Color.YELLOW
}
val canvas = Canvas(bmOverlay)
canvas.drawBitmap(bitmaps.first(), matrix, null)
canvas.drawBitmap(bitmaps.first(), matrix, paint)
var i = 0
bitmaps.forEach { bitmap ->
if (i > 0) {
matrix.setTranslate(i * 45F, 0F)
canvas.drawBitmap(bitmap, matrix, null)
canvas.drawBitmap(bitmap, matrix, paint)
}
i++
}

View File

@@ -3,7 +3,9 @@ package com.kouros.navigation.utils
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.util.Log
import androidx.car.app.model.Distance
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository
@@ -141,7 +143,7 @@ fun duration(
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds
} else {
1.seconds
3.seconds
//val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
//((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS))
}