Distance settings

This commit is contained in:
Dimitris
2026-03-03 16:34:03 +01:00
parent e1af3e19fa
commit 11e9dbb21e
39 changed files with 753 additions and 128 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 57 versionCode = 59
versionName = "0.2.0.57" versionName = "0.2.0.59"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -83,8 +83,8 @@ dependencies {
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(project(":common:data"))
implementation(project(":common:car")) implementation(project(":common:car"))
implementation(project(":common:data"))
implementation(libs.play.services.location) implementation(libs.play.services.location)
implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)

View File

@@ -20,12 +20,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.round import com.kouros.navigation.utils.round
private const val MANEUVER_TYPE_EXIT_RIGHT = 45 private const val MANEUVER_TYPE_EXIT_RIGHT = 45
private const val MANEUVER_TYPE_EXIT_LEFT = 46 private const val MANEUVER_TYPE_EXIT_LEFT = 46
private const val METERS_PER_KILOMETER = 1000.0 private const val METERS_PER_KILOMETER = 1000.0
private const val DISTANCE_THRESHOLD_METERS = 1000 private const val DISTANCE_THRESHOLD = 1000
private val CardTopPadding = 60.dp private val CardTopPadding = 60.dp
private val CardElevation = 6.dp private val CardElevation = 6.dp
@@ -92,8 +93,9 @@ fun NavigationInfo(
@Composable @Composable
private fun DistanceText(distance: Double) { private fun DistanceText(distance: Double) {
val distancexx = formattedDistance(2, distance)
val formattedDistance = when { val formattedDistance = when {
distance < DISTANCE_THRESHOLD_METERS -> "${distance.toInt()} m" distance < DISTANCE_THRESHOLD -> "${distance.toInt()} m"
else -> "${(distance / METERS_PER_KILOMETER).round(1)} km" else -> "${(distance / METERS_PER_KILOMETER).round(1)} km"
} }

View File

@@ -1,24 +0,0 @@
package com.kouros.navigation
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.kouros.navigation", appContext.packageName)
}
}

View File

@@ -1,17 +0,0 @@
package com.kouros.navigation
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -102,7 +102,7 @@ class RouteModelTest {
} else { } else {
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT) assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
} }
assertEquals(stepData.leftStepDistance, 300.0, 1.0) assertEquals(stepData.leftStepDistance, 301.0, 1.0)
} }
@Test @Test
@@ -192,7 +192,7 @@ class RouteModelTest {
val location: Location = location( 11.584578, 48.183653) val location: Location = location( 11.584578, 48.183653)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()) ) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()) )
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance, 650.0, 0.1) assertEquals(step.leftStepDistance, 645.0, 1.0)
} }
@Test @Test
@@ -200,6 +200,6 @@ class RouteModelTest {
val location: Location = location( 11.578911, 48.185565) val location: Location = location( 11.578911, 48.185565)
routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()) ) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository()) )
val step = routeModel.currentStep() val step = routeModel.currentStep()
assertEquals(step.leftStepDistance , 30.0, 1.0) assertEquals(step.leftStepDistance , 34.0, 1.0)
} }
} }

View File

@@ -43,7 +43,7 @@ class DeviceLocationManager(
* Only processes location if car location hardware is not being used. * Only processes location if car location hardware is not being used.
*/ */
private val locationListener: LocationListenerCompat = LocationListenerCompat { location -> private val locationListener: LocationListenerCompat = LocationListenerCompat { location ->
if (location != null && shouldUseDeviceLocation) { if (shouldUseDeviceLocation) {
onLocationUpdate(location) onLocationUpdate(location)
} }
} }

View File

@@ -1,7 +1,6 @@
package com.kouros.navigation.car package com.kouros.navigation.car
import android.Manifest import android.Manifest.permission
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
@@ -11,9 +10,10 @@ import androidx.car.app.Screen
import androidx.car.app.ScreenManager import androidx.car.app.ScreenManager
import androidx.car.app.Session import androidx.car.app.Session
import androidx.car.app.connection.CarConnection import androidx.car.app.connection.CarConnection
import androidx.core.net.toUri import androidx.car.app.navigation.NavigationManager
import androidx.car.app.navigation.NavigationManagerCallback
import androidx.car.app.navigation.model.Trip
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
@@ -23,6 +23,8 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG import com.kouros.navigation.data.Constants.TAG
@@ -35,9 +37,7 @@ import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel import com.kouros.navigation.utils.NavigationUtils.getViewModel
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import android.Manifest.permission
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
/** /**
* Main session for Android Auto/Automotive OS navigation. * Main session for Android Auto/Automotive OS navigation.
@@ -65,12 +65,17 @@ class NavigationSession : Session(), NavigationScreen.Listener {
// Manages device GPS location updates // Manages device GPS location updates
lateinit var deviceLocationManager: DeviceLocationManager lateinit var deviceLocationManager: DeviceLocationManager
lateinit var navigationManager: NavigationManager
/** /**
* Lifecycle observer for managing session lifecycle events. * Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed. * Cleans up resources when the session is destroyed.
*/ */
private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver { private val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
if (::navigationManager.isInitialized) {
navigationManager.clearNavigationManagerCallback()
}
if (::carSensorManager.isInitialized) { if (::carSensorManager.isInitialized) {
carSensorManager.cleanup() carSensorManager.cleanup()
} }
@@ -178,6 +183,21 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Initializes managers for rendering, sensors, and location. * Initializes managers for rendering, sensors, and location.
*/ */
private fun initializeManagers() { private fun initializeManagers() {
navigationManager = carContext.getCarService(NavigationManager::class.java)
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
// Called when the app should simulate navigation (e.g., for testing)
// Implement your simulation logic here
Log.d("CarApp", "Auto Drive Enabled")
}
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
Log.d("CarApp", "Stop Navigation Requested")
// Stop turn-by-turn logic and clean up
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
carSensorManager = CarSensorManager( carSensorManager = CarSensorManager(
@@ -346,6 +366,20 @@ class NavigationSession : Session(), NavigationScreen.Listener {
*/ */
override fun stopNavigation() { override fun stopNavigation() {
routeModel.stopNavigation() routeModel.stopNavigation()
navigationManager.navigationEnded()
}
/**
* Start navigation process.
* Called when user starts navigation
*/
override fun startNavigation() {
navigationManager.navigationStarted()
}
override fun updateTrip(trip: Trip) {
Log.d("Trip", trip.toString())
navigationManager.updateTrip(trip)
} }
companion object { companion object {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.navigation package com.kouros.navigation.car.navigation
import android.location.Location
import android.text.SpannableString import android.text.SpannableString
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
@@ -26,9 +25,7 @@ import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.formattedDistance
import java.util.Collections
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -87,24 +84,21 @@ class RouteCarModel() : RouteModel() {
return step return step
} }
fun travelEstimate(carContext: CarContext): TravelEstimate { fun travelEstimate(carContext: CarContext, distanceMode: Int): TravelEstimate {
val timeLeft = routeCalculator.travelLeftTime() val timeLeft = routeCalculator.travelLeftTime()
val timeToDestinationMillis = val timeToDestinationMillis =
TimeUnit.SECONDS.toMillis(timeLeft.toLong()) TimeUnit.SECONDS.toMillis(timeLeft.toLong())
val leftDistance = routeCalculator.travelLeftDistance() / 1000 val distance = formattedDistance(distanceMode, routeCalculator.travelLeftDistance())
val displayUnit = if (leftDistance > 1.0) {
Distance.UNIT_KILOMETERS
} else {
Distance.UNIT_METERS
}
val arrivalTime = DateTimeWithZone.create( val arrivalTime = DateTimeWithZone.create(
routeCalculator.arrivalTime(), routeCalculator.arrivalTime(),
TimeZone.getTimeZone("Europe/Berlin") TimeZone.getDefault()
) )
val traffic = (route.routes.first().summary.trafficDelay/60).toInt()
val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination. val travelBuilder = TravelEstimate.Builder( // The estimated distance to the destination.
Distance.create( Distance.create(
leftDistance, distance.first,
displayUnit distance.second
), // Arrival time at the destination with the destination time zone. ), // Arrival time at the destination with the destination time zone.
arrivalTime arrivalTime
) )
@@ -115,7 +109,7 @@ class RouteCarModel() : RouteModel() {
) )
.setRemainingTimeColor(CarColor.GREEN) .setRemainingTimeColor(CarColor.GREEN)
.setRemainingDistanceColor(CarColor.BLUE) .setRemainingDistanceColor(CarColor.BLUE)
.setTripText(CarText.create("$traffic min"))
if (navState.travelMessage.isNotEmpty()) { if (navState.travelMessage.isNotEmpty()) {
travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px))
travelBuilder.setTripText(CarText.create(navState.travelMessage)) travelBuilder.setTripText(CarText.create(navState.travelMessage))

View File

@@ -42,6 +42,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
R.string.dark_mode R.string.dark_mode
) )
) )
listBuilder.addItem(
buildRowForScreenTemplate(
DistanceSettings(carContext),
R.string.distance_units
)
)
return ListTemplate.Builder() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(

View File

@@ -0,0 +1,84 @@
package com.kouros.navigation.car.screen
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.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class DistanceSettings(private val carContext: CarContext) : Screen(carContext) {
private var distanceSettings = 0
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.distanceMode.first()
}
}
override fun onGetTemplate(): Template {
distanceSettings = settingsViewModel.distanceMode.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(
R.string.automaticaly,
)
)
.addItem(
buildRowForTemplate(
R.string.kilometer,
)
)
.addItem(
buildRowForTemplate(
R.string.miles,
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(distanceSettings)
.build()
return templateBuilder
.addSectionedList(
SectionedItemList.create(
radioList,
carContext.getString(R.string.distance_units)
)
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.distance_units))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
settingsViewModel.onDistanceModeChanged(index)
}
private fun buildRowForTemplate(title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.build()
}
}

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
@@ -18,12 +17,16 @@ import androidx.car.app.model.Distance
import androidx.car.app.model.Header import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.car.app.navigation.model.Destination
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.NavigationTemplate
import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.RoutingInfo
import androidx.car.app.navigation.model.Trip
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
@@ -31,12 +34,12 @@ import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location import com.kouros.navigation.utils.location
@@ -48,6 +51,10 @@ import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
/**
* Main screen for car navigation.
* Handles different navigation states and provides corresponding templates.
*/
class NavigationScreen( class NavigationScreen(
carContext: CarContext, carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer, private var surfaceRenderer: SurfaceRenderer,
@@ -60,10 +67,15 @@ class NavigationScreen(
interface Listener { interface Listener {
/** Stops navigation. */ /** Stops navigation. */
fun stopNavigation() fun stopNavigation()
/** Starts navigation. */
fun startNavigation()
/** Updates trip information. */
fun updateTrip(trip: Trip)
} }
val backGroundColor = CarColor.BLUE val backGroundColor = CarColor.BLUE
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlace = Place() var recentPlace = Place()
var navigationType = NavigationType.VIEW var navigationType = NavigationType.VIEW
@@ -74,18 +86,25 @@ class NavigationScreen(
val observerManager = NavigationObserverManager(navigationViewModel, this) val observerManager = NavigationObserverManager(navigationViewModel, this)
val repository = getSettingsRepository(carContext)
var distanceMode = 0
init { init {
observerManager.attachAllObservers(this) observerManager.attachAllObservers(this)
lifecycleScope.launch { lifecycleScope.launch {
getSettingsViewModel(carContext).routingEngine.first() getSettingsViewModel(carContext).routingEngine.first()
getSettingsViewModel(carContext).recentPlaces.first() getSettingsViewModel(carContext).recentPlaces.first()
distanceMode = repository.distanceModeFlow.first()
} }
} }
// NavigationObserverCallback implementations /**
* Handles the received route string.
* Starts navigation and invalidates the screen.
*/
override fun onRouteReceived(route: String) { override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) { if (route.isNotEmpty()) {
val repository = getSettingsRepository(carContext)
val routingEngine = runBlocking { repository.routingEngineFlow.first() } val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
@@ -94,26 +113,45 @@ class NavigationScreen(
getSettingsViewModel(carContext).onLastRouteChanged(route) getSettingsViewModel(carContext).onLastRouteChanged(route)
} }
surfaceRenderer.setRouteData() surfaceRenderer.setRouteData()
listener.startNavigation()
invalidate() invalidate()
} }
} }
/**
* Checks if navigation is currently active.
*/
override fun isNavigating(): Boolean = routeModel.isNavigating() override fun isNavigating(): Boolean = routeModel.isNavigating()
/**
* Handles the received recent place.
* Updates the navigation type to RECENT and invalidates the screen.
*/
override fun onRecentPlaceReceived(place: Place) { override fun onRecentPlaceReceived(place: Place) {
recentPlace = place recentPlace = place
navigationType = NavigationType.RECENT navigationType = NavigationType.RECENT
invalidate() invalidate()
} }
/**
* Handles received traffic data and updates the surface renderer.
*/
override fun onTrafficReceived(traffic: Map<String, String>) { override fun onTrafficReceived(traffic: Map<String, String>) {
surfaceRenderer.setTrafficData(traffic) surfaceRenderer.setTrafficData(traffic)
} }
/**
* Handles the received place search result.
* Navigates to the specified place.
*/
override fun onPlaceSearchResultReceived(place: Place) { override fun onPlaceSearchResultReceived(place: Place) {
navigateToPlace(place) navigateToPlace(place)
} }
/**
* Handles received speed camera data.
* Updates the surface renderer with the camera locations.
*/
override fun onSpeedCamerasReceived(cameras: List<Elements>) { override fun onSpeedCamerasReceived(cameras: List<Elements>) {
speedCameras = cameras speedCameras = cameras
val coordinates = mutableListOf<List<Double>>() val coordinates = mutableListOf<List<Double>>()
@@ -124,15 +162,27 @@ class NavigationScreen(
surfaceRenderer.speedCamerasData.value = speedData surfaceRenderer.speedCamerasData.value = speedData
} }
/**
* Handles received maximum speed data and updates the surface renderer.
*/
override fun onMaxSpeedReceived(speed: Int) { override fun onMaxSpeedReceived(speed: Int) {
surfaceRenderer.maxSpeed.value = speed surfaceRenderer.maxSpeed.value = speed
} }
/**
* Invalidates the screen.
*/
override fun invalidateScreen() { override fun invalidateScreen() {
invalidate() invalidate()
} }
/**
* Returns the appropriate template based on the current navigation state.
*/
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
val actionStripBuilder = createActionStripBuilder() val actionStripBuilder = createActionStripBuilder()
return when (navigationType) { return when (navigationType) {
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder) NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
@@ -143,21 +193,28 @@ class NavigationScreen(
} }
} }
/**
* Creates and returns a NavigationTemplate for the active navigation state.
*/
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
actionStripBuilder.addAction( actionStripBuilder.addAction(
stopAction() stopAction()
) )
updateTrip()
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo( .setNavigationInfo(
getRoutingInfo() getRoutingInfo()
) )
.setDestinationTravelEstimate(routeModel.travelEstimate(carContext)) .setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode))
.setActionStrip(actionStripBuilder.build()) .setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build()) .setMapActionStrip(mapActionStripBuilder().build())
.setBackgroundColor(backGroundColor) .setBackgroundColor(backGroundColor)
.build() .build()
} }
/**
* Creates and returns a template for the default view state.
*/
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY) .setBackgroundColor(CarColor.SECONDARY)
@@ -167,6 +224,9 @@ class NavigationScreen(
} }
/**
* Creates and returns a template for the arrival or end state of navigation.
*/
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.navState.arrived) { if (routeModel.navState.arrived) {
val timer = object : CountDownTimer(8000, 1000) { val timer = object : CountDownTimer(8000, 1000) {
@@ -189,6 +249,9 @@ class NavigationScreen(
} }
/**
* Creates and returns a NavigationTemplate specifically for when the destination is reached.
*/
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
var street = "" var street = ""
if (routeModel.navState.destination.street != null) { if (routeModel.navState.destination.street != null) {
@@ -217,6 +280,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates and returns a template showing recent places or destinations.
*/
fun navigationRecentPlaceTemplate(): Template { fun navigationRecentPlaceTemplate(): Template {
val messageTemplate = MessageTemplate.Builder( val messageTemplate = MessageTemplate.Builder(
recentPlace.name + "\n" recentPlace.name + "\n"
@@ -242,6 +308,9 @@ class NavigationScreen(
return builder.build() return builder.build()
} }
/**
* Creates and returns a template for when the route is being recalculated.
*/
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template { fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder() return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build()) .setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
@@ -250,19 +319,15 @@ class NavigationScreen(
.build() .build()
} }
/**
* Builds and returns RoutingInfo based on the current step and distance.
*/
fun getRoutingInfo(): RoutingInfo { fun getRoutingInfo(): RoutingInfo {
var currentDistance = routeModel.routeCalculator.leftStepDistance() val distance = formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_KILOMETERS
} else {
Distance.UNIT_METERS
}
val routingInfo = RoutingInfo.Builder() val routingInfo = RoutingInfo.Builder()
.setCurrentStep( .setCurrentStep(
routeModel.currentStep(carContext = carContext), routeModel.currentStep(carContext = carContext),
Distance.create(currentDistance, displayUnit) Distance.create(distance.first, distance.second)
) )
if (routeModel.navState.nextStep) { if (routeModel.navState.nextStep) {
val nextStep = routeModel.nextStep(carContext = carContext) val nextStep = routeModel.nextStep(carContext = carContext)
@@ -271,6 +336,9 @@ class NavigationScreen(
return routingInfo.build() return routingInfo.build()
} }
/**
* Creates an ActionStrip builder with common search and settings actions.
*/
private fun createActionStripBuilder(): ActionStrip.Builder { private fun createActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction( actionStripBuilder.addAction(
@@ -282,6 +350,9 @@ class NavigationScreen(
return actionStripBuilder return actionStripBuilder
} }
/**
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
private fun mapActionStripBuilder(): ActionStrip.Builder { private fun mapActionStripBuilder(): ActionStrip.Builder {
val actionStripBuilder = ActionStrip.Builder() val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus()) .addAction(zoomPlus())
@@ -295,6 +366,9 @@ class NavigationScreen(
return actionStripBuilder return actionStripBuilder
} }
/**
* Creates a stop navigation action.
*/
private fun stopAction(): Action { private fun stopAction(): Action {
return Action.Builder() return Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title)) .setTitle(carContext.getString(R.string.stop_action_title))
@@ -313,6 +387,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to start navigation to a specific place.
*/
private fun navigateAction(): Action { private fun navigateAction(): Action {
navigationType = NavigationType.NAVIGATION navigationType = NavigationType.NAVIGATION
return Action.Builder() return Action.Builder()
@@ -338,6 +415,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to close the current view or template.
*/
private fun closeAction(): Action { private fun closeAction(): Action {
return Action.Builder() return Action.Builder()
.setIcon( .setIcon(
@@ -357,6 +437,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to start the search screen.
*/
private fun searchAction(): Action { private fun searchAction(): Action {
return Action.Builder() return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.search_48px)) .setIcon(routeModel.createCarIcon(carContext, R.drawable.search_48px))
@@ -366,6 +449,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to start the settings screen.
*/
private fun settingsAction(): Action { private fun settingsAction(): Action {
return Action.Builder() return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px)) .setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
@@ -375,6 +461,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to zoom in on the map.
*/
private fun zoomPlus(): Action { private fun zoomPlus(): Action {
return Action.Builder() return Action.Builder()
.setIcon( .setIcon(
@@ -392,6 +481,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to zoom out on the map.
*/
private fun zoomMinus(): Action { private fun zoomMinus(): Action {
return Action.Builder() return Action.Builder()
.setIcon( .setIcon(
@@ -409,6 +501,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Creates an action to enable map panning.
*/
private fun panAction(): Action { private fun panAction(): Action {
return Action.Builder() return Action.Builder()
.setIcon( .setIcon(
@@ -426,6 +521,9 @@ class NavigationScreen(
.build() .build()
} }
/**
* Pushes the search screen and handles the search result.
*/
private fun startSearchScreen() { private fun startSearchScreen() {
screenManager screenManager
.pushForResult( .pushForResult(
@@ -450,6 +548,9 @@ class NavigationScreen(
} }
} }
/**
* Loads a route to the specified place and sets it as the destination.
*/
fun navigateToPlace(place: Place) { fun navigateToPlace(place: Place) {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
val location = location(place.longitude, place.latitude) val location = location(place.longitude, place.latitude)
@@ -465,6 +566,9 @@ class NavigationScreen(
invalidate() invalidate()
} }
/**
* Stops navigation, resets state, and notifies listeners.
*/
fun stopNavigation() { fun stopNavigation() {
navigationType = NavigationType.VIEW navigationType = NavigationType.VIEW
listener.stopNavigation() listener.stopNavigation()
@@ -473,6 +577,9 @@ class NavigationScreen(
invalidate() invalidate()
} }
/**
* Initiates recalculation for a new route to the destination.
*/
fun calculateNewRoute(destination: Place) { fun calculateNewRoute(destination: Place) {
stopNavigation() stopNavigation()
navigationType = NavigationType.REROUTE navigationType = NavigationType.REROUTE
@@ -489,6 +596,9 @@ class NavigationScreen(
} }
} }
/**
* Re-requests a route for the specified destination.
*/
fun reRoute(destination: Place) { fun reRoute(destination: Place) {
val dest = location(destination.longitude, destination.latitude) val dest = location(destination.longitude, destination.latitude)
navigationViewModel.loadRoute( navigationViewModel.loadRoute(
@@ -499,6 +609,9 @@ class NavigationScreen(
) )
} }
/**
* Updates navigation state with the current location, checks for arrival, and traffic updates.
*/
fun updateTrip(location: Location) { fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC) val current = LocalDateTime.now(ZoneOffset.UTC)
val duration = Duration.between(current, lastTrafficDate) val duration = Duration.between(current, lastTrafficDate)
@@ -526,6 +639,24 @@ class NavigationScreen(
invalidate() invalidate()
} }
/**
* Updates the trip information and notifies the listener with a new Trip object.
* This includes destination name, address, travel estimate, and loading status.
*/
private fun updateTrip() {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")
.setAddress(routeModel.navState.destination.street ?: "")
.build()
tripBuilder.addDestination(destination, routeModel.travelEstimate(carContext, distanceMode))
tripBuilder.setLoading(false)
listener.updateTrip(tripBuilder.build())
}
/**
* Periodically requests speed camera information near the current location.
*/
private fun updateSpeedCamera(location: Location) { private fun updateSpeedCamera(location: Location) {
if (lastCameraSearch++ % 100 == 0) { if (lastCameraSearch++ % 100 == 0) {
navigationViewModel.getSpeedCameras(location, 5.0) navigationViewModel.getSpeedCameras(location, 5.0)
@@ -535,6 +666,9 @@ class NavigationScreen(
} }
} }
/**
* Updates distances to nearby speed cameras and checks for proximity alerts.
*/
private fun updateDistance( private fun updateDistance(
location: Location, location: Location,
) { ) {
@@ -565,6 +699,9 @@ class NavigationScreen(
} }
} }
/**
* Checks for a specific permission and updates the view model upon grant.
*/
fun checkPermission(permission: String) { fun checkPermission(permission: String) {
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
val permissions: MutableList<String?> = ArrayList() val permissions: MutableList<String?> = ArrayList()
@@ -588,6 +725,9 @@ class NavigationScreen(
} }
} }
/**
* Defines the possible states for the navigation UI.
*/
enum class NavigationType { enum class NavigationType {
VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL VIEW, NAVIGATION, REROUTE, RECENT, ARRIVAL
} }

View File

@@ -17,6 +17,7 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
@@ -25,6 +26,7 @@ import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
class PlaceListScreen( class PlaceListScreen(

View File

@@ -0,0 +1,15 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
/**
* Observer for max speed updates.
*/
class MaxSpeedObserver(
private val callback: NavigationObserverCallback
) : Observer<Int> {
override fun onChanged(value: Int) {
callback.onMaxSpeedReceived(value)
}
}

View File

@@ -0,0 +1,34 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.overpass.Elements
/**
* Callback interface for observer events that need to be handled by NavigationScreen.
*/
interface NavigationObserverCallback {
/** Called when a route is received and navigation should start */
fun onRouteReceived(route: String)
/** Called when a recent place is selected but navigation hasn't started */
fun onRecentPlaceReceived(place: Place)
/** Check if currently navigating */
fun isNavigating(): Boolean
/** Called when traffic data is updated */
fun onTrafficReceived(traffic: Map<String, String>)
/** Called when a place search result is received */
fun onPlaceSearchResultReceived(place: Place)
/** Called when speed cameras are updated */
fun onSpeedCamerasReceived(cameras: List<Elements>)
/** Called when max speed is updated */
fun onMaxSpeedReceived(speed: Int)
/** Called to request UI invalidation/refresh */
fun invalidateScreen()
}

View File

@@ -0,0 +1,42 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.model.NavigationViewModel
/**
* Manager class that handles all NavigationScreen observers.
* Centralizes observer creation, registration, and lifecycle management.
*/
class NavigationObserverManager(
private val viewModel: NavigationViewModel,
callback: NavigationObserverCallback
) {
val routeObserver = RouteObserver(callback)
val recentPlaceObserver = RecentPlaceObserver(callback)
val trafficObserver = TrafficObserver(callback)
val placeSearchObserver = PlaceSearchObserver(callback)
val speedCameraObserver = SpeedCameraObserver(callback)
val maxSpeedObserver = MaxSpeedObserver(callback)
/**
* Attaches all observers to the ViewModel.
* Call this from NavigationScreen's init block or lifecycle method.
*/
fun attachAllObservers(screen: androidx.car.app.Screen) {
viewModel.route.observe(screen, routeObserver)
viewModel.traffic.observe(screen, trafficObserver)
viewModel.recentPlace.observe(screen, recentPlaceObserver)
viewModel.placeLocation.observe(screen, placeSearchObserver)
viewModel.speedCameras.observe(screen, speedCameraObserver)
viewModel.maxSpeed.observe(screen, maxSpeedObserver)
}
/**
* Detaches all observers from the ViewModel.
* Call this when the screen is being destroyed.
*/
fun detachAllObservers() {
// LiveData observers are automatically removed when the lifecycle is destroyed,
// but we can manually remove them if needed for testing or cleanup
}
}

View File

@@ -0,0 +1,27 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
/**
* Observer for place search results. Converts SearchResult to Place and navigates to it.
*/
class PlaceSearchObserver(
private val callback: NavigationObserverCallback
) : Observer<SearchResult> {
override fun onChanged(value: SearchResult) {
val place = Place(
name = value.displayName,
street = value.address.road,
city = value.address.city,
latitude = value.lat.toDouble(),
longitude = value.lon.toDouble(),
category = Constants.CONTACTS,
postalCode = value.address.postcode
)
callback.onPlaceSearchResultReceived(place)
}
}

View File

@@ -0,0 +1,18 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.Place
/**
* Observer for recent place updates. Updates the recent place when navigation is not active.
*/
class RecentPlaceObserver(
private val callback: NavigationObserverCallback
) : Observer<Place> {
override fun onChanged(value: Place) {
if (!callback.isNavigating()) {
callback.onRecentPlaceReceived(value)
}
}
}

View File

@@ -0,0 +1,17 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
/**
* Observer for route updates. Triggers navigation start when a non-empty route is received.
*/
class RouteObserver(
private val callback: NavigationObserverCallback
) : Observer<String> {
override fun onChanged(value: String) {
if (value.isNotEmpty()) {
callback.onRouteReceived(value)
}
}
}

View File

@@ -0,0 +1,16 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.overpass.Elements
/**
* Observer for speed camera updates.
*/
class SpeedCameraObserver(
private val callback: NavigationObserverCallback
) : Observer<List<Elements>> {
override fun onChanged(value: List<Elements>) {
callback.onSpeedCamerasReceived(value)
}
}

View File

@@ -0,0 +1,16 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
/**
* Observer for traffic data updates.
*/
class TrafficObserver(
private val callback: NavigationObserverCallback
) : Observer<Map<String, String>> {
override fun onChanged(value: Map<String, String>) {
callback.onTrafficReceived(value)
callback.invalidateScreen()
}
}

View File

@@ -0,0 +1,153 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.data.nominatim.Address
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Tags
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.*
/**
* Unit tests for NavigationScreen observer classes using Mockito.
*/
class ObserversTest {
private lateinit var mockCallback: NavigationObserverCallback
@Before
fun setup() {
mockCallback = mock()
}
@Test
fun `RouteObserver triggers callback when route is not empty`() {
val observer = RouteObserver(mockCallback)
val testRoute = "test_route_data"
observer.onChanged(testRoute)
verify(mockCallback).onRouteReceived(testRoute)
}
@Test
fun `RouteObserver does not trigger callback when route is empty`() {
val observer = RouteObserver(mockCallback)
observer.onChanged("")
verify(mockCallback, never()).onRouteReceived(any())
}
@Test
fun `RecentPlaceObserver triggers callback when navigating is false`() {
val observer = RecentPlaceObserver(mockCallback)
val testPlace = createTestPlace()
whenever(mockCallback.isNavigating()).thenReturn(false)
observer.onChanged(testPlace)
verify(mockCallback).isNavigating()
verify(mockCallback).onRecentPlaceReceived(testPlace)
}
@Test
fun `RecentPlaceObserver does not trigger callback when navigating is true`() {
val observer = RecentPlaceObserver(mockCallback)
val testPlace = createTestPlace()
whenever(mockCallback.isNavigating()).thenReturn(true)
observer.onChanged(testPlace)
verify(mockCallback).isNavigating()
verify(mockCallback, never()).onRecentPlaceReceived(any())
}
@Test
fun `TrafficObserver triggers callback with traffic data and invalidates screen`() {
val observer = TrafficObserver(mockCallback)
val trafficData = mapOf("road1" to "congestion", "road2" to "clear")
observer.onChanged(trafficData)
verify(mockCallback).onTrafficReceived(trafficData)
verify(mockCallback).invalidateScreen()
}
@Test
fun `PlaceSearchObserver converts SearchResult to Place and triggers callback`() {
val observer = PlaceSearchObserver(mockCallback)
val searchResult = createTestSearchResult()
observer.onChanged(searchResult)
argumentCaptor<Place>().apply {
verify(mockCallback).onPlaceSearchResultReceived(capture())
assert(firstValue.name == "Test Place")
assert(firstValue.street == "Test Street")
assert(firstValue.city == "Test City")
assert(firstValue.latitude == 52.0)
assert(firstValue.longitude == 10.0)
assert(firstValue.category == Constants.CONTACTS)
assert(firstValue.postalCode == "12345")
}
}
@Test
fun `SpeedCameraObserver triggers callback with camera list`() {
val observer = SpeedCameraObserver(mockCallback)
val cameras = listOf(
createTestElements(10.0, 52.0, "50"),
createTestElements(10.1, 52.1, "30")
)
observer.onChanged(cameras)
verify(mockCallback).onSpeedCamerasReceived(cameras)
}
@Test
fun `MaxSpeedObserver triggers callback with speed value`() {
val observer = MaxSpeedObserver(mockCallback)
val testSpeed = 50
observer.onChanged(testSpeed)
verify(mockCallback).onMaxSpeedReceived(testSpeed)
}
// Helper methods
private fun createTestPlace(): Place {
return Place(
name = "Test Place",
street = "Test Street",
city = "Test City",
latitude = 52.0,
longitude = 10.0,
category = Constants.FAVORITES
)
}
private fun createTestSearchResult(): SearchResult {
return SearchResult(
displayName = "Test Place",
lat = "52.0",
lon = "10.0",
address = Address(
road = "Test Street",
city = "Test City",
postcode = "12345"
)
)
}
private fun createTestElements(lon: Double, lat: Double, maxSpeed: String): Elements {
return Elements(
lon = lon,
lat = lat,
tags = Tags(maxspeed = maxSpeed, direction = null)
)
}
}

View File

@@ -69,6 +69,4 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.androidx.runner) androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules) androidTestImplementation(libs.androidx.rules)
} }

View File

@@ -19,3 +19,4 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile

View File

@@ -21,11 +21,11 @@ private const val DATASTORE_NAME = "navigation_settings"
class DataStoreManager(private val context: Context) { class DataStoreManager(private val context: Context) {
companion object { companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME) val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)
} }
// Keys // Keys
private object PreferencesKeys { object PreferencesKeys {
val SHOW_3D = booleanPreferencesKey("Show3D") val SHOW_3D = booleanPreferencesKey("Show3D")
@@ -45,7 +45,7 @@ class DataStoreManager(private val context: Context) {
val RECENT_PLACES = stringPreferencesKey("RecentPlaces") val RECENT_PLACES = stringPreferencesKey("RecentPlaces")
val FAVORITES = stringPreferencesKey("Favorites") val DISTANCE_MODE = intPreferencesKey("DistanceMode")
} }
@@ -79,7 +79,7 @@ class DataStoreManager(private val context: Context) {
val routingEngineFlow: Flow<Int> = val routingEngineFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ROUTING_ENGINE] preferences[PreferencesKeys.ROUTING_ENGINE]
?: 0 ?: 2
} }
val lastRouteFlow: Flow<String> = val lastRouteFlow: Flow<String> =
@@ -100,10 +100,11 @@ class DataStoreManager(private val context: Context) {
?: "" ?: ""
} }
val favoritesFlow: Flow<String> =
val distanceModeFlow: Flow<Int> =
context.dataStore.data.map { preferences -> context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FAVORITES] preferences[PreferencesKeys.DISTANCE_MODE]
?: "" ?: 0
} }
// Save values // Save values
@@ -161,9 +162,10 @@ class DataStoreManager(private val context: Context) {
} }
} }
suspend fun setFavorites(apiKey: String) { suspend fun setDistanceMode(mode: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[PreferencesKeys.FAVORITES] = apiKey prefs[PreferencesKeys.DISTANCE_MODE] = mode
} }
} }
} }

View File

@@ -2,7 +2,11 @@ package com.kouros.navigation.data.route
data class Summary( data class Summary(
// sec // sec
var duration : Double = 0.0, var duration: Double = 0.0,
// km // m
var distance : Double = 0.0, var distance: Double = 0.0,
// sec
var trafficDelay: Double = 0.0,
// m
var trafficLength: Double = 0.0,
) )

View File

@@ -25,7 +25,9 @@ class TomTomRoute {
var points = listOf<List<Double>>() var points = listOf<List<Double>>()
val summary = Summary( val summary = Summary(
route.summary.travelTimeInSeconds.toDouble(), route.summary.travelTimeInSeconds.toDouble(),
route.summary.lengthInMeters.toDouble() route.summary.lengthInMeters.toDouble(),
route.summary.trafficDelayInSeconds.toDouble(),
route.summary.trafficLengthInMeters.toDouble()
) )
route.legs.forEach { leg -> route.legs.forEach { leg ->
points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision) points = decodePolyline(leg.encodedPolyline, leg.encodedPolylinePrecision)

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.model
//import com.kouros.navigation.data.Preferences.boxStore //import com.kouros.navigation.data.Preferences.boxStore
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.util.Log
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled import androidx.compose.ui.platform.isDebugInspectorInfoEnabled

View File

@@ -69,7 +69,7 @@ class RouteCalculator(var routeModel: RouteModel) {
distance distance
} }
} }
return (leftDistance / 10.0).roundToInt() * 10.0 return leftDistance.toDouble()
} }
/** Returns the left distance in m. */ /** Returns the left distance in m. */

View File

@@ -1,7 +1,10 @@
package com.kouros.navigation.model package com.kouros.navigation.model
import androidx.datastore.preferences.core.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.kouros.navigation.data.datastore.DataStoreManager.Companion.dataStore
import com.kouros.navigation.data.datastore.DataStoreManager.PreferencesKeys
import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
@@ -63,6 +66,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
"" ""
) )
val distanceMode = repository.distanceModeFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onShow3DChanged(enabled: Boolean) { fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) } viewModelScope.launch { repository.setShow3D(enabled) }
} }
@@ -94,4 +103,9 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onTomTomApiKeyChanged(key: String) { fun onTomTomApiKeyChanged(key: String) {
viewModelScope.launch { repository.setTomTomApiKey(key) } viewModelScope.launch { repository.setTomTomApiKey(key) }
} }
fun onDistanceModeChanged(mode: Int) {
viewModelScope.launch { repository.setDistanceMode(mode) }
}
} }

View File

@@ -33,8 +33,8 @@ class SettingsRepository(
val recentPlacesFlow: Flow<String> = val recentPlacesFlow: Flow<String> =
dataStoreManager.recentPlacesFlow dataStoreManager.recentPlacesFlow
val favoritesFlow: Flow<String> = val distanceModeFlow: Flow<Int> =
dataStoreManager.favoritesFlow dataStoreManager.distanceModeFlow
suspend fun setShow3D(enabled: Boolean) { suspend fun setShow3D(enabled: Boolean) {
@@ -73,7 +73,9 @@ class SettingsRepository(
dataStoreManager.setRecentPlaces(places) dataStoreManager.setRecentPlaces(places)
} }
suspend fun setFavorites(favorites: String) { suspend fun setDistanceMode(mode: Int) {
dataStoreManager.setFavorites(favorites) dataStoreManager.setDistanceMode(mode)
} }
} }

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.utils
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import androidx.car.app.model.Distance
import com.kouros.navigation.data.RouteEngine import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.osrm.OsrmRepository import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository import com.kouros.navigation.data.tomtom.TomTomRepository
@@ -16,6 +17,7 @@ import java.time.ZoneOffset
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -110,3 +112,33 @@ fun duration(preview: Boolean, bearing: Double, lastBearing: Double): Duration {
} }
return cameraDuration return cameraDuration
} }
fun isMetricSystem(): Boolean {
val country = Locale.getDefault().country
// Return true for metric, false for imperial
return !setOf("US", "UK", "LR", "MM").contains(country)
}
fun formattedDistance(distanceMode : Int, distance: Double): Pair<Double, Int> {
var currentDistance = distance
var displayUnit: Int
if (distanceMode == 1 || distanceMode == 0 && isMetricSystem()) {
displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_KILOMETERS
} else {
currentDistance = (currentDistance / 10.0).roundToInt() * 10.0
Distance.UNIT_METERS
}
} else {
currentDistance *= 0.621371
displayUnit = if (currentDistance > 1000.0) {
currentDistance /= 1000.0
Distance.UNIT_MILES
} else {
currentDistance = (currentDistance / 10.0).roundToInt() * 10.0
Distance.UNIT_FEET
}
}
return Pair(currentDistance, displayUnit)
}

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.utils
import android.content.Context import android.content.Context
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.model.Distance
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -13,6 +14,9 @@ import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository import com.kouros.navigation.repository.SettingsRepository
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.text.NumberFormat
import java.util.Locale
import kotlin.math.roundToInt
@Composable @Composable

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -36,8 +36,8 @@
<string name="use_telephon_settings">Verwende Telefon Einstellungen</string> <string name="use_telephon_settings">Verwende Telefon Einstellungen</string>
<string name="threed_building">3D Gebäude</string> <string name="threed_building">3D Gebäude</string>
<string name="drive_now">Losfahren</string> <string name="drive_now">Losfahren</string>
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen meiden"</string> <string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen meiden"</string> <string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
<string name="recent_destinations">Letzte Ziele</string> <string name="recent_destinations">Letzte Ziele</string>
<string name="contacts">Kontakte</string> <string name="contacts">Kontakte</string>
<string name="route_preview">Route Vorschau</string> <string name="route_preview">Route Vorschau</string>
@@ -54,5 +54,8 @@
<string name="use_car_settings">Verwende Auto Einstellungen</string> <string name="use_car_settings">Verwende Auto Einstellungen</string>
<string name="exit_number">Ausfahrt nummer</string> <string name="exit_number">Ausfahrt nummer</string>
<string name="navigation_icon_description">Navigations Icon</string> <string name="navigation_icon_description">Navigations Icon</string>
<string name="distance_units">Entfernungseinheiten</string>
<string name="automaticaly">Automatisch</string>
<string name="kilometer">Kilometer</string>
<string name="miles">Meilen</string>
</resources> </resources>

View File

@@ -18,7 +18,7 @@
<string name="avoid_highways_row_title">Avoid highways</string> <string name="avoid_highways_row_title">Avoid highways</string>
<string name="avoid_tolls_row_title">Avoid tolls rows</string> <string name="avoid_tolls_row_title">Avoid tolls rows</string>
<string name="no_places">No places</string> <string name="no_places">No places</string>
<string name="recent_destinations">Recent destination</string> <string name="recent_destinations">Recent destinations</string>
<string name="contacts">Contacts</string> <string name="contacts">Contacts</string>
<string name="favorites">Favorites</string> <string name="favorites">Favorites</string>
<string name="recent_Item_deleted">Recent item deleted</string> <string name="recent_Item_deleted">Recent item deleted</string>
@@ -40,4 +40,8 @@
<string name="use_car_settings">Use car settings</string> <string name="use_car_settings">Use car settings</string>
<string name="exit_number">Exit number</string> <string name="exit_number">Exit number</string>
<string name="navigation_icon_description">Navigation icon</string> <string name="navigation_icon_description">Navigation icon</string>
<string name="distance_units">Distance units</string>
<string name="automaticaly">Automaticaly</string>
<string name="kilometer">Kilometer</string>
<string name="miles">Miles</string>
</resources> </resources>

View File

@@ -35,33 +35,33 @@ class IconMapperTest {
@Test @Test
fun `addLanes returns correct lane direction`() { fun `addLanes returns correct lane direction`() {
val stepDataNormalLeft = StepData("", 0.0, Maneuver.TYPE_TURN_NORMAL_LEFT, 0, 0L, 0.0) val stepDataNormalLeft = StepData("", "", 0.0, Maneuver.TYPE_TURN_NORMAL_LEFT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_NORMAL_LEFT, iconMapper.addLanes("left_straight", stepDataNormalLeft)) assertEquals(LaneDirection.SHAPE_NORMAL_LEFT, iconMapper.addLanes("left_straight", stepDataNormalLeft))
assertEquals(LaneDirection.SHAPE_NORMAL_LEFT, iconMapper.addLanes("left", stepDataNormalLeft)) assertEquals(LaneDirection.SHAPE_NORMAL_LEFT, iconMapper.addLanes("left", stepDataNormalLeft))
assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("left_slight", stepDataNormalLeft)) assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("left_slight", stepDataNormalLeft))
assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("slight_left", stepDataNormalLeft)) assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("slight_left", stepDataNormalLeft))
val stepDataStraight = StepData("", 0.0, Maneuver.TYPE_STRAIGHT, 0, 0L, 0.0) val stepDataStraight = StepData("", "", 0.0, Maneuver.TYPE_STRAIGHT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("left_straight", stepDataStraight)) assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("left_straight", stepDataStraight))
assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataStraight)) assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataStraight))
assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("right_straight", stepDataStraight)) assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("right_straight", stepDataStraight))
val stepDataKeepLeft = StepData("", 0.0, Maneuver.TYPE_KEEP_LEFT, 0, 0L, 0.0) val stepDataKeepLeft = StepData("", "", 0.0, Maneuver.TYPE_KEEP_LEFT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataKeepLeft)) assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataKeepLeft))
assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("left_slight", stepDataKeepLeft)) assertEquals(LaneDirection.SHAPE_SLIGHT_LEFT, iconMapper.addLanes("left_slight", stepDataKeepLeft))
val stepDataKeepRight = StepData("", 0.0, Maneuver.TYPE_KEEP_RIGHT, 0, 0L, 0.0) val stepDataKeepRight = StepData("", "", 0.0, Maneuver.TYPE_KEEP_RIGHT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataKeepRight)) assertEquals(LaneDirection.SHAPE_STRAIGHT, iconMapper.addLanes("straight", stepDataKeepRight))
val stepDataNormalRight = StepData("", 0.0, Maneuver.TYPE_TURN_NORMAL_RIGHT, 0, 0L, 0.0) val stepDataNormalRight = StepData("", "", 0.0, Maneuver.TYPE_TURN_NORMAL_RIGHT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right", stepDataNormalRight)) assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right", stepDataNormalRight))
assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right_straight", stepDataNormalRight)) assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right_straight", stepDataNormalRight))
val stepDataSlightRight = StepData("", 0.0, Maneuver.TYPE_TURN_SLIGHT_RIGHT, 0, 0L, 0.0) val stepDataSlightRight = StepData("", "", 0.0, Maneuver.TYPE_TURN_SLIGHT_RIGHT, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right_slight", stepDataSlightRight)) assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("right_slight", stepDataSlightRight))
assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("slight_right", stepDataSlightRight)) assertEquals(LaneDirection.SHAPE_NORMAL_RIGHT, iconMapper.addLanes("slight_right", stepDataSlightRight))
val stepDataUnknown = StepData("", 0.0, Maneuver.TYPE_UNKNOWN, 0, 0L, 0.0) val stepDataUnknown = StepData("", "", 0.0, Maneuver.TYPE_UNKNOWN, 0, 0L, 0.0)
assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("left_straight", stepDataUnknown)) assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("left_straight", stepDataUnknown))
assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("left", stepDataUnknown)) assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("left", stepDataUnknown))
assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("straight", stepDataUnknown)) assertEquals(LaneDirection.SHAPE_UNKNOWN, iconMapper.addLanes("straight", stepDataUnknown))
@@ -74,24 +74,24 @@ class IconMapperTest {
@Test @Test
fun `laneToResource returns correct resource string`() { fun `laneToResource returns correct resource string`() {
val stepDataNormalLeft = StepData("", 0.0, Maneuver.TYPE_TURN_NORMAL_LEFT, 0, 0L, 0.0) val stepDataNormalLeft = StepData("", "", 0.0, Maneuver.TYPE_TURN_NORMAL_LEFT, 0, 0L, 0.0)
assertEquals("left_o_straight_x", iconMapper.laneToResource(listOf("left", "straight"), stepDataNormalLeft)) assertEquals("left_o_straight_x", iconMapper.laneToResource(listOf("left", "straight"), stepDataNormalLeft))
assertEquals("left_o", iconMapper.laneToResource(listOf("left"), stepDataNormalLeft)) assertEquals("left_o", iconMapper.laneToResource(listOf("left"), stepDataNormalLeft))
assertEquals("slight_left_o", iconMapper.laneToResource(listOf("slight_left"), stepDataNormalLeft)) assertEquals("slight_left_o", iconMapper.laneToResource(listOf("slight_left"), stepDataNormalLeft))
val stepDataStraight = StepData("", 0.0, Maneuver.TYPE_STRAIGHT, 0, 0L, 0.0) val stepDataStraight = StepData("", "", 0.0, Maneuver.TYPE_STRAIGHT, 0, 0L, 0.0)
assertEquals("left_x_straight_o", iconMapper.laneToResource(listOf("left", "straight"), stepDataStraight)) assertEquals("left_x_straight_o", iconMapper.laneToResource(listOf("left", "straight"), stepDataStraight))
assertEquals("straight_o", iconMapper.laneToResource(listOf("straight"), stepDataStraight)) assertEquals("straight_o", iconMapper.laneToResource(listOf("straight"), stepDataStraight))
assertEquals("right_x_straight_o", iconMapper.laneToResource(listOf("right_straight"), stepDataStraight)) assertEquals("right_x_straight_o", iconMapper.laneToResource(listOf("right_straight"), stepDataStraight))
val stepDataNormalRight = StepData("", 0.0, Maneuver.TYPE_TURN_NORMAL_RIGHT, 0, 0L, 0.0) val stepDataNormalRight = StepData("", "", 0.0, Maneuver.TYPE_TURN_NORMAL_RIGHT, 0, 0L, 0.0)
assertEquals("right_x_straight_x", iconMapper.laneToResource(listOf("right_straight"), stepDataNormalRight)) assertEquals("right_x_straight_x", iconMapper.laneToResource(listOf("right_straight"), stepDataNormalRight))
assertEquals("right_o", iconMapper.laneToResource(listOf("right"), stepDataNormalRight)) assertEquals("right_o", iconMapper.laneToResource(listOf("right"), stepDataNormalRight))
val stepDataSlightRight = StepData("", 0.0, Maneuver.TYPE_TURN_SLIGHT_RIGHT, 0, 0L, 0.0) val stepDataSlightRight = StepData("", "", 0.0, Maneuver.TYPE_TURN_SLIGHT_RIGHT, 0, 0L, 0.0)
assertEquals("right_o_straight_o", iconMapper.laneToResource(listOf("right_straight"), stepDataSlightRight)) assertEquals("right_o_straight_o", iconMapper.laneToResource(listOf("right_straight"), stepDataSlightRight))
val stepDataUnknown = StepData("", 0.0, Maneuver.TYPE_UNKNOWN, 0, 0L, 0.0) val stepDataUnknown = StepData("", "", 0.0, Maneuver.TYPE_UNKNOWN, 0, 0L, 0.0)
assertEquals("left_x_straight_x", iconMapper.laneToResource(listOf("left", "straight"), stepDataUnknown)) assertEquals("left_x_straight_x", iconMapper.laneToResource(listOf("left", "straight"), stepDataUnknown))
assertEquals("", iconMapper.laneToResource(listOf("invalid"), stepDataUnknown)) assertEquals("", iconMapper.laneToResource(listOf("invalid"), stepDataUnknown))
} }

View File

@@ -94,7 +94,6 @@ androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "m
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" } kotlin-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }