Preview
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,7 +439,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateTrip(trip: Trip) {
|
override fun updateTrip(trip: Trip) {
|
||||||
navigationManager.updateTrip(trip)
|
if (navigationManagerStarted) {
|
||||||
|
navigationManager.updateTrip(trip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -61,10 +52,23 @@ class CategoriesScreen(
|
|||||||
itemListBuilder.addItem(
|
itemListBuilder.addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle(it.name)
|
.setTitle(it.name)
|
||||||
.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,28 +145,24 @@ 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,
|
location(it.lon, it.lat),
|
||||||
location(it.lon, it.lat),
|
surfaceRenderer.carOrientation
|
||||||
surfaceRenderer.carOrientation
|
)
|
||||||
|
setResult(
|
||||||
|
Place(
|
||||||
|
name = name,
|
||||||
|
category = CHARGING_STATION,
|
||||||
|
latitude = it.lat,
|
||||||
|
longitude = it.lon
|
||||||
)
|
)
|
||||||
setResult(
|
)
|
||||||
Place(
|
finish()
|
||||||
name = name,
|
}))
|
||||||
category = CHARGING_STATION,
|
|
||||||
latitude = it.lat,
|
|
||||||
longitude = it.lon
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
init {
|
private var tripSuggestion = false
|
||||||
observerManager.attachAllObservers(this)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
settingsViewModel.routingEngine.first()
|
|
||||||
settingsViewModel.recentPlaces.first()
|
|
||||||
}
|
|
||||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
|
||||||
distanceMode = it
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private var tripSuggestionCalled = false
|
||||||
* Handles the received route string.
|
|
||||||
* Starts navigation and invalidates the screen.
|
private var routingEngine = 0
|
||||||
*/
|
|
||||||
override fun onRouteReceived(route: String) {
|
private var showTraffic = false;
|
||||||
if (route.isNotEmpty()) {
|
private var arrivalTimer: CountDownTimer? = null
|
||||||
prepareRoute(route)
|
private var reRouteTimer: CountDownTimer? = null
|
||||||
|
|
||||||
|
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||||
|
recentPlaces = newPlaces
|
||||||
|
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||||
|
tripSuggestionCalled = true
|
||||||
|
navigationType = NavigationType.RECENT
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* Prepare route and start navigation
|
observerManager.attachAllObservers(this)
|
||||||
*/
|
lifecycleScope.launch {
|
||||||
private fun prepareRoute(route: String) {
|
settingsViewModel.tripSuggestion.first()
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
settingsViewModel.routingEngine.first()
|
||||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
|
||||||
navigationType = NavigationType.NAVIGATION
|
|
||||||
routeModel.startNavigation(route)
|
|
||||||
if (routeModel.hasLegs()) {
|
|
||||||
settingsViewModel.onLastRouteChanged(route)
|
|
||||||
}
|
}
|
||||||
surfaceRenderer.setRouteData()
|
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||||
listener.startNavigation()
|
distanceMode = it
|
||||||
}
|
})
|
||||||
|
|
||||||
/**
|
repository.trafficFlow.asLiveData().observe(this, Observer {
|
||||||
* Checks if navigation is currently active.
|
showTraffic = it
|
||||||
*/
|
})
|
||||||
override fun isNavigating(): Boolean = routeModel.isNavigating()
|
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
||||||
|
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||||
/**
|
tripSuggestion = it
|
||||||
* Handles the received recent place.
|
})
|
||||||
* Updates the navigation type to RECENT and invalidates the screen.
|
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||||
*/
|
routingEngine = it
|
||||||
override fun onRecentPlaceReceived(place: Place) {
|
})
|
||||||
recentPlace = place
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
navigationType = NavigationType.RECENT
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
invalidate()
|
arrivalTimer?.cancel()
|
||||||
}
|
reRouteTimer?.cancel()
|
||||||
|
}
|
||||||
/**
|
})
|
||||||
* 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,38 +197,41 @@ 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)
|
||||||
navigationType = NavigationType.VIEW
|
navigationType = NavigationType.VIEW
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
timer.start()
|
|
||||||
return navigationArrivedTemplate(actionStripBuilder)
|
|
||||||
} else {
|
|
||||||
return NavigationTemplate.Builder()
|
|
||||||
.setBackgroundColor(CarColor.SECONDARY)
|
|
||||||
.setActionStrip(actionStripBuilder.build())
|
|
||||||
.setMapActionStrip(mapActionStripBuilder().build())
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
arrivalTimer?.start()
|
||||||
|
return navigationArrivedTemplate(actionStripBuilder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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(
|
||||||
.setHeader(
|
{
|
||||||
Header.Builder()
|
createAction(
|
||||||
.setTitle(carContext.getString(R.string.drive_now))
|
carContext,
|
||||||
.build()
|
R.drawable.search_48px,
|
||||||
|
FLAG_IS_PERSISTENT,
|
||||||
|
{ startSearchScreen() })
|
||||||
|
},
|
||||||
|
{ settingsAction() })
|
||||||
)
|
)
|
||||||
.addAction(navigateAction())
|
}
|
||||||
.addAction(closeAction())
|
val listBuilder = ItemList.Builder()
|
||||||
.build()
|
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(
|
||||||
|
Header.Builder()
|
||||||
|
.setTitle(carContext.getString(R.string.drive_now))
|
||||||
|
.addEndHeaderAction(closeAction())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.setSingleList(listBuilder.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
|
||||||
carContext,
|
.pushForResult(
|
||||||
R.drawable.navigation_48px
|
RoutePreviewScreen(
|
||||||
)
|
carContext,
|
||||||
)
|
RoutePreviewType.SINGLE_ROUTE,
|
||||||
.build()
|
surfaceRenderer,
|
||||||
)
|
place,
|
||||||
.setOnClickListener {
|
navigationViewModel,
|
||||||
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
)
|
||||||
navigationViewModel.loadPreviewRoute(
|
) { obj: Any? ->
|
||||||
carContext,
|
if (obj != null) {
|
||||||
surfaceRenderer.lastLocation,
|
navigateToPlace(place)
|
||||||
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 =
|
} else {
|
||||||
carContext.getCarService(ScreenManager::class.java)
|
prepareRoute(route)
|
||||||
screenManager
|
}
|
||||||
.push(
|
invalidate()
|
||||||
RequestPermissionScreen(
|
|
||||||
carContext,
|
|
||||||
permissionCheckCallback = {
|
|
||||||
screenManager.pop()
|
|
||||||
navigationViewModel.permissionGranted.value = true
|
|
||||||
},
|
|
||||||
permissions
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
navigationViewModel.permissionGranted.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 ->
|
val repository = getSettingsRepository(carContext)
|
||||||
if (route.isNotEmpty()) {
|
|
||||||
val repository = getSettingsRepository(carContext)
|
private var routingEngine = 0
|
||||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
|
||||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
|
||||||
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 {
|
||||||
navigationViewModel.recentPlaces.value = emptyList()
|
routingEngine = it
|
||||||
navigationViewModel.previewRoute.value = ""
|
})
|
||||||
|
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||||
mPlaces.addAll(places)
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
navigationViewModel.previewRoute.observe(this, previewObserver)
|
navigationViewModel.recentPlaces.value = emptyList()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
carContext,
|
RoutePreviewScreen(
|
||||||
surfaceRenderer.lastLocation,
|
carContext,
|
||||||
location,
|
RoutePreviewType.MULTI_ROUTE,
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer,
|
||||||
)
|
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +137,9 @@ class RoutePreviewScreen(
|
|||||||
)
|
)
|
||||||
var index = 0
|
var index = 0
|
||||||
routeModel.route.routes.forEach { route ->
|
routeModel.route.routes.forEach { route ->
|
||||||
itemListBuilder.addItem(createRow(route, index++))
|
if (index < listLimit) {
|
||||||
|
itemListBuilder.addItem(createRow(route, index++))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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()
|
routeType = RoutePreviewType.MULTI_ROUTE
|
||||||
.setIcon(selectRouteIcon)
|
invalidate()
|
||||||
.setOnClickListener {
|
})
|
||||||
routeType = RoutePreviewType.MULTI_ROUTE
|
val listContent = MessageTemplate.Builder(message)
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
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 = {
|
||||||
return actionStripBuilder
|
surfaceRenderer.handleScale(1)
|
||||||
}
|
invalidate()
|
||||||
|
|
||||||
private fun navigateAction(): Action {
|
|
||||||
return Action.Builder()
|
|
||||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.navigation_48px))
|
|
||||||
.setFlags(FLAG_DEFAULT)
|
|
||||||
.setOnClickListener {
|
|
||||||
onNavigate(routeModel.navState.currentRouteIndex)
|
|
||||||
}
|
}
|
||||||
.build()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun favoriteAction(): Action = Action.Builder()
|
/**
|
||||||
.setIcon(
|
* Creates an action to zoom out on the map.
|
||||||
CarIcon.Builder(
|
*/
|
||||||
IconCompat.createWithResource(
|
private fun zoomMinus(): Action {
|
||||||
carContext,
|
return createAction(
|
||||||
if (isFavorite)
|
carContext, R.drawable.ic_zoom_out_24,
|
||||||
R.drawable.ic_favorite_filled_white_24dp
|
onClickAction = {
|
||||||
else
|
surfaceRenderer.handleScale(-1)
|
||||||
R.drawable.ic_favorite_white_24dp
|
invalidate()
|
||||||
)
|
}
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
)
|
||||||
.setOnClickListener {
|
}
|
||||||
|
|
||||||
|
private fun favoriteAction(): Action =
|
||||||
|
createAction(
|
||||||
|
carContext, if (isFavorite)
|
||||||
|
R.drawable.ic_favorite_filled_white_24dp
|
||||||
|
else
|
||||||
|
R.drawable.ic_favorite_white_24dp
|
||||||
|
, FLAG_IS_PERSISTENT,
|
||||||
|
) {
|
||||||
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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 ->
|
||||||
searchResult = newSearch
|
if (newSearch.isNotEmpty()) {
|
||||||
invalidate()
|
navigationViewModel.searchPlaces.value = emptyList()
|
||||||
}
|
searchResult = newSearch
|
||||||
|
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,20 +87,22 @@ class SearchScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (it.id == RECENT) {
|
screenManager
|
||||||
navigationViewModel.loadRecentPlaces(
|
.pushForResult(
|
||||||
carContext,
|
PlaceListScreen(
|
||||||
surfaceRenderer.lastLocation,
|
carContext,
|
||||||
surfaceRenderer.carOrientation
|
surfaceRenderer,
|
||||||
)
|
it.id,
|
||||||
}
|
navigationViewModel,
|
||||||
if (it.id == FAVORITES) {
|
recentPlaces
|
||||||
navigationViewModel.loadFavorites(
|
)
|
||||||
carContext,
|
) { obj: Any? ->
|
||||||
surfaceRenderer.lastLocation,
|
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||||
surfaceRenderer.carOrientation
|
if (obj != null) {
|
||||||
)
|
setResult(obj)
|
||||||
}
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setBrowsable(true)
|
.setBrowsable(true)
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import com.kouros.navigation.data.overpass.Elements
|
|||||||
interface NavigationObserverCallback {
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = ""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,25 +89,25 @@ class TomTomRoute {
|
|||||||
lastLane = lane
|
lastLane = lane
|
||||||
}
|
}
|
||||||
intersections.add(Intersection(waypoints[startIndex], lanes))
|
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||||
|
|
||||||
}
|
}
|
||||||
stepDistance =
|
|
||||||
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
|
||||||
stepDuration =
|
|
||||||
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
|
||||||
val step = Step(
|
|
||||||
index = stepIndex,
|
|
||||||
street = street,
|
|
||||||
distance = stepDistance,
|
|
||||||
duration = stepDuration,
|
|
||||||
maneuver = maneuver,
|
|
||||||
intersection = intersections
|
|
||||||
)
|
|
||||||
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
|
||||||
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
|
||||||
steps.add(step)
|
|
||||||
stepIndex += 1
|
|
||||||
}
|
}
|
||||||
|
stepDistance =
|
||||||
|
route.guidance.instructions[index].routeOffsetInMeters - stepDistance
|
||||||
|
stepDuration =
|
||||||
|
route.guidance.instructions[index].travelTimeInSeconds - stepDuration
|
||||||
|
val step = Step(
|
||||||
|
index = stepIndex,
|
||||||
|
street = street,
|
||||||
|
distance = stepDistance,
|
||||||
|
duration = stepDuration,
|
||||||
|
maneuver = maneuver,
|
||||||
|
intersection = intersections,
|
||||||
|
countryCode = lastInstruction.countryCode
|
||||||
|
)
|
||||||
|
stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble()
|
||||||
|
stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble()
|
||||||
|
steps.add(step)
|
||||||
|
stepIndex += 1
|
||||||
}
|
}
|
||||||
legs.add(Leg(steps))
|
legs.add(Leg(steps))
|
||||||
val routeGeoJson = createLineStringCollection(waypoints)
|
val routeGeoJson = createLineStringCollection(waypoints)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
common/data/src/main/res/drawable/chevron_right_24px.xml
Normal file
11
common/data/src/main/res/drawable/chevron_right_24px.xml
Normal 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>
|
||||||
10
common/data/src/main/res/drawable/traffic_jam_48px.xml
Normal file
10
common/data/src/main/res/drawable/traffic_jam_48px.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user