This commit is contained in:
Dimitris
2026-03-16 15:09:03 +01:00
parent 5198725879
commit ada878b23c
21 changed files with 235 additions and 8724 deletions

View File

@@ -7,17 +7,12 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.data.R
import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Test
@@ -25,7 +20,6 @@ import org.junit.runner.RunWith
import org.junit.Assert.*
import org.junit.Before
import org.maplibre.compose.expressions.dsl.step
import kotlin.collections.forEach
/**
@@ -44,7 +38,7 @@ class RouteModelTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val repository = getSettingsRepository(appContext)
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
val routeJson = appContext.resources.openRawResource(R.raw.tomom_routing)
val routeJson = appContext.resources.openRawResource(R.raw.tomtom_routing)
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
assertNotEquals("", routeJsonString)
routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal)

View File

@@ -418,6 +418,7 @@ class SurfaceRenderer(
* Sets route data for active navigation and switches to VIEW mode.
*/
fun clearRouteData() {
updateLocation(lastLocation)
routeData.value = ""
viewStyle = ViewStyle.VIEW
cameraPosition.postValue(

View File

@@ -85,14 +85,10 @@ class NavigationScreen(
var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0)
var lastCameraSearch = 0
var speedCameras = listOf<Elements>()
val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext)
var distanceMode = 0
init {
@@ -112,19 +108,26 @@ class NavigationScreen(
*/
override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) {
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData()
listener.startNavigation()
prepareRoute(route)
invalidate()
}
}
/**
* Prepare route and start navigation
*/
private fun prepareRoute(route: String) {
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
if (routeModel.hasLegs()) {
settingsViewModel.onLastRouteChanged(route)
}
surfaceRenderer.setRouteData()
listener.startNavigation()
}
/**
* Checks if navigation is currently active.
*/
@@ -176,6 +179,16 @@ class NavigationScreen(
surfaceRenderer.maxSpeed.value = speed
}
/**
* Handles the received previe route string.
* Starts navigation and invalidates the screen.
*/
override fun onPreviewRouteReceived(route: String) {
if (navigationType == NavigationType.NAVIGATION && route.isNotEmpty()) {
startPreviewScreen(route)
}
}
/**
* Invalidates the screen.
*/
@@ -409,13 +422,20 @@ class NavigationScreen(
)
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
navigationViewModel.loadRoute(
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
navigateTo,
surfaceRenderer.carOrientation
)
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
// val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
// navigationViewModel.loadRoute(
// carContext,
// surfaceRenderer.lastLocation,
// navigateTo,
// surfaceRenderer.carOrientation
// )
// routeModel.navState = routeModel.navState.copy(destination = recentPlace)
}
.build()
}
@@ -458,7 +478,7 @@ class NavigationScreen(
* Creates an action to start the settings screen.
*/
private fun settingsAction(): Action {
return Action.Builder()
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext, navigationViewModel))
@@ -554,12 +574,44 @@ class NavigationScreen(
}
}
/**
* Pushes the search screen and handles the search result.
*/
private fun startPreviewScreen(route: String) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer,
recentPlace,
navigationViewModel,
routeModel = routeModel
)
) { obj: Any? ->
if (obj != null) {
navigateToPlace(recentPlace)
} else {
routeModel.stopNavigation()
navigationType = NavigationType.VIEW
surfaceRenderer.clearRouteData()
invalidate()
}
}
}
/**
* Loads a route to the specified place and sets it as the destination.
*/
fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value
navigationType = NavigationType.VIEW
navigationViewModel.previewRoute.value = ""
navigationType = NavigationType.NAVIGATION
val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location
@@ -663,7 +715,7 @@ class NavigationScreen(
* This includes destination name, address, travel estimate, and loading status.
*/
private fun updateTrip() {
if (routeModel.isNavigating()) {
if (routeModel.isNavigating() && !routeModel.navState.destination.name.isNullOrEmpty()) {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")

View File

@@ -56,6 +56,7 @@ class PlaceListScreen(
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.MULTI_ROUTE,
surfaceRenderer,
place,
navigationViewModel,

View File

@@ -1,7 +1,8 @@
package com.kouros.navigation.car.screen
import android.os.CountDownTimer
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.activity.OnBackPressedCallback
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
@@ -11,9 +12,11 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.DurationSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
@@ -24,7 +27,6 @@ import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Lifecycle
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils
@@ -32,14 +34,15 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.Duration
import kotlin.math.min
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
class RoutePreviewScreen(
carContext: CarContext,
private var routeType: RoutePreviewType,
private var surfaceRenderer: SurfaceRenderer,
private var destination: Place,
private val navigationViewModel: NavigationViewModel,
@@ -51,6 +54,7 @@ class RoutePreviewScreen(
val maxListItems: Int = 3
val navigationUtils = NavigationUtils(carContext)
var routeSelected = false
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
invalidate()
@@ -63,7 +67,6 @@ class RoutePreviewScreen(
override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder()
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
val listLimit = min(
@@ -79,12 +82,16 @@ class RoutePreviewScreen(
}
}
val street = if (destination.street.isNullOrEmpty()) {
carContext.getString((R.string.route_preview))
} else {
destination.street.toString()
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.route_preview))
.setTitle(street)
if (routeModel.route.routes.size == 1) {
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
header.addEndHeaderAction(
favoriteAction()
)
@@ -99,17 +106,8 @@ class RoutePreviewScreen(
CarText.Builder("Wait")
.build()
}
if (routeModel.route.routes.size == 1) {
val timer = object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
onNavigate(0)
}
}
timer.start()
}
val content = if (routeModel.route.routes.size > 1) {
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
ListTemplate.Builder()
.setHeader(header.build())
.setSingleList(itemListBuilder.build())
@@ -120,26 +118,63 @@ class RoutePreviewScreen(
carContext, R.drawable.navigation_48px
)
).build()
val selectRouteIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(0) }
.setOnClickListener { onNavigate(routeModel.navState.currentRouteIndex) }
.build()
val selectRouteAction = Action.Builder()
.setIcon(selectRouteIcon)
.setOnClickListener {
routeType = RoutePreviewType.MULTI_ROUTE
invalidate()
}
.build()
MessageTemplate.Builder(
message
)
.setHeader(header.build())
.addAction(navigateAction)
.addAction(selectRouteAction)
.setLoading(message.toString() == "Wait")
.build()
}
return MapWithContentTemplate.Builder()
val template = MapWithContentTemplate.Builder()
.setContentTemplate(content)
.setMapController(
MapController.Builder().setMapActionStrip(
getMapActionStrip()
).build()
)
if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
template.setActionStrip(createActionStripBuilder().build())
}
return template.build()
}
private fun createActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
navigateAction()
)
return actionStripBuilder
}
private fun navigateAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.navigation_48px))
.setFlags(FLAG_DEFAULT)
.setOnClickListener {
onNavigate(routeModel.navState.currentRouteIndex)
}
.build()
}
@@ -187,7 +222,7 @@ class RoutePreviewScreen(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_pan_24
R.drawable.heart_minus_48px
)
)
.build()
@@ -230,7 +265,6 @@ class RoutePreviewScreen(
street = it.street
}
}
val delay = (route.summary.trafficDelay / 60).toInt().toString()
val row = Row.Builder()
.setTitle(routeText)
@@ -239,11 +273,27 @@ class RoutePreviewScreen(
.addAction(navigateAction)
if (route.summary.trafficDelay > 60) {
row.addText("$delay min")
row.addText(createDelay(route))
}
return row.build()
}
private fun createDelay(route: Routes): SpannableStringBuilder {
val delay = (route.summary.trafficDelay / 60)
val delayBuilder = SpannableStringBuilder()
delayBuilder.append(
" ",
DurationSpan.create(Duration.ofMinutes(delay.toLong())),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
delayBuilder.setSpan(
ForegroundCarColorSpan.create(CarColor.RED),
0,
1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
return delayBuilder
}
private fun onNavigate(index: Int) {
destination.routeIndex = index
setResult(destination)
@@ -253,6 +303,8 @@ class RoutePreviewScreen(
private fun onRouteSelected(index: Int) {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
surfaceRenderer.setPreviewRouteData(routeModel)
routeSelected = true
invalidate()
}
fun getMapActionStrip(): ActionStrip {
@@ -276,3 +328,7 @@ class RoutePreviewScreen(
.build()
}
}
enum class RoutePreviewType {
SINGLE_ROUTE, MULTI_ROUTE
}

View File

@@ -28,7 +28,11 @@ interface NavigationObserverCallback {
/** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int)
/** Called when a preview route is received and navigation should start */
fun onPreviewRouteReceived(route: String)
/** Called to request UI invalidation/refresh */
fun invalidateScreen()
}

View File

@@ -17,7 +17,8 @@ class NavigationObserverManager(
val placeSearchObserver = PlaceSearchObserver(callback)
val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback)
val previewObserver = PreviewRouteObserver(callback)
/**
* Attaches all observers to the ViewModel.
* Call this from NavigationScreen's init block or lifecycle method.
@@ -29,6 +30,7 @@ class NavigationObserverManager(
viewModel.placeLocation.observe(screen, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver)
viewModel.previewRoute.observe(screen, previewObserver)
}
/**

View File

@@ -0,0 +1,14 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
class PreviewRouteObserver(
private val callback: NavigationObserverCallback
) : Observer<String> {
override fun onChanged(value: String) {
if (value.isNotEmpty()) {
callback.onPreviewRouteReceived(value)
}
}
}