Compare commits

..

3 Commits

Author SHA1 Message Date
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
17 changed files with 639 additions and 439 deletions

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ import com.kouros.data.R
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.data.Place
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -67,7 +68,7 @@ internal class ClusterSession : Session(), NavigationListener {
OnClickListener {}) OnClickListener {})
.build() .build()
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
// mNavigationScreen = // mNavigationScreen =
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface); // new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
@@ -99,6 +100,10 @@ internal class ClusterSession : Session(), NavigationListener {
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
} }
override fun navigateToPlace(place: Place) {
}
companion object { companion object {
val TAG: String = ClusterSession::class.java.getSimpleName() 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) * @param minDistanceM Minimum distance between updates in meters (default: 5m)
*/ */
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
fun startLocationUpdates(minTimeMs: Long = 500, minDistanceM: Float = 5f) { fun startLocationUpdates(minTimeMs: Long = 1000, minDistanceM: Float = 5f) {
if (isListening) return if (isListening) return
// Get and deliver last known location first // Get and deliver last known location first

View File

@@ -14,7 +14,7 @@ import com.kouros.navigation.data.Constants.TAG
class NavigationCarAppService : CarAppService() { class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP = val intentActionNavNotificationOpenApp =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP" "com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
val channelId: String = "NavigationSessionChannel" val channelId: String = "NavigationSessionChannel"
@@ -31,7 +31,6 @@ class NavigationCarAppService : CarAppService() {
} }
override fun onCreateSession(sessionInfo: SessionInfo): Session { override fun onCreateSession(sessionInfo: SessionInfo): Session {
Log.d(TAG, "Display Type: ${sessionInfo.displayType}")
if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) { if (sessionInfo.displayType == SessionInfo.DISPLAY_TYPE_CLUSTER) {
return ClusterSession() return ClusterSession()
} else { } else {

View File

@@ -9,6 +9,7 @@ import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.util.Log
import androidx.car.app.notification.CarAppExtender import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent import androidx.car.app.notification.CarPendingIntent
@@ -16,6 +17,7 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.Constants.TAG
import java.math.RoundingMode import java.math.RoundingMode
import java.text.DecimalFormat import java.text.DecimalFormat
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -44,17 +46,12 @@ class NavigationNotificationService : Service() {
Handler(Looper.getMainLooper(), HandlerCallback()) Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val message = intent.getStringExtra("EXTRA_MESSAGE") ?: "Navigating..."
initNotifications(this) initNotifications(this)
startForeground( val notification = getNavigationNotification(this, message)
NAV_NOTIFICATION_ID, // This updates the existing notification if the service is already running
getNavigationNotification(this, mNotificationCount).build() CarNotificationManager.from(this).notify(NAV_NOTIFICATION_ID, notification)
) startForeground(NAV_NOTIFICATION_ID, notification.build())
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return START_NOT_STICKY return START_NOT_STICKY
} }
@@ -71,11 +68,12 @@ class NavigationNotificationService : Service() {
*/ */
internal inner class HandlerCallback : Handler.Callback { internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean { override fun handleMessage(msg: Message): Boolean {
Log.d(TAG, "Notification handleMessage: $msg")
if (msg.what == MSG_SEND_NOTIFICATION) { if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify( CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID, NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount) getNavigationNotification(context, "Nachricht")
) )
mNotificationCount++ mNotificationCount++
mHandler.sendMessageDelayed( mHandler.sendMessageDelayed(
@@ -96,6 +94,12 @@ class NavigationNotificationService : Service() {
val mOnlyAlertOnce: Boolean val mOnlyAlertOnce: Boolean
) )
fun startForeground(message: String) {
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, message).build()
)
}
companion object { companion object {
private const val MSG_SEND_NOTIFICATION = 1 private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00" private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
@@ -124,21 +128,40 @@ class NavigationNotificationService : Service() {
CarNotificationManager.from(context).createNotificationChannel(navChannel) 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. */ /** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification( fun getNavigationNotification(
context: Context, notificationCount: Int context: Context
): NotificationCompat.Builder { ): NotificationCompat.Builder {
val builder = val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID) 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 // 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. // heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp( val pendingIntent = CarPendingIntent.getCarApp(
context, context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(), NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
Intent( Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP NavigationCarAppService().intentActionNavNotificationOpenApp
).setComponent( ).setComponent(
ComponentName( ComponentName(
context, context,
@@ -146,7 +169,7 @@ class NavigationNotificationService : Service() {
) )
).setData( ).setData(
NavigationCarAppService().createDeepLinkUri( NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP NavigationCarAppService().intentActionNavNotificationOpenApp
) )
), ),
0 0
@@ -179,54 +202,59 @@ class NavigationNotificationService : Service() {
) )
} }
/** fun getNavigationNotification(
* Returns a [DirectionInfo] that corresponds to the given notification count. context: Context, message: String
* ): NotificationCompat.Builder {
* val builder =
* There are 5 directions, repeating in order. For each direction, the alert will only show NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
* once, but the distance will update on every count on the rail widget. val directionInfo = getDirectionInfo(context, message)
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo { // Set an intent to open the car app. The app receives this intent when the user taps the
val formatter = DecimalFormat("#.##") // heads-up notification or the rail widget.
formatter.setRoundingMode(RoundingMode.DOWN) val pendingIntent = CarPendingIntent.getCarApp(
val repeatingCount = notificationCount % 35 context,
if (repeatingCount in 0..<10) { NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
// Distance decreases from 1km to 0.1km Intent(
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km" NavigationCarAppService().intentActionNavNotificationOpenApp
return DirectionInfo( ).setComponent(
context.getString(R.string.stop_action_title), ComponentName(
distance, context,
R.drawable.arrow_back_24px, NavigationCarAppService()::class.java
repeatingCount > 0
) )
} else if (repeatingCount in 10..<20) { ).setData(
// Distance decreases from 5km to 0.5km NavigationCarAppService().createDeepLinkUri(
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km" NavigationCarAppService().intentActionNavNotificationOpenApp
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
) )
} else if (repeatingCount in 20..<25) { ),
// Distance decreases from 200m to 40m 0
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
) )
} else {
// Distance decreases from 1km to 0.1km return builder
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km" // This title, text, and icon will be shown in both phone and car screen. These
return DirectionInfo( // values can
context.getString(R.string.charging_station), // be overridden in the extender below, to customize notifications in the car
distance, // screen.
R.drawable.local_gas_station_24, .setContentTitle(directionInfo.mTitle)
repeatingCount > 25 .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 package com.kouros.navigation.car
import android.Manifest.permission import android.Manifest.permission
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
@@ -15,13 +11,11 @@ import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.navigation.NavigationManager import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Destination import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver 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.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission 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.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.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG 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.RouteEngine
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository 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.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel 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.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository 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.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset 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. * car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events. * Implements NavigationScreen.Listener for handling navigation events.
*/ */
class NavigationSession : Session(), NavigationListener { class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature // Flag to enable/disable contact access feature
val useContacts = false val useContacts = false
@@ -75,6 +82,8 @@ class NavigationSession : Session(), NavigationListener {
// Model for managing route state and navigation logic for Android Auto // Model for managing route state and navigation logic for Android Auto
lateinit var routeModel: RouteCarModel lateinit var routeModel: RouteCarModel
var route = ""
// Main navigation screen displayed to the user // Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen lateinit var navigationScreen: NavigationScreen
@@ -91,18 +100,44 @@ class NavigationSession : Session(), NavigationListener {
lateinit var textToSpeechManager: TextToSpeechManager lateinit var textToSpeechManager: TextToSpeechManager
lateinit var notificationManager: NotificationManager
var autoDriveEnabled = false var autoDriveEnabled = false
val simulation = Simulation() 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 navigationManagerStarted = false
var notificationActive = false
/** /**
* Lifecycle observer for managing session lifecycle events. * Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed. * Cleans up resources when the session is destroyed.
*/ */
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { 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) { override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) { if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback() navigationManager.clearNavigationManagerCallback()
@@ -116,6 +151,13 @@ class NavigationSession : Session(), NavigationListener {
if (::textToSpeechManager.isInitialized) { if (::textToSpeechManager.isInitialized) {
textToSpeechManager.cleanup() textToSpeechManager.cleanup()
} }
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
Log.i(TAG, "NavigationSession destroyed") Log.i(TAG, "NavigationSession destroyed")
} }
} }
@@ -130,8 +172,26 @@ class NavigationSession : Session(), NavigationListener {
var guidanceAudio = 0 var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
init { init {
lifecycle.addObserver(lifecycleObserver) 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()) RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository()) else -> NavigationViewModel(TomTomRepository())
} }
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
} }
/** /**
@@ -166,11 +228,13 @@ class NavigationSession : Session(), NavigationListener {
when (connectionState) { when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> { 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 -> { 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( carSensorManager = CarSensorManager(
carContext = carContext, carContext = carContext,
@@ -272,6 +336,7 @@ class NavigationSession : Session(), NavigationListener {
repository.guidanceAudioFlow.asLiveData().observe(this, Observer { repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it guidanceAudio = it
}) })
notificationManager = NotificationManager(carContext, this)
} }
/** /**
@@ -281,7 +346,6 @@ class NavigationSession : Session(), NavigationListener {
navigationScreen = NavigationScreen( navigationScreen = NavigationScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
routeModel,
this, this,
navigationViewModel navigationViewModel
) )
@@ -369,6 +433,11 @@ class NavigationSession : Session(), NavigationListener {
* Handles route snapping, deviation detection for rerouting, and map updates. * Handles route snapping, deviation detection for rerouting, and map updates.
*/ */
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
val streetName = if (routeModel.isNavigating()) {
routeModel.currentStep().street
} else {
""
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) { if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed) surfaceRenderer.updateCarSpeed(location.speed)
} }
@@ -376,8 +445,8 @@ class NavigationSession : Session(), NavigationListener {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
handleNavigationLocation(location) handleNavigationLocation(location)
} else { } else {
navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location, streetName)
} }
} }
@@ -395,43 +464,111 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute. * Snaps location to route and checks for deviation requiring reroute.
*/ */
private fun handleNavigationLocation(location: Location) { private fun handleNavigationLocation(location: Location) {
routeModel.updateLocation(location, navigationViewModel)
if (routeModel.navState.arrived) return
if (guidanceAudio == 1) { if (guidanceAudio == 1) {
handleGuidanceAudio() handleGuidanceAudio()
} }
navigationScreen.updateTrip(location) val streetName = routeModel.currentStep().street
if (routeModel.navState.arrived) return val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
checkRoute(currentDate, location)
checkArrival()
updateNavigationScreen()
snapLocation(location, streetName)
}
/**
* 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 snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation) val distance = location.distanceTo(snappedLocation)
when { when {
distance > MAXIMAL_ROUTE_DEVIATION -> { distance > MAXIMAL_ROUTE_DEVIATION -> {
stopNavigation()
navigationScreen.calculateNewRoute(routeModel.navState.destination) navigationScreen.calculateNewRoute(routeModel.navState.destination)
} }
distance < MAXIMAL_SNAP_CORRECTION -> { distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation) surfaceRenderer.updateLocation(snappedLocation, streetName)
} }
else -> { else -> {
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location, streetName)
} }
} }
} }
/** /**
* Stops active navigation and clears route state. * Updates the navigation screen with new trip information.
* Called when user exits navigation or arrives at destination.
*/ */
override fun stopNavigation() { fun updateNavigationScreen() {
routeModel.stopNavigation() if (routeModel.navState.destination.name.isEmpty()
navigationManager.navigationEnded() && routeModel.navState.destination.street.isEmpty()) {
if (autoDriveEnabled) { return
simulation.stopSimulation()
autoDriveEnabled = false
} }
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.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW }
navigationScreen.navigationType = NavigationType.VIEW
} }
/** /**
@@ -449,6 +586,27 @@ class NavigationSession : Session(), NavigationListener {
updateLocation(location) 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) { override fun updateTrip(trip: Trip) {
@@ -467,6 +625,195 @@ class NavigationSession : Session(), NavigationListener {
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) { if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message) textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index 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( class SurfaceRenderer(
private var carContext: CarContext, private var carContext: CarContext,
private var lifecycle: Lifecycle, private var lifecycle: Lifecycle,
private var routeModel: RouteCarModel, //private var routeModel: RouteCarModel,
private var viewModelStoreOwner: ViewModelStoreOwner private var viewModelStoreOwner: ViewModelStoreOwner
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
@@ -362,13 +362,9 @@ class SurfaceRenderer(
* Calculates appropriate bearing, zoom, and maintains view style. * Calculates appropriate bearing, zoom, and maintains view style.
* 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) { fun updateLocation(location: Location, streetName : String) {
synchronized(this) { synchronized(this) {
if (routeModel.isNavigating()) { street.value = streetName
street.value = routeModel.currentStep().street
} else {
street.value = ""
}
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = if (carOrientation == 999F) { val bearing = if (carOrientation == 999F) {
if (location.hasBearing()) { if (location.hasBearing()) {
@@ -402,8 +398,8 @@ class SurfaceRenderer(
/** /**
* Sets route data for active navigation and switches to VIEW mode. * Sets route data for active navigation and switches to VIEW mode.
*/ */
fun setRouteData() { fun setRouteData(routeGeoJson: String) {
routeData.value = routeModel.curRoute.routeGeoJson routeData.value = routeGeoJson
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
} }
@@ -413,7 +409,7 @@ class SurfaceRenderer(
fun activateNavigationView() { fun activateNavigationView() {
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
tilt = TILT tilt = TILT
updateLocation(lastLocation) updateLocation(lastLocation, "")
} }
/** /**
@@ -481,11 +477,11 @@ class SurfaceRenderer(
* Updates car location from the connected car system. * Updates car location from the connected car system.
* Only updates location when using OSRM routing engine. * Only updates location when using OSRM routing engine.
*/ */
fun updateCarLocation(location: Location) { fun updateCarLocation(location: Location, streetName: String) {
val repository = getSettingsRepository(carContext) val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() } val routingEngine = runBlocking { repository.routingEngineFlow.first() }
if (routingEngine == RouteEngine.OSRM.ordinal) { if (routingEngine == RouteEngine.OSRM.ordinal) {
updateLocation(location) updateLocation(location, streetName)
} }
} }

View File

@@ -115,8 +115,9 @@ 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(1000)
lastTime = p.time lastTime = p.time
lastLocation = fakeLocation lastLocation = fakeLocation
} }

View File

@@ -1,7 +1,7 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import com.kouros.navigation.data.Place
/** A listener for navigation start and stop signals. */ /** A listener for navigation start and stop signals. */
@@ -14,4 +14,6 @@ interface NavigationListener {
/** Updates trip information. */ /** Updates trip information. */
fun updateTrip(trip: Trip) fun updateTrip(trip: Trip)
fun navigateToPlace(place: Place)
} }

View File

@@ -1,7 +1,11 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.content.Context
import android.content.Intent
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -9,6 +13,7 @@ import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
@@ -21,7 +26,8 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo 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.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@@ -29,30 +35,18 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.NavigationNotificationService
import com.kouros.navigation.car.SurfaceRenderer 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.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants 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.Place
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel 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.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch 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. * Main screen for car navigation.
@@ -61,41 +55,37 @@ import kotlin.math.absoluteValue
open class NavigationScreen( open class NavigationScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel,
private var listener: NavigationListener, private var listener: NavigationListener,
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback { ) : Screen(carContext) {
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlaces = mutableListOf<Place>() var recentPlaces = mutableListOf<Place>()
var recentPlace: Place = Place() var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW 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 repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext) val settingsViewModel = getSettingsViewModel(carContext)
private var distanceMode = 0
private var tripSuggestion = false private var tripSuggestion = false
private var tripSuggestionCalled = false private var tripSuggestionCalled = false
private var routingEngine = 0
private var showTraffic = false;
private var arrivalTimer: CountDownTimer? = null private var arrivalTimer: CountDownTimer? = null
private var reRouteTimer: 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 -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
@@ -106,25 +96,14 @@ open class NavigationScreen(
} }
init { init {
observerManager.attachAllObservers(this)
lifecycleScope.launch { lifecycleScope.launch {
settingsViewModel.tripSuggestion.first() 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 { repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it tripSuggestion = it
}) })
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
arrivalTimer?.cancel() arrivalTimer?.cancel()
@@ -164,12 +143,11 @@ open class NavigationScreen(
0, 0,
{ stopNavigation() }) { stopNavigation() })
) )
updateTrip()
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
.setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode)) .setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -185,7 +163,7 @@ open class NavigationScreen(
) )
}) })
) )
.setBackgroundColor(routeModel.backGroundColor()) .setBackgroundColor(backGroundColor)
.build() .build()
} }
@@ -194,7 +172,7 @@ open class NavigationScreen(
*/ */
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setBackgroundColor(routeModel.backGroundColor()) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -221,7 +199,7 @@ open class NavigationScreen(
arrivalTimer = object : CountDownTimer(8000, 1000) { arrivalTimer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
routeModel.navState = routeModel.navState.copy(arrived = false) // routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
@@ -235,8 +213,8 @@ open class NavigationScreen(
*/ */
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = "" var street = ""
if (routeModel.navState.destination.street != null) { if (destinations.first().address != null) {
street = routeModel.navState.destination.street!! street = destinations.first().address.toString()
} }
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
@@ -255,7 +233,7 @@ open class NavigationScreen(
) )
.build() .build()
) )
.setBackgroundColor(routeModel.backGroundColor()) // .setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(
@@ -294,12 +272,12 @@ open class NavigationScreen(
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach { recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder() val row = Row.Builder()
.setTitle(it.name!!) .setTitle(it.name)
.addAction( .addAction(
createNavigateAction(it) createNavigateAction(it)
) )
.setOnClickListener { .setOnClickListener {
navigateToPlace(it) listener.navigateToPlace(it)
} }
listBuilder.addItem( listBuilder.addItem(
row.build() row.build()
@@ -349,7 +327,7 @@ open class NavigationScreen(
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setBackgroundColor(routeModel.backGroundColor()) // .setBackgroundColor(routeModel.backGroundColor())
.build() .build()
} }
@@ -357,16 +335,13 @@ open class NavigationScreen(
* Builds and returns RoutingInfo based on the current step and distance. * Builds and returns RoutingInfo based on the current step and distance.
*/ */
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
val distance =
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val routingInfo = RoutingInfo.Builder() val routingInfo = RoutingInfo.Builder()
.setCurrentStep( .setCurrentStep(
routeModel.currentStep(carContext = carContext), steps.first(),
Distance.create(distance.first, distance.second) stepRemainingDistance
) )
if (routeModel.navState.nextStep) { if (shouldShowNextStep && steps.size > 1) {
val nextStep = routeModel.nextStep(carContext = carContext) routingInfo.setNextStep(steps[1])
routingInfo.setNextStep(nextStep)
} }
return routingInfo.build() return routingInfo.build()
} }
@@ -391,7 +366,7 @@ open class NavigationScreen(
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
navigateToPlace(place) listener.navigateToPlace(place)
} }
} }
} }
@@ -471,48 +446,22 @@ open class NavigationScreen(
if (place.longitude == 0.0) { if (place.longitude == 0.0) {
navigationViewModel.findAddress( navigationViewModel.findAddress(
"${obj.city} ${obj.street}},", "${obj.city} ${obj.street}},",
currentNavigationLocation surfaceRenderer.lastLocation
) )
// result see observer // result see observer
} else { } 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. * Stops navigation, resets state, and notifies listeners.
*/ */
fun stopNavigation() { fun stopNavigation() {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
listener.stopNavigation() listener.stopNavigation()
lastCameraSearch = 0
invalidate() invalidate()
} }
@@ -520,7 +469,6 @@ open class NavigationScreen(
* Initiates recalculation for a new route to the destination. * Initiates recalculation for a new route to the destination.
*/ */
fun calculateNewRoute(destination: Place) { fun calculateNewRoute(destination: Place) {
stopNavigation()
navigationType = NavigationType.REROUTE navigationType = NavigationType.REROUTE
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
@@ -553,224 +501,36 @@ 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.
*/ */
fun updateTrip(location: Location) { fun updateTrip(
val currentDate = LocalDateTime.now(ZoneOffset.UTC) isNavigating: Boolean,
checkRoute(currentDate, location) isRerouting: Boolean,
checkTraffic(currentDate, location) hasArrived: Boolean,
destinations: MutableList<Destination>,
updateSpeedCamera(location) steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
routeModel.updateLocation(location, navigationViewModel) stepTravelEstimate: TravelEstimate,
checkArrival() stepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
invalidate() shouldShowLanes: Boolean,
} junctionImage: CarIcon?,
backGroundColor: CarColor
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude
)
navigationViewModel.loadRoute(
carContext,
location,
destination,
surfaceRenderer.carOrientation
)
}
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) { ) {
listener.stopNavigation() this.isNavigating = isNavigating
settingsViewModel.onLastRouteChanged("") this.isRerouting = isRerouting
routeModel.navState = routeModel.navState.copy(arrived = true) this.hasArrived = hasArrived
surfaceRenderer.routeData.value = "" this.destinations = destinations
navigationType = NavigationType.ARRIVAL this.steps = steps
invalidate() 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 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() invalidate()
} }
} }
/** /**

View File

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

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.car.screen.observers package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.car.NavigationSession
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
/** /**
@@ -17,16 +18,12 @@ class NavigationObserverManager(
val speedCameraObserver = SpeedCameraObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback)
/** fun attachAllObservers(session: NavigationSession) {
* Attaches all observers to the ViewModel. viewModel.route.observe(session, routeObserver)
* Call this from NavigationScreen's init block or lifecycle method. viewModel.traffic.observe(session, trafficObserver)
*/ viewModel.placeLocation.observe(session, placeSearchObserver)
fun attachAllObservers(screen: androidx.car.app.Screen) { viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.route.observe(screen, routeObserver) viewModel.maxSpeed.observe(session, maxSpeedObserver)
viewModel.traffic.observe(screen, trafficObserver)
viewModel.placeLocation.observe(screen, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver)
} }
/** /**

View File

@@ -34,17 +34,18 @@ data class Places(
@Serializable @Serializable
data class Place( data class Place(
var id: Long = 0, var id: Long = 0,
var name: String? = null, var name: String = "",
var category: String? = null, var category: String = "",
var latitude: Double = 0.0, var latitude: Double = 0.0,
var longitude: Double = 0.0, var longitude: Double = 0.0,
var postalCode: String? = null, var postalCode: String = "",
var city: String? = null, var city: String = "",
var street: String? = null, var street: String = "",
var distance: Float = 0F, var distance: Float = 0F,
//var avatar: Uri? = null, //var avatar: Uri? = null,
var lastDate: Long = 0, var lastDate: Long = 0,
var routeIndex: Int = 0 var routeIndex: Int = 0,
var route: String = "",
) )
data class ContactData( data class ContactData(

View File

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