Automotive, CarSession
This commit is contained in:
@@ -42,7 +42,6 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.maplibre.compose)
|
||||
implementation(libs.androidx.app.projected)
|
||||
implementation(project(":common:data"))
|
||||
implementation(libs.androidx.runtime.livedata)
|
||||
implementation(libs.androidx.compose.foundation)
|
||||
|
||||
@@ -2,11 +2,11 @@ package com.kouros.navigation.car
|
||||
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.route.ManeuverType
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
@@ -32,6 +32,37 @@ class RouteModelTest {
|
||||
val routeModel = RouteModel()
|
||||
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
|
||||
fun setup() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
@@ -49,8 +80,8 @@ class RouteModelTest {
|
||||
@Test
|
||||
fun checkRoute() {
|
||||
assertEquals(true, routeModel.isNavigating())
|
||||
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
|
||||
assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0)
|
||||
assertEquals(routeModel.curRoute.summary.distance, 11108.0, 10.0)
|
||||
assertEquals(routeModel.curRoute.summary.duration, 1094.0, 10.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,11 +90,11 @@ class RouteModelTest {
|
||||
location.longitude = 11.579034
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
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.leftStepDistance, 20.0, 5.0)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -74,11 +105,11 @@ class RouteModelTest {
|
||||
location.longitude = 11.576652
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
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.leftStepDistance, 0.0, 1.0)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -89,13 +120,13 @@ class RouteModelTest {
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
if (routeModel.navState.nextStep) {
|
||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_STRAIGHT)
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_STRAIGHT.value)
|
||||
assertEquals(stepData.instruction, "Ingolstädter Straße")
|
||||
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")
|
||||
} 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)
|
||||
}
|
||||
@@ -107,14 +138,14 @@ class RouteModelTest {
|
||||
location.bearing = 180.0F
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
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.leftStepDistance, 170.0, 10.0)
|
||||
assertEquals(stepData.lane.size, 4)
|
||||
assertEquals(stepData.lane.first().valid, true)
|
||||
assertEquals(stepData.lane.last().valid, false)
|
||||
val nextStepData = routeModel.nextStep()
|
||||
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_KEEP_LEFT)
|
||||
assertEquals(nextStepData.currentManeuverType, ManeuverType.TYPE_KEEP_LEFT.value)
|
||||
assertEquals(nextStepData.instruction, "Schenkendorfstraße")
|
||||
}
|
||||
|
||||
@@ -124,7 +155,7 @@ class RouteModelTest {
|
||||
location.longitude = homeHohenwaldeck.longitude
|
||||
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.nextStep()
|
||||
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_DESTINATION_LEFT)
|
||||
assertEquals(stepData.currentManeuverType, ManeuverType.TYPE_DESTINATION_LEFT.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,11 +173,11 @@ class RouteModelTest {
|
||||
if (index in 61..61) {
|
||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.lane.size, 3)
|
||||
assertEquals(stepData.lane.size, 2)
|
||||
assertEquals(stepData.lane.first().valid, true)
|
||||
assertEquals(stepData.lane.first().indications.first(), "STRAIGHT")
|
||||
}
|
||||
if (index in 74..74) {
|
||||
if (index in 74..75) {
|
||||
routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository()))
|
||||
val stepData = routeModel.currentStep()
|
||||
assertEquals(stepData.lane.size, 3)
|
||||
@@ -180,9 +211,9 @@ class RouteModelTest {
|
||||
val stepData = routeModel.currentStep()
|
||||
//println("${stepData.instruction} ${System.currentTimeMillis() - start}")
|
||||
if (stepData.lane.isNotEmpty()) {
|
||||
println(stepData.street)
|
||||
// println(stepData.street)
|
||||
stepData.lane.forEach {
|
||||
println("${it.indications} ${it.valid}")
|
||||
// println("${it.indications} ${it.valid}")
|
||||
}
|
||||
}
|
||||
// val nextData = routeModel.nextStep()
|
||||
@@ -206,4 +237,19 @@ class RouteModelTest {
|
||||
val step = routeModel.currentStep()
|
||||
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.res.Configuration
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
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.navigation.model.Trip
|
||||
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.ViewModelStoreOwner
|
||||
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.screen.NavigationListener
|
||||
import com.kouros.navigation.car.screen.NavigationScreen
|
||||
import com.kouros.navigation.data.Constants
|
||||
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.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Session class for the Navigation sample app. */
|
||||
internal class ClusterSession : Session(), NavigationListener {
|
||||
var mNavigationScreen: NavigationScreen? = null
|
||||
internal class ClusterSession : CarSession(), NavigationListener {
|
||||
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 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 {
|
||||
Log.i(TAG, "In onCreateScreen()")
|
||||
|
||||
setupViewModelStore()
|
||||
mSettingsAction =
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.alt_route_48px
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener(
|
||||
OnClickListener {})
|
||||
.build()
|
||||
|
||||
mNavigationCarSurface = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
||||
|
||||
// mNavigationScreen =
|
||||
// new NavigationScreen(getCarContext(), mSettingsAction, this, mNavigationCarSurface);
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||
navigationViewModel = NavigationViewModel(TomTomRepository())
|
||||
mNavigationScreen =
|
||||
NavigationScreen(carContext, surfaceRenderer, this, navigationViewModel)
|
||||
val action = intent.action
|
||||
if (CarContext.ACTION_NAVIGATE == action) {
|
||||
Log.i(TAG, "In onCreateScreen() Navigation intent")
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
"Navigation intent: " + intent.dataString,
|
||||
@@ -81,8 +113,29 @@ internal class ClusterSession : Session(), NavigationListener {
|
||||
)
|
||||
.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) {
|
||||
@@ -125,4 +178,8 @@ internal class ClusterSession : Session(), NavigationListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidateNavigationScreen() {
|
||||
mNavigationScreen.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import com.kouros.navigation.data.Constants.TAG
|
||||
|
||||
class NavigationCarAppService : CarAppService() {
|
||||
|
||||
val intentActionNavNotificationOpenApp =
|
||||
val intentActionNavNotificationOpenApp =
|
||||
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
|
||||
|
||||
val channelId: String = "NavigationSessionChannel"
|
||||
@@ -26,7 +26,7 @@ class NavigationCarAppService : CarAppService() {
|
||||
@SuppressLint("PrivateResource")
|
||||
override fun createHostValidator(): HostValidator {
|
||||
|
||||
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||
return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kouros.navigation.car
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission
|
||||
import android.content.Intent
|
||||
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.model.NavigationViewModel
|
||||
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.snapLocation
|
||||
import com.kouros.navigation.utils.NavigationUtils.getViewModel
|
||||
@@ -65,6 +68,7 @@ import kotlinx.coroutines.launch
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import kotlin.collections.listOf
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
@@ -74,7 +78,7 @@ import kotlin.math.absoluteValue
|
||||
* car hardware sensors, routing engine selection, and screen navigation.
|
||||
* Implements NavigationScreen.Listener for handling navigation events.
|
||||
*/
|
||||
class NavigationSession : Session(), NavigationListener, NavigationObserverCallback {
|
||||
class NavigationSession : CarSession(), NavigationListener, NavigationObserverCallback {
|
||||
|
||||
// Flag to enable/disable contact access feature
|
||||
val useContacts = false
|
||||
@@ -82,7 +86,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
// Model for managing route state and navigation logic for Android Auto
|
||||
lateinit var routeModel: RouteCarModel
|
||||
|
||||
var route = ""
|
||||
var route = ""
|
||||
|
||||
// Main navigation screen displayed to the user
|
||||
lateinit var navigationScreen: NavigationScreen
|
||||
@@ -175,24 +179,12 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
var lastTrafficDate: LocalDateTime = LocalDateTime.MIN
|
||||
lateinit var observerManager: NavigationObserverManager
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
lateinit var repository: SettingsRepository
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
lateinit var settingsViewModel: SettingsViewModel
|
||||
|
||||
init {
|
||||
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.
|
||||
* Sets up ViewModel store, initializes components, checks permissions,
|
||||
* Sets up ViewModel store, initializes settings, components, checks permissions,
|
||||
* and returns appropriate starting screen.
|
||||
*/
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
initializeSettings()
|
||||
setupViewModelStore()
|
||||
initializeViewModels()
|
||||
initializeManagers()
|
||||
@@ -255,6 +248,26 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
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.
|
||||
*/
|
||||
@@ -309,7 +322,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
}
|
||||
}
|
||||
})
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner)
|
||||
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, viewModelStoreOwner, this)
|
||||
|
||||
carSensorManager = CarSensorManager(
|
||||
carContext = carContext,
|
||||
@@ -335,7 +348,6 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
|
||||
|
||||
textToSpeechManager = TextToSpeechManager(carContext)
|
||||
val repository = getSettingsRepository(carContext)
|
||||
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
|
||||
guidanceAudio = it
|
||||
})
|
||||
@@ -381,6 +393,10 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
screenManager.push(navigationScreen)
|
||||
return RequestPermissionScreen(
|
||||
carContext,
|
||||
listOf(
|
||||
permission.ACCESS_COARSE_LOCATION,
|
||||
permission.ACCESS_FINE_LOCATION,
|
||||
),
|
||||
permissionCheckCallback = { screenManager.pop() }
|
||||
)
|
||||
}
|
||||
@@ -488,7 +504,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
* Updates the surface renderer with snapped location and street name.
|
||||
* 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 distance = location.distanceTo(snappedLocation)
|
||||
when {
|
||||
@@ -514,7 +530,8 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
*/
|
||||
fun updateNavigationScreen() {
|
||||
if (routeModel.isNavigating() && routeModel.navState.destination.name.isEmpty()
|
||||
&& routeModel.navState.destination.street.isEmpty()) {
|
||||
&& routeModel.navState.destination.street.isEmpty()
|
||||
) {
|
||||
return
|
||||
}
|
||||
val travelEstimateTrip = routeModel.travelEstimateTrip(carContext, distanceMode)
|
||||
@@ -728,7 +745,7 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
coordinates.add(listOf(it.lon, it.lat))
|
||||
}
|
||||
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
|
||||
navigationViewModel.previewRoute.value = ""
|
||||
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 {
|
||||
listOf(location(place.longitude, place.latitude))
|
||||
}
|
||||
@@ -853,7 +873,11 @@ class NavigationSession : Session(), NavigationListener, NavigationObserverCallb
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
override fun invalidateNavigationScreen() {
|
||||
navigationScreen.invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
// URI host for deep linking
|
||||
var uriHost: String = "navigation"
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.car.app.AppManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.SurfaceCallback
|
||||
import androidx.car.app.SurfaceContainer
|
||||
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.TILT
|
||||
import com.kouros.navigation.data.DarkMode
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.ViewStyle
|
||||
import com.kouros.navigation.model.BaseStyleModel
|
||||
import com.kouros.navigation.utils.bearing
|
||||
import com.kouros.navigation.utils.calculateTilt
|
||||
import com.kouros.navigation.utils.calculateZoom
|
||||
import com.kouros.navigation.utils.duration
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import com.kouros.navigation.utils.previewZoom
|
||||
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.CameraState
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
@@ -60,8 +57,8 @@ import java.time.LocalDateTime
|
||||
class SurfaceRenderer(
|
||||
private var carContext: CarContext,
|
||||
lifecycle: Lifecycle,
|
||||
//private var routeModel: RouteCarModel,
|
||||
private var viewModelStoreOwner: ViewModelStoreOwner
|
||||
private var viewModelStoreOwner: ViewModelStoreOwner,
|
||||
private var navigationSession: CarSession
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
// Last known location for bearing calculations
|
||||
@@ -99,7 +96,7 @@ class SurfaceRenderer(
|
||||
val trafficData = MutableLiveData(emptyMap<String, String>())
|
||||
|
||||
// Speed camera locations as GeoJSON
|
||||
val speedCamerasData = MutableLiveData("")
|
||||
val speedCameraData = MutableLiveData("")
|
||||
|
||||
// Current speed in km/h
|
||||
val speed = MutableLiveData(0F)
|
||||
@@ -182,7 +179,7 @@ class SurfaceRenderer(
|
||||
this.setViewTreeLifecycleOwner(lifecycleOwner)
|
||||
this.setViewTreeSavedStateRegistryOwner(lifecycleOwner)
|
||||
setContent {
|
||||
MapView()
|
||||
MapView()
|
||||
}
|
||||
}
|
||||
presentation = Presentation(carContext, virtualDisplay.display)
|
||||
@@ -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) {
|
||||
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).
|
||||
*/
|
||||
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 route: String? by routeData.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 cameraState = cameraState(paddingValues, position, tilt)
|
||||
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
|
||||
@@ -273,7 +282,7 @@ class SurfaceRenderer(
|
||||
route,
|
||||
traffic,
|
||||
viewStyle,
|
||||
speedCameras,
|
||||
speedCamera,
|
||||
showBuildings
|
||||
)
|
||||
ShowPosition(cameraState, position, paddingValues, dark)
|
||||
@@ -342,9 +351,9 @@ class SurfaceRenderer(
|
||||
viewStyle = ViewStyle.PAN_VIEW
|
||||
}
|
||||
val newZoom = if (zoomSign < 0) {
|
||||
cameraPosition.value!!.zoom - 1.0
|
||||
cameraPosition.value!!.zoom - 0.2
|
||||
} else {
|
||||
cameraPosition.value!!.zoom + 1.0
|
||||
cameraPosition.value!!.zoom + 0.2
|
||||
}
|
||||
if (viewStyle == ViewStyle.VIEW) {
|
||||
tilt = calculateTilt(newZoom, tilt)
|
||||
@@ -416,7 +425,8 @@ class SurfaceRenderer(
|
||||
* Updates camera position with new bearing, zoom, and target.
|
||||
* 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) {
|
||||
cameraPosition.postValue(
|
||||
cameraPosition.value!!.copy(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kouros.navigation.car.map
|
||||
|
||||
import android.location.Location
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.LocationPuckSizes
|
||||
import org.maplibre.compose.location.UserLocationState
|
||||
import org.maplibre.compose.map.GestureOptions
|
||||
import org.maplibre.compose.map.MapOptions
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.map.OrnamentOptions
|
||||
@@ -100,7 +102,8 @@ fun MapLibre(
|
||||
OrnamentOptions(isScaleBarEnabled = false)
|
||||
),
|
||||
cameraState = cameraState,
|
||||
baseStyle = baseStyle
|
||||
baseStyle = baseStyle,
|
||||
|
||||
) {
|
||||
getBaseSource(id = "openmaptiles")?.let { tiles ->
|
||||
if (!showBuildings) {
|
||||
|
||||
@@ -54,7 +54,7 @@ class Simulation {
|
||||
longitude = point[0]
|
||||
bearing = curBearing
|
||||
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
|
||||
speed = 13.0f // ~50 km/h
|
||||
speed = 5.0f
|
||||
time = System.currentTimeMillis()
|
||||
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ interface NavigationListener {
|
||||
fun navigateToPlace(place: Place)
|
||||
|
||||
fun recalcRoute(destination: Place)
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
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.MessageInfo
|
||||
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.Step
|
||||
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.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.getSettingsViewModel
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -81,6 +82,8 @@ open class NavigationScreen(
|
||||
private lateinit var steps: MutableList<Step>
|
||||
private var junctionImage: CarIcon? = null
|
||||
private var backGroundColor = CarColor.BLUE
|
||||
|
||||
private var showAlternativeRoute = false
|
||||
val observerRecentPlaces = Observer<List<Place>> { newPlaces ->
|
||||
recentPlaces.addAll(newPlaces)
|
||||
if (newPlaces.isNotEmpty() && !tripSuggestionCalled) {
|
||||
@@ -99,6 +102,9 @@ open class NavigationScreen(
|
||||
navigationViewModel.recentPlaces.observe(this, observerRecentPlaces)
|
||||
tripSuggestion = it
|
||||
})
|
||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||
showAlternativeRoute = it
|
||||
})
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
arrivalTimer?.cancel()
|
||||
@@ -145,17 +151,17 @@ open class NavigationScreen(
|
||||
.setDestinationTravelEstimate(destinationTravelEstimate)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(
|
||||
mapActionStrip(
|
||||
mapActionStrip(carContext,
|
||||
surfaceRenderer.viewStyle,
|
||||
{ zoomPlus() }, { zoomMinus() }, {
|
||||
createAction(
|
||||
carContext = carContext, R.drawable.ic_pan_24,
|
||||
0,
|
||||
onClickAction = {
|
||||
Action.Builder()
|
||||
.setIcon(createCarIcon(carContext, R.drawable.ic_recenter_24))
|
||||
.setFlags(0)
|
||||
.setOnClickListener {
|
||||
surfaceRenderer.setStandardView()
|
||||
invalidate()
|
||||
}
|
||||
)
|
||||
.build()
|
||||
})
|
||||
)
|
||||
.setBackgroundColor(backGroundColor)
|
||||
@@ -166,24 +172,25 @@ open class NavigationScreen(
|
||||
* Creates and returns a template for the default view state.
|
||||
*/
|
||||
private fun navigationView(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
val mapActionStrip = mapActionStrip(carContext,
|
||||
surfaceRenderer.viewStyle,
|
||||
{ zoomPlus() }, { zoomMinus() }, {
|
||||
createAction(
|
||||
carContext = carContext, R.drawable.ic_recenter_24,
|
||||
0,
|
||||
onClickAction = {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
invalidate()
|
||||
})
|
||||
})
|
||||
return NavigationTemplate.Builder()
|
||||
.setBackgroundColor(backGroundColor)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(
|
||||
mapActionStrip(
|
||||
surfaceRenderer.viewStyle,
|
||||
{ zoomPlus() }, { zoomMinus() }, {
|
||||
createAction(
|
||||
carContext = carContext, R.drawable.ic_pan_24,
|
||||
0,
|
||||
onClickAction = {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
invalidate()
|
||||
})
|
||||
})
|
||||
)
|
||||
.setMapActionStrip(mapActionStrip)
|
||||
.setPanModeListener { isInPanMode: Boolean ->
|
||||
Log.d(TAG, "PanMode $isInPanMode")
|
||||
}
|
||||
.build()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,11 +238,11 @@ open class NavigationScreen(
|
||||
.setBackgroundColor(backGroundColor)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(
|
||||
mapActionStrip(
|
||||
mapActionStrip(carContext,
|
||||
surfaceRenderer.viewStyle,
|
||||
{ zoomPlus() }, { zoomMinus() }, {
|
||||
createAction(
|
||||
carContext = carContext, R.drawable.ic_pan_24,
|
||||
carContext = carContext, R.drawable.ic_recenter_24,
|
||||
0,
|
||||
onClickAction = {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
@@ -292,7 +299,7 @@ open class NavigationScreen(
|
||||
val builder = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(contentTemplate)
|
||||
.setActionStrip(
|
||||
mapActionStrip(
|
||||
mapActionStrip(carContext,
|
||||
ViewStyle.VIEW,
|
||||
{ settingsAction() },
|
||||
{
|
||||
@@ -304,7 +311,7 @@ open class NavigationScreen(
|
||||
},
|
||||
{
|
||||
createAction(
|
||||
carContext = carContext, R.drawable.ic_zoom_out_24,
|
||||
carContext = carContext, R.drawable.ic_recenter_24,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
surfaceRenderer.viewStyle = ViewStyle.VIEW
|
||||
@@ -358,6 +365,7 @@ open class NavigationScreen(
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
showAlternativeRoute
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
|
||||
@@ -46,10 +46,15 @@ class PlaceListScreen(
|
||||
|
||||
private var routingEngine = 0
|
||||
|
||||
private var showAlternativeRoute = false
|
||||
|
||||
init {
|
||||
repository.routingEngineFlow.asLiveData().observe(this, Observer {
|
||||
routingEngine = it
|
||||
})
|
||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||
showAlternativeRoute = it
|
||||
})
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
navigationViewModel.recentPlaces.value = emptyList()
|
||||
@@ -137,10 +142,11 @@ class PlaceListScreen(
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
RoutePreviewType.MULTI_ROUTE,
|
||||
if (showAlternativeRoute) RoutePreviewType.MULTI_ROUTE else RoutePreviewType.SINGLE_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
showAlternativeRoute
|
||||
)
|
||||
) { obj: Any? ->
|
||||
if (obj != null) {
|
||||
|
||||
+4
-5
@@ -16,9 +16,8 @@ import androidx.car.app.model.Template
|
||||
/** Screen for asking the user to grant location permission. */
|
||||
class RequestPermissionScreen(
|
||||
carContext: CarContext,
|
||||
var permissionCheckCallback: PermissionCheckCallback,
|
||||
//var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
|
||||
val permissions: MutableList<String?> = ArrayList()
|
||||
val permissions: List<String?> = ArrayList(),
|
||||
var permissionCheckCallback: PermissionCheckCallback
|
||||
) : Screen(carContext) {
|
||||
|
||||
/** Callback called when the permission is granted. */
|
||||
@@ -29,7 +28,7 @@ class RequestPermissionScreen(
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
|
||||
var message = ""
|
||||
var message = "This app needs access to location"
|
||||
if (permissions.contains(permission.ACCESS_FINE_LOCATION))
|
||||
message = "This app needs access to location and to car speed"
|
||||
if (permissions.contains("android.car.permission.CAR_SPEED"))
|
||||
@@ -83,7 +82,7 @@ fun checkPermission(carContext: CarContext, permission: String) : Boolean {
|
||||
screenManager.pop()
|
||||
return@RequestPermissionScreen
|
||||
},
|
||||
permissions
|
||||
permissions = permissions
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -55,6 +55,7 @@ class RoutePreviewScreen(
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var destination: Place,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
private var showAlternativeRoute: Boolean
|
||||
) :
|
||||
Screen(carContext) {
|
||||
private var isFavorite = false
|
||||
@@ -71,7 +72,7 @@ class RoutePreviewScreen(
|
||||
|
||||
var loading = true
|
||||
|
||||
var showAlternativeRoute = true
|
||||
var flag = FLAG_DEFAULT
|
||||
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
@@ -84,7 +85,7 @@ class RoutePreviewScreen(
|
||||
routeModel.startNavigation(route)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
loading = false
|
||||
if (routeModel.route.routes.size == 1) {
|
||||
if (routeModel.route.routes.size == 1 && showAlternativeRoute) {
|
||||
routeType = RoutePreviewType.SINGLE_ROUTE
|
||||
showAlternativeRoute = false
|
||||
}
|
||||
@@ -116,6 +117,10 @@ class RoutePreviewScreen(
|
||||
routingEngine = it
|
||||
|
||||
})
|
||||
|
||||
repository.alternativeRoutesFlow.asLiveData().observe(this, Observer {
|
||||
showAlternativeRoute = it
|
||||
})
|
||||
lifecycleScope.launch {
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
@@ -178,7 +183,7 @@ class RoutePreviewScreen(
|
||||
listContent.build()
|
||||
} else {
|
||||
val navigateAction =
|
||||
createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{
|
||||
createAction(carContext, R.drawable.navigation_48px, flag,{
|
||||
onNavigate(routeModel.navState.currentRouteIndex)
|
||||
})
|
||||
val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, {
|
||||
@@ -202,7 +207,7 @@ class RoutePreviewScreen(
|
||||
.setContentTemplate(content)
|
||||
.setMapController(
|
||||
MapController.Builder().setMapActionStrip(
|
||||
mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
|
||||
mapActionStrip(carContext, ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, {
|
||||
zoomMinus()
|
||||
} )).build()
|
||||
|
||||
@@ -225,6 +230,7 @@ class RoutePreviewScreen(
|
||||
carContext, R.drawable.ic_zoom_in_24,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
flag = FLAG_IS_PERSISTENT
|
||||
surfaceRenderer.handleScale(1)
|
||||
invalidate()
|
||||
}
|
||||
@@ -239,6 +245,7 @@ class RoutePreviewScreen(
|
||||
carContext, R.drawable.ic_zoom_out_24,
|
||||
FLAG_IS_PERSISTENT,
|
||||
onClickAction = {
|
||||
flag = FLAG_IS_PERSISTENT
|
||||
surfaceRenderer.handleScale(-1)
|
||||
invalidate()
|
||||
}
|
||||
@@ -297,7 +304,7 @@ class RoutePreviewScreen(
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
val routeText = createRouteText(route)
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.core.graphics.createBitmap
|
||||
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.FUEL_STATION
|
||||
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.
|
||||
*/
|
||||
fun mapActionStrip(
|
||||
carContext: CarContext,
|
||||
viewStyle: ViewStyle,
|
||||
zoomPlus: () -> Action,
|
||||
zoomMinus: () -> Action,
|
||||
panAction: () -> Action
|
||||
recenterAction: () -> Action
|
||||
): ActionStrip {
|
||||
val actionStripBuilder = ActionStrip.Builder()
|
||||
.addAction(zoomPlus())
|
||||
.addAction(zoomMinus())
|
||||
actionStripBuilder.addAction(
|
||||
Action.Builder(Action.PAN)
|
||||
.setIcon(createCarIcon(carContext, R.drawable.ic_pan_24))
|
||||
.setFlags(0)
|
||||
.build()
|
||||
)
|
||||
if (viewStyle == ViewStyle.PAN_VIEW) {
|
||||
actionStripBuilder
|
||||
.addAction(
|
||||
panAction()
|
||||
recenterAction()
|
||||
)
|
||||
}
|
||||
|
||||
return actionStripBuilder.build()
|
||||
}
|
||||
|
||||
@@ -106,7 +115,7 @@ fun createAction(
|
||||
carContext: CarContext,
|
||||
@DrawableRes iconRes: Int,
|
||||
flag: Int = FLAG_DEFAULT,
|
||||
onClickAction: () -> Unit
|
||||
onClickAction: () -> Unit,
|
||||
): Action {
|
||||
return Action.Builder()
|
||||
.setIcon(createCarIcon(carContext, iconRes))
|
||||
|
||||
+36
-37
@@ -16,17 +16,16 @@ import kotlinx.coroutines.flow.map
|
||||
private const val DATASTORE_NAME = "navigation_settings"
|
||||
|
||||
|
||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
|
||||
|
||||
|
||||
/**
|
||||
* Central manager for app settings using DataStore
|
||||
*/
|
||||
class DataStoreManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
|
||||
}
|
||||
|
||||
// Keys
|
||||
object PreferencesKeys {
|
||||
companion object PreferencesKeys {
|
||||
|
||||
val SHOW_3D = booleanPreferencesKey("Show3D")
|
||||
|
||||
@@ -66,186 +65,186 @@ class DataStoreManager(private val context: Context) {
|
||||
|
||||
val show3DFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.SHOW_3D] == true
|
||||
preferences[SHOW_3D] == true
|
||||
}
|
||||
val darkModeFlow: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.DARK_MODE]
|
||||
preferences[DARK_MODE]
|
||||
?: 0
|
||||
}
|
||||
|
||||
val avoidMotorwayFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_MOTORWAY] == true
|
||||
preferences[AVOID_MOTORWAY] == true
|
||||
}
|
||||
|
||||
val avoidTollwayFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
|
||||
preferences[AVOID_TOLLWAY] == true
|
||||
}
|
||||
|
||||
val avoidFerryFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_FERRY] == true
|
||||
preferences[AVOID_FERRY] == true
|
||||
}
|
||||
|
||||
val useCarLocationFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.CAR_LOCATION] == true
|
||||
preferences[CAR_LOCATION] == true
|
||||
}
|
||||
|
||||
val routingEngineFlow: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.ROUTING_ENGINE]
|
||||
preferences[ROUTING_ENGINE]
|
||||
?: 2
|
||||
}
|
||||
|
||||
val lastRouteFlow: Flow<String> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.LAST_ROUTE]
|
||||
preferences[LAST_ROUTE]
|
||||
?: ""
|
||||
}
|
||||
|
||||
val tomTomApiKeyFlow: Flow<String> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.TOMTOM_APIKEY]
|
||||
preferences[TOMTOM_APIKEY]
|
||||
?: ""
|
||||
}
|
||||
|
||||
val recentPlacesFlow: Flow<String> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.RECENT_PLACES]
|
||||
preferences[RECENT_PLACES]
|
||||
?: ""
|
||||
}
|
||||
|
||||
|
||||
val distanceModeFlow: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.DISTANCE_MODE]
|
||||
preferences[DISTANCE_MODE]
|
||||
?: 0
|
||||
}
|
||||
|
||||
val guidanceAudioFlow: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.GUIDANCE_AUDIO]
|
||||
preferences[GUIDANCE_AUDIO]
|
||||
?: 0
|
||||
}
|
||||
|
||||
val trafficFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.TRAFFIC] == true
|
||||
preferences[TRAFFIC] == true
|
||||
}
|
||||
|
||||
val tripSuggestionFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
|
||||
preferences[TRIP_SUGGESTION] == true
|
||||
}
|
||||
|
||||
val engineTypeFlow: Flow<Int> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.ENGINE_TYPE]
|
||||
preferences[ENGINE_TYPE]
|
||||
?: EngineType.COMBUSTION.ordinal
|
||||
}
|
||||
|
||||
val alternativeRoutesFlow: Flow<Boolean> =
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[PreferencesKeys.ALTERNATIVE_ROUTES] == true
|
||||
preferences[ALTERNATIVE_ROUTES] == true
|
||||
}
|
||||
|
||||
// Save values
|
||||
suspend fun setShow3D(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.SHOW_3D] = enabled
|
||||
preferences[SHOW_3D] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setDarkMode(mode: Int) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.DARK_MODE] = mode
|
||||
prefs[DARK_MODE] = mode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAvoidMotorway(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_MOTORWAY] = enabled
|
||||
preferences[AVOID_MOTORWAY] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAvoidTollway(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_TOLLWAY] = enabled
|
||||
preferences[AVOID_TOLLWAY] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAvoidFerry(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.AVOID_FERRY] = enabled
|
||||
preferences[AVOID_FERRY] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setCarLocation(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.CAR_LOCATION] = enabled
|
||||
preferences[CAR_LOCATION] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setRoutingEngine(mode: Int) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.ROUTING_ENGINE] = mode
|
||||
prefs[ROUTING_ENGINE] = mode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setLastRoute(route: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.LAST_ROUTE] = route
|
||||
prefs[LAST_ROUTE] = route
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setTomtomApiKey(apiKey: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.TOMTOM_APIKEY] = apiKey
|
||||
prefs[TOMTOM_APIKEY] = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setRecentPlaces(apiKey: String) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.RECENT_PLACES] = apiKey
|
||||
prefs[RECENT_PLACES] = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setDistanceMode(mode: Int) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.DISTANCE_MODE] = mode
|
||||
prefs[DISTANCE_MODE] = mode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setGuidanceAudio(mode: Int) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.GUIDANCE_AUDIO] = mode
|
||||
prefs[GUIDANCE_AUDIO] = mode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setTraffic(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.TRAFFIC] = enabled
|
||||
preferences[TRAFFIC] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setTripSuggestion(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
|
||||
preferences[TRIP_SUGGESTION] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setEngineType(mode: Int) {
|
||||
context.dataStore.edit { prefs ->
|
||||
prefs[PreferencesKeys.ENGINE_TYPE] = mode
|
||||
prefs[ENGINE_TYPE] = mode
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setAlternativeRoutes(enabled: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.ALTERNATIVE_ROUTES] = enabled
|
||||
preferences[ALTERNATIVE_ROUTES] = enabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
|
||||
private const val tomtomFields =
|
||||
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
|
||||
|
||||
val useLocal = BuildConfig.DEBUG
|
||||
val useLocal = BuildConfig.DEBUG
|
||||
|
||||
val useLocalTraffic = BuildConfig.DEBUG
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ class TomTomRoute {
|
||||
) {
|
||||
val lanes = mutableListOf<Lane>()
|
||||
var startIndex = 0
|
||||
var lastLane: Lane? = null
|
||||
section.lanes?.forEach { itLane ->
|
||||
val lane = Lane(
|
||||
location = location(
|
||||
@@ -91,13 +90,7 @@ class TomTomRoute {
|
||||
endIndex = section.endPointIndex
|
||||
)
|
||||
startIndex = section.startPointIndex
|
||||
if (lastLane == null
|
||||
|| (!(lastLane.valid && lane.valid
|
||||
&& lastLane.indications == lane.indications))
|
||||
) {
|
||||
lanes.add(lane)
|
||||
}
|
||||
lastLane = lane
|
||||
lanes.add(lane)
|
||||
}
|
||||
intersections.add(Intersection(waypoints[startIndex], lanes))
|
||||
}
|
||||
|
||||
@@ -76,48 +76,9 @@ open class RouteModel {
|
||||
navState = navState.copy(lastLocation = navState.currentLocation)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the current step
|
||||
*/
|
||||
fun currentStep(): StepData {
|
||||
val distanceToNextStep = routeCalculator.leftStepDistance()
|
||||
// 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
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.kouros.navigation.model
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.lifecycle.ViewModel
|
||||
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 kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@@ -29,6 +29,7 @@ import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
@@ -138,7 +139,7 @@ fun duration(
|
||||
lastLocationUpdate: LocalDateTime
|
||||
): Duration {
|
||||
if (preview) {
|
||||
return 3.seconds
|
||||
return 10.milliseconds
|
||||
}
|
||||
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
|
||||
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>
|
||||
Reference in New Issue
Block a user