Automotive, CarSession

This commit is contained in:
Dimitris
2026-04-03 09:54:43 +02:00
parent 52f8dec2e6
commit 24173412e8
38 changed files with 437 additions and 236 deletions
+4 -3
View File
@@ -17,8 +17,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 90 versionCode = 91
versionName = "0.2.3.90" versionName = "0.2.3.91"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -101,9 +101,10 @@ dependencies {
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(project(":common:car")) implementation(project(":common:car"))
implementation(project(":common:data")) implementation(project(":common:data"))
implementation(libs.androidx.car.app)
implementation(libs.androidx.app.projected)
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
-1
View File
@@ -37,7 +37,6 @@ android {
dependencies { dependencies {
implementation(libs.androidx.app.automotive) implementation(libs.androidx.app.automotive)
implementation(libs.androidx.car.app)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(project(":common:car")) implementation(project(":common:car"))
@@ -4,10 +4,10 @@
android:viewportWidth="960" android:viewportWidth="960"
android:viewportHeight="960" android:viewportHeight="960"
android:tint="#1A7416"> android:tint="#1A7416">
<group android:scaleX="0.7888" <group android:scaleX="0.58"
android:scaleY="0.7888" android:scaleY="0.58"
android:translateX="101.376" android:translateX="201.6"
android:translateY="101.376"> android:translateY="201.6">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/> android:pathData="M319,680L480,607L641,680L656,665L480,240L304,665L319,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

-1
View File
@@ -42,7 +42,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
implementation(project(":common:data")) implementation(project(":common:data"))
implementation(libs.androidx.runtime.livedata) implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
@@ -2,11 +2,11 @@ package com.kouros.navigation.car
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.car.app.navigation.model.Maneuver
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.kouros.navigation.data.Constants.homeHohenwaldeck import com.kouros.navigation.data.Constants.homeHohenwaldeck
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.route.ManeuverType
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
@@ -32,6 +32,37 @@ class RouteModelTest {
val routeModel = RouteModel() val routeModel = RouteModel()
val location = Location(LocationManager.GPS_PROVIDER) val location = Location(LocationManager.GPS_PROVIDER)
val distance = listOf(
1025.5,
989.8,
963.5,
923.7,
915.8,
914.6,
871.0,
822.7,
769.7,
713.8,
644.8,
577.6,
501.7,
489.7,
452.5,
437.4,
398.0,
390.1,
341.3,
266.6,
219.5,
140.7,
77.4,
55.1,
40.0,
30.0,
19.0,
4.0
)
@Before @Before
fun setup() { fun setup() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext val appContext = InstrumentationRegistry.getInstrumentation().targetContext
@@ -49,8 +80,8 @@ class RouteModelTest {
@Test @Test
fun checkRoute() { fun checkRoute() {
assertEquals(true, routeModel.isNavigating()) assertEquals(true, routeModel.isNavigating())
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0) assertEquals(routeModel.curRoute.summary.distance, 11108.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0) assertEquals(routeModel.curRoute.summary.duration, 1094.0, 10.0)
} }
@Test @Test
@@ -59,11 +90,11 @@ class RouteModelTest {
location.longitude = 11.579034 location.longitude = 11.579034
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Silcherstraße") assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 20.0, 5.0) assertEquals(stepData.leftStepDistance, 20.0, 5.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße") assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
} }
@@ -74,11 +105,11 @@ class RouteModelTest {
location.longitude = 11.576652 location.longitude = 11.576652
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(stepData.instruction, "Schmalkaldener Straße") assertEquals(stepData.instruction, "Schmalkaldener Straße")
assertEquals(stepData.leftStepDistance, 0.0, 1.0) assertEquals(stepData.leftStepDistance, 0.0, 1.0)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_RIGHT.value)
assertEquals(nextStepData.instruction, "Ingolstädter Straße") assertEquals(nextStepData.instruction, "Ingolstädter Straße")
} }
@@ -89,13 +120,13 @@ class RouteModelTest {
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
if (routeModel.navState.nextStep) { if (routeModel.navState.nextStep) {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_STRAIGHT.value)
assertEquals(stepData.instruction, "Ingolstädter Straße") assertEquals(stepData.instruction, "Ingolstädter Straße")
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße") assertEquals(nextStepData.instruction, "Schenkendorfstraße")
} else { } else {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
} }
assertEquals(stepData.leftStepDistance, 301.0, 1.0) assertEquals(stepData.leftStepDistance, 301.0, 1.0)
} }
@@ -107,14 +138,14 @@ class RouteModelTest {
location.bearing = 180.0F location.bearing = 180.0F
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_TURN_NORMAL_LEFT.value)
assertEquals(stepData.instruction, "Schenkendorfstraße") assertEquals(stepData.instruction, "Schenkendorfstraße")
assertEquals(stepData.leftStepDistance, 170.0, 10.0) assertEquals(stepData.leftStepDistance, 170.0, 10.0)
assertEquals(stepData.lane.size, 4) assertEquals(stepData.lane.size, 4)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.last().valid, false) assertEquals(stepData.lane.last().valid, false)
val nextStepData = routeModel.nextStep() val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT) assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_KEEP_LEFT.value)
assertEquals(nextStepData.instruction, "Schenkendorfstraße") assertEquals(nextStepData.instruction, "Schenkendorfstraße")
} }
@@ -124,7 +155,7 @@ class RouteModelTest {
location.longitude = homeHohenwaldeck.longitude location.longitude = homeHohenwaldeck.longitude
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.nextStep() val stepData = routeModel.nextStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT) assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
} }
@Test @Test
@@ -142,11 +173,11 @@ class RouteModelTest {
if (index in 61..61) { if (index in 61..61) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3) assertEquals(stepData.lane.size, 2)
assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT") assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
} }
if (index in 74..74) { if (index in 74..75) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
assertEquals(stepData.lane.size, 3) assertEquals(stepData.lane.size, 3)
@@ -180,9 +211,9 @@ class RouteModelTest {
val stepData = routeModel.currentStep() val stepData = routeModel.currentStep()
//println("${stepData.instruction} ${System.currentTimeMillis() - start}") //println("${stepData.instruction} ${System.currentTimeMillis() - start}")
if (stepData.lane.isNotEmpty()) { if (stepData.lane.isNotEmpty()) {
println(stepData.street) // println(stepData.street)
stepData.lane.forEach { stepData.lane.forEach {
println("${it.indications} ${it.valid}") // println("${it.indications} ${it.valid}")
} }
} }
// val nextData = routeModel.nextStep() // val nextData = routeModel.nextStep()
@@ -206,4 +237,19 @@ class RouteModelTest {
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 26.0, 1.0) assertEquals(step.leftStepDistance, 26.0, 1.0)
} }
@Test
fun leftStepDistance() {
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) {
if (index in 16..43) {
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
val stepData = routeModel.currentStep()
assertEquals(stepData.leftStepDistance, distance[index-16], 1.0)
}
}
}
}
} }
@@ -0,0 +1,10 @@
package com.kouros.navigation.car
import androidx.car.app.Session
import kotlinx.coroutines.flow.Flow
abstract class CarSession : Session() {
abstract fun invalidateNavigationScreen()
}
@@ -17,6 +17,7 @@ package com.kouros.navigation.car
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast import androidx.car.app.CarToast
@@ -27,6 +28,9 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.OnClickListener import androidx.car.app.model.OnClickListener
import androidx.car.app.navigation.model.Trip import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -34,46 +38,74 @@ import com.kouros.data.R
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys.CAR_LOCATION
import com.kouros.navigation.data.datastore.dataStore
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.model.NavigationViewModel
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** Session class for the Navigation sample app. */ /** Session class for the Navigation sample app. */
internal class ClusterSession : Session(), NavigationListener { internal class ClusterSession : CarSession(), NavigationListener {
var mNavigationScreen: NavigationScreen? = null lateinit var mNavigationScreen: NavigationScreen
var mNavigationCarSurface: SurfaceRenderer? = null lateinit var surfaceRenderer: SurfaceRenderer
var mSettingsAction: Action? = null
var routeModel = RouteCarModel() var routeModel = RouteCarModel()
lateinit var viewModelStoreOwner: ViewModelStoreOwner lateinit var viewModelStoreOwner: ViewModelStoreOwner
lateinit var navigationViewModel: NavigationViewModel
lateinit var deviceLocationManager: DeviceLocationManager
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession paused")
super.onPause(owner)
}
override fun onResume(owner: LifecycleOwner) {
Log.d(Constants.TAG, "NavigationSession resumed")
super.onResume(owner)
}
override fun onDestroy(owner: LifecycleOwner) {
if (::deviceLocationManager.isInitialized) {
deviceLocationManager.stopLocationUpdates()
}
carContext
.stopService(
Intent(
carContext,
NavigationNotificationService::class.java
)
)
}
}
init {
lifecycle.addObserver(lifecycleObserver)
}
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
Log.i(TAG, "In onCreateScreen()") Log.i(TAG, "In onCreateScreen()")
setupViewModelStore() setupViewModelStore()
mSettingsAction =
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext, R.drawable.alt_route_48px
)
)
.build()
)
.setOnClickListener(
OnClickListener {})
.build()
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
navigationViewModel = NavigationViewModel(TomTomRepository())
// mNavigationScreen = mNavigationScreen =
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface); NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
val action = intent.action val action = intent.action
if (CarContext.ACTION_NAVIGATE == action) { if (CarContext.ACTION_NAVIGATE == action) {
Log.i(TAG, "In onCreateScreen() Navigation intent")
CarToast.makeText( CarToast.makeText(
carContext, carContext,
"Navigation intent: " + intent.dataString, "Navigation intent: " + intent.dataString,
@@ -81,8 +113,29 @@ internal class ClusterSession : Session(), NavigationListener {
) )
.show() .show()
} }
initializeManagers()
return mNavigationScreen
}
return mNavigationScreen!! /**
* Initializes managers for rendering, sensors, and location.
*/
private fun initializeManagers() {
deviceLocationManager = DeviceLocationManager(
carContext = carContext,
lifecycleOwner = this,
shouldUseCarLocationFlow = flowOf(false),
onLocationUpdate = ::updateLocation,
onInitialLocation = { location ->
})
deviceLocationManager.startLocationUpdates()
}
fun updateLocation(location: Location) {
Log.d(TAG, "updateLocation $location")
surfaceRenderer.updateLocation(location, "")
} }
override fun onCarConfigurationChanged(newConfiguration: Configuration) { override fun onCarConfigurationChanged(newConfiguration: Configuration) {
@@ -125,4 +178,8 @@ internal class ClusterSession : Session(), NavigationListener {
} }
} }
} }
override fun invalidateNavigationScreen() {
mNavigationScreen.invalidate()
}
} }
@@ -1,5 +1,6 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.Manifest
import android.Manifest.permission import android.Manifest.permission
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -53,6 +54,8 @@ import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.GeoUtils.snapLocation import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
@@ -65,6 +68,7 @@ import kotlinx.coroutines.launch
import java.time.Duration import java.time.Duration
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.collections.listOf
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@@ -74,7 +78,7 @@ import kotlin.math.absoluteValue
* car hardware sensors, routing engine selection, and screen navigation. * car hardware sensors, routing engine selection, and screen navigation.
* Implements NavigationScreen.Listener for handling navigation events. * Implements NavigationScreen.Listener for handling navigation events.
*/ */
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback { class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
// Flag to enable/disable contact access feature // Flag to enable/disable contact access feature
val useContacts = false val useContacts = false
@@ -175,24 +179,12 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
lateinit var observerManager: NavigationObserverManager lateinit var observerManager: NavigationObserverManager
val repository = getSettingsRepository(carContext) lateinit var repository: SettingsRepository
val settingsViewModel = getSettingsViewModel(carContext) lateinit var settingsViewModel: SettingsViewModel
init { init {
lifecycle.addObserver(lifecycleObserver) lifecycle.addObserver(lifecycleObserver)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
} }
/** /**
@@ -244,10 +236,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
/** /**
* Creates the initial screen for the session. * Creates the initial screen for the session.
* Sets up ViewModel store, initializes components, checks permissions, * Sets up ViewModel store, initializes settings, components, checks permissions,
* and returns appropriate starting screen. * and returns appropriate starting screen.
*/ */
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
initializeSettings()
setupViewModelStore() setupViewModelStore()
initializeViewModels() initializeViewModels()
initializeManagers() initializeManagers()
@@ -255,6 +248,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
return checkPermissionsAndGetScreen() return checkPermissionsAndGetScreen()
} }
/*
* Initializes the settings repository and ViewModel.
*/
private fun initializeSettings() {
repository = getSettingsRepository(carContext)
settingsViewModel = getSettingsViewModel(carContext)
repository.routingEngineFlow.asLiveData().observe(this, Observer {
onRoutingEngineStateUpdated(it)
routingEngine = it
})
repository.trafficFlow.asLiveData().observe(this, Observer {
showTraffic = it
})
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/** /**
* Sets up ViewModelStoreOwner and manages its lifecycle. * Sets up ViewModelStoreOwner and manages its lifecycle.
*/ */
@@ -309,7 +322,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
} }
} }
}) })
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
carSensorManager = CarSensorManager( carSensorManager = CarSensorManager(
carContext = carContext, carContext = carContext,
@@ -335,7 +348,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
textToSpeechManager = TextToSpeechManager(carContext) textToSpeechManager = TextToSpeechManager(carContext)
val repository = getSettingsRepository(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer { repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it guidanceAudio = it
}) })
@@ -381,6 +393,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
screenManager.push(navigationScreen) screenManager.push(navigationScreen)
return RequestPermissionScreen( return RequestPermissionScreen(
carContext, carContext,
listOf(
permission.ACCESS_COARSE_LOCATION,
permission.ACCESS_FINE_LOCATION,
),
permissionCheckCallback = { screenManager.pop() } permissionCheckCallback = { screenManager.pop() }
) )
} }
@@ -488,7 +504,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
* Updates the surface renderer with snapped location and street name. * Updates the surface renderer with snapped location and street name.
* Checks if maximal route deviation is exceeded and reroutes if needed. * Checks if maximal route deviation is exceeded and reroutes if needed.
*/ */
private fun snapLocation(location: Location, streetName: String) : Boolean { private fun snapLocation(location: Location, streetName: String): Boolean {
val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations()) val snappedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snappedLocation) val distance = location.distanceTo(snappedLocation)
when { when {
@@ -514,7 +530,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
*/ */
fun updateNavigationScreen() { fun updateNavigationScreen() {
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty() if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
&& routeModel.navState.destination.street.isEmpty()) { && routeModel.navState.destination.street.isEmpty()
) {
return return
} }
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode) val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
@@ -728,7 +745,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
coordinates.add(listOf(it.lon, it.lat)) coordinates.add(listOf(it.lon, it.lat))
} }
val speedData = GeoUtils.createPointCollection(coordinates, "radar") val speedData = GeoUtils.createPointCollection(coordinates, "radar")
surfaceRenderer.speedCamerasData.value = speedData surfaceRenderer.speedCameraData.value = speedData
} }
/** /**
@@ -755,7 +772,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
val preview = place.route val preview = place.route
navigationViewModel.previewRoute.value = "" navigationViewModel.previewRoute.value = ""
val location = if (place.stopOver && prevDestination.latitude != 0.0) { val location = if (place.stopOver && prevDestination.latitude != 0.0) {
listOf(location(place.longitude, place.latitude), location(prevDestination.longitude, prevDestination.latitude)) listOf(
location(place.longitude, place.latitude),
location(prevDestination.longitude, prevDestination.latitude)
)
} else { } else {
listOf(location(place.longitude, place.latitude)) listOf(location(place.longitude, place.latitude))
} }
@@ -853,6 +873,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
} }
} }
override fun invalidateNavigationScreen() {
navigationScreen.invalidate()
}
companion object { companion object {
// URI host for deep linking // URI host for deep linking
var uriHost: String = "navigation" var uriHost: String = "navigation"
@@ -8,6 +8,7 @@ import android.location.Location
import android.util.Log import android.util.Log
import androidx.car.app.AppManager import androidx.car.app.AppManager
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Session
import androidx.car.app.SurfaceCallback import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer import androidx.car.app.SurfaceContainer
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -32,19 +33,15 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.DarkMode import com.kouros.navigation.data.DarkMode
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearing import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt import com.kouros.navigation.utils.calculateTilt
import com.kouros.navigation.utils.calculateZoom import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.duration import com.kouros.navigation.utils.duration
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
import com.kouros.navigation.utils.previewZoom import com.kouros.navigation.utils.previewZoom
import com.kouros.navigation.utils.settingsViewModel import com.kouros.navigation.utils.settingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
@@ -60,8 +57,8 @@ import java.time.LocalDateTime
class SurfaceRenderer( class SurfaceRenderer(
private var carContext: CarContext, private var carContext: CarContext,
lifecycle: Lifecycle, lifecycle: Lifecycle,
//private var routeModel: RouteCarModel, private var viewModelStoreOwner: ViewModelStoreOwner,
private var viewModelStoreOwner: ViewModelStoreOwner private var navigationSession: CarSession
) : DefaultLifecycleObserver { ) : DefaultLifecycleObserver {
// Last known location for bearing calculations // Last known location for bearing calculations
@@ -99,7 +96,7 @@ class SurfaceRenderer(
val trafficData = MutableLiveData(emptyMap<String, String>()) val trafficData = MutableLiveData(emptyMap<String, String>())
// Speed camera locations as GeoJSON // Speed camera locations as GeoJSON
val speedCamerasData = MutableLiveData("") val speedCameraData = MutableLiveData("")
// Current speed in km/h // Current speed in km/h
val speed = MutableLiveData(0F) val speed = MutableLiveData(0F)
@@ -227,10 +224,20 @@ class SurfaceRenderer(
} }
/** /**
* Called when user scrolls the map (not currently implemented). * Called when user scrolls the map .
*/ */
override fun onScroll(distanceX: Float, distanceY: Float) { override fun onScroll(distanceX: Float, distanceY: Float) {
synchronized(this@SurfaceRenderer) { synchronized(this@SurfaceRenderer) {
viewStyle = ViewStyle.PAN_VIEW
if (distanceX != 0.0F) {
lastLocation.longitude += (distanceX / 1000) / cameraPosition.value!!.zoom
}
if (distanceY != 0.0F) {
lastLocation.latitude += (distanceY / 1000) / cameraPosition.value!!.zoom
}
val pos = Position(lastLocation.longitude, lastLocation.latitude)
updateCameraPosition( target = pos)
navigationSession.invalidateNavigationScreen()
} }
} }
@@ -238,7 +245,9 @@ class SurfaceRenderer(
* Called when user scales (zooms) the map (not currently implemented). * Called when user scales (zooms) the map (not currently implemented).
*/ */
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) { override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
synchronized(this@SurfaceRenderer) {
Log.d(TAG, "onScale")
}
} }
} }
@@ -260,7 +269,7 @@ class SurfaceRenderer(
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val traffic: Map<String, String>? by trafficData.observeAsState() val traffic: Map<String, String>? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState() val speedCamera: String? by speedCameraData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle) val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode) val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
@@ -273,7 +282,7 @@ class SurfaceRenderer(
route, route,
traffic, traffic,
viewStyle, viewStyle,
speedCameras, speedCamera,
showBuildings showBuildings
) )
ShowPosition(cameraState, position, paddingValues, dark) ShowPosition(cameraState, position, paddingValues, dark)
@@ -342,9 +351,9 @@ class SurfaceRenderer(
viewStyle = ViewStyle.PAN_VIEW viewStyle = ViewStyle.PAN_VIEW
} }
val newZoom = if (zoomSign < 0) { val newZoom = if (zoomSign < 0) {
cameraPosition.value!!.zoom - 1.0 cameraPosition.value!!.zoom - 0.2
} else { } else {
cameraPosition.value!!.zoom + 1.0 cameraPosition.value!!.zoom + 0.2
} }
if (viewStyle == ViewStyle.VIEW) { if (viewStyle == ViewStyle.VIEW) {
tilt = calculateTilt(newZoom, tilt) tilt = calculateTilt(newZoom, tilt)
@@ -416,7 +425,8 @@ class SurfaceRenderer(
* Updates camera position with new bearing, zoom, and target. * Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation. * Posts update to LiveData for UI observation.
*/ */
fun updateCameraPosition(bearing: Double = 0.0, zoom: Double, target: Position, tilt: Double) { fun updateCameraPosition(bearing: Double = 0.0, zoom: Double = cameraPosition.value!!.zoom ,
target: Position, tilt: Double = 0.0) {
synchronized(this) { synchronized(this) {
cameraPosition.postValue( cameraPosition.postValue(
cameraPosition.value!!.copy( cameraPosition.value!!.copy(
@@ -1,6 +1,7 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -52,6 +53,7 @@ import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.UserLocationState import org.maplibre.compose.location.UserLocationState
import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.map.OrnamentOptions import org.maplibre.compose.map.OrnamentOptions
@@ -100,7 +102,8 @@ fun MapLibre(
OrnamentOptions(isScaleBarEnabled = false) OrnamentOptions(isScaleBarEnabled = false)
), ),
cameraState = cameraState, cameraState = cameraState,
baseStyle = baseStyle baseStyle = baseStyle,
) { ) {
getBaseSource(id = "openmaptiles")?.let { tiles -> getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!showBuildings) { if (!showBuildings) {
@@ -54,7 +54,7 @@ class Simulation {
longitude = point[0] longitude = point[0]
bearing = curBearing bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h speed = 5.0f
time = System.currentTimeMillis() time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
} }
@@ -18,4 +18,5 @@ interface NavigationListener {
fun navigateToPlace(place: Place) fun navigateToPlace(place: Place)
fun recalcRoute(destination: Place) fun recalcRoute(destination: Place)
} }
@@ -4,6 +4,7 @@ import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
@@ -20,6 +21,7 @@ import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.PanModeListener
import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.Step
import androidx.car.app.navigation.model.TravelEstimate import androidx.car.app.navigation.model.TravelEstimate
@@ -39,7 +41,6 @@ import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -81,6 +82,8 @@ open class NavigationScreen(
private lateinit var steps: MutableList<Step> private lateinit var steps: MutableList<Step>
private var junctionImage: CarIcon? = null private var junctionImage: CarIcon? = null
private var backGroundColor = CarColor.BLUE private var backGroundColor = CarColor.BLUE
private var showAlternativeRoute = false
val observerRecentPlaces = Observer<List<Place>> { newPlaces -> val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
recentPlaces.addAll(newPlaces) recentPlaces.addAll(newPlaces)
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
@@ -99,6 +102,9 @@ open class NavigationScreen(
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
tripSuggestion = it tripSuggestion = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
arrivalTimer?.cancel() arrivalTimer?.cancel()
@@ -145,17 +151,17 @@ open class NavigationScreen(
.setDestinationTravelEstimate(destinationTravelEstimate) .setDestinationTravelEstimate(destinationTravelEstimate)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( Action.Builder()
carContext = carContext, R.drawable.ic_pan_24, .setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
0, .setFlags(0)
onClickAction = { .setOnClickListener {
surfaceRenderer.setStandardView() surfaceRenderer.setStandardView()
invalidate() invalidate()
} }
) .build()
}) })
) )
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
@@ -166,24 +172,25 @@ open class NavigationScreen(
* Creates and returns a template for the default view state. * Creates and returns a template for the default view state.
*/ */
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() val mapActionStrip = mapActionStrip(carContext,
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( createAction(
carContext = carContext, R.drawable.ic_pan_24, carContext = carContext, R.drawable.ic_recenter_24,
0, 0,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
invalidate() invalidate()
}) })
}) })
) return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStrip)
.setPanModeListener { isInPanMode: Boolean ->
Log.d(TAG, "PanMode $isInPanMode")
}
.build() .build()
} }
/** /**
@@ -231,11 +238,11 @@ open class NavigationScreen(
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip( .setMapActionStrip(
mapActionStrip( mapActionStrip(carContext,
surfaceRenderer.viewStyle, surfaceRenderer.viewStyle,
{ zoomPlus() }, { zoomMinus() }, { { zoomPlus() }, { zoomMinus() }, {
createAction( createAction(
carContext = carContext, R.drawable.ic_pan_24, carContext = carContext, R.drawable.ic_recenter_24,
0, 0,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -292,7 +299,7 @@ open class NavigationScreen(
val builder = MapWithContentTemplate.Builder() val builder = MapWithContentTemplate.Builder()
.setContentTemplate(contentTemplate) .setContentTemplate(contentTemplate)
.setActionStrip( .setActionStrip(
mapActionStrip( mapActionStrip(carContext,
ViewStyle.VIEW, ViewStyle.VIEW,
{ settingsAction() }, { settingsAction() },
{ {
@@ -304,7 +311,7 @@ open class NavigationScreen(
}, },
{ {
createAction( createAction(
carContext = carContext, R.drawable.ic_zoom_out_24, carContext = carContext, R.drawable.ic_recenter_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
surfaceRenderer.viewStyle = ViewStyle.VIEW surfaceRenderer.viewStyle = ViewStyle.VIEW
@@ -358,6 +365,7 @@ open class NavigationScreen(
surfaceRenderer, surfaceRenderer,
place, place,
navigationViewModel, navigationViewModel,
showAlternativeRoute
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
@@ -46,10 +46,15 @@ class PlaceListScreen(
private var routingEngine = 0 private var routingEngine = 0
private var showAlternativeRoute = false
init { init {
repository.routingEngineFlow.asLiveData().observe(this, Observer { repository.routingEngineFlow.asLiveData().observe(this, Observer {
routingEngine = it routingEngine = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycle.addObserver(object : DefaultLifecycleObserver { lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
navigationViewModel.recentPlaces.value = emptyList() navigationViewModel.recentPlaces.value = emptyList()
@@ -137,10 +142,11 @@ class PlaceListScreen(
.pushForResult( .pushForResult(
RoutePreviewScreen( RoutePreviewScreen(
carContext, carContext,
RoutePreviewType.MULTI_ROUTE, if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
surfaceRenderer, surfaceRenderer,
place, place,
navigationViewModel, navigationViewModel,
showAlternativeRoute
) )
) { obj: Any? -> ) { obj: Any? ->
if (obj != null) { if (obj != null) {
@@ -16,9 +16,8 @@ import androidx.car.app.model.Template
/** Screen for asking the user to grant location permission. */ /** Screen for asking the user to grant location permission. */
class RequestPermissionScreen( class RequestPermissionScreen(
carContext: CarContext, carContext: CarContext,
var permissionCheckCallback: PermissionCheckCallback, val permissions: List<String?> = ArrayList(),
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback, var permissionCheckCallback: PermissionCheckCallback
val permissions: MutableList<String?> = ArrayList()
) : Screen(carContext) { ) : Screen(carContext) {
/** Callback called when the permission is granted. */ /** Callback called when the permission is granted. */
@@ -29,7 +28,7 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
var message = "" var message = "This app needs access to location"
if (permissions.contains(permission.ACCESS_FINE_LOCATION)) if (permissions.contains(permission.ACCESS_FINE_LOCATION))
message = "This app needs access to location and to car speed" message = "This app needs access to location and to car speed"
if (permissions.contains("android.car.permission.CAR_SPEED")) if (permissions.contains("android.car.permission.CAR_SPEED"))
@@ -83,7 +82,7 @@ fun checkPermission(carContext: CarContext, permission: String) : Boolean {
screenManager.pop() screenManager.pop()
return@RequestPermissionScreen return@RequestPermissionScreen
}, },
permissions permissions = permissions
) )
) )
} else { } else {
@@ -55,6 +55,7 @@ class RoutePreviewScreen(
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
private var destination: Place, private var destination: Place,
private val navigationViewModel: NavigationViewModel, private val navigationViewModel: NavigationViewModel,
private var showAlternativeRoute: Boolean
) : ) :
Screen(carContext) { Screen(carContext) {
private var isFavorite = false private var isFavorite = false
@@ -71,7 +72,7 @@ class RoutePreviewScreen(
var loading = true var loading = true
var showAlternativeRoute = true var flag = FLAG_DEFAULT
private val backPressedCallback = object : OnBackPressedCallback(false) { private val backPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -84,7 +85,7 @@ class RoutePreviewScreen(
routeModel.startNavigation(route) routeModel.startNavigation(route)
surfaceRenderer.setPreviewRouteData(routeModel) surfaceRenderer.setPreviewRouteData(routeModel)
loading = false loading = false
if (routeModel.route.routes.size == 1) { if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
routeType = RoutePreviewType.SINGLE_ROUTE routeType = RoutePreviewType.SINGLE_ROUTE
showAlternativeRoute = false showAlternativeRoute = false
} }
@@ -116,6 +117,10 @@ class RoutePreviewScreen(
routingEngine = it routingEngine = it
}) })
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
showAlternativeRoute = it
})
lifecycleScope.launch { lifecycleScope.launch {
navigationViewModel.loadPreviewRoute( navigationViewModel.loadPreviewRoute(
carContext, carContext,
@@ -178,7 +183,7 @@ class RoutePreviewScreen(
listContent.build() listContent.build()
} else { } else {
val navigateAction = val navigateAction =
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{ createAction(carContext, R.drawable.navigation_48px, flag,{
onNavigate(routeModel.navState.currentRouteIndex) onNavigate(routeModel.navState.currentRouteIndex)
}) })
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, { val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
@@ -202,7 +207,7 @@ class RoutePreviewScreen(
.setContentTemplate(content) .setContentTemplate(content)
.setMapController( .setMapController(
MapController.Builder().setMapActionStrip( MapController.Builder().setMapActionStrip(
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, { mapActionStrip(carContext, ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
zoomMinus() zoomMinus()
} )).build() } )).build()
@@ -225,6 +230,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_in_24, carContext, R.drawable.ic_zoom_in_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(1) surfaceRenderer.handleScale(1)
invalidate() invalidate()
} }
@@ -239,6 +245,7 @@ class RoutePreviewScreen(
carContext, R.drawable.ic_zoom_out_24, carContext, R.drawable.ic_zoom_out_24,
FLAG_IS_PERSISTENT, FLAG_IS_PERSISTENT,
onClickAction = { onClickAction = {
flag = FLAG_IS_PERSISTENT
surfaceRenderer.handleScale(-1) surfaceRenderer.handleScale(-1)
invalidate() invalidate()
} }
@@ -297,7 +304,7 @@ class RoutePreviewScreen(
} }
private fun createRow(route: Routes, index: Int): Row { private fun createRow(route: Routes, index: Int): Row {
val navigateAction = createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT ) { val navigateAction = createAction(carContext, R.drawable.navigation_48px, flag ) {
this.onNavigate(index) this.onNavigate(index)
} }
val routeText = createRouteText(route) val routeText = createRouteText(route)
@@ -12,6 +12,7 @@ import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.CHARGING_STATION import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY import com.kouros.navigation.data.Constants.PHARMACY
@@ -82,20 +83,28 @@ fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): Acti
* Creates an ActionStrip builder for map-related actions like zoom and pan. * Creates an ActionStrip builder for map-related actions like zoom and pan.
*/ */
fun mapActionStrip( fun mapActionStrip(
carContext: CarContext,
viewStyle: ViewStyle, viewStyle: ViewStyle,
zoomPlus: () -> Action, zoomPlus: () -> Action,
zoomMinus: () -> Action, zoomMinus: () -> Action,
panAction: () -> Action recenterAction: () -> Action
): ActionStrip { ): ActionStrip {
val actionStripBuilder = ActionStrip.Builder() val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus()) .addAction(zoomPlus())
.addAction(zoomMinus()) .addAction(zoomMinus())
actionStripBuilder.addAction(
Action.Builder(Action.PAN)
.setIcon(createCarIcon(carContext, R.drawable.ic_pan_24))
.setFlags(0)
.build()
)
if (viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.PAN_VIEW) {
actionStripBuilder actionStripBuilder
.addAction( .addAction(
panAction() recenterAction()
) )
} }
return actionStripBuilder.build() return actionStripBuilder.build()
} }
@@ -106,7 +115,7 @@ fun createAction(
carContext: CarContext, carContext: CarContext,
@DrawableRes iconRes: Int, @DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT, flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit onClickAction: () -> Unit,
): Action { ): Action {
return Action.Builder() return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes)) .setIcon(createCarIcon(carContext, iconRes))
@@ -16,17 +16,16 @@ import kotlinx.coroutines.flow.map
private const val DATASTORE_NAME = "navigation_settings" private const val DATASTORE_NAME = "navigation_settings"
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
/** /**
* Central manager for app settings using DataStore * Central manager for app settings using DataStore
*/ */
class DataStoreManager(private val context: Context) { class DataStoreManager(private val context: Context) {
companion object {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
}
// Keys // Keys
object PreferencesKeys { companion object PreferencesKeys {
val SHOW_3D = booleanPreferencesKey("Show3D") val SHOW_3D = booleanPreferencesKey("Show3D")
@@ -66,186 +65,186 @@ class DataStoreManager(private val context: Context) {
val show3DFlow: Flow<Boolean> = val show3DFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.SHOW_3D] == true preferences[SHOW_3D] == true
} }
val darkModeFlow: Flow<Int> = val darkModeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DARK_MODE] preferences[DARK_MODE]
?: 0 ?: 0
} }
val avoidMotorwayFlow: Flow<Boolean> = val avoidMotorwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] == true preferences[AVOID_MOTORWAY] == true
} }
val avoidTollwayFlow: Flow<Boolean> = val avoidTollwayFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] == true preferences[AVOID_TOLLWAY] == true
} }
val avoidFerryFlow: Flow<Boolean> = val avoidFerryFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] == true preferences[AVOID_FERRY] == true
} }
val useCarLocationFlow: Flow<Boolean> = val useCarLocationFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] == true preferences[CAR_LOCATION] == true
} }
val routingEngineFlow: Flow<Int> = val routingEngineFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ROUTING_ENGINE] preferences[ROUTING_ENGINE]
?: 2 ?: 2
} }
val lastRouteFlow: Flow<String> = val lastRouteFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.LAST_ROUTE] preferences[LAST_ROUTE]
?: "" ?: ""
} }
val tomTomApiKeyFlow: Flow<String> = val tomTomApiKeyFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TOMTOM_APIKEY] preferences[TOMTOM_APIKEY]
?: "" ?: ""
} }
val recentPlacesFlow: Flow<String> = val recentPlacesFlow: Flow<String> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.RECENT_PLACES] preferences[RECENT_PLACES]
?: "" ?: ""
} }
val distanceModeFlow: Flow<Int> = val distanceModeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.DISTANCE_MODE] preferences[DISTANCE_MODE]
?: 0 ?: 0
} }
val guidanceAudioFlow: Flow<Int> = val guidanceAudioFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.GUIDANCE_AUDIO] preferences[GUIDANCE_AUDIO]
?: 0 ?: 0
} }
val trafficFlow: Flow<Boolean> = val trafficFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRAFFIC] == true preferences[TRAFFIC] == true
} }
val tripSuggestionFlow: Flow<Boolean> = val tripSuggestionFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] == true preferences[TRIP_SUGGESTION] == true
} }
val engineTypeFlow: Flow<Int> = val engineTypeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ENGINE_TYPE] preferences[ENGINE_TYPE]
?: EngineType.COMBUSTION.ordinal ?: EngineType.COMBUSTION.ordinal
} }
val alternativeRoutesFlow: Flow<Boolean> = val alternativeRoutesFlow: Flow<Boolean> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ALTERNATIVE_ROUTES] == true preferences[ALTERNATIVE_ROUTES] == true
} }
// Save values // Save values
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.SHOW_3D] = enabled preferences[SHOW_3D] = enabled
} }
} }
suspend fun setDarkMode(mode: Int) { suspend fun setDarkMode(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.DARK_MODE] = mode prefs[DARK_MODE] = mode
} }
} }
suspend fun setAvoidMotorway(enabled: Boolean) { suspend fun setAvoidMotorway(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_MOTORWAY] = enabled preferences[AVOID_MOTORWAY] = enabled
} }
} }
suspend fun setAvoidTollway(enabled: Boolean) { suspend fun setAvoidTollway(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_TOLLWAY] = enabled preferences[AVOID_TOLLWAY] = enabled
} }
} }
suspend fun setAvoidFerry(enabled: Boolean) { suspend fun setAvoidFerry(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] = enabled preferences[AVOID_FERRY] = enabled
} }
} }
suspend fun setCarLocation(enabled: Boolean) { suspend fun setCarLocation(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] = enabled preferences[CAR_LOCATION] = enabled
} }
} }
suspend fun setRoutingEngine(mode: Int) { suspend fun setRoutingEngine(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ROUTING_ENGINE] = mode prefs[ROUTING_ENGINE] = mode
} }
} }
suspend fun setLastRoute(route: String) { suspend fun setLastRoute(route: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.LAST_ROUTE] = route prefs[LAST_ROUTE] = route
} }
} }
suspend fun setTomtomApiKey(apiKey: String) { suspend fun setTomtomApiKey(apiKey: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.TOMTOM_APIKEY] = apiKey prefs[TOMTOM_APIKEY] = apiKey
} }
} }
suspend fun setRecentPlaces(apiKey: String) { suspend fun setRecentPlaces(apiKey: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.RECENT_PLACES] = apiKey prefs[RECENT_PLACES] = apiKey
} }
} }
suspend fun setDistanceMode(mode: Int) { suspend fun setDistanceMode(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.DISTANCE_MODE] = mode prefs[DISTANCE_MODE] = mode
} }
} }
suspend fun setGuidanceAudio(mode: Int) { suspend fun setGuidanceAudio(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.GUIDANCE_AUDIO] = mode prefs[GUIDANCE_AUDIO] = mode
} }
} }
suspend fun setTraffic(enabled: Boolean) { suspend fun setTraffic(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRAFFIC] = enabled preferences[TRAFFIC] = enabled
} }
} }
suspend fun setTripSuggestion(enabled: Boolean) { suspend fun setTripSuggestion(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled preferences[TRIP_SUGGESTION] = enabled
} }
} }
suspend fun setEngineType(mode: Int) { suspend fun setEngineType(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ENGINE_TYPE] = mode prefs[ENGINE_TYPE] = mode
} }
} }
suspend fun setAlternativeRoutes(enabled: Boolean) { suspend fun setAlternativeRoutes(enabled: Boolean) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.ALTERNATIVE_ROUTES] = enabled preferences[ALTERNATIVE_ROUTES] = enabled
} }
} }
@@ -78,7 +78,6 @@ class TomTomRoute {
) { ) {
val lanes = mutableListOf<Lane>() val lanes = mutableListOf<Lane>()
var startIndex = 0 var startIndex = 0
var lastLane: Lane? = null
section.lanes?.forEach { itLane -> section.lanes?.forEach { itLane ->
val lane = Lane( val lane = Lane(
location = location( location = location(
@@ -91,14 +90,8 @@ class TomTomRoute {
endIndex = section.endPointIndex endIndex = section.endPointIndex
) )
startIndex = section.startPointIndex startIndex = section.startPointIndex
if (lastLane == null
|| (!(lastLane.valid && lane.valid
&& lastLane.indications == lane.indications))
) {
lanes.add(lane) lanes.add(lane)
} }
lastLane = lane
}
intersections.add(Intersection(waypoints[startIndex], lanes)) intersections.add(Intersection(waypoints[startIndex], lanes))
} }
} }
@@ -76,48 +76,9 @@ open class RouteModel {
navState = navState.copy(lastLocation = navState.currentLocation) navState = navState.copy(lastLocation = navState.currentLocation)
} }
fun nextStep(): StepData { /*
val distanceToNextStep = routeCalculator.leftStepDistance() * Returns the current step
val nextStep = navState.route.nextStep(1) */
var streetName = nextStep.street
var maneuverType = currentStep.maneuver.type
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
streetName = nextStep.maneuver.street
maneuverType = nextStep.maneuver.type
}
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
street = "",
leftStepDistance = distanceToNextStep,
currentManeuverType = maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
exitNumber = nextStep.maneuver.exit,
message = nextStep.maneuver.message
)
}
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
currentStep.intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance =
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
if (distance < NEXT_STEP_THRESHOLD) {
lanes = it.lane
return@forEach
}
}
}
}
return lanes
}
fun currentStep(): StepData { fun currentStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance() val distanceToNextStep = routeCalculator.leftStepDistance()
// Determine the maneuver type and corresponding icon // Determine the maneuver type and corresponding icon
@@ -148,6 +109,54 @@ open class RouteModel {
) )
} }
/*
* Returns the next step
*/
fun nextStep(): StepData {
val distanceToNextStep = routeCalculator.leftStepDistance()
val nextStep = navState.route.nextStep(1)
var streetName = nextStep.street
var maneuverType = currentStep.maneuver.type
if (distanceToNextStep < NEXT_STEP_THRESHOLD) {
streetName = nextStep.maneuver.street
maneuverType = nextStep.maneuver.type
}
val maneuverIcon = navState.iconMapper.maneuverIcon(maneuverType)
// Construct and return the final StepData object
return StepData(
instruction = streetName,
street = "",
leftStepDistance = distanceToNextStep,
currentManeuverType = maneuverType,
icon = maneuverIcon,
arrivalTime = routeCalculator.arrivalTime(),
leftDistance = routeCalculator.travelLeftDistance(),
exitNumber = nextStep.maneuver.exit,
message = nextStep.maneuver.message
)
}
/*
* Returns the current lanes
*/
private fun currentLanes(): List<Lane> {
var lanes = emptyList<Lane>()
if (navState.route.legs().isNotEmpty()) {
currentStep.intersection.forEach {
if (it.lane.isNotEmpty()) {
val distance =
navState.lastLocation.distanceTo(location(it.location[0], it.location[1]))
if (distance < NEXT_STEP_THRESHOLD) {
lanes = it.lane
return@forEach
}
}
}
}
return lanes
}
/** /**
* Checks for navigating * Checks for navigating
*/ */
@@ -1,10 +1,7 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kouros.navigation.data.datastore.DataStoreManager.Companion.dataStore
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys
import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -29,6 +29,7 @@ import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
@@ -138,7 +139,7 @@ fun duration(
lastLocationUpdate: LocalDateTime lastLocationUpdate: LocalDateTime
): Duration { ): Duration {
if (preview) { if (preview) {
return 3.seconds return 10.milliseconds
} }
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) { val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds 2.seconds
@@ -0,0 +1,10 @@
<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">
<path
android:fillColor="@android:color/white"
android:pathData="M240,600L240,640Q240,657 228.5,668.5Q217,680 200,680L160,680Q143,680 131.5,668.5Q120,657 120,640L120,320L204,80Q210,62 225.5,51Q241,40 260,40L700,40Q719,40 734.5,51Q750,62 756,80L840,320L840,640Q840,657 828.5,668.5Q817,680 800,680L760,680Q743,680 731.5,668.5Q720,657 720,640L720,600L240,600ZM232,240L728,240L686,120L274,120L232,240ZM200,320L200,320L200,520L200,520L200,320ZM300,480Q325,480 342.5,462.5Q360,445 360,420Q360,395 342.5,377.5Q325,360 300,360Q275,360 257.5,377.5Q240,395 240,420Q240,445 257.5,462.5Q275,480 300,480ZM660,480Q685,480 702.5,462.5Q720,445 720,420Q720,395 702.5,377.5Q685,360 660,360Q635,360 617.5,377.5Q600,395 600,420Q600,445 617.5,462.5Q635,480 660,480ZM520,920L280,800L440,800L440,720L680,840L520,840L520,920ZM200,520L760,520L760,320L200,320L200,520Z"/>
</vector>
@@ -0,0 +1,25 @@
<!--
Copyright 2021 The Android Open Source Project
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10 10,-4.49 10,-10S17.51,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM15,12c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3 1.34,-3 3,-3 3,1.34 3,3z"/>
</vector>