Automotive, CarSession

This commit is contained in:
Dimitris
2026-04-03 09:54:43 +02:00
parent 52f8dec2e6
commit 24173412e8
38 changed files with 437 additions and 236 deletions
-1
View File
@@ -42,7 +42,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui)
implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
implementation(project(":common:data"))
implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.compose.foundation)
@@ -2,11 +2,11 @@ package com.kouros.navigation.car
import android.location.Location
import android.location.LocationManager
import androidx.car.app.navigation.model.Maneuver
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
@@ -32,6 +32,37 @@ class RouteModelTest {
val routeModel = RouteModel()
val location = Location(LocationManager.GPS_PROVIDER)
val distance = listOf(
1025.5,
989.8,
963.5,
923.7,
915.8,
914.6,
871.0,
822.7,
769.7,
713.8,
644.8,
577.6,
501.7,
489.7,
452.5,
437.4,
398.0,
390.1,
341.3,
266.6,
219.5,
140.7,
77.4,
55.1,
40.0,
30.0,
19.0,
4.0
)
@Before
fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
@@ -49,8 +80,8 @@ class RouteModelTest {
@Test
fun checkRoute() {
assertEquals(true, routeModel.isNavigating())
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0)
assertEquals(routeModel.curRoute.summary.distance, 11108.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1094.0, 10.0)
}
@Test
@@ -59,11 +90,11 @@ class RouteModelTest {
location.longitude = 11.579034
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 20.0, 5.0)
val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
}
@@ -74,11 +105,11 @@ class RouteModelTest {
location.longitude = 11.576652
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Schmalkaldener Straße")
assertEquals(stepData.leftStepDistance, 0.0, 1.0)
val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Ingolstädter Straße")
}
@@ -89,13 +120,13 @@ class RouteModelTest {
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
if (routeModel.navState.nextStep) {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_STRAIGHT.value)
assertEquals(stepData.instruction, "Ingolstädter Straße")
val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
} else {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
}
assertEquals(stepData.leftStepDistance, 301.0, 1.0)
}
@@ -107,14 +138,14 @@ class RouteModelTest {
location.bearing = 180.0F
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(stepData.instruction, "Schenkendorfstraße")
assertEquals(stepData.leftStepDistance, 170.0, 10.0)
assertEquals(stepData.lane.size, 4)
assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.last().valid, false)
val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT)
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_KEEP_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
}
@@ -124,7 +155,7 @@ class RouteModelTest {
location.longitude = homeHohenwaldeck.longitude
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.nextStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT)
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
}
@Test
@@ -142,11 +173,11 @@ class RouteModelTest {
if (index in 61..61) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3)
assertEquals(stepData.lane.size, 2)
assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
}
if (index in 74..74) {
if (index in 74..75) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3)
@@ -180,9 +211,9 @@ class RouteModelTest {
val stepData = routeModel.currentStep()
//println("${stepData.instruction} ${System.currentTimeMillis() - start}")
if (stepData.lane.isNotEmpty()) {
println(stepData.street)
// println(stepData.street)
stepData.lane.forEach {
println("${it.indications} ${it.valid}")
// println("${it.indications} ${it.valid}")
}
}
// val nextData = routeModel.nextStep()
@@ -206,4 +237,19 @@ class RouteModelTest {
val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 26.0, 1.0)
}
}
@Test
fun leftStepDistance() {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) {
if (index in 16..43) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, distance[index-16], 1.0)
}
}
}
}
}
@@ -0,0 +1,10 @@
package com.kouros.navigation.car
import androidx.car.app.Session
import kotlinx.coroutines.flow.Flow
abstract class CarSession : Session() {
abstract fun invalidateNavigationScreen()
}
@@ -17,6 +17,7 @@ package com.kouros.navigation.car
import android.content.Intent
import android.content.res.Configuration
import android.location.Location
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.CarToast
@@ -27,6 +28,9 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.OnClickListener
import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope
@@ -34,46 +38,74 @@ 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.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys.CAR_LOCATION
import com.kouros.navigation.data.datastore.dataStore
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
/** Session class for the Navigation sample app. */
internal class ClusterSession : Session(), NavigationListener {
var mNavigationScreen: NavigationScreen? = null
internal class ClusterSession : CarSession(), NavigationListener {
lateinit var mNavigationScreen: NavigationScreen
var mNavigationCarSurface: SurfaceRenderer? = null
lateinit var surfaceRenderer: SurfaceRenderer
var mSettingsAction: Action? = null
var routeModel = RouteCarModel()
var routeModel = RouteCarModel()
lateinit var viewModelStoreOwner: ViewModelStoreOwner
lateinit var navigationViewModel: NavigationViewModel
lateinit var deviceLocationManager: DeviceLocationManager
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onDestroy(owner: LifecycleOwner) {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
}
}
init {
lifecycle.addObserver(lifecycleObserver)
}
override fun onCreateScreen(intent: Intent): Screen {
Log.i(TAG, "In onCreateScreen()")
setupViewModelStore()
mSettingsAction =
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
)
.build()
)
.setOnClickListener(
OnClickListener {})
.build()
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
// mNavigationScreen =
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
navigationViewModel = NavigationViewModel(TomTomRepository())
mNavigationScreen =
NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
val action = intent.action
if (CarContext.ACTION_NAVIGATE == action) {
Log.i(TAG, "In onCreateScreen() Navigation intent")
CarToast.makeText(
carContext,
"Navigation intent: " + intent.dataString,
@@ -81,8 +113,29 @@ internal class ClusterSession : Session(), NavigationListener {
)
.show()
}
initializeManagers()
return mNavigationScreen
}
return mNavigationScreen!!
/**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = flowOf(false),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
})
deviceLocationManager.startLocationUpdates()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation $location")
surfaceRenderer.updateLocation(location, "")
}
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
@@ -125,4 +178,8 @@ internal class ClusterSession : Session(), NavigationListener {
}
}
}
override fun invalidateNavigationScreen() {
mNavigationScreen.invalidate()
}
}
@@ -14,7 +14,7 @@ import com.kouros.navigation.data.Constants.TAG
class NavigationCarAppService : CarAppService() {
val intentActionNavNotificationOpenApp =
val intentActionNavNotificationOpenApp =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
val channelId: String = "NavigationSessionChannel"
@@ -26,7 +26,7 @@ class NavigationCarAppService : CarAppService() {
@SuppressLint("PrivateResource")
override fun createHostValidator(): HostValidator {
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
}
@@ -1,5 +1,6 @@
package com.kouros.navigation.car
import android.Manifest
import android.Manifest.permission
import android.content.Intent
import android.content.pm.PackageManager
@@ -53,6 +54,8 @@ 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.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
@@ -65,6 +68,7 @@ import kotlinx.coroutines.launch
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
import kotlin.collections.listOf
import kotlin.math.absoluteValue
@@ -74,7 +78,7 @@ import kotlin.math.absoluteValue
* car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events.
*/
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature
val useContacts = false
@@ -82,7 +86,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
// Model for managing route state and navigation logic for Android Auto
lateinit var routeModel: RouteCarModel
var route = ""
var route = ""
// Main navigation screen displayed to the user
lateinit var navigationScreen: NavigationScreen
@@ -175,24 +179,12 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager
val repository = getSettingsRepository(carContext)
lateinit var repository: SettingsRepository
val settingsViewModel = getSettingsViewModel(carContext)
lateinit var settingsViewModel: SettingsViewModel
init {
lifecycle.addObserver(lifecycleObserver)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
@@ -244,10 +236,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
/**
* Creates the initial screen for the session.
* Sets up ViewModel store, initializes components, checks permissions,
* Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen.
*/
override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore()
initializeViewModels()
initializeManagers()
@@ -255,6 +248,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
return checkPermissionsAndGetScreen()
}
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
* Sets up ViewModelStoreOwner and manages its lifecycle.
*/
@@ -309,7 +322,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager(
carContext = carContext,
@@ -335,7 +348,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
textToSpeechManager = TextToSpeechManager(carContext)
val repository = getSettingsRepository(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
@@ -381,6 +393,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
screenManager.push(navigationScreen)
return RequestPermissionScreen(
carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
permissionCheckCallback = { screenManager.pop() }
)
}
@@ -488,7 +504,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
* 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) : Boolean {
private fun snapLocation(location: Location, streetName: String): Boolean {
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation)
when {
@@ -514,7 +530,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
*/
fun updateNavigationScreen() {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()) {
&& routeModel.navState.destination.street.isEmpty()
) {
return
}
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
@@ -728,7 +745,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
coordinates.add(listOf(it.lon, it.lat))
}
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value = speedData
surfaceRenderer.speedCameraData.value = speedData
}
/**
@@ -755,7 +772,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
val preview = place.route
navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(location(place.longitude, place.latitude), location(prevDestination.longitude, prevDestination.latitude))
listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else {
listOf(location(place.longitude, place.latitude))
}
@@ -853,7 +873,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
}
}
companion object {
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object {
// URI host for deep linking
var uriHost: String = "navigation"
@@ -8,6 +8,7 @@ import android.location.Location
import android.util.Log
import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.Session
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.compose.foundation.layout.PaddingValues
@@ -32,19 +33,15 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.DarkMode
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.previewZoom
import com.kouros.navigation.utils.settingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.style.BaseStyle
@@ -60,8 +57,8 @@ import java.time.LocalDateTime
class SurfaceRenderer(
private var carContext: CarContext,
lifecycle: Lifecycle,
//private var routeModel: RouteCarModel,
private var viewModelStoreOwner: ViewModelStoreOwner
private var viewModelStoreOwner: ViewModelStoreOwner,
private var navigationSession: CarSession
) : DefaultLifecycleObserver {
// Last known location for bearing calculations
@@ -99,7 +96,7 @@ class SurfaceRenderer(
val trafficData = MutableLiveData(emptyMap<String, String>())
// Speed camera locations as GeoJSON
val speedCamerasData = MutableLiveData("")
val speedCameraData = MutableLiveData("")
// Current speed in km/h
val speed = MutableLiveData(0F)
@@ -182,7 +179,7 @@ class SurfaceRenderer(
this.setViewTreeLifecycleOwner(lifecycleOwner)
this.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
setContent {
MapView()
MapView()
}
}
presentation = Presentation(carContext, virtualDisplay.display)
@@ -227,10 +224,20 @@ class SurfaceRenderer(
}
/**
* Called when user scrolls the map (not currently implemented).
* Called when user scrolls the map .
*/
override fun onScroll(distanceX: Float, distanceY: Float) {
synchronized(this@SurfaceRenderer) {
viewStyle = ViewStyle.PAN_VIEW
if (distanceX != 0.0F) {
lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom
}
if (distanceY != 0.0F) {
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
}
val pos = Position(lastLocation.longitude, lastLocation.latitude)
updateCameraPosition( target = pos)
navigationSession.invalidateNavigationScreen()
}
}
@@ -238,7 +245,9 @@ class SurfaceRenderer(
* Called when user scales (zooms) the map (not currently implemented).
*/
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
synchronized(this@SurfaceRenderer) {
Log.d(TAG, "onScale")
}
}
}
@@ -260,7 +269,7 @@ class SurfaceRenderer(
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val traffic: Map<String, String>? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState()
val speedCamera: String? by speedCameraData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
@@ -273,7 +282,7 @@ class SurfaceRenderer(
route,
traffic,
viewStyle,
speedCameras,
speedCamera,
showBuildings
)
ShowPosition(cameraState, position, paddingValues, dark)
@@ -342,9 +351,9 @@ class SurfaceRenderer(
viewStyle = ViewStyle.PAN_VIEW
}
val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1.0
cameraPosition.value!!.zoom - 0.2
} else {
cameraPosition.value!!.zoom + 1.0
cameraPosition.value!!.zoom + 0.2
}
if (viewStyle == ViewStyle.VIEW) {
tilt = calculateTilt(newZoom, tilt)
@@ -416,7 +425,8 @@ class SurfaceRenderer(
* Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation.
*/
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double, target: Position, tilt: Double) {
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double = cameraPosition.value!!.zoom ,
target: Position, tilt: Double = 0.0) {
synchronized(this) {
cameraPosition.postValue(
cameraPosition.value!!.copy(
@@ -1,6 +1,7 @@
package com.kouros.navigation.car.map
import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@@ -52,6 +53,7 @@ import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions
@@ -100,7 +102,8 @@ fun MapLibre(
OrnamentOptions(isScaleBarEnabled = false)
),
cameraState = cameraState,
baseStyle = baseStyle
baseStyle = baseStyle,
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!showBuildings) {
@@ -54,7 +54,7 @@ class Simulation {
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
speed = 5.0f
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
@@ -18,4 +18,5 @@ interface NavigationListener {
fun navigateToPlace(place: Place)
fun recalcRoute(destination: Place)
}
@@ -4,6 +4,7 @@ import android.os.CountDownTimer
import android.os.Handler
import android.util.Log
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
@@ -20,6 +21,7 @@ import androidx.car.app.navigation.model.Destination
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.PanModeListener
import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate
@@ -39,7 +41,6 @@ import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel
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
@@ -81,6 +82,8 @@ open class NavigationScreen(
private lateinit var steps: MutableList<Step>
private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE
private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
@@ -99,6 +102,9 @@ open class NavigationScreen(
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it
})
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
arrivalTimer?.cancel()
@@ -145,17 +151,17 @@ open class NavigationScreen(
.setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
mapActionStrip(carContext,
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_pan_24,
0,
onClickAction = {
Action.Builder()
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
.setFlags(0)
.setOnClickListener {
surfaceRenderer.setStandardView()
invalidate()
}
)
.build()
})
)
.setBackgroundColor(backGroundColor)
@@ -166,24 +172,25 @@ open class NavigationScreen(
* Creates and returns a template for the default view state.
*/
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
val mapActionStrip = mapActionStrip(carContext,
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_recenter_24,
0,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
})
return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_pan_24,
0,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
})
)
.setMapActionStrip(mapActionStrip)
.setPanModeListener { isInPanMode: Boolean ->
Log.d(TAG, "PanMode $isInPanMode")
}
.build()
}
/**
@@ -231,11 +238,11 @@ open class NavigationScreen(
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
mapActionStrip(carContext,
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_pan_24,
carContext = carContext, R.drawable.ic_recenter_24,
0,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -292,7 +299,7 @@ open class NavigationScreen(
val builder = MapWithContentTemplate.Builder()
.setContentTemplate(contentTemplate)
.setActionStrip(
mapActionStrip(
mapActionStrip(carContext,
ViewStyle.VIEW,
{ settingsAction() },
{
@@ -304,7 +311,7 @@ open class NavigationScreen(
},
{
createAction(
carContext = carContext, R.drawable.ic_zoom_out_24,
carContext = carContext, R.drawable.ic_recenter_24,
FLAG_IS_PERSISTENT,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -358,6 +365,7 @@ open class NavigationScreen(
surfaceRenderer,
place,
navigationViewModel,
showAlternativeRoute
)
) { obj: Any? ->
if (obj != null) {
@@ -46,10 +46,15 @@ class PlaceListScreen(
private var routingEngine = 0
private var showAlternativeRoute = false
init {
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
navigationViewModel.recentPlaces.value = emptyList()
@@ -137,10 +142,11 @@ class PlaceListScreen(
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.MULTI_ROUTE,
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer,
place,
navigationViewModel,
showAlternativeRoute
)
) { obj: Any? ->
if (obj != null) {
@@ -16,9 +16,8 @@ import androidx.car.app.model.Template
/** Screen for asking the user to grant location permission. */
class RequestPermissionScreen(
carContext: CarContext,
var permissionCheckCallback: PermissionCheckCallback,
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
val permissions: MutableList<String?> = ArrayList()
val permissions: List<String?> = ArrayList(),
var permissionCheckCallback: PermissionCheckCallback
) : Screen(carContext) {
/** Callback called when the permission is granted. */
@@ -29,7 +28,7 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template {
var message = ""
var message = "This app needs access to location"
if (permissions.contains(permission.ACCESS_FINE_LOCATION))
message = "This app needs access to location and to car speed"
if (permissions.contains("android.car.permission.CAR_SPEED"))
@@ -83,7 +82,7 @@ fun checkPermission(carContext: CarContext, permission: String) : Boolean {
screenManager.pop()
return@RequestPermissionScreen
},
permissions
permissions = permissions
)
)
} else {
@@ -55,6 +55,7 @@ class RoutePreviewScreen(
private var surfaceRenderer: SurfaceRenderer,
private var destination: Place,
private val navigationViewModel: NavigationViewModel,
private var showAlternativeRoute: Boolean
) :
Screen(carContext) {
private var isFavorite = false
@@ -71,7 +72,7 @@ class RoutePreviewScreen(
var loading = true
var showAlternativeRoute = true
var flag = FLAG_DEFAULT
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
@@ -84,7 +85,7 @@ class RoutePreviewScreen(
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
loading = false
if (routeModel.route.routes.size == 1) {
if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
routeType = RoutePreviewType.SINGLE_ROUTE
showAlternativeRoute = false
}
@@ -116,6 +117,10 @@ class RoutePreviewScreen(
routingEngine = it
})
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycleScope.launch {
navigationViewModel.loadPreviewRoute(
carContext,
@@ -178,7 +183,7 @@ class RoutePreviewScreen(
listContent.build()
} else {
val navigateAction =
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{
createAction(carContext, R.drawable.navigation_48px, flag,{
onNavigate(routeModel.navState.currentRouteIndex)
})
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
@@ -202,7 +207,7 @@ class RoutePreviewScreen(
.setContentTemplate(content)
.setMapController(
MapController.Builder().setMapActionStrip(
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
mapActionStrip(carContext, ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
zoomMinus()
} )).build()
@@ -225,6 +230,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_in_24,
FLAG_IS_PERSISTENT,
onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(1)
invalidate()
}
@@ -239,6 +245,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_out_24,
FLAG_IS_PERSISTENT,
onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(-1)
invalidate()
}
@@ -297,7 +304,7 @@ class RoutePreviewScreen(
}
private fun createRow(route: Routes, index: Int): Row {
val navigateAction = createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT ) {
val navigateAction = createAction(carContext, R.drawable.navigation_48px, flag ) {
this.onNavigate(index)
}
val routeText = createRouteText(route)
@@ -12,6 +12,7 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
@@ -82,20 +83,28 @@ fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): Acti
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
fun mapActionStrip(
carContext: CarContext,
viewStyle: ViewStyle,
zoomPlus: () -> Action,
zoomMinus: () -> Action,
panAction: () -> Action
recenterAction: () -> Action
): ActionStrip {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
actionStripBuilder.addAction(
Action.Builder(Action.PAN)
.setIcon(createCarIcon(carContext, R.drawable.ic_pan_24))
.setFlags(0)
.build()
)
if (viewStyle == ViewStyle.PAN_VIEW) {
actionStripBuilder
.addAction(
panAction()
recenterAction()
)
}
return actionStripBuilder.build()
}
@@ -106,7 +115,7 @@ fun createAction(
carContext: CarContext,
@DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit
onClickAction: () -> Unit,
): Action {
return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes))