diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3af789b..e5b5403 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "com.kouros.navigation" minSdk = 33 targetSdk = 36 - versionCode = 90 - versionName = "0.2.3.90" + versionCode = 91 + versionName = "0.2.3.91" base.archivesName = "navi-$versionName" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -101,9 +101,10 @@ dependencies { implementation(libs.maplibre.compose) implementation(libs.accompanist.permissions) - implementation(project(":common:car")) implementation(project(":common:data")) + implementation(libs.androidx.car.app) + implementation(libs.androidx.app.projected) implementation(libs.play.services.location) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.navigation.compose) diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 342e444..5d00284 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -37,7 +37,6 @@ android { dependencies { implementation(libs.androidx.app.automotive) - implementation(libs.androidx.car.app) implementation(libs.androidx.material3) implementation(libs.androidx.runtime.livedata) implementation(project(":common:car")) diff --git a/automotive/src/main/res/drawable/ic_launcher_foreground.xml b/automotive/src/main/res/drawable/ic_launcher_foreground.xml index 1b2a683..764aeca 100644 --- a/automotive/src/main/res/drawable/ic_launcher_foreground.xml +++ b/automotive/src/main/res/drawable/ic_launcher_foreground.xml @@ -4,10 +4,10 @@ android:viewportWidth="960" android:viewportHeight="960" android:tint="#1A7416"> - + diff --git a/automotive/src/main/res/mipmap-anydpi/ic_launcher.xml b/automotive/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index 6f3b755..0000000 --- a/automotive/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/automotive/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/automotive/src/main/res/mipmap-anydpi/ic_launcher_round.xml deleted file mode 100644 index 6f3b755..0000000 --- a/automotive/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/automotive/src/main/res/mipmap-hdpi/ic_launcher.webp b/automotive/src/main/res/mipmap-hdpi/ic_launcher.webp index e0d523c..3a95d86 100644 Binary files a/automotive/src/main/res/mipmap-hdpi/ic_launcher.webp and b/automotive/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b0800bc..170d499 100644 Binary files a/automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/automotive/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/automotive/src/main/res/mipmap-mdpi/ic_launcher.webp b/automotive/src/main/res/mipmap-mdpi/ic_launcher.webp index 892b0b7..1f9e363 100644 Binary files a/automotive/src/main/res/mipmap-mdpi/ic_launcher.webp and b/automotive/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 6662a13..0a88a05 100644 Binary files a/automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/automotive/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp b/automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp index 5b55a3a..10bbd7a 100644 Binary files a/automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/automotive/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index c918987..c7f3b91 100644 Binary files a/automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/automotive/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 97c5070..a2e8cae 100644 Binary files a/automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/automotive/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 60383ba..8677fcd 100644 Binary files a/automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/automotive/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index 3464553..f416d78 100644 Binary files a/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index dafc417..2980d77 100644 Binary files a/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/automotive/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index 29b90ca..c2f0950 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -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) diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt index ee0092a..228687e 100644 --- a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt @@ -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) } -} \ No newline at end of file + + @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) + } + } + } + } +} + diff --git a/common/car/src/main/java/com/kouros/navigation/car/CarSession.kt b/common/car/src/main/java/com/kouros/navigation/car/CarSession.kt new file mode 100644 index 0000000..ea98230 --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/CarSession.kt @@ -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() + +} \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt index 6c5a3c3..4336412 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/ClusterSession.kt @@ -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() + } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt index cc9cce1..1a3a671 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationCarAppService.kt @@ -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 } diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index f2ed5ce..6c5ebff 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -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" diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index a714a6f..0334915 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -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()) // 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? 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( diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index e233816..73aa347 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -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) { diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt index c568736..76179b2 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -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() } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt index 801535e..0c66e5b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt @@ -18,4 +18,5 @@ interface NavigationListener { fun navigateToPlace(place: Place) fun recalcRoute(destination: Place) + } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index fcaaf7e..d245965 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -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 private var junctionImage: CarIcon? = null private var backGroundColor = CarColor.BLUE + + private var showAlternativeRoute = false val observerRecentPlaces = Observer> { 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) { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index 04d1ebf..3a15baf 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -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) { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt index dfc3e1c..ff29ab0 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt @@ -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 = ArrayList() + val permissions: List = 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 { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 3e77566..c9e8371 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -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) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt index 137c762..5f02389 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt @@ -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)) diff --git a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt index 0640d5f..52444ca 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt @@ -16,17 +16,16 @@ import kotlinx.coroutines.flow.map private const val DATASTORE_NAME = "navigation_settings" +val Context.dataStore: DataStore by preferencesDataStore(name = DATASTORE_NAME) + + /** * Central manager for app settings using DataStore */ class DataStoreManager(private val context: Context) { - companion object { - val Context.dataStore: DataStore 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 = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.SHOW_3D] == true + preferences[SHOW_3D] == true } val darkModeFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.DARK_MODE] + preferences[DARK_MODE] ?: 0 } val avoidMotorwayFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.AVOID_MOTORWAY] == true + preferences[AVOID_MOTORWAY] == true } val avoidTollwayFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.AVOID_TOLLWAY] == true + preferences[AVOID_TOLLWAY] == true } val avoidFerryFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.AVOID_FERRY] == true + preferences[AVOID_FERRY] == true } val useCarLocationFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.CAR_LOCATION] == true + preferences[CAR_LOCATION] == true } val routingEngineFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.ROUTING_ENGINE] + preferences[ROUTING_ENGINE] ?: 2 } val lastRouteFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.LAST_ROUTE] + preferences[LAST_ROUTE] ?: "" } val tomTomApiKeyFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.TOMTOM_APIKEY] + preferences[TOMTOM_APIKEY] ?: "" } val recentPlacesFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.RECENT_PLACES] + preferences[RECENT_PLACES] ?: "" } val distanceModeFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.DISTANCE_MODE] + preferences[DISTANCE_MODE] ?: 0 } val guidanceAudioFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.GUIDANCE_AUDIO] + preferences[GUIDANCE_AUDIO] ?: 0 } val trafficFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.TRAFFIC] == true + preferences[TRAFFIC] == true } val tripSuggestionFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.TRIP_SUGGESTION] == true + preferences[TRIP_SUGGESTION] == true } val engineTypeFlow: Flow = context.dataStore.data.map { preferences -> - preferences[PreferencesKeys.ENGINE_TYPE] + preferences[ENGINE_TYPE] ?: EngineType.COMBUSTION.ordinal } val alternativeRoutesFlow: Flow = 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 } } diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 6fc716d..f2f3bf4 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt index 6fe5ecc..9984065 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt @@ -78,7 +78,6 @@ class TomTomRoute { ) { val lanes = mutableListOf() 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)) } diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 7ad710c..5a462f4 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -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 { - var lanes = emptyList() - 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 { + var lanes = emptyList() + 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 */ diff --git a/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt index efc1e9d..6c5dcd8 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt index cb59527..96e297d 100644 --- a/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt +++ b/common/data/src/main/java/com/kouros/navigation/utils/NavigationUtils.kt @@ -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 diff --git a/common/data/src/main/res/drawable/electric_car_24px.xml b/common/data/src/main/res/drawable/electric_car_24px.xml new file mode 100644 index 0000000..57baad3 --- /dev/null +++ b/common/data/src/main/res/drawable/electric_car_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/drawable/ic_recenter_24.xml b/common/data/src/main/res/drawable/ic_recenter_24.xml new file mode 100644 index 0000000..52cfb95 --- /dev/null +++ b/common/data/src/main/res/drawable/ic_recenter_24.xml @@ -0,0 +1,25 @@ + + + + +