This commit is contained in:
Dimitris
2026-03-21 12:24:53 +01:00
parent ada878b23c
commit d1968cfa68
45 changed files with 1121 additions and 935 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 71 versionCode = 73
versionName = "0.2.0.71" versionName = "0.2.0.73"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -294,7 +294,7 @@ class MainActivity : ComponentActivity() {
fun updateLocation(location: Location?) { fun updateLocation(location: Location?) {
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) { if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
val currentLocation = location(location.position.longitude, location.position.latitude) val currentLocation = location(location.position.longitude, location.position.latitude)
if (location.bearing != null) { if (location.bearing != null && location.bearingAccuracy!! <= 20.0) {
currentLocation.bearing = location.bearing!!.toFloat() currentLocation.bearing = location.bearing!!.toFloat()
} }
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {

View File

@@ -1,22 +1,17 @@
package com.kouros.navigation.ui.settings package com.kouros.navigation.ui.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -24,7 +19,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -36,7 +30,6 @@ import com.kouros.navigation.ui.components.SectionTitle
import com.kouros.navigation.ui.components.SettingSwitch import com.kouros.navigation.ui.components.SettingSwitch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
@@ -45,6 +38,7 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
val show3D by viewModel.show3D.collectAsState() val show3D by viewModel.show3D.collectAsState()
val showTraffic by viewModel.traffic.collectAsState() val showTraffic by viewModel.traffic.collectAsState()
val distanceMode by viewModel.distanceMode.collectAsState() val distanceMode by viewModel.distanceMode.collectAsState()
val driveSuggestion by viewModel.tripSuggestion.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = {
@@ -90,6 +84,12 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
checked = showTraffic, checked = showTraffic,
onCheckedChange = viewModel::onTraffic onCheckedChange = viewModel::onTraffic
) )
SettingSwitch(
title = stringResource(R.string.trip_suggestion),
checked = driveSuggestion,
onCheckedChange = viewModel::onTripSuggestion
)
} }
} }

View File

@@ -1,3 +1,4 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
@@ -31,6 +32,9 @@ android {
} }
} }
val mockitoAgent = configurations.create("mockitoAgent")
dependencies { dependencies {
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.car.app) implementation(libs.androidx.car.app)
@@ -48,10 +52,19 @@ dependencies {
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.androidx.datastore.core) implementation(libs.androidx.datastore.core)
implementation(libs.androidx.monitor) implementation(libs.androidx.monitor)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules)
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.mockito.core) testImplementation(libs.mockito.core)
testImplementation(libs.mockito.kotlin) testImplementation(libs.mockito.kotlin)
androidTestImplementation(libs.androidx.runner) testImplementation(libs.androidx.car.app.testing)
androidTestImplementation(libs.androidx.rules) testImplementation(libs.robolectric)
testImplementation(libs.google.truth)
testImplementation(libs.androidx.test.core)
mockitoAgent(libs.mockito.core) { isTransitive = false }
} }

View File

@@ -38,8 +38,10 @@ class RouteModelTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
val repository = getSettingsRepository(appContext) val repository = getSettingsRepository(appContext)
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
val routeJson = appContext.resources.openRawResource(R.raw.tomtom_routing) val routeJsonString = TomTomRepository().fetchUrl(
val routeJsonString = routeJson.bufferedReader().use { it.readText() } "https://kouros-online.de/tomtom_routing.json",
false
)
assertNotEquals("", routeJsonString) assertNotEquals("", routeJsonString)
routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal) routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal)
routeModel.startNavigation(routeJsonString) routeModel.startNavigation(routeJsonString)
@@ -49,7 +51,7 @@ class RouteModelTest {
fun checkRoute() { fun checkRoute() {
assertEquals(true, routeModel.isNavigating()) assertEquals(true, routeModel.isNavigating())
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0) assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1483.0, 10.0) assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0)
} }
@Test @Test
@@ -60,7 +62,7 @@ class RouteModelTest {
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(stepData.instruction, "Silcherstraße") assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 25.0, 1.0) assertEquals(stepData.leftStepDistance, 20.0, 5.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße") assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
@@ -141,7 +143,7 @@ class RouteModelTest {
if (index in 61..61) { if (index in 61..61) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 2) assertEquals(stepData.lane.size, 3)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT") assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
} }
@@ -203,6 +205,6 @@ class RouteModelTest {
val location: Location = location(11.578911, 48.185565) val location: Location = location(11.578911, 48.185565)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 34.0, 1.0) assertEquals(step.leftStepDistance, 26.0, 1.0)
} }
} }

View File

@@ -24,9 +24,11 @@ import androidx.lifecycle.coroutineScope
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation import com.kouros.navigation.car.navigation.Simulation
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.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.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
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
@@ -41,9 +43,12 @@ import com.kouros.navigation.model.NavigationViewModel
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.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.time.LocalDateTime
import java.time.ZoneOffset
/** /**
@@ -52,7 +57,7 @@ import kotlinx.coroutines.launch
* 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(), NavigationScreen.Listener { class NavigationSession : Session(), NavigationListener {
// Flag to enable/disable contact access feature // Flag to enable/disable contact access feature
val useContacts = false val useContacts = false
@@ -80,6 +85,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
val simulation = Simulation() val simulation = Simulation()
var navigationManagerStarted = 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.
@@ -148,11 +155,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
when (connectionState) { when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
CarConnection.CONNECTION_TYPE_NATIVE -> { CarConnection.CONNECTION_TYPE_NATIVE -> {
navigationScreen.checkPermission(AUTOMOTIVE_CAR_SPEED_PERMISSION) navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION)
} }
CarConnection.CONNECTION_TYPE_PROJECTION -> { CarConnection.CONNECTION_TYPE_PROJECTION -> {
navigationScreen.checkPermission(GMS_CAR_SPEED_PERMISSION) navigationViewModel.permissionGranted.value = checkPermission(carContext, GMS_CAR_SPEED_PERMISSION)
} }
} }
} }
@@ -210,6 +217,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
// Called when the app should simulate navigation (e.g., for testing) // Called when the app should simulate navigation (e.g., for testing)
deviceLocationManager.stopLocationUpdates() deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true autoDriveEnabled = true
surfaceRenderer.viewStyle = ViewStyle.VIEW
simulation.startSimulation( simulation.startSimulation(
routeModel, lifecycle.coroutineScope routeModel, lifecycle.coroutineScope
) { location -> ) { location ->
@@ -241,10 +249,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(), shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
onLocationUpdate = ::updateLocation, onLocationUpdate = ::updateLocation,
onInitialLocation = { location -> onInitialLocation = { location ->
navigationViewModel.loadRecentPlace( navigationViewModel.loadRecentPlaces(
carContext,
location, location,
surfaceRenderer.carOrientation, surfaceRenderer.carOrientation,
carContext
) )
} }
) )
@@ -325,7 +333,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
private fun handleNavigateIntent(screenManager: ScreenManager) { private fun handleNavigateIntent(screenManager: ScreenManager) {
screenManager.popToRoot() screenManager.popToRoot()
screenManager.pushForResult( screenManager.pushForResult(
SearchScreen(carContext, surfaceRenderer, navigationViewModel) SearchScreen(carContext, surfaceRenderer, navigationViewModel, emptyList())
) { result -> ) { result ->
// Handle search result if needed // Handle search result if needed
} }
@@ -359,6 +367,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
handleNavigationLocation(location) handleNavigationLocation(location)
} else { } else {
navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
surfaceRenderer.updateLocation(location) surfaceRenderer.updateLocation(location)
} }
} }
@@ -417,7 +426,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Called when user starts navigation * Called when user starts navigation
*/ */
override fun startNavigation() { override fun startNavigation() {
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationManager.navigationStarted() navigationManager.navigationStarted()
navigationManagerStarted = true
if (autoDriveEnabled) { if (autoDriveEnabled) {
simulation.startSimulation( simulation.startSimulation(
routeModel, lifecycle.coroutineScope routeModel, lifecycle.coroutineScope
@@ -428,8 +439,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
override fun updateTrip(trip: Trip) { override fun updateTrip(trip: Trip) {
if (navigationManagerStarted) {
navigationManager.updateTrip(trip) navigationManager.updateTrip(trip)
} }
}
/** /**
* Handle guidance audio * Handle guidance audio

View File

@@ -392,6 +392,7 @@ 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() {
println("SetRouteData")
routeData.value = routeModel.curRoute.routeGeoJson routeData.value = routeModel.curRoute.routeGeoJson
viewStyle = ViewStyle.VIEW viewStyle = ViewStyle.VIEW
} }
@@ -457,6 +458,11 @@ class SurfaceRenderer(
} }
fun updateTrafficData(routeModel: RouteCarModel) {
}
/** /**
* Displays a specific location (e.g., amenity/POI) on the map. * Displays a specific location (e.g., amenity/POI) on the map.
*/ */

View File

@@ -31,30 +31,6 @@ import java.util.Locale
class NavigationUtils(private var carContext: CarContext) { class NavigationUtils(private var carContext: CarContext) {
private fun createToastAction(
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
flags: Int
): Action {
return Action.Builder()
.setOnClickListener { showToast(toastStringRes) }
.setTitle(createCarText(titleRes))
.setFlags(flags)
.build()
}
fun showToast(@StringRes toastStringRes: Int) {
CarToast.makeText(
carContext,
carContext.getString(toastStringRes),
CarToast.LENGTH_SHORT
)
.show()
}
fun createCarText(@StringRes stringRes: Int): CarText {
return CarText.create(carContext.getString(stringRes))
}
fun createCarIcon(@DrawableRes iconRes: Int): CarIcon { fun createCarIcon(@DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()

View File

@@ -1,9 +1,8 @@
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.speech.tts.TextToSpeech
import android.text.SpannableString import android.text.SpannableString
import android.util.Log import android.text.SpannableStringBuilder
import androidx.annotation.DrawableRes import android.text.Spanned
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -16,6 +15,8 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.DateTimeWithZone import androidx.car.app.model.DateTimeWithZone
import androidx.car.app.model.Distance import androidx.car.app.model.Distance
import androidx.car.app.model.DurationSpan
import androidx.car.app.model.ForegroundCarColorSpan
import androidx.car.app.navigation.model.Lane import androidx.car.app.navigation.model.Lane
import androidx.car.app.navigation.model.LaneDirection import androidx.car.app.navigation.model.LaneDirection
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
@@ -25,10 +26,11 @@ import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.formattedDistance import com.kouros.navigation.utils.formattedDistance
import java.util.Locale import java.time.Duration
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -115,8 +117,8 @@ class RouteCarModel : RouteModel() {
.setRemainingTimeColor(CarColor.GREEN) .setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.BLUE) .setRemainingDistanceColor(CarColor.BLUE)
if (traffic > 0) { if (traffic > 0) {
travelBuilder.setTripText(CarText.create("$traffic min")) travelBuilder.setTripText(createDelay(traffic))
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
} }
if (navState.travelMessage.isNotEmpty()) { if (navState.travelMessage.isNotEmpty()) {
@@ -125,7 +127,22 @@ class RouteCarModel : RouteModel() {
} }
return travelBuilder.build() return travelBuilder.build()
} }
private fun createDelay(delay: Int): CarText {
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 CarText.Builder(delayBuilder)
.build()
}
fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) : Boolean { fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) : Boolean {
var laneImageAdded = false var laneImageAdded = false
stepData.lane.forEach { stepData.lane.forEach {
@@ -167,9 +184,6 @@ class RouteCarModel : RouteModel() {
return CarText.create(carContext.getString(stringRes)) return CarText.create(carContext.getString(stringRes))
} }
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun createCarIcon(iconCompat: IconCompat): CarIcon { fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build() return CarIcon.Builder(iconCompat).build()

View File

@@ -19,10 +19,6 @@ class Simulation {
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit updateLocation: (Location) -> Unit
) { ) {
// A92
//updateLocation(location(11.709508, 48.338923 ))
//updateLocation(homeVogelhart)
if (routeModel.navState.route.isRouteValid()) { if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return if (points.isEmpty()) return

View File

@@ -30,9 +30,8 @@ class CategoriesScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
) : Screen(carContext), CategoryObserverCallback { ) : Screen(carContext) {
private val categoryObserver = CategoryObserver(this)
private var category = "" private var category = ""
var categories: List<Category> = listOf( var categories: List<Category> = listOf(
@@ -41,17 +40,9 @@ class CategoriesScreen(
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station)) Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
) )
private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
navigationViewModel.elements.value = emptyList()
invalidate()
}
}
init { init {
navigationViewModel.elements.value = emptyList()
navigationViewModel.elements.observe(this, categoryObserver)
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -64,7 +55,20 @@ class CategoriesScreen(
.setImage(carIcon(carContext, it.id, -1)) .setImage(carIcon(carContext, it.id, -1))
.setOnClickListener { .setOnClickListener {
category = it.id category = it.id
navigationViewModel.getAmenities(it.id, surfaceRenderer.lastLocation) screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
category,
navigationViewModel,
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
.setBrowsable(true) .setBrowsable(true)
.build() .build()
@@ -83,36 +87,6 @@ class CategoriesScreen(
.setSingleList(itemListBuilder.build()) .setSingleList(itemListBuilder.build())
.build() .build()
} }
override fun onCategoryElementsReady(
elements: List<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
) {
val loc = location(centerLon, centerLat)
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
category,
navigationViewModel,
elements
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
override fun invalidateScreen() {
}
} }
fun carIcon(context: CarContext, category: String, index: Int): CarIcon { fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
@@ -125,7 +99,12 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
} }
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build() return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else { } else {
return CarIcon.Builder(NavigationUtils(context).createNumberIcon(category, index.toString())).build() return CarIcon.Builder(
NavigationUtils(context).createNumberIcon(
category,
index.toString()
)
).build()
} }
} }

View File

@@ -5,6 +5,8 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
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.CarText import androidx.car.app.model.CarText
import androidx.car.app.model.Header import androidx.car.app.model.Header
@@ -15,6 +17,8 @@ import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.navigation.NavigationUtils
@@ -37,11 +41,25 @@ class CategoryScreen(
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val category: String, private val category: String,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private var elements: List<Elements>,
) : Screen(carContext) { ) : Screen(carContext), CategoryObserverCallback {
val maxListItems: Int = 30 val maxListItems: Int = 30
var elements: List<Elements> = emptyList()
private val categoryObserver = CategoryObserver(this)
private var loading = true
init {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
navigationViewModel.elements.value = emptyList()
}
})
navigationViewModel.elements.observe(this, categoryObserver)
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
}
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
@@ -72,17 +90,22 @@ class CategoryScreen(
.setTitle(getTitle(carContext, category)) .setTitle(getTitle(carContext, category))
.build() .build()
val builder = MapWithContentTemplate.Builder() val builder = MapWithContentTemplate.Builder()
.setContentTemplate(
ListTemplate.Builder()
.setHeader(header)
.setSingleList(listBuilder.build())
.build()
)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
getMapActionStrip() getMapActionStrip()
).build() ).build()
) )
val content = ListTemplate.Builder()
.setHeader(header)
if (loading) {
content.setLoading(true)
} else {
content.setSingleList(listBuilder.build())
}
.build()
builder.setContentTemplate(content.build())
return builder.build() return builder.build()
} }
@@ -122,10 +145,8 @@ class CategoryScreen(
} else { } else {
row.addText(carText("${it.tags.openingHours}")) row.addText(carText("${it.tags.openingHours}"))
} }
val navigationUtils = NavigationUtils(carContext)
row.addAction( row.addAction(
Action.Builder() createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
.setOnClickListener {
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
carContext, carContext,
currentLocation = surfaceRenderer.lastLocation, currentLocation = surfaceRenderer.lastLocation,
@@ -141,9 +162,7 @@ class CategoryScreen(
) )
) )
finish() finish()
} }))
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
.build())
return row.build() return row.build()
} }
@@ -179,10 +198,25 @@ class CategoryScreen(
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
scale: Int scale: Int
): Action { ): Action {
val navigationUtils = NavigationUtils(carContext) return createAction(carContext, iconRes, FLAG_IS_PERSISTENT, {
return Action.Builder() surfaceRenderer.handleScale(scale)
.setOnClickListener { surfaceRenderer.handleScale(scale) } })
.setIcon(navigationUtils.createCarIcon(iconRes)) }
.build()
override fun onCategoryElementsReady(
elements: List<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
) {
val loc = location(centerLon, centerLat)
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
this.elements = elements
loading = false
}
override fun invalidateScreen() {
invalidate()
} }
} }

View File

@@ -0,0 +1,17 @@
package com.kouros.navigation.car.screen
import androidx.car.app.navigation.model.Trip
/** A listener for navigation start and stop signals. */
interface NavigationListener {
/** Stops navigation. */
fun stopNavigation()
/** Starts navigation. */
fun startNavigation()
/** Updates trip information. */
fun updateTrip(trip: Trip)
}

View File

@@ -1,30 +1,33 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
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.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
import androidx.car.app.model.MessageTemplate import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.Destination import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.MapWithContentTemplate 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.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -35,11 +38,14 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager 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.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.ROUTE_UPDATE
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE 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.overpass.Elements 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.GeoUtils
import com.kouros.navigation.utils.formattedDistance import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
@@ -47,7 +53,6 @@ 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 kotlinx.coroutines.runBlocking
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -57,154 +62,98 @@ import kotlin.math.absoluteValue
* Main screen for car navigation. * Main screen for car navigation.
* Handles different navigation states and provides corresponding templates. * Handles different navigation states and provides corresponding templates.
*/ */
class NavigationScreen( open class NavigationScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var routeModel: RouteCarModel, private var routeModel: RouteCarModel,
private var listener: Listener, private var listener: NavigationListener,
private val navigationViewModel: NavigationViewModel private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback { ) : Screen(carContext), NavigationObserverCallback {
/** A listener for navigation start and stop signals. */ val backGroundColor = CarColor.GREEN
interface Listener {
/** Stops navigation. */
fun stopNavigation()
/** Starts navigation. */
fun startNavigation()
/** Updates trip information. */
fun updateTrip(trip: Trip)
}
val backGroundColor = CarColor.BLUE
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlace = Place()
var recentPlaces = emptyList<Place>()
var recentPlace: Place = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0) var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
var lastRouteDate: LocalDateTime = LocalDateTime.MIN
var lastCameraSearch = 0 var lastCameraSearch = 0
var speedCameras = listOf<Elements>() var speedCameras = listOf<Elements>()
val observerManager = NavigationObserverManager(navigationViewModel, this) val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext) val repository = getSettingsRepository(carContext)
val settingsViewModel = getSettingsViewModel(carContext) val settingsViewModel = getSettingsViewModel(carContext)
var distanceMode = 0 private var distanceMode = 0
private var tripSuggestion = false
private var tripSuggestionCalled = false
private var routingEngine = 0
private var showTraffic = false;
private var arrivalTimer: CountDownTimer? = null
private var reRouteTimer: CountDownTimer? = null
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces = newPlaces
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
tripSuggestionCalled = true
navigationType = NavigationType.RECENT
invalidate()
}
}
init { init {
observerManager.attachAllObservers(this) observerManager.attachAllObservers(this)
lifecycleScope.launch { lifecycleScope.launch {
settingsViewModel.tripSuggestion.first()
settingsViewModel.routingEngine.first() settingsViewModel.routingEngine.first()
settingsViewModel.recentPlaces.first()
} }
repository.distanceModeFlow.asLiveData().observe(this, Observer { repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it distanceMode = it
}) })
}
/** repository.trafficFlow.asLiveData().observe(this, Observer {
* Handles the received route string. showTraffic = it
* Starts navigation and invalidates the screen. })
*/ repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
override fun onRouteReceived(route: String) { navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
if (route.isNotEmpty()) { tripSuggestion = it
prepareRoute(route) })
invalidate() repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
arrivalTimer?.cancel()
reRouteTimer?.cancel()
} }
} })
/**
* 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.
*/
override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles the received recent place.
* Updates the navigation type to RECENT and invalidates the screen.
*/
override fun onRecentPlaceReceived(place: Place) {
recentPlace = place
navigationType = NavigationType.RECENT
invalidate()
}
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) {
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
}
/**
* 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.
*/
override fun invalidateScreen() {
invalidate()
} }
/** /**
* Returns the appropriate template based on the current navigation state. * Returns the appropriate template based on the current navigation state.
*/ */
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val actionStripBuilder = createActionStripBuilder({
val actionStripBuilder = createActionStripBuilder() createAction(
carContext,
R.drawable.search_48px,
FLAG_IS_PERSISTENT,
onClickAction = { startSearchScreen()}
)
}, { settingsAction() })
return when (navigationType) { return when (navigationType) {
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder) NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
NavigationType.RECENT -> navigationRecentPlaceTemplate() NavigationType.RECENT -> navigationRecentPlacesTemplate()
NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder) NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder)
NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder) NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder)
else -> navigationViewTemplate(actionStripBuilder) else -> navigationViewTemplate(actionStripBuilder)
@@ -214,9 +163,9 @@ class NavigationScreen(
/** /**
* Creates and returns a NavigationTemplate for the active navigation state. * Creates and returns a NavigationTemplate for the active navigation state.
*/ */
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): Template {
actionStripBuilder.addAction( actionStripBuilder.addAction(
stopAction() createAction(carContext, R.drawable.ic_close_white_24dp, FLAG_IS_PERSISTENT,{ stopNavigation() })
) )
updateTrip() updateTrip()
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
@@ -225,7 +174,20 @@ class NavigationScreen(
) )
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode)) .setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode))
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(
mapActionStrip(
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_zoom_out_24,
FLAG_IS_PERSISTENT,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
}
)
})
)
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.build() .build()
} }
@@ -235,19 +197,30 @@ class NavigationScreen(
*/ */
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(
mapActionStrip(
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_zoom_out_24,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
})
)
.build() .build()
} }
/** /**
* Creates and returns a template for the arrival or end state of navigation. * Creates and returns a template for the arrival.
*/ */
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.navState.arrived) { arrivalTimer?.cancel()
val timer = 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)
@@ -255,17 +228,9 @@ class NavigationScreen(
invalidate() invalidate()
} }
} }
timer.start() arrivalTimer?.start()
return navigationArrivedTemplate(actionStripBuilder) return navigationArrivedTemplate(actionStripBuilder)
} else {
return NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
} }
}
/** /**
* Creates and returns a NavigationTemplate specifically for when the destination is reached. * Creates and returns a NavigationTemplate specifically for when the destination is reached.
@@ -292,36 +257,82 @@ class NavigationScreen(
) )
.build() .build()
) )
.setBackgroundColor(CarColor.SECONDARY) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(
mapActionStrip(
surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, {
createAction(
carContext = carContext, R.drawable.ic_zoom_out_24,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
})
)
.build() .build()
} }
/** /**
* Creates and returns a template showing recent places or destinations. * Creates and returns a template showing recent places or destinations.
*/ */
fun navigationRecentPlaceTemplate(): Template { fun navigationRecentPlacesTemplate(): Template {
val messageTemplate = MessageTemplate.Builder( if (!tripSuggestion || recentPlaces.isEmpty()) {
recentPlace.name + "\n" navigationType = NavigationType.VIEW
+ recentPlace.city return navigationViewTemplate(
createActionStripBuilder(
{
createAction(
carContext,
R.drawable.search_48px,
FLAG_IS_PERSISTENT,
{ startSearchScreen() })
},
{ settingsAction() })
) )
}
val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT }.forEach {
val row = Row.Builder()
.setTitle(it.name!!)
.addAction(
createNavigateAction(it)
)
.setOnClickListener {
navigateToPlace(it)
}
listBuilder.addItem(
row.build()
)
}
val contentTemplate =
ListTemplate.Builder()
.setHeader( .setHeader(
Header.Builder() Header.Builder()
.setTitle(carContext.getString(R.string.drive_now)) .setTitle(carContext.getString(R.string.drive_now))
.addEndHeaderAction(closeAction())
.build() .build()
) )
.addAction(navigateAction()) .setSingleList(listBuilder.build())
.addAction(closeAction())
.build() .build()
val builder = MapWithContentTemplate.Builder() val builder = MapWithContentTemplate.Builder()
.setContentTemplate(messageTemplate) .setContentTemplate(contentTemplate)
.setActionStrip( .setActionStrip(
mapActionStripBuilder() mapActionStrip(
.addAction(settingsAction()) ViewStyle.VIEW,
.addAction(searchAction()) { settingsAction() },
.build() { createAction(carContext, R.drawable.search_48px, FLAG_IS_PERSISTENT,{ startSearchScreen() }) },
{
createAction(
carContext = carContext, R.drawable.ic_zoom_out_24,
FLAG_IS_PERSISTENT,
onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
})
})
) )
return builder.build() return builder.build()
} }
@@ -355,208 +366,98 @@ class NavigationScreen(
return routingInfo.build() return routingInfo.build()
} }
/**
* Creates an ActionStrip builder with common search and settings actions.
*/
private fun createActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
searchAction()
)
actionStripBuilder.addAction(
settingsAction()
)
return actionStripBuilder
}
/**
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
private fun mapActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
if (surfaceRenderer.viewStyle == ViewStyle.PAN_VIEW) {
actionStripBuilder
.addAction(
panAction()
)
}
return actionStripBuilder
}
/**
* Creates a stop navigation action.
*/
private fun stopAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
stopNavigation()
}
.build()
}
/** /**
* Creates an action to start navigation to a specific place. * Creates an action to start navigation to a specific place.
*/ */
private fun navigateAction(): Action { private fun createNavigateAction(place: Place): Action {
navigationType = NavigationType.NAVIGATION recentPlace = place
return Action.Builder() return createAction(
.setIcon( carContext, R.drawable.chevron_right_24px,
CarIcon.Builder( onClickAction = {
IconCompat.createWithResource( screenManager
.pushForResult(
RoutePreviewScreen(
carContext, carContext,
R.drawable.navigation_48px RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer,
place,
navigationViewModel,
) )
) ) { obj: Any? ->
.build() if (obj != null) {
) navigateToPlace(place)
.setOnClickListener {
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
navigateTo,
surfaceRenderer.carOrientation
)
// val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
// navigationViewModel.loadRoute(
// carContext,
// surfaceRenderer.lastLocation,
// navigateTo,
// surfaceRenderer.carOrientation
// )
// routeModel.navState = routeModel.navState.copy(destination = recentPlace)
} }
.build() }
}
)
} }
/** /**
* Creates an action to close the current view or template. * Creates an action to close the current view or template.
*/ */
private fun closeAction(): Action { private fun closeAction(): Action {
return Action.Builder() return createAction(
.setIcon( carContext, R.drawable.ic_close_white_24dp,
CarIcon.Builder( onClickAction = {
IconCompat.createWithResource(
carContext,
R.drawable.ic_close_white_24dp
)
)
.build()
)
.setOnClickListener {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
invalidate() invalidate()
} }
.setFlags(FLAG_DEFAULT) )
.build()
}
/**
* Creates an action to start the search screen.
*/
private fun searchAction(): Action {
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.search_48px))
.setOnClickListener {
startSearchScreen()
}
.build()
} }
/** /**
* Creates an action to start the settings screen. * Creates an action to start the settings screen.
*/ */
private fun settingsAction(): Action { private fun settingsAction(): Action {
return Action.Builder() return createAction(
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px)) carContext, R.drawable.settings_48px,
.setOnClickListener { FLAG_IS_PERSISTENT,
onClickAction = {
screenManager.push(SettingsScreen(carContext, navigationViewModel)) screenManager.push(SettingsScreen(carContext, navigationViewModel))
} }
.build() )
} }
/** /**
* Creates an action to zoom in on the map. * Creates an action to zoom in on the map.
*/ */
private fun zoomPlus(): Action { private fun zoomPlus(): Action {
return Action.Builder() return createAction(
.setIcon( carContext, R.drawable.ic_zoom_in_24,
CarIcon.Builder( FLAG_IS_PERSISTENT,
IconCompat.createWithResource( onClickAction = {
carContext,
R.drawable.ic_zoom_in_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(1) surfaceRenderer.handleScale(1)
invalidate() invalidate()
} }
.build() )
} }
/** /**
* Creates an action to zoom out on the map. * Creates an action to zoom out on the map.
*/ */
private fun zoomMinus(): Action { private fun zoomMinus(): Action {
return Action.Builder() return createAction(
.setIcon( carContext, R.drawable.ic_zoom_out_24,
CarIcon.Builder( onClickAction = {
IconCompat.createWithResource(
carContext,
R.drawable.ic_zoom_out_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.handleScale(-1) surfaceRenderer.handleScale(-1)
invalidate() invalidate()
} }
.build() )
} }
/**
* Creates an action to enable map panning.
*/
private fun panAction(): Action {
return Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_pan_24
)
)
.build()
).setOnClickListener {
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate()
}
.build()
}
/** /**
* Pushes the search screen and handles the search result. * Pushes the search screen and handles the search result.
*/ */
private fun startSearchScreen() { private fun startSearchScreen() {
screenManager screenManager
.pushForResult( .pushForResult(
SearchScreen( SearchScreen(
carContext, carContext,
surfaceRenderer, surfaceRenderer,
navigationViewModel navigationViewModel,
recentPlaces
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
@@ -574,44 +475,12 @@ 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. * Loads a route to the specified place and sets it as the destination.
*/ */
fun navigateToPlace(place: Place) { fun navigateToPlace(place: Place) {
val preview = navigationViewModel.previewRoute.value val preview = navigationViewModel.previewRoute.value
navigationViewModel.previewRoute.value = "" navigationViewModel.previewRoute.value = ""
navigationType = NavigationType.NAVIGATION
val location = location(place.longitude, place.latitude) val location = location(place.longitude, place.latitude)
navigationViewModel.saveRecent(carContext, place) navigationViewModel.saveRecent(carContext, place)
currentNavigationLocation = location currentNavigationLocation = location
@@ -627,6 +496,7 @@ class NavigationScreen(
navigationViewModel.route.value = preview navigationViewModel.route.value = preview
} }
routeModel.navState = routeModel.navState.copy(destination = place) routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate() invalidate()
} }
@@ -650,25 +520,27 @@ class NavigationScreen(
invalidate() invalidate()
val mainThreadHandler = Handler(carContext.mainLooper) val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post { mainThreadHandler.post {
object : CountDownTimer(2000, 1000) { reRouteTimer?.cancel()
reRouteTimer = object : CountDownTimer(2000, 1000) {
override fun onTick(millisUntilFinished: Long) {} override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() { override fun onFinish() {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
reRoute(destination) reRoute(destination)
} }
}.start() }
reRouteTimer?.start()
} }
} }
/** /**
* Re-requests a route for the specified destination. * Re-requests a route for the specified place.
*/ */
fun reRoute(destination: Place) { fun reRoute(place: Place) {
val dest = location(destination.longitude, destination.latitude) val destination = location(place.longitude, place.latitude)
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
carContext, carContext,
surfaceRenderer.lastLocation, surfaceRenderer.lastLocation,
dest, destination,
surfaceRenderer.carOrientation surfaceRenderer.carOrientation
) )
} }
@@ -678,32 +550,58 @@ class NavigationScreen(
*/ */
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
//checkRoute(current, location)
checkTraffic(current, 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(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastRouteDate)
if (duration.abs().seconds > ROUTE_UPDATE) {
lastRouteDate = current
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) val duration = Duration.between(current, lastTrafficDate)
if (duration.abs().seconds > TRAFFIC_UPDATE) { if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
lastTrafficDate = current lastTrafficDate = current
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation) navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
} }
updateSpeedCamera(location)
with(routeModel) {
updateLocation(location, navigationViewModel)
checkArrival()
}
invalidate()
} }
/** /**
* Checks for arrival * Checks for arrival
*/ */
private fun RouteCarModel.checkArrival() { fun checkArrival() {
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION if (routeModel.isArrival()
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
) { ) {
listener.stopNavigation() listener.stopNavigation()
settingsViewModel.onLastRouteChanged("") settingsViewModel.onLastRouteChanged("")
navState = navState.copy(arrived = true) routeModel.navState = routeModel.navState.copy(arrived = true)
surfaceRenderer.routeData.value = "" surfaceRenderer.routeData.value = ""
navigationType = NavigationType.ARRIVAL navigationType = NavigationType.ARRIVAL
invalidate() invalidate()
@@ -757,7 +655,7 @@ class NavigationScreen(
updatedCameras.add(it) updatedCameras.add(it)
} }
val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
val camera = sortedList.first() val camera = sortedList.firstOrNull() ?: return
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
val bearingSpeedCamera = if (camera.tags.direction != null) { val bearingSpeedCamera = if (camera.tags.direction != null) {
try { try {
@@ -776,29 +674,94 @@ class NavigationScreen(
} }
/** /**
* Checks for a specific permission and updates the view model upon grant. * Handles the received route string.
* Starts navigation and invalidates the screen.
*/ */
fun checkPermission(permission: String) { override fun onRouteReceived(route: String) {
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { if (route.isNotEmpty()) {
val permissions: MutableList<String?> = ArrayList() if (routeModel.isNavigating()) {
permissions.add(permission) updateRoute(route)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(
RequestPermissionScreen(
carContext,
permissionCheckCallback = {
screenManager.pop()
navigationViewModel.permissionGranted.value = true
},
permissions
)
)
} else { } else {
navigationViewModel.permissionGranted.value = true 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 routeModel = RouteModel()
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
}
/**
* 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

@@ -3,6 +3,7 @@ package com.kouros.navigation.car.screen
import android.net.Uri import android.net.Uri
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
@@ -16,7 +17,10 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
@@ -26,17 +30,13 @@ import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
class PlaceListScreen( class PlaceListScreen(
private val carContext: CarContext, private val carContext: CarContext,
private val surfaceRenderer: SurfaceRenderer, private val surfaceRenderer: SurfaceRenderer,
private val category: String, private val category: String,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private val places: List<Place> private val recentPlaces: List<Place>,
) : Screen(carContext) { ) : Screen(carContext) {
val routeModel = RouteCarModel() val routeModel = RouteCarModel()
@@ -45,45 +45,28 @@ class PlaceListScreen(
var mPlaces = mutableListOf<Place>() var mPlaces = mutableListOf<Place>()
val previewObserver = Observer<String> { route ->
if (route.isNotEmpty()) {
val repository = getSettingsRepository(carContext) val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) private var routingEngine = 0
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
screenManager
.pushForResult(
RoutePreviewScreen(
carContext,
RoutePreviewType.MULTI_ROUTE,
surfaceRenderer,
place,
navigationViewModel,
routeModel = routeModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
}
init { init {
// loadPlaces() repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
navigationViewModel.recentPlaces.value = emptyList() navigationViewModel.recentPlaces.value = emptyList()
navigationViewModel.previewRoute.value = "" }
})
mPlaces.addAll(places)
navigationViewModel.previewRoute.observe(this, previewObserver)
} }
/**
* Returns the appropriate template based on the current navigation state.
*/
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
.setNoItemsMessage(carContext.getString(R.string.no_places)) .setNoItemsMessage(carContext.getString(R.string.no_places))
mPlaces.forEach { recentPlaces.filter { it.category == category }.forEach {
val street = if (it.street != null) { val street = if (it.street != null) {
it.street it.street
} else { } else {
@@ -104,13 +87,21 @@ class PlaceListScreen(
it.street, it.street,
// avatar = null // avatar = null
) )
val location = location(place.longitude, place.latitude) screenManager
navigationViewModel.loadPreviewRoute( .pushForResult(
RoutePreviewScreen(
carContext, carContext,
surfaceRenderer.lastLocation, RoutePreviewType.MULTI_ROUTE,
location, surfaceRenderer,
surfaceRenderer.carOrientation place,
navigationViewModel,
) )
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
if (category != CONTACTS) { if (category != CONTACTS) {
row.addText(SpannableString(" ").apply { row.addText(SpannableString(" ").apply {
@@ -148,14 +139,11 @@ class PlaceListScreen(
.build() .build()
} }
private fun deleteAction(place: Place): Action = Action.Builder() /**
.setIcon( * Creates an Action to delete a place.
RouteCarModel().createCarIcon( */
carContext, private fun deleteAction(place: Place): Action =
R.drawable.ic_close_white_24dp createAction(carContext, R.drawable.ic_close_white_24dp) {
)
)
.setOnClickListener {
navigationViewModel.deletePlace(carContext, place) navigationViewModel.deletePlace(carContext, place)
CarToast.makeText( CarToast.makeText(
carContext, carContext,
@@ -164,7 +152,7 @@ class PlaceListScreen(
mPlaces.remove(place) mPlaces.remove(place)
invalidate() invalidate()
} }
.build()
fun contactIcon(avatar: Uri?, category: String?): CarIcon { fun contactIcon(avatar: Uri?, category: String?): CarIcon {
if (category == RECENT || avatar == null) { if (category == RECENT || avatar == null) {

View File

@@ -1,9 +1,11 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.Manifest.permission import android.Manifest.permission
import android.content.pm.PackageManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.CarColor import androidx.car.app.model.CarColor
import androidx.car.app.model.MessageTemplate import androidx.car.app.model.MessageTemplate
@@ -46,7 +48,6 @@ class RequestPermissionScreen(
).show() ).show()
if (!approved!!.isEmpty()) { if (!approved!!.isEmpty()) {
permissionCheckCallback.onPermissionGranted() permissionCheckCallback.onPermissionGranted()
//mContactsPermissionCheckCallback.onPermissionGranted()
finish() finish()
} }
} }
@@ -62,4 +63,31 @@ class RequestPermissionScreen(
.addAction(action) .addAction(action)
.build() .build()
} }
}
/**
* Checks for a specific permission and return the result of the check.
*/
fun checkPermission(carContext: CarContext, permission: String) : Boolean {
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(
RequestPermissionScreen(
carContext,
permissionCheckCallback = {
screenManager.pop()
return@RequestPermissionScreen
},
permissions
)
)
} else {
return true
}
return false
} }

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.car.screen
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.util.Log
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.car.app.CarContext import androidx.car.app.CarContext
@@ -11,6 +12,7 @@ import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
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.CarColor
import androidx.car.app.model.CarIcon import androidx.car.app.model.CarIcon
@@ -27,26 +29,34 @@ import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.versioning.CarAppApiLevels import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.launch
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.time.Duration import java.time.Duration
import kotlin.math.min import kotlin.math.min
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ /** Creates a screen using the new [MapWithContentTemplate] */
class RoutePreviewScreen( class RoutePreviewScreen(
carContext: CarContext, carContext: CarContext,
private var routeType: RoutePreviewType, private var routeType: RoutePreviewType,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var destination: Place, private var destination: Place,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private val routeModel: RouteCarModel
) : ) :
Screen(carContext) { Screen(carContext) {
private var isFavorite = false private var isFavorite = false
@@ -54,19 +64,68 @@ class RoutePreviewScreen(
val maxListItems: Int = 3 val maxListItems: Int = 3
val navigationUtils = NavigationUtils(carContext) val navigationUtils = NavigationUtils(carContext)
val repository = getSettingsRepository(carContext)
var routingEngine = -1
var routeSelected = false var routeSelected = false
val routeModel = RouteCarModel()
var loading = true
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
invalidate() invalidate()
} }
} }
val observer = Observer<String> { route ->
if (route.isNotEmpty() && routingEngine != -1) {
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel)
loading = false
invalidate()
}
}
val trafficObserver = Observer<Map<String, String>> { traffic ->
if (traffic.isNotEmpty()) {
navigationViewModel.traffic.value = emptyMap()
surfaceRenderer.setTrafficData(traffic)
}
}
init { init {
navigationViewModel.previewRoute.observe(this, observer)
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback) carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
navigationViewModel.traffic.observe(this, trafficObserver)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
navigationViewModel.previewRoute.value = ""
}
})
repository.trafficFlow.asLiveData().observe(this, Observer {
if (it) {
getTraffic()
}
})
repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it
})
lifecycleScope.launch {
navigationViewModel.loadPreviewRoute(
carContext,
surfaceRenderer.lastLocation,
location(destination.longitude, destination.latitude),
surfaceRenderer.carOrientation
)
}
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) { if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
val listLimit = min( val listLimit = min(
@@ -78,9 +137,11 @@ class RoutePreviewScreen(
) )
var index = 0 var index = 0
routeModel.route.routes.forEach { route -> routeModel.route.routes.forEach { route ->
if (index < listLimit) {
itemListBuilder.addItem(createRow(route, index++)) itemListBuilder.addItem(createRow(route, index++))
} }
} }
}
val street = if (destination.street.isNullOrEmpty()) { val street = if (destination.street.isNullOrEmpty()) {
carContext.getString((R.string.route_preview)) carContext.getString((R.string.route_preview))
@@ -106,12 +167,16 @@ class RoutePreviewScreen(
CarText.Builder("Wait") CarText.Builder("Wait")
.build() .build()
} }
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) { val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
ListTemplate.Builder() val listContent = ListTemplate.Builder()
.setHeader(header.build()) .setHeader(header.build())
.setSingleList(itemListBuilder.build()) if (loading) {
listContent.setLoading(true)
} else {
listContent.setSingleList(itemListBuilder.build())
}
.build() .build()
listContent.build()
} else { } else {
val navigateActionIcon: CarIcon = CarIcon.Builder( val navigateActionIcon: CarIcon = CarIcon.Builder(
IconCompat.createWithResource( IconCompat.createWithResource(
@@ -123,75 +188,78 @@ class RoutePreviewScreen(
carContext, R.drawable.alt_route_48px carContext, R.drawable.alt_route_48px
) )
).build() ).build()
val navigateAction = Action.Builder() val navigateAction =
.setFlags(FLAG_DEFAULT) createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{
.setIcon(navigateActionIcon) onNavigate(routeModel.navState.currentRouteIndex)
.setOnClickListener { onNavigate(routeModel.navState.currentRouteIndex) } })
.build() val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
val selectRouteAction = Action.Builder()
.setIcon(selectRouteIcon)
.setOnClickListener {
routeType = RoutePreviewType.MULTI_ROUTE routeType = RoutePreviewType.MULTI_ROUTE
invalidate() invalidate()
} })
.build() val listContent = MessageTemplate.Builder(message)
MessageTemplate.Builder(
message
)
.setHeader(header.build()) .setHeader(header.build())
.addAction(navigateAction) .addAction(navigateAction)
.addAction(selectRouteAction) .addAction(selectRouteAction)
.setLoading(message.toString() == "Wait") if (loading) {
.build() listContent.setLoading(true)
}
listContent.build()
} }
val template = MapWithContentTemplate.Builder() val template = MapWithContentTemplate.Builder()
.setContentTemplate(content) .setContentTemplate(content)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
getMapActionStrip() mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
).build() zoomMinus()
} )).build()
) )
if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
template.setActionStrip(createActionStripBuilder().build()) template.setActionStrip(createActionStrip {
createAction(
carContext, R.drawable.navigation_48px,
onClickAction = {
onNavigate(routeModel.navState.currentRouteIndex)
}
)
})
} }
return template.build() return template.build()
} }
private fun createActionStripBuilder(): ActionStrip.Builder { private fun zoomPlus(): Action {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() return createAction(
actionStripBuilder.addAction( carContext, R.drawable.ic_zoom_in_24,
navigateAction() FLAG_IS_PERSISTENT,
onClickAction = {
surfaceRenderer.handleScale(1)
invalidate()
}
) )
return actionStripBuilder
} }
private fun navigateAction(): Action { /**
return Action.Builder() * Creates an action to zoom out on the map.
.setIcon(routeModel.createCarIcon(carContext, R.drawable.navigation_48px)) */
.setFlags(FLAG_DEFAULT) private fun zoomMinus(): Action {
.setOnClickListener { return createAction(
onNavigate(routeModel.navState.currentRouteIndex) carContext, R.drawable.ic_zoom_out_24,
onClickAction = {
surfaceRenderer.handleScale(-1)
invalidate()
} }
.build() )
} }
private fun favoriteAction(): Action = Action.Builder() private fun favoriteAction(): Action =
.setIcon( createAction(
CarIcon.Builder( carContext, if (isFavorite)
IconCompat.createWithResource(
carContext,
if (isFavorite)
R.drawable.ic_favorite_filled_white_24dp R.drawable.ic_favorite_filled_white_24dp
else else
R.drawable.ic_favorite_white_24dp R.drawable.ic_favorite_white_24dp
) , FLAG_IS_PERSISTENT,
) ) {
.build()
)
.setOnClickListener {
isFavorite = !isFavorite isFavorite = !isFavorite
CarToast.makeText( CarToast.makeText(
carContext, carContext,
@@ -208,26 +276,17 @@ class RoutePreviewScreen(
navigationViewModel.saveFavorite(carContext, destination) navigationViewModel.saveFavorite(carContext, destination)
invalidate() invalidate()
} }
.build()
private fun deleteFavoriteAction(): Action = Action.Builder()
.setOnClickListener { private fun deleteFavoriteAction(): Action =
createAction(carContext, R.drawable.heart_minus_48px, FLAG_IS_PERSISTENT,{
if (isFavorite) { if (isFavorite) {
navigationViewModel.deleteFavorite(carContext, destination) navigationViewModel.deleteFavorite(carContext, destination)
} }
isFavorite = !isFavorite isFavorite = !isFavorite
finish() finish()
} })
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.heart_minus_48px
)
)
.build()
)
.build()
private fun createRouteText(route: Routes): CarText { private fun createRouteText(route: Routes): CarText {
val time = route.summary.duration val time = route.summary.duration
@@ -245,21 +304,13 @@ class RoutePreviewScreen(
} }
private fun createRow(route: Routes, index: Int): Row { private fun createRow(route: Routes, index: Int): Row {
val navigateActionIcon: CarIcon = CarIcon.Builder( val navigateAction = createAction(carContext, R.drawable.navigation_48px ) {
IconCompat.createWithResource( this.onNavigate(index)
carContext, R.drawable.navigation_48px }
)
).build()
val navigateAction = Action.Builder()
.setFlags(FLAG_DEFAULT)
.setIcon(navigateActionIcon)
.setOnClickListener { this.onNavigate(index) }
.build()
val routeText = createRouteText(route) val routeText = createRouteText(route)
var street = "" var street = ""
var maxDistance = 0.0 var maxDistance = 0.0
routeModel.route.routes[index].legs.first().steps.forEach { routeModel.steps.forEach {
if (it.distance > maxDistance) { if (it.distance > maxDistance) {
maxDistance = it.distance maxDistance = it.distance
street = it.street street = it.street
@@ -271,9 +322,9 @@ class RoutePreviewScreen(
.setOnClickListener { onRouteSelected(index) } .setOnClickListener { onRouteSelected(index) }
.addText(street) .addText(street)
.addAction(navigateAction) .addAction(navigateAction)
if (route.summary.trafficDelay > 60) { if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route)) row.addText(createDelay(route))
row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px))
} }
return row.build() return row.build()
} }
@@ -294,6 +345,7 @@ class RoutePreviewScreen(
) )
return delayBuilder return delayBuilder
} }
private fun onNavigate(index: Int) { private fun onNavigate(index: Int) {
destination.routeIndex = index destination.routeIndex = index
setResult(destination) setResult(destination)
@@ -303,30 +355,19 @@ class RoutePreviewScreen(
private fun onRouteSelected(index: Int) { private fun onRouteSelected(index: Int) {
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index) routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
surfaceRenderer.setPreviewRouteData(routeModel) surfaceRenderer.setPreviewRouteData(routeModel)
surfaceRenderer.updateTrafficData(routeModel)
routeSelected = true routeSelected = true
invalidate() invalidate()
} }
fun getMapActionStrip(): ActionStrip { private fun getTraffic() {
return ActionStrip.Builder() navigationViewModel.loadTraffic(
.addAction( carContext,
createToastAction(R.drawable.ic_zoom_in_24) surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
) )
.addAction(
createToastAction(R.drawable.ic_zoom_out_24)
)
.addAction(Action.PAN)
.build()
} }
private fun createToastAction(
@DrawableRes iconRes: Int
): Action {
return Action.Builder()
.setOnClickListener { surfaceRenderer.handleScale(-1) }
.setIcon(navigationUtils.createCarIcon(iconRes))
.build()
}
} }
enum class RoutePreviewType { enum class RoutePreviewType {

View File

@@ -0,0 +1,62 @@
package com.kouros.navigation.car.screen
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
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.CarIcon
import androidx.core.graphics.drawable.IconCompat
import com.kouros.navigation.car.ViewStyle
fun createActionStrip(executeAction: () -> Action): ActionStrip {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
executeAction()
)
return actionStripBuilder.build()
}
fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
action1()
)
actionStripBuilder.addAction(
action2()
)
return actionStripBuilder
}
/**
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -> Action , panAction: () -> Action): ActionStrip {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
if (viewStyle == ViewStyle.PAN_VIEW) {
actionStripBuilder
.addAction(
panAction()
)
}
return actionStripBuilder.build()
}
/**
* Creates an action to do something.
*/
fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int = FLAG_DEFAULT, onClickAction: () -> Unit): Action {
return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes))
.setFlags(flag)
.setOnClickListener {
onClickAction()
}
.build()
}
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}

View File

@@ -28,6 +28,7 @@ class SearchScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private val recentPlaces: List<Place>,
) : Screen(carContext) { ) : Screen(carContext) {
var isSearchComplete: Boolean = false var isSearchComplete: Boolean = false
@@ -44,54 +45,16 @@ class SearchScreen(
lateinit var searchResult: List<SearchResult> lateinit var searchResult: List<SearchResult>
val observer = Observer<List<SearchResult>> { newSearch -> val observer = Observer<List<SearchResult>> { newSearch ->
if (newSearch.isNotEmpty()) {
navigationViewModel.searchPlaces.value = emptyList()
searchResult = newSearch searchResult = newSearch
invalidate() invalidate()
} }
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
if (newPlaces.isNotEmpty()) {
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
RECENT,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
surfaceRenderer.clearRouteData()
if (obj != null) {
setResult(obj)
finish()
}
}
}
}
val observerFavorites = Observer<List<Place>> { newPlaces ->
screenManager
.pushForResult(
PlaceListScreen(
carContext,
surfaceRenderer,
FAVORITES,
navigationViewModel,
newPlaces
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
} }
init { init {
navigationViewModel.searchPlaces.observe(this, observer) navigationViewModel.searchPlaces.observe(this, observer)
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
navigationViewModel.favorites.observe(this, observerFavorites)
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -124,19 +87,21 @@ class SearchScreen(
} }
} }
} else { } else {
if (it.id == RECENT) { screenManager
navigationViewModel.loadRecentPlaces( .pushForResult(
PlaceListScreen(
carContext, carContext,
surfaceRenderer.lastLocation, surfaceRenderer,
surfaceRenderer.carOrientation it.id,
navigationViewModel,
recentPlaces
) )
) { obj: Any? ->
surfaceRenderer.viewStyle = ViewStyle.VIEW
if (obj != null) {
setResult(obj)
finish()
} }
if (it.id == FAVORITES) {
navigationViewModel.loadFavorites(
carContext,
surfaceRenderer.lastLocation,
surfaceRenderer.carOrientation
)
} }
} }
} }

View File

@@ -11,9 +11,6 @@ interface NavigationObserverCallback {
/** Called when a route is received and navigation should start */ /** Called when a route is received and navigation should start */
fun onRouteReceived(route: String) fun onRouteReceived(route: String)
/** Called when a recent place is selected but navigation hasn't started */
fun onRecentPlaceReceived(place: Place)
/** Check if currently navigating */ /** Check if currently navigating */
fun isNavigating(): Boolean fun isNavigating(): Boolean
@@ -29,9 +26,6 @@ interface NavigationObserverCallback {
/** Called when max speed is updated */ /** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int) 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 */ /** Called to request UI invalidation/refresh */
fun invalidateScreen() fun invalidateScreen()

View File

@@ -12,12 +12,10 @@ class NavigationObserverManager(
) { ) {
val routeObserver = RouteObserver(callback) val routeObserver = RouteObserver(callback)
val recentPlaceObserver = RecentPlaceObserver(callback)
val trafficObserver = TrafficObserver(callback) val trafficObserver = TrafficObserver(callback)
val placeSearchObserver = PlaceSearchObserver(callback) val placeSearchObserver = PlaceSearchObserver(callback)
val speedCameraObserver = SpeedCameraObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback)
val previewObserver = PreviewRouteObserver(callback)
/** /**
* Attaches all observers to the ViewModel. * Attaches all observers to the ViewModel.
@@ -26,11 +24,9 @@ class NavigationObserverManager(
fun attachAllObservers(screen: androidx.car.app.Screen) { fun attachAllObservers(screen: androidx.car.app.Screen) {
viewModel.route.observe(screen, routeObserver) viewModel.route.observe(screen, routeObserver)
viewModel.traffic.observe(screen, trafficObserver) viewModel.traffic.observe(screen, trafficObserver)
viewModel.recentPlace.observe(screen, recentPlaceObserver)
viewModel.placeLocation.observe(screen, placeSearchObserver) viewModel.placeLocation.observe(screen, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver) viewModel.speedCameras.observe(screen, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver) viewModel.maxSpeed.observe(screen, maxSpeedObserver)
viewModel.previewRoute.observe(screen, previewObserver)
} }
/** /**

View File

@@ -1,14 +0,0 @@
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)
}
}
}

View File

@@ -1,18 +0,0 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Place
/**
* Observer for recent place updates. Updates the recent place when navigation is not active.
*/
class RecentPlaceObserver(
private val callback: NavigationObserverCallback
) : Observer<Place> {
override fun onChanged(value: Place) {
if (!callback.isNavigating()) {
callback.onRecentPlaceReceived(value)
}
}
}

View File

@@ -23,6 +23,8 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
private var showTraffic = false private var showTraffic = false
private var tripSuggestion = false
val settingsViewModel = getSettingsViewModel(carContext) val settingsViewModel = getSettingsViewModel(carContext)
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
@@ -34,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
buildingToggleState = it buildingToggleState = it
invalidate() invalidate()
} }
settingsViewModel.tripSuggestion.asLiveData().observe(this) {
tripSuggestion = it
invalidate()
}
val listBuilder = ItemList.Builder() val listBuilder = ItemList.Builder()
val buildingToggle: Toggle = val buildingToggle: Toggle =
Toggle.Builder { checked: Boolean -> Toggle.Builder { checked: Boolean ->
settingsViewModel.onShow3DChanged(checked) settingsViewModel.onShow3DChanged(checked)
@@ -49,6 +56,13 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
}.setChecked(showTraffic).build() }.setChecked(showTraffic).build()
listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle)) listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle))
val tripSuggestionToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onTripSuggestion(checked)
tripSuggestion = !tripSuggestion
}.setChecked(tripSuggestion).build()
listBuilder.addItem(buildRowForTemplate(R.string.trip_suggestion, tripSuggestionToggle))
listBuilder.addItem( listBuilder.addItem(
buildRowForScreenTemplate( buildRowForScreenTemplate(
DarkModeSettings(carContext), DarkModeSettings(carContext),

View File

@@ -0,0 +1,133 @@
package com.kouros.navigation.car.screen
import androidx.car.app.testing.ScreenController
import androidx.car.app.testing.TestCarContext
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.lifecycle.MutableLiveData
import androidx.test.core.app.ApplicationProvider
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.model.NavigationViewModel
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import com.google.common.truth.Truth.assertThat
import com.kouros.navigation.data.NavigationState
import com.kouros.navigation.data.Place
import com.kouros.navigation.model.RouteCalculator
@RunWith(RobolectricTestRunner::class)
class NavigationScreenTest {
private lateinit var testCarContext: TestCarContext
@Mock
private lateinit var mockSurfaceRenderer: SurfaceRenderer
@Mock
private lateinit var mockRouteModel: RouteCarModel
@Mock
private lateinit var mockListener: NavigationListener
@Mock
private lateinit var mockViewModel: NavigationViewModel
private lateinit var navigationScreen: NavigationScreen
private lateinit var screenController: ScreenController
@Mock
private lateinit var mockRouteCalculator: RouteCalculator
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
testCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext())
// Setup initial state
`when`(mockRouteModel.isNavigating()).thenReturn(true)
`when`(mockViewModel.route).thenReturn(MutableLiveData())
`when`(mockViewModel.traffic).thenReturn(MutableLiveData())
`when`(mockViewModel.recentPlaces).thenReturn(MutableLiveData())
`when`(mockViewModel.placeLocation).thenReturn(MutableLiveData())
`when`(mockViewModel.speedCameras).thenReturn(MutableLiveData())
`when`(mockViewModel.maxSpeed).thenReturn(MutableLiveData())
`when`(mockViewModel.previewRoute).thenReturn(MutableLiveData())
`when`(mockSurfaceRenderer.routeData).thenReturn(MutableLiveData())
navigationScreen = NavigationScreen(
testCarContext,
mockSurfaceRenderer,
mockRouteModel,
mockListener,
mockViewModel
)
screenController = ScreenController(navigationScreen)
}
@Test
fun onGetTemplate_whenNavigating_returnsNavigationTemplate() {
// Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION
// Act
val template = screenController.screen
// Assert
assertThat(template).isInstanceOf(NavigationScreen::class.java)
}
@Test
fun startNavigation_updatesState() {
// Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION
// Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION)
}
@Test
fun stopNavigation_updatesState() {
// Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION
// Act
navigationScreen.stopNavigation()
// Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.VIEW)
}
@Test
fun rerouteNavigation_updatesState() {
// Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION
// Act
navigationScreen.calculateNewRoute(Place())
// Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE)
}
@Test
fun arrivalNavigation_updatesState() {
// Arrange
navigationScreen.navigationType = NavigationType.NAVIGATION
`when`(mockRouteModel.isArrival()).thenReturn(true)
`when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator)
`when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0)
`when`(mockRouteModel.navState).thenReturn(NavigationState())
// Act
navigationScreen.checkArrival()
// Assert
assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL)
}
}

View File

@@ -41,30 +41,6 @@ class ObserversTest {
verify(mockCallback, never()).onRouteReceived(any()) verify(mockCallback, never()).onRouteReceived(any())
} }
@Test
fun `RecentPlaceObserver triggers callback when navigating is false`() {
val observer = RecentPlaceObserver(mockCallback)
val testPlace = createTestPlace()
whenever(mockCallback.isNavigating()).thenReturn(false)
observer.onChanged(testPlace)
verify(mockCallback).isNavigating()
verify(mockCallback).onRecentPlaceReceived(testPlace)
}
@Test
fun `RecentPlaceObserver does not trigger callback when navigating is true`() {
val observer = RecentPlaceObserver(mockCallback)
val testPlace = createTestPlace()
whenever(mockCallback.isNavigating()).thenReturn(true)
observer.onChanged(testPlace)
verify(mockCallback).isNavigating()
verify(mockCallback, never()).onRecentPlaceReceived(any())
}
@Test @Test
fun `TrafficObserver triggers callback with traffic data and invalidates screen`() { fun `TrafficObserver triggers callback with traffic data and invalidates screen`() {
val observer = TrafficObserver(mockCallback) val observer = TrafficObserver(mockCallback)
@@ -119,15 +95,15 @@ class ObserversTest {
} }
// Helper methods // Helper methods
private fun createTestPlace(): Place { private fun createTestPlace():List<Place> {
return Place( return listOf(Place(
name = "Test Place", name = "Test Place",
street = "Test Street", street = "Test Street",
city = "Test City", city = "Test City",
latitude = 52.0, latitude = 52.0,
longitude = 10.0, longitude = 10.0,
category = Constants.FAVORITES category = Constants.FAVORITES
) ))
} }
private fun createTestSearchResult(): SearchResult { private fun createTestSearchResult(): SearchResult {

View File

@@ -130,6 +130,8 @@ object Constants {
const val TRAFFIC_UPDATE = 300 const val TRAFFIC_UPDATE = 300
const val ROUTE_UPDATE = 60
const val INSTRUCTION_DISTANCE = 50 const val INSTRUCTION_DISTANCE = 50
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED" const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"

View File

@@ -1,29 +1,7 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kouros.navigation.data package com.kouros.navigation.data
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import com.google.gson.GsonBuilder
import com.kouros.data.R
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator import java.net.Authenticator
import java.net.HttpURLConnection import java.net.HttpURLConnection
@@ -41,7 +19,7 @@ abstract class NavigationRepository {
abstract fun getRoute( abstract fun getRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, destination: Location,
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String ): String
@@ -59,7 +37,7 @@ abstract class NavigationRepository {
} }
fun searchPlaces(search: String, location: Location): String { fun searchPlaces(search: String, location: Location): String {
val box = calculateSquareRadius(location.latitude, location.longitude, 100.0) val box = calculateSquareRadius(location.latitude, location.longitude, 800.0)
val viewbox = "&bounded=1&viewbox=${box}" val viewbox = "&bounded=1&viewbox=${box}"
return fetchUrl( return fetchUrl(
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", "${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",

View File

@@ -53,6 +53,8 @@ class DataStoreManager(private val context: Context) {
val TRAFFIC = booleanPreferencesKey("Traffic") val TRAFFIC = booleanPreferencesKey("Traffic")
val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion")
} }
// Read values // Read values
@@ -129,6 +131,11 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRAFFIC] == true preferences[PreferencesKeys.TRAFFIC] == true
} }
val tripSuggestionFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
}
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
@@ -207,4 +214,10 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRAFFIC] = enabled preferences[PreferencesKeys.TRAFFIC] = enabled
} }
} }
suspend fun setTripSuggestion(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
}
}
} }

View File

@@ -12,4 +12,5 @@ data class Step(
val distance: Double = 0.0, val distance: Double = 0.0,
val street : String = "", val street : String = "",
val intersection: List<Intersection> = mutableListOf(), val intersection: List<Intersection> = mutableListOf(),
val countryCode : String = ""
) )

View File

@@ -19,9 +19,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields = private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useAsset = false const val useLocal = false
const val useAssetTraffic = false const val useLocalTraffic = false
class TomTomRepository : NavigationRepository() { class TomTomRepository : NavigationRepository() {
@@ -32,12 +32,11 @@ class TomTomRepository : NavigationRepository() {
carOrientation: Float, carOrientation: Float,
searchFilter: SearchFilter searchFilter: SearchFilter
): String { ): String {
if (useAsset) { if (useLocal) {
val resourceId: Int = context.resources return fetchUrl(
.getIdentifier("tomtom_routing", "raw", context.packageName) "https://kouros-online.de/tomtom_routing.json",
val routeJson = context.resources.openRawResource(resourceId) false
val routeJsonString = routeJson.bufferedReader().use { it.readText() } )
return routeJsonString
} }
var filter = "" var filter = ""
if (searchFilter.avoidMotorway) { if (searchFilter.avoidMotorway) {
@@ -76,11 +75,11 @@ class TomTomRepository : NavigationRepository() {
return "" return ""
} }
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0) val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAssetTraffic) { return if (useLocalTraffic) {
val resourceId: Int = context.resources fetchUrl(
.getIdentifier("tomtom_traffic", "raw", context.packageName) "https://kouros-online.de/tomtom_traffic.json",
val trafficJson = context.resources.openRawResource(resourceId) false
trafficJson.bufferedReader().use { it.readText() } )
} else { } else {
val trafficResult = fetchUrl( val trafficResult = fetchUrl(
"$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present", "$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",

View File

@@ -62,7 +62,9 @@ class TomTomRoute {
lastPointIndex = instruction.pointIndex lastPointIndex = instruction.pointIndex
val intersections = mutableListOf<Intersection>() val intersections = mutableListOf<Intersection>()
route.sections?.forEach { section -> route.sections?.forEach { section ->
if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex && section.endPointIndex >= lastPointIndex) { if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex
&& section.endPointIndex >= lastPointIndex
) {
val lanes = mutableListOf<Lane>() val lanes = mutableListOf<Lane>()
var startIndex = 0 var startIndex = 0
var lastLane: Lane? = null var lastLane: Lane? = null
@@ -87,7 +89,7 @@ class TomTomRoute {
lastLane = lane lastLane = lane
} }
intersections.add(Intersection(waypoints[startIndex], lanes)) intersections.add(Intersection(waypoints[startIndex], lanes))
}
} }
stepDistance = stepDistance =
route.guidance.instructions[index].routeOffsetInMeters - stepDistance route.guidance.instructions[index].routeOffsetInMeters - stepDistance
@@ -99,14 +101,14 @@ class TomTomRoute {
distance = stepDistance, distance = stepDistance,
duration = stepDuration, duration = stepDuration,
maneuver = maneuver, maneuver = maneuver,
intersection = intersections intersection = intersections,
countryCode = lastInstruction.countryCode
) )
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
steps.add(step) steps.add(step)
stepIndex += 1 stepIndex += 1
} }
}
legs.add(Leg(steps)) legs.add(Leg(steps))
val routeGeoJson = createLineStringCollection(waypoints) val routeGeoJson = createLineStringCollection(waypoints)
val centerLocation = createCenterLocation(createLineStringCollection(waypoints)) val centerLocation = createCenterLocation(createLineStringCollection(waypoints))

View File

@@ -16,7 +16,7 @@ import com.kouros.navigation.data.StepData
import java.util.Collections import java.util.Collections
import java.util.Locale import java.util.Locale
class IconMapper() { class IconMapper {
fun maneuverIcon(routeManeuverType: Int): Int { fun maneuverIcon(routeManeuverType: Int): Int {
var currentTurnIcon = R.drawable.ic_turn_name_change var currentTurnIcon = R.drawable.ic_turn_name_change

View File

@@ -5,6 +5,8 @@ import android.content.Context
import android.location.Location import android.location.Location
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -60,11 +62,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
MutableLiveData() MutableLiveData()
} }
/** LiveData containing list of favorite saved places */
val favorites: MutableLiveData<List<Place>> by lazy {
MutableLiveData()
}
/** LiveData containing search results from Nominatim geocoding */ /** LiveData containing search results from Nominatim geocoding */
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy { val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
MutableLiveData() MutableLiveData()
@@ -147,7 +144,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
var id: Long = 0 var id: Long = 0
if (rp.isNotEmpty()) { if (rp.isNotEmpty()) {
for (place in places.places) { for (place in places.places) {
if (place.category.equals(Constants.RECENT)) { if (place.category.equals(Constants.RECENT)
|| place.category.equals(Constants.FAVORITES)) {
val plLocation = location(place.longitude, place.latitude) val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) { if (place.latitude != 0.0) {
val distance = val distance =
@@ -172,43 +170,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Loads favorite places from Preferences and calculates distances.
* Posts the sorted list to favorites LiveData.
*/
fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) {
try {
val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java)
val pl = mutableListOf<Place>()
if (rp.isNotEmpty()) {
for (place in recentPlaces.places) {
if (place.category.equals(Constants.FAVORITES)) {
val plLocation = location(place.longitude, place.latitude)
if (place.latitude != 0.0) {
val distance =
repository.getRouteDistance(
location,
plLocation,
carOrientation,
context
)
place.distance = distance.toFloat()
}
pl.add(place)
}
}
}
favorites.postValue(pl)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/** /**
* Calculates a route between current location and destination. * Calculates a route between current location and destination.
* Posts the route JSON to route LiveData. * Posts the route JSON to route LiveData.
@@ -216,7 +177,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
fun loadRoute( fun loadRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
location: Location, destination: Location,
carOrientation: Float carOrientation: Float
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -225,7 +186,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
repository.getRoute( repository.getRoute(
context, context,
currentLocation, currentLocation,
location, destination,
carOrientation, carOrientation,
getSearchFilter(context) getSearchFilter(context)
) )
@@ -296,7 +257,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
currentLocation: Location, currentLocation: Location,
location: Location, location: Location,
carOrientation: Float carOrientation: Float
) { ): String? {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
previewRoute.postValue( previewRoute.postValue(
@@ -312,8 +273,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
e.printStackTrace() e.printStackTrace()
} }
} }
return previewRoute.value
} }
/** /**
* Loads device contacts with addresses and converts to Place objects. * Loads device contacts with addresses and converts to Place objects.
* Posts results to contactAddress LiveData. * Posts results to contactAddress LiveData.

View File

@@ -13,7 +13,7 @@ import com.kouros.navigation.data.route.Leg
import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.data.route.Step import com.kouros.navigation.data.route.Step
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import kotlin.math.absoluteValue
open class RouteModel { open class RouteModel {
@@ -33,6 +33,9 @@ open class RouteModel {
val currentStep: Step val currentStep: Step
get() = navState.route.nextStep(0) get() = navState.route.nextStep(0)
val steps: List<Step>
get() = curLeg.steps
fun startNavigation(routeString: String) { fun startNavigation(routeString: String) {
navState = navState.copy( navState = navState.copy(
route = Route.Builder() route = Route.Builder()
@@ -145,7 +148,20 @@ open class RouteModel {
) )
} }
/**
* Checks for navigating
*/
fun isNavigating(): Boolean { fun isNavigating(): Boolean {
return navState.navigating return navState.navigating
} }
/**
* Checks for arrival
*/
fun isArrival(): Boolean {
return navState.maneuverType == Maneuver.TYPE_DESTINATION
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT
}
} }

View File

@@ -90,6 +90,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false false
) )
val tripSuggestion = repository.tripSuggestionFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -138,4 +144,8 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onTraffic(enabled: Boolean) { fun onTraffic(enabled: Boolean) {
viewModelScope.launch { repository.setTraffic(enabled) } viewModelScope.launch { repository.setTraffic(enabled) }
} }
fun onTripSuggestion(enabled: Boolean) {
viewModelScope.launch { repository.setTripSuggestion(enabled) }
}
} }

View File

@@ -44,6 +44,9 @@ class SettingsRepository(
val trafficFlow: Flow<Boolean> = val trafficFlow: Flow<Boolean> =
dataStoreManager.trafficFlow dataStoreManager.trafficFlow
val tripSuggestionFlow: Flow<Boolean> =
dataStoreManager.tripSuggestionFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled) dataStoreManager.setShow3D(enabled)
} }
@@ -95,4 +98,8 @@ class SettingsRepository(
suspend fun setTraffic(enabled: Boolean) { suspend fun setTraffic(enabled: Boolean) {
dataStoreManager.setTraffic(enabled) dataStoreManager.setTraffic(enabled)
} }
suspend fun setTripSuggestion(enabled: Boolean) {
dataStoreManager.setTripSuggestion(enabled)
}
} }

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M70,880Q58,880 49,871.5Q40,863 40,850L40,517L128,308Q133,295 144,287.5Q155,280 169,280L552,280Q566,280 577,287.5Q588,295 593,308L680,517L680,850Q680,863 671.5,871.5Q663,880 650,880L609,880Q597,880 588,871.5Q579,863 579,850L579,800L141,800L141,850Q141,863 132.5,871.5Q124,880 111,880L70,880ZM130,458L591,458L542,340L179,340L130,458ZM246.5,664Q261,649 261,629Q261,608 246.5,593Q232,578 211,578Q190,578 175,593Q160,608 160,629Q160,649 175,664Q190,679 211,679Q232,679 246.5,664ZM545.5,664Q560,649 560,629Q560,608 545.5,593Q531,578 510,578Q489,578 474,593Q459,608 459,629Q459,649 474,664Q489,679 510,679Q531,679 545.5,664ZM740,757L740,413L659,220L237,220L251,188Q256,175 267,167.5Q278,160 292,160L669,160Q683,160 694.5,167.5Q706,175 711,188L800,401L800,727Q800,740 791.5,748.5Q783,757 770,757L740,757ZM860,634L860,290L780,100L360,100L374,68Q379,55 390,47.5Q401,40 415,40L790,40Q804,40 815.5,47.5Q827,55 832,68L920,278L920,604Q920,617 911.5,625.5Q903,634 890,634L860,634Z"/>
</vector>

View File

@@ -65,4 +65,5 @@
<string name="no_categories">Keine Kategorien</string> <string name="no_categories">Keine Kategorien</string>
<string name="general">Allgemein</string> <string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string> <string name="traffic">Verkehr anzeigen</string>
<string name="trip_suggestion">Fahrten-Vorschläge</string>
</resources> </resources>

View File

@@ -49,4 +49,5 @@
<string name="no_categories">Δεν υπάρχουν κατηγορίες</string> <string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
<string name="general">Γενικά</string> <string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string> <string name="traffic">Εμφάνιση κίνησης</string>
<string name="trip_suggestion">Προτάσεις διαδρομής</string>
</resources> </resources>

View File

@@ -49,4 +49,5 @@
<string name="no_categories">Brak kategorii do wyświetlenia</string> <string name="no_categories">Brak kategorii do wyświetlenia</string>
<string name="general">Ogólne</string> <string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string> <string name="traffic">Pokaż natężenie ruchu</string>
<string name="trip_suggestion">Sugestie dotyczące podróży</string>
</resources> </resources>

View File

@@ -52,4 +52,5 @@
<string name="no_categories">No categories to show</string> <string name="no_categories">No categories to show</string>
<string name="general">General</string> <string name="general">General</string>
<string name="traffic">Show traffic</string> <string name="traffic">Show traffic</string>
<string name="trip_suggestion">Trip suggestions</string>
</resources> </resources>

View File

@@ -86,21 +86,6 @@ class RouteModelTest {
assert(routeModel.navState.currentLocation.longitude == 11.57936) assert(routeModel.navState.currentLocation.longitude == 11.57936)
} }
@Test
fun `currentStep returns StepData `() {
val stepData = routeModel.currentStep()
assert(stepData.leftStepDistance == 0.0)
assert(stepData.instruction == "Milbertshofener Straße")
}
@Test
fun `nextStep returns StepData `() {
routeModel.currentStep()
val stepData = routeModel.nextStep()
assert(stepData.leftStepDistance == 0.0)
assert(stepData.instruction == "Bad-Soden-Straße")
}
@Test @Test
fun `stopNavigation updates route and sets navigating to false `() { fun `stopNavigation updates route and sets navigating to false `() {
routeModel.stopNavigation() routeModel.stopNavigation()

View File

@@ -4,25 +4,25 @@ androidGpxParser = "2.3.1"
androidSdkTurf = "6.0.1" androidSdkTurf = "6.0.1"
datastore = "1.2.1" datastore = "1.2.1"
gradle = "9.1.0" gradle = "9.1.0"
koinAndroid = "4.1.1" koinAndroid = "4.2.0"
koinAndroidxCompose = "4.1.1" koinAndroidxCompose = "4.2.0"
koinComposeViewmodel = "4.1.1" koinComposeViewmodel = "4.2.0"
koinCore = "4.1.1" koinCore = "4.2.0"
kotlin = "2.3.10" kotlin = "2.3.20"
coreKtx = "1.18.0" coreKtx = "1.18.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
kotlinxSerializationJson = "1.10.0" kotlinxSerializationJson = "1.10.0"
lifecycleRuntimeKtx = "2.10.0" lifecycleRuntimeKtx = "2.10.0"
composeBom = "2026.02.01" composeBom = "2026.03.00"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.13.0"
carApp = "1.7.0" carApp = "1.7.0"
androidx-car = "1.7.0" androidx-car = "1.7.0"
materialIconsExtended = "1.7.8" materialIconsExtended = "1.7.8"
mockitoCore = "5.23.0" mockitoCore = "5.23.0"
mockitoKotlin = "6.2.3" mockitoKotlin = "6.3.0"
rules = "1.7.0" rules = "1.7.0"
runner = "1.7.0" runner = "1.7.0"
material3 = "1.4.0" material3 = "1.4.0"
@@ -44,6 +44,9 @@ foundationLayout = "1.10.5"
datastorePreferences = "1.2.1" datastorePreferences = "1.2.1"
datastoreCore = "1.2.1" datastoreCore = "1.2.1"
monitor = "1.8.0" monitor = "1.8.0"
robolectric = "4.16.1"
truth = "1.4.5"
testCore = "1.7.0"
[libraries] [libraries]
android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" }
@@ -69,6 +72,7 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" } material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" }
androidx-car-app-testing = { group = "androidx.car.app", name = "app-testing", version.ref = "carApp" }
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" }
@@ -91,6 +95,9 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }
androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
google-truth = { module = "com.google.truth:truth", version.ref = "truth" }
androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }