Preview
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "com.kouros.navigation"
|
||||
minSdk = 33
|
||||
targetSdk = 36
|
||||
versionCode = 71
|
||||
versionName = "0.2.0.71"
|
||||
versionCode = 73
|
||||
versionName = "0.2.0.73"
|
||||
base.archivesName = "navi-$versionName"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ class MainActivity : ComponentActivity() {
|
||||
fun updateLocation(location: Location?) {
|
||||
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
|
||||
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()
|
||||
}
|
||||
if (routeModel.isNavigating()) {
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
package com.kouros.navigation.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
@@ -24,7 +19,6 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -36,7 +30,6 @@ import com.kouros.navigation.ui.components.SectionTitle
|
||||
import com.kouros.navigation.ui.components.SettingSwitch
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
|
||||
@@ -45,6 +38,7 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
|
||||
val show3D by viewModel.show3D.collectAsState()
|
||||
val showTraffic by viewModel.traffic.collectAsState()
|
||||
val distanceMode by viewModel.distanceMode.collectAsState()
|
||||
val driveSuggestion by viewModel.tripSuggestion.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -90,6 +84,12 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
|
||||
checked = showTraffic,
|
||||
onCheckedChange = viewModel::onTraffic
|
||||
)
|
||||
|
||||
SettingSwitch(
|
||||
title = stringResource(R.string.trip_suggestion),
|
||||
checked = driveSuggestion,
|
||||
onCheckedChange = viewModel::onTripSuggestion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
@@ -31,6 +32,9 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
val mockitoAgent = configurations.create("mockitoAgent")
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.car.app)
|
||||
@@ -48,10 +52,19 @@ dependencies {
|
||||
implementation(libs.play.services.location)
|
||||
implementation(libs.androidx.datastore.core)
|
||||
implementation(libs.androidx.monitor)
|
||||
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.runner)
|
||||
androidTestImplementation(libs.androidx.rules)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.mockito.core)
|
||||
testImplementation(libs.mockito.kotlin)
|
||||
androidTestImplementation(libs.androidx.runner)
|
||||
androidTestImplementation(libs.androidx.rules)
|
||||
}
|
||||
testImplementation(libs.androidx.car.app.testing)
|
||||
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 repository = getSettingsRepository(appContext)
|
||||
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
|
||||
val routeJson = appContext.resources.openRawResource(R.raw.tomtom_routing)
|
||||
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
|
||||
val routeJsonString = TomTomRepository().fetchUrl(
|
||||
"https://kouros-online.de/tomtom_routing.json",
|
||||
false
|
||||
)
|
||||
assertNotEquals("", routeJsonString)
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal)
|
||||
routeModel.startNavigation(routeJsonString)
|
||||
@@ -49,7 +51,7 @@ class RouteModelTest {
|
||||
fun checkRoute() {
|
||||
assertEquals(true, routeModel.isNavigating())
|
||||
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
|
||||
@@ -60,7 +62,7 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||
assertEquals(stepData.instruction, "Silcherstraße")
|
||||
assertEquals(stepData.leftStepDistance, 25.0, 1.0)
|
||||
assertEquals(stepData.leftStepDistance, 20.0, 5.0)
|
||||
val nextStepData = routeModel.nextStep()
|
||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
|
||||
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
|
||||
@@ -141,7 +143,7 @@ class RouteModelTest {
|
||||
if (index in 61..61) {
|
||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.lane.size, 2)
|
||||
assertEquals(stepData.lane.size, 3)
|
||||
assertEquals(stepData.lane.first().valid, true)
|
||||
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
|
||||
}
|
||||
@@ -203,6 +205,6 @@ class RouteModelTest {
|
||||
val location: Location = location(11.578911, 48.185565)
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
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 com.kouros.navigation.car.navigation.RouteCarModel
|
||||
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.RequestPermissionScreen
|
||||
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.GMS_CAR_SPEED_PERMISSION
|
||||
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.NavigationUtils.getViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.first
|
||||
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.
|
||||
* Implements NavigationScreen.Listener for handling navigation events.
|
||||
*/
|
||||
class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
class NavigationSession : Session(), NavigationListener {
|
||||
|
||||
// Flag to enable/disable contact access feature
|
||||
val useContacts = false
|
||||
@@ -80,6 +85,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
val simulation = Simulation()
|
||||
|
||||
var navigationManagerStarted = false
|
||||
|
||||
/**
|
||||
* Lifecycle observer for managing session lifecycle events.
|
||||
* Cleans up resources when the session is destroyed.
|
||||
@@ -148,11 +155,11 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
when (connectionState) {
|
||||
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit
|
||||
CarConnection.CONNECTION_TYPE_NATIVE -> {
|
||||
navigationScreen.checkPermission(AUTOMOTIVE_CAR_SPEED_PERMISSION)
|
||||
navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION)
|
||||
}
|
||||
|
||||
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)
|
||||
deviceLocationManager.stopLocationUpdates()
|
||||
autoDriveEnabled = true
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
simulation.startSimulation(
|
||||
routeModel, lifecycle.coroutineScope
|
||||
) { location ->
|
||||
@@ -241,10 +249,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(),
|
||||
onLocationUpdate = ::updateLocation,
|
||||
onInitialLocation = { location ->
|
||||
navigationViewModel.loadRecentPlace(
|
||||
navigationViewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
location,
|
||||
surfaceRenderer.carOrientation,
|
||||
carContext
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -325,7 +333,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
private fun handleNavigateIntent(screenManager: ScreenManager) {
|
||||
screenManager.popToRoot()
|
||||
screenManager.pushForResult(
|
||||
SearchScreen(carContext, surfaceRenderer, navigationViewModel)
|
||||
SearchScreen(carContext, surfaceRenderer, navigationViewModel, emptyList())
|
||||
) { result ->
|
||||
// Handle search result if needed
|
||||
}
|
||||
@@ -359,6 +367,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
if (routeModel.isNavigating()) {
|
||||
handleNavigationLocation(location)
|
||||
} else {
|
||||
navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location)
|
||||
surfaceRenderer.updateLocation(location)
|
||||
}
|
||||
}
|
||||
@@ -417,7 +426,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
* Called when user starts navigation
|
||||
*/
|
||||
override fun startNavigation() {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
navigationManager.navigationStarted()
|
||||
navigationManagerStarted = true
|
||||
if (autoDriveEnabled) {
|
||||
simulation.startSimulation(
|
||||
routeModel, lifecycle.coroutineScope
|
||||
@@ -428,7 +439,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
fun setRouteData() {
|
||||
println("SetRouteData")
|
||||
routeData.value = routeModel.curRoute.routeGeoJson
|
||||
viewStyle = ViewStyle.VIEW
|
||||
}
|
||||
@@ -457,6 +458,11 @@ class SurfaceRenderer(
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun updateTrafficData(routeModel: RouteCarModel) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
|
||||
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 {
|
||||
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.kouros.navigation.car.navigation
|
||||
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.text.SpannableString
|
||||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.AppManager
|
||||
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.DateTimeWithZone
|
||||
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.LaneDirection
|
||||
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.core.graphics.drawable.IconCompat
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.screen.createCarIcon
|
||||
import com.kouros.navigation.data.StepData
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.formattedDistance
|
||||
import java.util.Locale
|
||||
import java.time.Duration
|
||||
import java.util.TimeZone
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -115,8 +117,8 @@ class RouteCarModel : RouteModel() {
|
||||
.setRemainingTimeColor(CarColor.GREEN)
|
||||
.setRemainingDistanceColor(CarColor.BLUE)
|
||||
if (traffic > 0) {
|
||||
travelBuilder.setTripText(CarText.create("$traffic min"))
|
||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
|
||||
travelBuilder.setTripText(createDelay(traffic))
|
||||
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px))
|
||||
}
|
||||
|
||||
if (navState.travelMessage.isNotEmpty()) {
|
||||
@@ -125,7 +127,22 @@ class RouteCarModel : RouteModel() {
|
||||
}
|
||||
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 {
|
||||
var laneImageAdded = false
|
||||
stepData.lane.forEach {
|
||||
@@ -167,9 +184,6 @@ class RouteCarModel : RouteModel() {
|
||||
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 {
|
||||
return CarIcon.Builder(iconCompat).build()
|
||||
|
||||
@@ -19,10 +19,6 @@ class Simulation {
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
updateLocation: (Location) -> Unit
|
||||
) {
|
||||
// A92
|
||||
//updateLocation(location(11.709508, 48.338923 ))
|
||||
//updateLocation(homeVogelhart)
|
||||
|
||||
if (routeModel.navState.route.isRouteValid()) {
|
||||
val points = routeModel.curRoute.waypoints
|
||||
if (points.isEmpty()) return
|
||||
|
||||
@@ -30,9 +30,8 @@ class CategoriesScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
) : Screen(carContext), CategoryObserverCallback {
|
||||
) : Screen(carContext) {
|
||||
|
||||
private val categoryObserver = CategoryObserver(this)
|
||||
|
||||
private var category = ""
|
||||
var categories: List<Category> = listOf(
|
||||
@@ -41,17 +40,9 @@ class CategoriesScreen(
|
||||
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 {
|
||||
navigationViewModel.elements.value = emptyList()
|
||||
navigationViewModel.elements.observe(this, categoryObserver)
|
||||
carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback)
|
||||
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -61,10 +52,23 @@ class CategoriesScreen(
|
||||
itemListBuilder.addItem(
|
||||
Row.Builder()
|
||||
.setTitle(it.name)
|
||||
.setImage(carIcon(carContext,it.id, -1))
|
||||
.setImage(carIcon(carContext, it.id, -1))
|
||||
.setOnClickListener {
|
||||
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)
|
||||
.build()
|
||||
@@ -83,36 +87,6 @@ class CategoriesScreen(
|
||||
.setSingleList(itemListBuilder.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 {
|
||||
@@ -125,7 +99,12 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
|
||||
}
|
||||
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
|
||||
} 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.constraints.ConstraintManager
|
||||
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.CarText
|
||||
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.MapWithContentTemplate
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.NavigationUtils
|
||||
@@ -37,11 +41,25 @@ class CategoryScreen(
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val category: String,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private var elements: List<Elements>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
) : Screen(carContext), CategoryObserverCallback {
|
||||
|
||||
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 {
|
||||
val listBuilder = ItemList.Builder()
|
||||
@@ -72,17 +90,22 @@ class CategoryScreen(
|
||||
.setTitle(getTitle(carContext, category))
|
||||
.build()
|
||||
val builder = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(
|
||||
ListTemplate.Builder()
|
||||
.setHeader(header)
|
||||
.setSingleList(listBuilder.build())
|
||||
.build()
|
||||
)
|
||||
.setMapController(
|
||||
MapController.Builder().setMapActionStrip(
|
||||
getMapActionStrip()
|
||||
).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()
|
||||
}
|
||||
|
||||
@@ -122,28 +145,24 @@ class CategoryScreen(
|
||||
} else {
|
||||
row.addText(carText("${it.tags.openingHours}"))
|
||||
}
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
row.addAction(
|
||||
Action.Builder()
|
||||
.setOnClickListener {
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
currentLocation = surfaceRenderer.lastLocation,
|
||||
location(it.lon, it.lat),
|
||||
surfaceRenderer.carOrientation
|
||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, {
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
currentLocation = surfaceRenderer.lastLocation,
|
||||
location(it.lon, it.lat),
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = CHARGING_STATION,
|
||||
latitude = it.lat,
|
||||
longitude = it.lon
|
||||
)
|
||||
setResult(
|
||||
Place(
|
||||
name = name,
|
||||
category = CHARGING_STATION,
|
||||
latitude = it.lat,
|
||||
longitude = it.lon
|
||||
)
|
||||
)
|
||||
finish()
|
||||
}
|
||||
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
|
||||
.build())
|
||||
)
|
||||
finish()
|
||||
}))
|
||||
return row.build()
|
||||
}
|
||||
|
||||
@@ -179,10 +198,25 @@ class CategoryScreen(
|
||||
@DrawableRes iconRes: Int,
|
||||
scale: Int
|
||||
): Action {
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
return Action.Builder()
|
||||
.setOnClickListener { surfaceRenderer.handleScale(scale) }
|
||||
.setIcon(navigationUtils.createCarIcon(iconRes))
|
||||
.build()
|
||||
return createAction(carContext, iconRes, FLAG_IS_PERSISTENT, {
|
||||
surfaceRenderer.handleScale(scale)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
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.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
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.navigation.model.Destination
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.navigation.model.MessageInfo
|
||||
import androidx.car.app.navigation.model.NavigationTemplate
|
||||
import androidx.car.app.navigation.model.RoutingInfo
|
||||
import androidx.car.app.navigation.model.Trip
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
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.NavigationObserverManager
|
||||
import com.kouros.navigation.car.screen.settings.SettingsScreen
|
||||
import com.kouros.navigation.data.Constants
|
||||
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
|
||||
import com.kouros.navigation.data.Constants.ROUTE_UPDATE
|
||||
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.overpass.Elements
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.GeoUtils
|
||||
import com.kouros.navigation.utils.formattedDistance
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
@@ -47,7 +53,6 @@ import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
@@ -57,154 +62,98 @@ import kotlin.math.absoluteValue
|
||||
* Main screen for car navigation.
|
||||
* Handles different navigation states and provides corresponding templates.
|
||||
*/
|
||||
class NavigationScreen(
|
||||
open class NavigationScreen(
|
||||
carContext: CarContext,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var routeModel: RouteCarModel,
|
||||
private var listener: Listener,
|
||||
private var listener: NavigationListener,
|
||||
private val navigationViewModel: NavigationViewModel
|
||||
) : Screen(carContext), NavigationObserverCallback {
|
||||
|
||||
/** A listener for navigation start and stop signals. */
|
||||
interface Listener {
|
||||
/** Stops navigation. */
|
||||
fun stopNavigation()
|
||||
val backGroundColor = CarColor.GREEN
|
||||
|
||||
/** Starts navigation. */
|
||||
fun startNavigation()
|
||||
|
||||
/** Updates trip information. */
|
||||
fun updateTrip(trip: Trip)
|
||||
}
|
||||
|
||||
val backGroundColor = CarColor.BLUE
|
||||
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
var recentPlace = Place()
|
||||
|
||||
var recentPlaces = emptyList<Place>()
|
||||
|
||||
var recentPlace: Place = Place()
|
||||
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 speedCameras = listOf<Elements>()
|
||||
val observerManager = NavigationObserverManager(navigationViewModel, this)
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
var distanceMode = 0
|
||||
private var distanceMode = 0
|
||||
|
||||
init {
|
||||
observerManager.attachAllObservers(this)
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.routingEngine.first()
|
||||
settingsViewModel.recentPlaces.first()
|
||||
}
|
||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||
distanceMode = it
|
||||
})
|
||||
}
|
||||
private var tripSuggestion = false
|
||||
|
||||
/**
|
||||
* Handles the received route string.
|
||||
* Starts navigation and invalidates the screen.
|
||||
*/
|
||||
override fun onRouteReceived(route: String) {
|
||||
if (route.isNotEmpty()) {
|
||||
prepareRoute(route)
|
||||
private var tripSuggestionCalled = false
|
||||
|
||||
private var routingEngine = 0
|
||||
|
||||
private var showTraffic = false;
|
||||
private var arrivalTimer: CountDownTimer? = null
|
||||
private var reRouteTimer: CountDownTimer? = null
|
||||
|
||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||
recentPlaces = newPlaces
|
||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||
tripSuggestionCalled = true
|
||||
navigationType = NavigationType.RECENT
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare route and start navigation
|
||||
*/
|
||||
private fun prepareRoute(route: String) {
|
||||
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
routeModel.startNavigation(route)
|
||||
if (routeModel.hasLegs()) {
|
||||
settingsViewModel.onLastRouteChanged(route)
|
||||
init {
|
||||
observerManager.attachAllObservers(this)
|
||||
lifecycleScope.launch {
|
||||
settingsViewModel.tripSuggestion.first()
|
||||
settingsViewModel.routingEngine.first()
|
||||
}
|
||||
surfaceRenderer.setRouteData()
|
||||
listener.startNavigation()
|
||||
}
|
||||
repository.distanceModeFlow.asLiveData().observe(this, Observer {
|
||||
distanceMode = it
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if navigation is currently active.
|
||||
*/
|
||||
override fun isNavigating(): Boolean = routeModel.isNavigating()
|
||||
|
||||
/**
|
||||
* Handles the received recent place.
|
||||
* Updates the navigation type to RECENT and invalidates the screen.
|
||||
*/
|
||||
override fun onRecentPlaceReceived(place: Place) {
|
||||
recentPlace = place
|
||||
navigationType = NavigationType.RECENT
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles received traffic data and updates the surface renderer.
|
||||
*/
|
||||
override fun onTrafficReceived(traffic: Map<String, String>) {
|
||||
surfaceRenderer.setTrafficData(traffic)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the received place search result.
|
||||
* Navigates to the specified place.
|
||||
*/
|
||||
override fun onPlaceSearchResultReceived(place: Place) {
|
||||
navigateToPlace(place)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles received speed camera data.
|
||||
* Updates the surface renderer with the camera locations.
|
||||
*/
|
||||
override fun onSpeedCamerasReceived(cameras: List<Elements>) {
|
||||
speedCameras = cameras
|
||||
val coordinates = mutableListOf<List<Double>>()
|
||||
cameras.forEach {
|
||||
coordinates.add(listOf(it.lon, it.lat))
|
||||
}
|
||||
val speedData = GeoUtils.createPointCollection(coordinates, "radar")
|
||||
surfaceRenderer.speedCamerasData.value = speedData
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles received maximum speed data and updates the surface renderer.
|
||||
*/
|
||||
override fun onMaxSpeedReceived(speed: Int) {
|
||||
surfaceRenderer.maxSpeed.value = speed
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the received previe route string.
|
||||
* Starts navigation and invalidates the screen.
|
||||
*/
|
||||
override fun onPreviewRouteReceived(route: String) {
|
||||
if (navigationType == NavigationType.NAVIGATION && route.isNotEmpty()) {
|
||||
startPreviewScreen(route)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the screen.
|
||||
*/
|
||||
override fun invalidateScreen() {
|
||||
invalidate()
|
||||
repository.trafficFlow.asLiveData().observe(this, Observer {
|
||||
showTraffic = it
|
||||
})
|
||||
repository.tripSuggestionFlow.asLiveData().observe(this, Observer {
|
||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||
tripSuggestion = it
|
||||
})
|
||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||
routingEngine = it
|
||||
})
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
arrivalTimer?.cancel()
|
||||
reRouteTimer?.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate template based on the current navigation state.
|
||||
*/
|
||||
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) {
|
||||
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
|
||||
NavigationType.RECENT -> navigationRecentPlaceTemplate()
|
||||
NavigationType.RECENT -> navigationRecentPlacesTemplate()
|
||||
NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder)
|
||||
NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder)
|
||||
else -> navigationViewTemplate(actionStripBuilder)
|
||||
@@ -214,9 +163,9 @@ class NavigationScreen(
|
||||
/**
|
||||
* 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(
|
||||
stopAction()
|
||||
createAction(carContext, R.drawable.ic_close_white_24dp, FLAG_IS_PERSISTENT,{ stopNavigation() })
|
||||
)
|
||||
updateTrip()
|
||||
return NavigationTemplate.Builder()
|
||||
@@ -225,7 +174,20 @@ class NavigationScreen(
|
||||
)
|
||||
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode))
|
||||
.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)
|
||||
.build()
|
||||
}
|
||||
@@ -235,38 +197,41 @@ class NavigationScreen(
|
||||
*/
|
||||
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
return NavigationTemplate.Builder()
|
||||
.setBackgroundColor(CarColor.SECONDARY)
|
||||
.setBackgroundColor(backGroundColor)
|
||||
.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()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (routeModel.navState.arrived) {
|
||||
val timer = object : CountDownTimer(8000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
routeModel.navState = routeModel.navState.copy(arrived = false)
|
||||
navigationType = NavigationType.VIEW
|
||||
invalidate()
|
||||
}
|
||||
arrivalTimer?.cancel()
|
||||
arrivalTimer = object : CountDownTimer(8000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
routeModel.navState = routeModel.navState.copy(arrived = false)
|
||||
navigationType = NavigationType.VIEW
|
||||
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.
|
||||
*/
|
||||
@@ -292,36 +257,82 @@ class NavigationScreen(
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setBackgroundColor(CarColor.SECONDARY)
|
||||
.setBackgroundColor(backGroundColor)
|
||||
.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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a template showing recent places or destinations.
|
||||
*/
|
||||
fun navigationRecentPlaceTemplate(): Template {
|
||||
val messageTemplate = MessageTemplate.Builder(
|
||||
recentPlace.name + "\n"
|
||||
+ recentPlace.city
|
||||
)
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setTitle(carContext.getString(R.string.drive_now))
|
||||
.build()
|
||||
fun navigationRecentPlacesTemplate(): Template {
|
||||
if (!tripSuggestion || recentPlaces.isEmpty()) {
|
||||
navigationType = NavigationType.VIEW
|
||||
return navigationViewTemplate(
|
||||
createActionStripBuilder(
|
||||
{
|
||||
createAction(
|
||||
carContext,
|
||||
R.drawable.search_48px,
|
||||
FLAG_IS_PERSISTENT,
|
||||
{ startSearchScreen() })
|
||||
},
|
||||
{ settingsAction() })
|
||||
)
|
||||
.addAction(navigateAction())
|
||||
.addAction(closeAction())
|
||||
.build()
|
||||
}
|
||||
val listBuilder = ItemList.Builder()
|
||||
recentPlaces.filter { it.category == Constants.RECENT }.forEach {
|
||||
val row = Row.Builder()
|
||||
.setTitle(it.name!!)
|
||||
.addAction(
|
||||
createNavigateAction(it)
|
||||
)
|
||||
.setOnClickListener {
|
||||
navigateToPlace(it)
|
||||
}
|
||||
listBuilder.addItem(
|
||||
row.build()
|
||||
)
|
||||
}
|
||||
val contentTemplate =
|
||||
ListTemplate.Builder()
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setTitle(carContext.getString(R.string.drive_now))
|
||||
.addEndHeaderAction(closeAction())
|
||||
.build()
|
||||
)
|
||||
.setSingleList(listBuilder.build())
|
||||
.build()
|
||||
|
||||
val builder = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(messageTemplate)
|
||||
.setContentTemplate(contentTemplate)
|
||||
.setActionStrip(
|
||||
mapActionStripBuilder()
|
||||
.addAction(settingsAction())
|
||||
.addAction(searchAction())
|
||||
.build()
|
||||
mapActionStrip(
|
||||
ViewStyle.VIEW,
|
||||
{ settingsAction() },
|
||||
{ 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()
|
||||
}
|
||||
@@ -355,208 +366,98 @@ class NavigationScreen(
|
||||
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.
|
||||
*/
|
||||
private fun navigateAction(): Action {
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
return Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.navigation_48px
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
navigateTo,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
// val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||
// navigationViewModel.loadRoute(
|
||||
// carContext,
|
||||
// surfaceRenderer.lastLocation,
|
||||
// navigateTo,
|
||||
// surfaceRenderer.carOrientation
|
||||
// )
|
||||
// routeModel.navState = routeModel.navState.copy(destination = recentPlace)
|
||||
private fun createNavigateAction(place: Place): Action {
|
||||
recentPlace = place
|
||||
return createAction(
|
||||
carContext, R.drawable.chevron_right_24px,
|
||||
onClickAction = {
|
||||
screenManager
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
RoutePreviewType.SINGLE_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
navigateToPlace(place)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to close the current view or template.
|
||||
*/
|
||||
private fun closeAction(): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_close_white_24dp
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
return createAction(
|
||||
carContext, R.drawable.ic_close_white_24dp,
|
||||
onClickAction = {
|
||||
navigationType = NavigationType.VIEW
|
||||
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.
|
||||
*/
|
||||
private fun settingsAction(): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
||||
.setOnClickListener {
|
||||
return createAction(
|
||||
carContext, R.drawable.settings_48px,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
screenManager.push(SettingsScreen(carContext, navigationViewModel))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to zoom in on the map.
|
||||
*/
|
||||
private fun zoomPlus(): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_zoom_in_24
|
||||
)
|
||||
)
|
||||
.build()
|
||||
).setOnClickListener {
|
||||
return createAction(
|
||||
carContext, R.drawable.ic_zoom_in_24,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
surfaceRenderer.handleScale(1)
|
||||
invalidate()
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to zoom out on the map.
|
||||
*/
|
||||
private fun zoomMinus(): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_zoom_out_24
|
||||
)
|
||||
)
|
||||
.build()
|
||||
).setOnClickListener {
|
||||
return createAction(
|
||||
carContext, R.drawable.ic_zoom_out_24,
|
||||
onClickAction = {
|
||||
surfaceRenderer.handleScale(-1)
|
||||
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.
|
||||
*/
|
||||
private fun startSearchScreen() {
|
||||
|
||||
screenManager
|
||||
.pushForResult(
|
||||
SearchScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
navigationViewModel
|
||||
navigationViewModel,
|
||||
recentPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
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.
|
||||
*/
|
||||
fun navigateToPlace(place: Place) {
|
||||
val preview = navigationViewModel.previewRoute.value
|
||||
navigationViewModel.previewRoute.value = ""
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
val location = location(place.longitude, place.latitude)
|
||||
navigationViewModel.saveRecent(carContext, place)
|
||||
currentNavigationLocation = location
|
||||
@@ -627,6 +496,7 @@ class NavigationScreen(
|
||||
navigationViewModel.route.value = preview
|
||||
}
|
||||
routeModel.navState = routeModel.navState.copy(destination = place)
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@@ -650,25 +520,27 @@ class NavigationScreen(
|
||||
invalidate()
|
||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||
mainThreadHandler.post {
|
||||
object : CountDownTimer(2000, 1000) {
|
||||
reRouteTimer?.cancel()
|
||||
reRouteTimer = object : CountDownTimer(2000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
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) {
|
||||
val dest = location(destination.longitude, destination.latitude)
|
||||
fun reRoute(place: Place) {
|
||||
val destination = location(place.longitude, place.latitude)
|
||||
navigationViewModel.loadRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
dest,
|
||||
destination,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
@@ -678,32 +550,58 @@ class NavigationScreen(
|
||||
*/
|
||||
fun updateTrip(location: Location) {
|
||||
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)
|
||||
if (duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||
if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) {
|
||||
lastTrafficDate = current
|
||||
navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation)
|
||||
}
|
||||
updateSpeedCamera(location)
|
||||
with(routeModel) {
|
||||
updateLocation(location, navigationViewModel)
|
||||
checkArrival()
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for arrival
|
||||
*/
|
||||
private fun RouteCarModel.checkArrival() {
|
||||
if ((navState.maneuverType == Maneuver.TYPE_DESTINATION
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT
|
||||
|| navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT)
|
||||
&& routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
fun checkArrival() {
|
||||
if (routeModel.isArrival()
|
||||
&& routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE
|
||||
) {
|
||||
listener.stopNavigation()
|
||||
settingsViewModel.onLastRouteChanged("")
|
||||
navState = navState.copy(arrived = true)
|
||||
routeModel.navState = routeModel.navState.copy(arrived = true)
|
||||
surfaceRenderer.routeData.value = ""
|
||||
navigationType = NavigationType.ARRIVAL
|
||||
invalidate()
|
||||
@@ -757,7 +655,7 @@ class NavigationScreen(
|
||||
updatedCameras.add(it)
|
||||
}
|
||||
val sortedList = updatedCameras.sortedWith(compareBy { it.distance })
|
||||
val camera = sortedList.first()
|
||||
val camera = sortedList.firstOrNull() ?: return
|
||||
val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location)
|
||||
val bearingSpeedCamera = if (camera.tags.direction != null) {
|
||||
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) {
|
||||
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()
|
||||
navigationViewModel.permissionGranted.value = true
|
||||
},
|
||||
permissions
|
||||
)
|
||||
)
|
||||
} else {
|
||||
navigationViewModel.permissionGranted.value = true
|
||||
override fun onRouteReceived(route: String) {
|
||||
if (route.isNotEmpty()) {
|
||||
if (routeModel.isNavigating()) {
|
||||
updateRoute(route)
|
||||
} else {
|
||||
prepareRoute(route)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare route and start navigation
|
||||
*/
|
||||
private fun prepareRoute(route: String) {
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
routeModel.startNavigation(route)
|
||||
if (routeModel.hasLegs()) {
|
||||
settingsViewModel.onLastRouteChanged(route)
|
||||
}
|
||||
surfaceRenderer.setRouteData()
|
||||
listener.startNavigation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update route and traffic data
|
||||
*/
|
||||
private fun updateRoute(route: String) {
|
||||
val 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.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
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.Template
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.asLiveData
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
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.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class PlaceListScreen(
|
||||
private val carContext: CarContext,
|
||||
private val surfaceRenderer: SurfaceRenderer,
|
||||
private val category: String,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private val places: List<Place>
|
||||
private val recentPlaces: List<Place>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
val routeModel = RouteCarModel()
|
||||
@@ -45,45 +45,28 @@ class PlaceListScreen(
|
||||
|
||||
var mPlaces = mutableListOf<Place>()
|
||||
|
||||
val previewObserver = Observer<String> { route ->
|
||||
if (route.isNotEmpty()) {
|
||||
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.MULTI_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
routeModel = routeModel
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
private var routingEngine = 0
|
||||
|
||||
init {
|
||||
// loadPlaces()
|
||||
navigationViewModel.recentPlaces.value = emptyList()
|
||||
navigationViewModel.previewRoute.value = ""
|
||||
|
||||
mPlaces.addAll(places)
|
||||
navigationViewModel.previewRoute.observe(this, previewObserver)
|
||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||
routingEngine = it
|
||||
})
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
navigationViewModel.recentPlaces.value = emptyList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate template based on the current navigation state.
|
||||
*/
|
||||
override fun onGetTemplate(): Template {
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
.setNoItemsMessage(carContext.getString(R.string.no_places))
|
||||
mPlaces.forEach {
|
||||
recentPlaces.filter { it.category == category }.forEach {
|
||||
val street = if (it.street != null) {
|
||||
it.street
|
||||
} else {
|
||||
@@ -104,13 +87,21 @@ class PlaceListScreen(
|
||||
it.street,
|
||||
// avatar = null
|
||||
)
|
||||
val location = location(place.longitude, place.latitude)
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
location,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
screenManager
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
RoutePreviewType.MULTI_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (category != CONTACTS) {
|
||||
row.addText(SpannableString(" ").apply {
|
||||
@@ -148,14 +139,11 @@ class PlaceListScreen(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun deleteAction(place: Place): Action = Action.Builder()
|
||||
.setIcon(
|
||||
RouteCarModel().createCarIcon(
|
||||
carContext,
|
||||
R.drawable.ic_close_white_24dp
|
||||
)
|
||||
)
|
||||
.setOnClickListener {
|
||||
/**
|
||||
* Creates an Action to delete a place.
|
||||
*/
|
||||
private fun deleteAction(place: Place): Action =
|
||||
createAction(carContext, R.drawable.ic_close_white_24dp) {
|
||||
navigationViewModel.deletePlace(carContext, place)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
@@ -164,7 +152,7 @@ class PlaceListScreen(
|
||||
mPlaces.remove(place)
|
||||
invalidate()
|
||||
}
|
||||
.build()
|
||||
|
||||
|
||||
fun contactIcon(avatar: Uri?, category: String?): CarIcon {
|
||||
if (category == RECENT || avatar == null) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.kouros.navigation.car.screen
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
@@ -46,7 +48,6 @@ class RequestPermissionScreen(
|
||||
).show()
|
||||
if (!approved!!.isEmpty()) {
|
||||
permissionCheckCallback.onPermissionGranted()
|
||||
//mContactsPermissionCheckCallback.onPermissionGranted()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -62,4 +63,31 @@ class RequestPermissionScreen(
|
||||
.addAction(action)
|
||||
.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.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.util.Log
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.car.app.CarContext
|
||||
@@ -11,6 +12,7 @@ import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Action.FLAG_DEFAULT
|
||||
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import androidx.car.app.model.CarColor
|
||||
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.versioning.CarAppApiLevels
|
||||
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.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.ViewStyle
|
||||
import com.kouros.navigation.car.navigation.NavigationUtils
|
||||
import com.kouros.navigation.car.navigation.RouteCarModel
|
||||
import com.kouros.navigation.data.Place
|
||||
import com.kouros.navigation.data.route.Routes
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.time.Duration
|
||||
import kotlin.math.min
|
||||
|
||||
/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */
|
||||
/** Creates a screen using the new [MapWithContentTemplate] */
|
||||
class RoutePreviewScreen(
|
||||
carContext: CarContext,
|
||||
private var routeType: RoutePreviewType,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var destination: Place,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private val routeModel: RouteCarModel
|
||||
) :
|
||||
Screen(carContext) {
|
||||
private var isFavorite = false
|
||||
@@ -54,19 +64,68 @@ class RoutePreviewScreen(
|
||||
val maxListItems: Int = 3
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
var routingEngine = -1
|
||||
|
||||
var routeSelected = false
|
||||
|
||||
val routeModel = RouteCarModel()
|
||||
|
||||
var loading = true
|
||||
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
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 {
|
||||
navigationViewModel.previewRoute.observe(this, observer)
|
||||
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 {
|
||||
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||
val listLimit = min(
|
||||
@@ -78,7 +137,9 @@ class RoutePreviewScreen(
|
||||
)
|
||||
var index = 0
|
||||
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")
|
||||
.build()
|
||||
}
|
||||
|
||||
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
|
||||
ListTemplate.Builder()
|
||||
val listContent = ListTemplate.Builder()
|
||||
.setHeader(header.build())
|
||||
.setSingleList(itemListBuilder.build())
|
||||
if (loading) {
|
||||
listContent.setLoading(true)
|
||||
} else {
|
||||
listContent.setSingleList(itemListBuilder.build())
|
||||
}
|
||||
.build()
|
||||
listContent.build()
|
||||
} else {
|
||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
@@ -123,75 +188,78 @@ class RoutePreviewScreen(
|
||||
carContext, R.drawable.alt_route_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { onNavigate(routeModel.navState.currentRouteIndex) }
|
||||
.build()
|
||||
val selectRouteAction = Action.Builder()
|
||||
.setIcon(selectRouteIcon)
|
||||
.setOnClickListener {
|
||||
routeType = RoutePreviewType.MULTI_ROUTE
|
||||
invalidate()
|
||||
}
|
||||
.build()
|
||||
MessageTemplate.Builder(
|
||||
message
|
||||
)
|
||||
val navigateAction =
|
||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{
|
||||
onNavigate(routeModel.navState.currentRouteIndex)
|
||||
})
|
||||
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
|
||||
routeType = RoutePreviewType.MULTI_ROUTE
|
||||
invalidate()
|
||||
})
|
||||
val listContent = MessageTemplate.Builder(message)
|
||||
.setHeader(header.build())
|
||||
.addAction(navigateAction)
|
||||
.addAction(selectRouteAction)
|
||||
.setLoading(message.toString() == "Wait")
|
||||
.build()
|
||||
if (loading) {
|
||||
listContent.setLoading(true)
|
||||
}
|
||||
listContent.build()
|
||||
}
|
||||
|
||||
|
||||
|
||||
val template = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(content)
|
||||
.setMapController(
|
||||
MapController.Builder().setMapActionStrip(
|
||||
getMapActionStrip()
|
||||
).build()
|
||||
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
|
||||
zoomMinus()
|
||||
} )).build()
|
||||
|
||||
)
|
||||
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()
|
||||
}
|
||||
|
||||
private fun createActionStripBuilder(): ActionStrip.Builder {
|
||||
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
|
||||
actionStripBuilder.addAction(
|
||||
navigateAction()
|
||||
)
|
||||
return actionStripBuilder
|
||||
}
|
||||
|
||||
private fun navigateAction(): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.navigation_48px))
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setOnClickListener {
|
||||
onNavigate(routeModel.navState.currentRouteIndex)
|
||||
private fun zoomPlus(): Action {
|
||||
return createAction(
|
||||
carContext, R.drawable.ic_zoom_in_24,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
surfaceRenderer.handleScale(1)
|
||||
invalidate()
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun favoriteAction(): Action = Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
if (isFavorite)
|
||||
R.drawable.ic_favorite_filled_white_24dp
|
||||
else
|
||||
R.drawable.ic_favorite_white_24dp
|
||||
)
|
||||
)
|
||||
.build()
|
||||
/**
|
||||
* Creates an action to zoom out on the map.
|
||||
*/
|
||||
private fun zoomMinus(): Action {
|
||||
return createAction(
|
||||
carContext, R.drawable.ic_zoom_out_24,
|
||||
onClickAction = {
|
||||
surfaceRenderer.handleScale(-1)
|
||||
invalidate()
|
||||
}
|
||||
)
|
||||
.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
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
@@ -208,26 +276,17 @@ class RoutePreviewScreen(
|
||||
navigationViewModel.saveFavorite(carContext, destination)
|
||||
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) {
|
||||
navigationViewModel.deleteFavorite(carContext, destination)
|
||||
}
|
||||
isFavorite = !isFavorite
|
||||
finish()
|
||||
}
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.heart_minus_48px
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
})
|
||||
|
||||
|
||||
private fun createRouteText(route: Routes): CarText {
|
||||
val time = route.summary.duration
|
||||
@@ -245,21 +304,13 @@ class RoutePreviewScreen(
|
||||
}
|
||||
|
||||
private fun createRow(route: Routes, index: Int): Row {
|
||||
val navigateActionIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.navigation_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate(index) }
|
||||
.build()
|
||||
|
||||
val navigateAction = createAction(carContext, R.drawable.navigation_48px ) {
|
||||
this.onNavigate(index)
|
||||
}
|
||||
val routeText = createRouteText(route)
|
||||
var street = ""
|
||||
var maxDistance = 0.0
|
||||
routeModel.route.routes[index].legs.first().steps.forEach {
|
||||
routeModel.steps.forEach {
|
||||
if (it.distance > maxDistance) {
|
||||
maxDistance = it.distance
|
||||
street = it.street
|
||||
@@ -271,9 +322,9 @@ class RoutePreviewScreen(
|
||||
.setOnClickListener { onRouteSelected(index) }
|
||||
.addText(street)
|
||||
.addAction(navigateAction)
|
||||
|
||||
if (route.summary.trafficDelay > 60) {
|
||||
row.addText(createDelay(route))
|
||||
row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px))
|
||||
}
|
||||
return row.build()
|
||||
}
|
||||
@@ -294,6 +345,7 @@ class RoutePreviewScreen(
|
||||
)
|
||||
return delayBuilder
|
||||
}
|
||||
|
||||
private fun onNavigate(index: Int) {
|
||||
destination.routeIndex = index
|
||||
setResult(destination)
|
||||
@@ -303,30 +355,19 @@ class RoutePreviewScreen(
|
||||
private fun onRouteSelected(index: Int) {
|
||||
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
surfaceRenderer.updateTrafficData(routeModel)
|
||||
routeSelected = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun getMapActionStrip(): ActionStrip {
|
||||
return ActionStrip.Builder()
|
||||
.addAction(
|
||||
createToastAction(R.drawable.ic_zoom_in_24)
|
||||
)
|
||||
.addAction(
|
||||
createToastAction(R.drawable.ic_zoom_out_24)
|
||||
)
|
||||
.addAction(Action.PAN)
|
||||
.build()
|
||||
private fun getTraffic() {
|
||||
navigationViewModel.loadTraffic(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
|
||||
private fun createToastAction(
|
||||
@DrawableRes iconRes: Int
|
||||
): Action {
|
||||
return Action.Builder()
|
||||
.setOnClickListener { surfaceRenderer.handleScale(-1) }
|
||||
.setIcon(navigationUtils.createCarIcon(iconRes))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private val recentPlaces: List<Place>,
|
||||
) : Screen(carContext) {
|
||||
|
||||
var isSearchComplete: Boolean = false
|
||||
@@ -44,54 +45,16 @@ class SearchScreen(
|
||||
lateinit var searchResult: List<SearchResult>
|
||||
|
||||
val observer = Observer<List<SearchResult>> { newSearch ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
if (newSearch.isNotEmpty()) {
|
||||
navigationViewModel.searchPlaces.value = emptyList()
|
||||
searchResult = newSearch
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
val observerFavorites = Observer<List<Place>> { newPlaces ->
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
FAVORITES,
|
||||
navigationViewModel,
|
||||
newPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
navigationViewModel.searchPlaces.observe(this, observer)
|
||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||
navigationViewModel.favorites.observe(this, observerFavorites)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -124,20 +87,22 @@ class SearchScreen(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (it.id == RECENT) {
|
||||
navigationViewModel.loadRecentPlaces(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
if (it.id == FAVORITES) {
|
||||
navigationViewModel.loadFavorites(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
}
|
||||
screenManager
|
||||
.pushForResult(
|
||||
PlaceListScreen(
|
||||
carContext,
|
||||
surfaceRenderer,
|
||||
it.id,
|
||||
navigationViewModel,
|
||||
recentPlaces
|
||||
)
|
||||
) { obj: Any? ->
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
if (obj != null) {
|
||||
setResult(obj)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.setBrowsable(true)
|
||||
|
||||
@@ -10,10 +10,7 @@ import com.kouros.navigation.data.overpass.Elements
|
||||
interface NavigationObserverCallback {
|
||||
/** Called when a route is received and navigation should start */
|
||||
fun onRouteReceived(route: String)
|
||||
|
||||
/** Called when a recent place is selected but navigation hasn't started */
|
||||
fun onRecentPlaceReceived(place: Place)
|
||||
|
||||
|
||||
/** Check if currently navigating */
|
||||
fun isNavigating(): Boolean
|
||||
|
||||
@@ -29,9 +26,6 @@ interface NavigationObserverCallback {
|
||||
/** Called when max speed is updated */
|
||||
fun onMaxSpeedReceived(speed: Int)
|
||||
|
||||
/** Called when a preview route is received and navigation should start */
|
||||
fun onPreviewRouteReceived(route: String)
|
||||
|
||||
/** Called to request UI invalidation/refresh */
|
||||
fun invalidateScreen()
|
||||
|
||||
|
||||
@@ -12,12 +12,10 @@ class NavigationObserverManager(
|
||||
) {
|
||||
|
||||
val routeObserver = RouteObserver(callback)
|
||||
val recentPlaceObserver = RecentPlaceObserver(callback)
|
||||
val trafficObserver = TrafficObserver(callback)
|
||||
val placeSearchObserver = PlaceSearchObserver(callback)
|
||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||
val previewObserver = PreviewRouteObserver(callback)
|
||||
|
||||
/**
|
||||
* Attaches all observers to the ViewModel.
|
||||
@@ -26,11 +24,9 @@ class NavigationObserverManager(
|
||||
fun attachAllObservers(screen: androidx.car.app.Screen) {
|
||||
viewModel.route.observe(screen, routeObserver)
|
||||
viewModel.traffic.observe(screen, trafficObserver)
|
||||
viewModel.recentPlace.observe(screen, recentPlaceObserver)
|
||||
viewModel.placeLocation.observe(screen, placeSearchObserver)
|
||||
viewModel.speedCameras.observe(screen, speedCameraObserver)
|
||||
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 tripSuggestion = false
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -34,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
buildingToggleState = it
|
||||
invalidate()
|
||||
}
|
||||
settingsViewModel.tripSuggestion.asLiveData().observe(this) {
|
||||
tripSuggestion = it
|
||||
invalidate()
|
||||
}
|
||||
val listBuilder = ItemList.Builder()
|
||||
|
||||
val buildingToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
settingsViewModel.onShow3DChanged(checked)
|
||||
@@ -49,6 +56,13 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
}.setChecked(showTraffic).build()
|
||||
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(
|
||||
buildRowForScreenTemplate(
|
||||
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())
|
||||
}
|
||||
|
||||
@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
|
||||
fun `TrafficObserver triggers callback with traffic data and invalidates screen`() {
|
||||
val observer = TrafficObserver(mockCallback)
|
||||
@@ -119,15 +95,15 @@ class ObserversTest {
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private fun createTestPlace(): Place {
|
||||
return Place(
|
||||
private fun createTestPlace():List<Place> {
|
||||
return listOf(Place(
|
||||
name = "Test Place",
|
||||
street = "Test Street",
|
||||
city = "Test City",
|
||||
latitude = 52.0,
|
||||
longitude = 10.0,
|
||||
category = Constants.FAVORITES
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
private fun createTestSearchResult(): SearchResult {
|
||||
|
||||
@@ -130,6 +130,8 @@ object Constants {
|
||||
|
||||
const val TRAFFIC_UPDATE = 300
|
||||
|
||||
const val ROUTE_UPDATE = 60
|
||||
|
||||
const val INSTRUCTION_DISTANCE = 50
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
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 java.net.Authenticator
|
||||
import java.net.HttpURLConnection
|
||||
@@ -41,7 +19,7 @@ abstract class NavigationRepository {
|
||||
abstract fun getRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
destination: Location,
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String
|
||||
@@ -59,7 +37,7 @@ abstract class NavigationRepository {
|
||||
}
|
||||
|
||||
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}"
|
||||
return fetchUrl(
|
||||
"${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox",
|
||||
|
||||
@@ -53,6 +53,8 @@ class DataStoreManager(private val context: Context) {
|
||||
|
||||
val TRAFFIC = booleanPreferencesKey("Traffic")
|
||||
|
||||
val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion")
|
||||
|
||||
}
|
||||
|
||||
// Read values
|
||||
@@ -129,6 +131,11 @@ class DataStoreManager(private val context: Context) {
|
||||
preferences[PreferencesKeys.TRAFFIC] == true
|
||||
}
|
||||
|
||||
val tripSuggestionFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
|
||||
}
|
||||
|
||||
// Save values
|
||||
suspend fun setShow3D(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
@@ -207,4 +214,10 @@ class DataStoreManager(private val context: Context) {
|
||||
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 street : String = "",
|
||||
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 =
|
||||
"{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() {
|
||||
@@ -32,12 +32,11 @@ class TomTomRepository : NavigationRepository() {
|
||||
carOrientation: Float,
|
||||
searchFilter: SearchFilter
|
||||
): String {
|
||||
if (useAsset) {
|
||||
val resourceId: Int = context.resources
|
||||
.getIdentifier("tomtom_routing", "raw", context.packageName)
|
||||
val routeJson = context.resources.openRawResource(resourceId)
|
||||
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
|
||||
return routeJsonString
|
||||
if (useLocal) {
|
||||
return fetchUrl(
|
||||
"https://kouros-online.de/tomtom_routing.json",
|
||||
false
|
||||
)
|
||||
}
|
||||
var filter = ""
|
||||
if (searchFilter.avoidMotorway) {
|
||||
@@ -76,11 +75,11 @@ class TomTomRepository : NavigationRepository() {
|
||||
return ""
|
||||
}
|
||||
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
|
||||
return if (useAssetTraffic) {
|
||||
val resourceId: Int = context.resources
|
||||
.getIdentifier("tomtom_traffic", "raw", context.packageName)
|
||||
val trafficJson = context.resources.openRawResource(resourceId)
|
||||
trafficJson.bufferedReader().use { it.readText() }
|
||||
return if (useLocalTraffic) {
|
||||
fetchUrl(
|
||||
"https://kouros-online.de/tomtom_traffic.json",
|
||||
false
|
||||
)
|
||||
} else {
|
||||
val trafficResult = fetchUrl(
|
||||
"$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present",
|
||||
|
||||
@@ -62,7 +62,9 @@ class TomTomRoute {
|
||||
lastPointIndex = instruction.pointIndex
|
||||
val intersections = mutableListOf<Intersection>()
|
||||
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>()
|
||||
var startIndex = 0
|
||||
var lastLane: Lane? = null
|
||||
@@ -87,25 +89,25 @@ class TomTomRoute {
|
||||
lastLane = lane
|
||||
}
|
||||
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))
|
||||
val routeGeoJson = createLineStringCollection(waypoints)
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.kouros.navigation.data.StepData
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class IconMapper() {
|
||||
class IconMapper {
|
||||
|
||||
fun maneuverIcon(routeManeuverType: Int): Int {
|
||||
var currentTurnIcon = R.drawable.ic_turn_name_change
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Context
|
||||
import android.location.Location
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -60,11 +62,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
/** LiveData containing list of favorite saved places */
|
||||
val favorites: MutableLiveData<List<Place>> by lazy {
|
||||
MutableLiveData()
|
||||
}
|
||||
|
||||
/** LiveData containing search results from Nominatim geocoding */
|
||||
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
|
||||
MutableLiveData()
|
||||
@@ -147,7 +144,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
var id: Long = 0
|
||||
if (rp.isNotEmpty()) {
|
||||
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)
|
||||
if (place.latitude != 0.0) {
|
||||
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.
|
||||
* Posts the route JSON to route LiveData.
|
||||
@@ -216,7 +177,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
fun loadRoute(
|
||||
context: Context,
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
destination: Location,
|
||||
carOrientation: Float
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
@@ -225,7 +186,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
repository.getRoute(
|
||||
context,
|
||||
currentLocation,
|
||||
location,
|
||||
destination,
|
||||
carOrientation,
|
||||
getSearchFilter(context)
|
||||
)
|
||||
@@ -296,7 +257,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
currentLocation: Location,
|
||||
location: Location,
|
||||
carOrientation: Float
|
||||
) {
|
||||
): String? {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
previewRoute.postValue(
|
||||
@@ -312,8 +273,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return previewRoute.value
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads device contacts with addresses and converts to Place objects.
|
||||
* 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.Step
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
open class RouteModel {
|
||||
|
||||
@@ -33,6 +33,9 @@ open class RouteModel {
|
||||
val currentStep: Step
|
||||
get() = navState.route.nextStep(0)
|
||||
|
||||
val steps: List<Step>
|
||||
get() = curLeg.steps
|
||||
|
||||
fun startNavigation(routeString: String) {
|
||||
navState = navState.copy(
|
||||
route = Route.Builder()
|
||||
@@ -145,7 +148,20 @@ open class RouteModel {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for navigating
|
||||
*/
|
||||
fun isNavigating(): Boolean {
|
||||
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
|
||||
)
|
||||
|
||||
val tripSuggestion = repository.tripSuggestionFlow.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(5_000),
|
||||
false
|
||||
)
|
||||
|
||||
fun onShow3DChanged(enabled: Boolean) {
|
||||
viewModelScope.launch { repository.setShow3D(enabled) }
|
||||
}
|
||||
@@ -138,4 +144,8 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
|
||||
fun onTraffic(enabled: Boolean) {
|
||||
viewModelScope.launch { repository.setTraffic(enabled) }
|
||||
}
|
||||
|
||||
fun onTripSuggestion(enabled: Boolean) {
|
||||
viewModelScope.launch { repository.setTripSuggestion(enabled) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ class SettingsRepository(
|
||||
val trafficFlow: Flow<Boolean> =
|
||||
dataStoreManager.trafficFlow
|
||||
|
||||
val tripSuggestionFlow: Flow<Boolean> =
|
||||
dataStoreManager.tripSuggestionFlow
|
||||
|
||||
suspend fun setShow3D(enabled: Boolean) {
|
||||
dataStoreManager.setShow3D(enabled)
|
||||
}
|
||||
@@ -95,4 +98,8 @@ class SettingsRepository(
|
||||
suspend fun setTraffic(enabled: Boolean) {
|
||||
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="general">Allgemein</string>
|
||||
<string name="traffic">Verkehr anzeigen</string>
|
||||
<string name="trip_suggestion">Fahrten-Vorschläge</string>
|
||||
</resources>
|
||||
|
||||
@@ -49,4 +49,5 @@
|
||||
<string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
|
||||
<string name="general">Γενικά</string>
|
||||
<string name="traffic">Εμφάνιση κίνησης</string>
|
||||
<string name="trip_suggestion">Προτάσεις διαδρομής</string>
|
||||
</resources>
|
||||
|
||||
@@ -49,4 +49,5 @@
|
||||
<string name="no_categories">Brak kategorii do wyświetlenia</string>
|
||||
<string name="general">Ogólne</string>
|
||||
<string name="traffic">Pokaż natężenie ruchu</string>
|
||||
<string name="trip_suggestion">Sugestie dotyczące podróży</string>
|
||||
</resources>
|
||||
|
||||
@@ -52,4 +52,5 @@
|
||||
<string name="no_categories">No categories to show</string>
|
||||
<string name="general">General</string>
|
||||
<string name="traffic">Show traffic</string>
|
||||
<string name="trip_suggestion">Trip suggestions</string>
|
||||
</resources>
|
||||
@@ -86,21 +86,6 @@ class RouteModelTest {
|
||||
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
|
||||
fun `stopNavigation updates route and sets navigating to false `() {
|
||||
routeModel.stopNavigation()
|
||||
|
||||
@@ -4,25 +4,25 @@ androidGpxParser = "2.3.1"
|
||||
androidSdkTurf = "6.0.1"
|
||||
datastore = "1.2.1"
|
||||
gradle = "9.1.0"
|
||||
koinAndroid = "4.1.1"
|
||||
koinAndroidxCompose = "4.1.1"
|
||||
koinComposeViewmodel = "4.1.1"
|
||||
koinCore = "4.1.1"
|
||||
kotlin = "2.3.10"
|
||||
koinAndroid = "4.2.0"
|
||||
koinAndroidxCompose = "4.2.0"
|
||||
koinComposeViewmodel = "4.2.0"
|
||||
koinCore = "4.2.0"
|
||||
kotlin = "2.3.20"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxSerializationJson = "1.10.0"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
composeBom = "2026.02.01"
|
||||
composeBom = "2026.03.00"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
carApp = "1.7.0"
|
||||
androidx-car = "1.7.0"
|
||||
materialIconsExtended = "1.7.8"
|
||||
mockitoCore = "5.23.0"
|
||||
mockitoKotlin = "6.2.3"
|
||||
mockitoKotlin = "6.3.0"
|
||||
rules = "1.7.0"
|
||||
runner = "1.7.0"
|
||||
material3 = "1.4.0"
|
||||
@@ -44,6 +44,9 @@ foundationLayout = "1.10.5"
|
||||
datastorePreferences = "1.2.1"
|
||||
datastoreCore = "1.2.1"
|
||||
monitor = "1.8.0"
|
||||
robolectric = "4.16.1"
|
||||
truth = "1.4.5"
|
||||
testCore = "1.7.0"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
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-testing = { group = "androidx.car.app", name = "app-testing", version.ref = "carApp" }
|
||||
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" }
|
||||
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
|
||||
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-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }
|
||||
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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user