Navigation Screen to Session, Remove NavigationService

This commit is contained in:
Dimitris
2026-03-26 17:04:52 +01:00
parent aaa57c14b8
commit 263b5b576d
11 changed files with 462 additions and 337 deletions

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

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

@@ -136,9 +136,9 @@ class NavigationNotificationService : Service() {
// heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp(
context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(),
NavigationCarAppService().intentActionNavNotificationOpenApp.hashCode(),
Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
NavigationCarAppService().intentActionNavNotificationOpenApp
).setComponent(
ComponentName(
context,
@@ -146,7 +146,7 @@ class NavigationNotificationService : Service() {
)
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
NavigationCarAppService().intentActionNavNotificationOpenApp
)
),
0

View File

@@ -40,25 +40,38 @@ import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
@@ -67,7 +80,7 @@ import java.time.ZoneOffset
* car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events.
*/
class NavigationSession : Session(), NavigationListener {
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
@@ -75,6 +88,8 @@ class NavigationSession : Session(), NavigationListener {
// Model for managing route state and navigation logic for Android Auto
lateinit var routeModel: RouteCarModel
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
@@ -95,6 +110,15 @@ class NavigationSession : Session(), NavigationListener {
val simulation = Simulation()
private var routingEngine = 0
private var showTraffic = false;
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var navigationManagerStarted = false
/**
@@ -130,8 +154,22 @@ class NavigationSession : Session(), NavigationListener {
var guidanceAudio = 0
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycle.addObserver(lifecycleObserver)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
}
/**
@@ -144,6 +182,8 @@ class NavigationSession : Session(), NavigationListener {
RouteEngine.OSRM.ordinal -> NavigationViewModel(OsrmRepository())
else -> NavigationViewModel(TomTomRepository())
}
observerManager = NavigationObserverManager(navigationViewModel, this)
observerManager.attachAllObservers(this)
}
/**
@@ -166,11 +206,13 @@ class NavigationSession : Session(), NavigationListener {
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION)
navigationViewModel.permissionGranted.value =
checkPermission(carContext, AUTOMOTIVE_CAR_SPEED_PERMISSION)
}
CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationViewModel.permissionGranted.value = checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
navigationViewModel.permissionGranted.value =
checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
}
}
}
@@ -230,7 +272,7 @@ class NavigationSession : Session(), NavigationListener {
autoDriveEnabled = true
startNavigation()
CarToast.makeText(carContext, "Auto drive enabled", CarToast.LENGTH_LONG)
.show()
.show()
}
override fun onStopNavigation() {
@@ -242,7 +284,7 @@ class NavigationSession : Session(), NavigationListener {
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
carSensorManager = CarSensorManager(
carContext = carContext,
@@ -281,7 +323,6 @@ class NavigationSession : Session(), NavigationListener {
navigationScreen = NavigationScreen(
carContext,
surfaceRenderer,
routeModel,
this,
navigationViewModel
)
@@ -369,6 +410,11 @@ class NavigationSession : Session(), NavigationListener {
* Handles route snapping, deviation detection for rerouting, and map updates.
*/
fun updateLocation(location: Location) {
val streetName = if (routeModel.isNavigating()) {
routeModel.currentStep().street
} else {
""
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed)
}
@@ -376,8 +422,8 @@ class NavigationSession : Session(), NavigationListener {
if (routeModel.isNavigating()) {
handleNavigationLocation(location)
} else {
navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location)
checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location, streetName)
}
}
@@ -395,11 +441,19 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
navigationScreen.updateTrip(location)
val streetName = routeModel.currentStep().street
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
checkRoute(currentDate, location)
routeModel.updateLocation(location, navigationViewModel)
checkArrival()
updateTripNavigationScreen(location)
if (routeModel.navState.arrived) return
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation)
@@ -409,15 +463,81 @@ class NavigationSession : Session(), NavigationListener {
}
distance < MAXIMAL_SNAP_CORRECTION -> {
surfaceRenderer.updateLocation(snappedLocation)
surfaceRenderer.updateLocation(snappedLocation, streetName)
}
else -> {
surfaceRenderer.updateLocation(location)
surfaceRenderer.updateLocation(location, streetName)
}
}
}
fun updateTripNavigationScreen(location: Location) {
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, 0)
val travelEstimateStep = routeModel.travelEstimateStep(carContext, 0)
val steps = mutableListOf<Step>()
val street = if (routeModel.navState.destination.street != null) {
routeModel.navState.destination.street!!
} else {
// routeModel.navState.destination.name!!
"Street"
}
val destination = Destination.Builder()
.setName(street)
.setAddress(street)
.build()
val distance =
formattedDistance(0, routeModel.routeCalculator.leftStepDistance())
steps.add(routeModel.currentStep(carContext))
if (routeModel.navState.nextStep) {
steps.add(routeModel.nextStep(carContext = carContext))
}
navigationScreen.updateTrip(
isNavigating = routeModel.isNavigating(),
isRerouting = false,
hasArrived = routeModel.isArrival(),
destinationTravelEstimate = travelEstimateTrip,
stepTravelEstimate = travelEstimateStep,
destinations = mutableListOf(destination),
steps = steps,
nextStepRemainingDistance = Distance.create(distance.first, distance.second),
shouldShowNextStep = false,
shouldShowLanes = true,
junctionImage = null,
backGroundColor = routeModel.backGroundColor()
)
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
val tripBuilder = Trip.Builder()
tripBuilder.addDestination(
destination,
travelEstimateTrip
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(destination.name.toString())
tripBuilder.addStep(steps.first(), travelEstimateStep)
updateTrip(tripBuilder.build())
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
stopNavigation()
settingsViewModel.onLastRouteChanged("")
routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
}
}
/**
* Stops active navigation and clears route state.
* Called when user exits navigation or arrives at destination.
@@ -430,6 +550,7 @@ class NavigationSession : Session(), NavigationListener {
autoDriveEnabled = false
}
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
}
@@ -470,7 +591,196 @@ class NavigationSession : Session(), NavigationListener {
}
}
companion object {
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) {
this.route = route
if (routeModel.isNavigating()) {
updateRoute(route)
} else {
prepareRoute(route)
}
updateTripNavigationScreen(surfaceRenderer.lastLocation)
}
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData(routeModel.curRoute.routeGeoJson)
startNavigation()
updateTripNavigationScreen(surfaceRenderer.lastLocation)
//navigationScreen.updateTrip(surfaceRenderer.lastLocation)
}
/**
* Update route and traffic data
*/
private fun updateRoute(route: String) {
val newRouteModel = RouteModel()
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
newRouteModel.startNavigation(route)
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
//navigationScreen.updateTrip(surfaceRenderer.lastLocation)
updateTripNavigationScreen(surfaceRenderer.lastLocation)
}
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<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
navigationViewModel.previewRoute.value = ""
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
//currentNavigationLocation = location
if (preview.isEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
onRouteReceived(preview)
}
routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.activateNavigationView()
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
) {
val updatedCameras = mutableListOf<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
)
}
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"

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

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

@@ -9,6 +9,7 @@ import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
@@ -21,7 +22,8 @@ import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Trip
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
@@ -30,29 +32,16 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.math.absoluteValue
/**
* Main screen for car navigation.
@@ -61,10 +50,9 @@ import kotlin.math.absoluteValue
open class NavigationScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel,
private var listener: NavigationListener,
private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback {
) : Screen(carContext) {
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
@@ -73,13 +61,6 @@ open class NavigationScreen(
var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
var lastRouteDate: LocalDateTime = LocalDateTime.now()
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
@@ -89,13 +70,31 @@ open class NavigationScreen(
private var tripSuggestion = false
private var tripSuggestionCalled = false
private var routingEngine = 0
private var showTraffic = false;
private var arrivalTimer: CountDownTimer? = null
private var reRouteTimer: CountDownTimer? = null
private var isNavigating = false
private var isRerouting = false
private var hasArrived = false
private lateinit var destinations: MutableList<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>
var junctionImage: CarIcon? = null
var backGroundColor = CarColor.BLUE
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
@@ -106,25 +105,17 @@ open class NavigationScreen(
}
init {
observerManager.attachAllObservers(this)
lifecycleScope.launch {
settingsViewModel.tripSuggestion.first()
settingsViewModel.routingEngine.first()
}
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it
})
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
arrivalTimer?.cancel()
@@ -164,12 +155,11 @@ open class NavigationScreen(
0,
{ stopNavigation() })
)
updateTrip()
return NavigationTemplate.Builder()
.setNavigationInfo(
getRoutingInfo()
)
.setDestinationTravelEstimate(routeModel.travelEstimateTrip(carContext, distanceMode))
.setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -185,7 +175,7 @@ open class NavigationScreen(
)
})
)
.setBackgroundColor(routeModel.backGroundColor())
.setBackgroundColor(backGroundColor)
.build()
}
@@ -194,7 +184,7 @@ open class NavigationScreen(
*/
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder()
.setBackgroundColor(routeModel.backGroundColor())
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -221,7 +211,7 @@ open class NavigationScreen(
arrivalTimer = object : CountDownTimer(8000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
routeModel.navState = routeModel.navState.copy(arrived = false)
// routeModel.navState = routeModel.navState.copy(arrived = false)
navigationType = NavigationType.VIEW
invalidate()
}
@@ -235,8 +225,8 @@ open class NavigationScreen(
*/
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = ""
if (routeModel.navState.destination.street != null) {
street = routeModel.navState.destination.street!!
if (destinations.first().address != null) {
street = destinations.first().address.toString()
}
return NavigationTemplate.Builder()
.setNavigationInfo(
@@ -255,7 +245,7 @@ open class NavigationScreen(
)
.build()
)
.setBackgroundColor(routeModel.backGroundColor())
// .setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -299,7 +289,7 @@ open class NavigationScreen(
createNavigateAction(it)
)
.setOnClickListener {
navigateToPlace(it)
listener.navigateToPlace(it)
}
listBuilder.addItem(
row.build()
@@ -349,7 +339,7 @@ open class NavigationScreen(
return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build())
.setBackgroundColor(routeModel.backGroundColor())
// .setBackgroundColor(routeModel.backGroundColor())
.build()
}
@@ -357,16 +347,13 @@ open class NavigationScreen(
* Builds and returns RoutingInfo based on the current step and distance.
*/
fun getRoutingInfo(): RoutingInfo {
val distance =
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val routingInfo = RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(distance.first, distance.second)
steps.first(),
stepRemainingDistance
)
if (routeModel.navState.nextStep) {
val nextStep = routeModel.nextStep(carContext = carContext)
routingInfo.setNextStep(nextStep)
if (shouldShowNextStep && steps.size > 1) {
routingInfo.setNextStep(steps[1])
}
return routingInfo.build()
}
@@ -391,7 +378,7 @@ open class NavigationScreen(
)
) { obj: Any? ->
if (obj != null) {
navigateToPlace(place)
listener.navigateToPlace(place)
}
}
}
@@ -475,44 +462,18 @@ open class NavigationScreen(
)
// result see observer
} else {
navigateToPlace(place)
listener.navigateToPlace(place)
}
}
}
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value
navigationViewModel.previewRoute.value = ""
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location
if (preview.isNullOrEmpty()) {
navigationViewModel.loadRoute(
carContext,
surfaceRenderer.lastLocation,
location,
surfaceRenderer.carOrientation
)
} else {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = place.routeIndex)
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.activateNavigationView()
invalidate()
}
/**
* Stops navigation, resets state, and notifies listeners.
*/
fun stopNavigation() {
navigationType = NavigationType.VIEW
listener.stopNavigation()
lastCameraSearch = 0
invalidate()
}
@@ -550,227 +511,40 @@ open class NavigationScreen(
)
}
/**
* Updates navigation state with the current location, checks for arrival, and traffic updates.
*/
fun updateTrip(location: Location) {
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(currentDate, location)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
routeModel.updateLocation(location, navigationViewModel)
checkArrival()
invalidate()
}
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude
)
navigationViewModel.loadRoute(
carContext,
location,
destination,
surfaceRenderer.carOrientation
)
}
}
/**
* Checks if traffic data needs to be updated based on the time since the last update.
*/
fun checkTraffic(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastTrafficDate)
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
}
}
/**
* Checks for arrival
*/
fun checkArrival() {
if (routeModel.isArrival()
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) {
listener.stopNavigation()
settingsViewModel.onLastRouteChanged("")
routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL
invalidate()
}
}
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
private fun updateTrip() {
if (routeModel.isNavigating() && !routeModel.navState.destination.name.isNullOrEmpty()) {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")
.setAddress(routeModel.navState.destination.street ?: "")
.build()
tripBuilder.addDestination(
destination,
routeModel.travelEstimateTrip(carContext, distanceMode)
)
tripBuilder.setLoading(false)
tripBuilder.setCurrentRoad(routeModel.currentStep.street)
tripBuilder.addStep(routeModel.currentStep(carContext), routeModel.travelEstimateStep(carContext, distanceMode ))
listener.updateTrip(tripBuilder.build())
}
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0)
}
if (speedCameras.isNotEmpty()) {
updateDistance(location)
}
}
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance(
location: Location,
fun updateTrip(
isNavigating: Boolean,
isRerouting: Boolean,
hasArrived: Boolean,
destinations: MutableList<Destination>,
steps: MutableList<Step>,
destinationTravelEstimate: TravelEstimate,
stepTravelEstimate: TravelEstimate,
nextStepRemainingDistance: Distance,
shouldShowNextStep: Boolean,
shouldShowLanes: Boolean,
junctionImage: CarIcon?,
backGroundColor: CarColor
) {
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)
}
}
}
this.isNavigating = isNavigating
this.isRerouting = isRerouting
this.hasArrived = hasArrived
this.destinations = destinations
this.steps = steps
stepRemainingDistance = nextStepRemainingDistance
this.destinationTravelEstimate = destinationTravelEstimate
this.stepTravelEstimate = stepTravelEstimate
this.shouldShowNextStep = shouldShowNextStep
this.shouldShowLanes = shouldShowLanes
this.junctionImage = junctionImage
this.backGroundColor = backGroundColor
/**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) {
if (routeModel.isNavigating()) {
updateRoute(route)
} else {
prepareRoute(route)
}
invalidate()
}
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData()
listener.startNavigation()
}
/**
* Update route and traffic data
*/
private fun updateRoute(route: String) {
val newRouteModel = RouteModel()
newRouteModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
newRouteModel.startNavigation(route)
routeModel.curRoute.summary.trafficDelay = newRouteModel.curRoute.summary.trafficDelay
}
/**
* Checks if navigation is currently active.
*/
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<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,18 +18,14 @@ class NavigationObserverManager(
val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback)
/**
* Attaches all observers to the ViewModel.
* Call this from NavigationScreen's init block or lifecycle method.
*/
fun attachAllObservers(screen: androidx.car.app.Screen) {
viewModel.route.observe(screen, routeObserver)
viewModel.traffic.observe(screen, trafficObserver)
viewModel.placeLocation.observe(screen, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver)
fun attachAllObservers(session: NavigationSession) {
viewModel.route.observe(session, routeObserver)
viewModel.traffic.observe(session, trafficObserver)
viewModel.placeLocation.observe(session, placeSearchObserver)
viewModel.speedCameras.observe(session, speedCameraObserver)
viewModel.maxSpeed.observe(session, maxSpeedObserver)
}
/**
* Detaches all observers from the ViewModel.
* Call this when the screen is being destroyed.