Diverse Änderungen

This commit is contained in:
Dimitris
2026-02-24 16:29:13 +01:00
parent 71d3d17847
commit e4b539c4e6
31 changed files with 405 additions and 194 deletions

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.kouros.navigation" applicationId = "com.kouros.navigation"
minSdk = 33 minSdk = 33
targetSdk = 36 targetSdk = 36
versionCode = 45 versionCode = 49
versionName = "0.2.0.45" versionName = "0.2.0.49"
base.archivesName = "navi-$versionName" base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -82,6 +82,7 @@ dependencies {
implementation(libs.koin.androidx.compose) implementation(libs.koin.androidx.compose)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
//implementation(libs.maplibre.composeMaterial3) //implementation(libs.maplibre.composeMaterial3)
implementation(libs.androidx.app.projected)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(project(":common:data")) implementation(project(":common:data"))
@@ -97,7 +98,7 @@ dependencies {
implementation("com.github.ticofab:android-gpx-parser:2.3.1") implementation("com.github.ticofab:android-gpx-parser:2.3.1")
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.foundation.layout) implementation(libs.androidx.compose.foundation.layout)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -7,11 +7,16 @@ import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) { class MockLocation (private var locationManager: LocationManager) {
var curSpeed = 0F var curSpeed = 0F
fun setMockLocation(latitude: Double, longitude: Double) { fun setMockLocation(latitude: Double, longitude: Double, bearing : Float) {
try { try {
// Set mock location for all providers // Set mock location for all providers
setMockLocationForProvider(LocationManager.GPS_PROVIDER, latitude, longitude) setMockLocationForProvider(LocationManager.GPS_PROVIDER, latitude, longitude, bearing)
setMockLocationForProvider(LocationManager.NETWORK_PROVIDER, latitude, longitude) setMockLocationForProvider(
LocationManager.NETWORK_PROVIDER,
latitude,
longitude,
bearing
)
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
} catch (e: SecurityException) { } catch (e: SecurityException) {
} catch (e: Exception) { } catch (e: Exception) {
@@ -19,7 +24,12 @@ class MockLocation (private var locationManager: LocationManager) {
} }
} }
private fun setMockLocationForProvider(provider: String, latitude: Double, longitude: Double) { private fun setMockLocationForProvider(
provider: String,
latitude: Double,
longitude: Double,
bearing: Float
) {
try { try {
// Check if provider exists // Check if provider exists
if (!locationManager.allProviders.contains(provider)) { if (!locationManager.allProviders.contains(provider)) {
@@ -52,10 +62,10 @@ class MockLocation (private var locationManager: LocationManager) {
this.speed = 0f this.speed = 0f
this.time = System.currentTimeMillis() this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos() this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
this.bearingAccuracyDegrees = 0.0f this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f this.speedAccuracyMetersPerSecond = 0.0f
this.bearing = bearing
} }
// Set the mock location // Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation) locationManager.setTestProviderLocation(provider, mockLocation)
@@ -79,6 +89,7 @@ class MockLocation (private var locationManager: LocationManager) {
this.bearingAccuracyDegrees = 0.0f this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f this.speedAccuracyMetersPerSecond = 0.0f
this.bearing = bearing
} }
locationManager.setTestProviderLocation(provider, mockLocation) locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) { } catch (ex: Exception) {

View File

@@ -77,19 +77,16 @@ import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
val routeData = MutableLiveData("") val routeData = MutableLiveData("")
val routeModel = RouteModel() val routeModel = RouteModel()
var tilt = 50.0 var tilt = 50.0
val useMock = false val useMock = false
val type = 1 // 1 simulate 2 test 3 gpx 4 testSingle val type = 3 // 1 simulate 2 test 3 gpx 4 testSingle
val stepData: MutableLiveData<StepData> by lazy { val stepData: MutableLiveData<StepData> by lazy {
MutableLiveData() MutableLiveData()
@@ -125,7 +122,6 @@ class MainActivity : ComponentActivity() {
private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation private lateinit var mock: MockLocation
private var loadRecentPlaces = false private var loadRecentPlaces = false
lateinit var baseStyle: BaseStyle.Json
@RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) @RequiresPermission(allOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -140,7 +136,7 @@ class MainActivity : ComponentActivity() {
if (useMock) { if (useMock) {
mock = MockLocation(locationManager) mock = MockLocation(locationManager)
mock.setMockLocation( mock.setMockLocation(
homeVogelhart.latitude, homeVogelhart.longitude homeVogelhart.latitude, homeVogelhart.longitude, 0F
) )
navigationViewModel.route.observe(this, observer) navigationViewModel.route.observe(this, observer)
} }
@@ -190,8 +186,9 @@ class MainActivity : ComponentActivity() {
val appViewModel: AppViewModel = appViewModel() val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState() val darkMode by appViewModel.darkMode.collectAsState()
val sheetPeekHeight = 250.dp val sheetPeekHeight = 250.dp
baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1) val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val scaffoldState = rememberBottomSheetScaffoldState() val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -240,7 +237,7 @@ class MainActivity : ComponentActivity() {
cameraPosition, cameraPosition,
routeData, routeData,
tilt, tilt,
baseStyle baseStyle,
) )
} }
if (!routeModel.isNavigating()) { if (!routeModel.isNavigating()) {
@@ -304,6 +301,9 @@ class MainActivity : ComponentActivity() {
fun updateLocation(location: Location?) { fun updateLocation(location: Location?) {
if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) { if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) {
val currentLocation = location(location.position.longitude, location.position.latitude) val currentLocation = location(location.position.longitude, location.position.latitude)
if (location.bearing != null) {
currentLocation.bearing = location.bearing!!.toFloat()
}
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val snapedLocation = val snapedLocation =
snapLocation(currentLocation, routeModel.route.maneuverLocations()) snapLocation(currentLocation, routeModel.route.maneuverLocations())
@@ -315,7 +315,15 @@ class MainActivity : ComponentActivity() {
} }
fun updateLocationInternal(currentLocation: android.location.Location, location: Location?) { fun updateLocationInternal(currentLocation: android.location.Location, location: Location?) {
val bearing = bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing) if (currentLocation.hasBearing()) {
routeModel.navState = routeModel.navState.copy(routeBearing = currentLocation.bearing)
}
val bearing = if (currentLocation.hasBearing()) {
currentLocation.bearing.toDouble()
} else {
bearing(lastLocation, currentLocation, cameraPosition.value!!.bearing)
}
with(routeModel) { with(routeModel) {
if (isNavigating()) { if (isNavigating()) {
updateLocation(applicationContext, currentLocation, navigationViewModel) updateLocation(applicationContext, currentLocation, navigationViewModel)
@@ -348,7 +356,7 @@ class MainActivity : ComponentActivity() {
closeSheet() closeSheet()
routeModel.stopNavigation(applicationContext) routeModel.stopNavigation(applicationContext)
if (useMock) { if (useMock) {
mock.setMockLocation(latitude, longitude) mock.setMockLocation(latitude, longitude, 0F)
} }
routeData.value = "" routeData.value = ""
stepData.value = StepData("", 0.0, 0, 0, 0, 0.0) stepData.value = StepData("", 0.0, 0, 0, 0, 0.0)
@@ -380,14 +388,18 @@ class MainActivity : ComponentActivity() {
fun simulate() { fun simulate() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) { for ((index, waypoint) in routeModel.curRoute.waypoints.withIndex()) {
val curLocation = location(waypoint[0], waypoint[1])
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
val deviation = 0.0 val deviation = 0.0
if (index in 0..routeModel.curRoute.waypoints.size) { if (index in 0..routeModel.curRoute.waypoints.size) {
mock.setMockLocation(waypoint[1], waypoint[0]) val bearing = lastLocation.bearingTo(curLocation)
mock.setMockLocation(waypoint[1], waypoint[0], bearing)
Thread.sleep(1000) Thread.sleep(1000)
} }
} }
lastLocation = curLocation
} }
} }
} }
@@ -416,7 +428,7 @@ class MainActivity : ComponentActivity() {
fun testSingleUpdate(latitude: Double, longitude: Double) { fun testSingleUpdate(latitude: Double, longitude: Double) {
if (1 == 1) { if (1 == 1) {
mock.setMockLocation(latitude, longitude) mock.setMockLocation(latitude, longitude, 0F)
} else { } else {
routeModel.updateLocation( routeModel.updateLocation(
applicationContext, applicationContext,
@@ -425,12 +437,12 @@ class MainActivity : ComponentActivity() {
} }
val step = routeModel.currentStep() val step = routeModel.currentStep()
val nextStep = routeModel.nextStep() val nextStep = routeModel.nextStep()
println("Step: ${step.instruction} ${step.leftStepDistance} ${nextStep.currentManeuverType}")
Thread.sleep(1_000) Thread.sleep(1_000)
} }
fun gpx(context: Context) { fun gpx(context: Context) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
val parser = GPXParser() val parser = GPXParser()
val input = context.resources.openRawResource(R.raw.vh) val input = context.resources.openRawResource(R.raw.vh)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
@@ -441,18 +453,23 @@ class MainActivity : ComponentActivity() {
segments!!.forEach { seg -> segments!!.forEach { seg ->
var lastTime = DateTime.now() var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p -> seg!!.trackPoints.forEach { p ->
val curLocation = location(p.longitude, p.latitude)
val ext = p.extensions val ext = p.extensions
val speed: Double? val speed: Double?
if (ext != null) { if (ext != null) {
speed = ext.speed speed = ext.speed
mock.curSpeed = speed.toFloat() mock.curSpeed = speed.toFloat()
} }
val duration = p.time.millis - lastTime.millis val duration = p.time.millis - lastTime.millis
mock.setMockLocation(p.latitude, p.longitude) val bearing = lastLocation.bearingTo(curLocation)
println("Bearing $bearing")
mock.setMockLocation(p.latitude, p.longitude, bearing)
if (duration > 0) { if (duration > 0) {
delay(duration / 5) delay(duration / 5)
} }
lastTime = p.time lastTime = p.time
lastLocation = curLocation
} }
} }
} }

View File

@@ -4,26 +4,21 @@ import android.content.Context
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.data.StepData import com.kouros.navigation.data.StepData
import com.kouros.navigation.ui.app.AppViewModel import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.utils.settingsViewModel
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.LocationTrackingEffect
@@ -64,8 +59,6 @@ fun MapView(
) )
) )
val rememberBaseStyle = rememberBaseStyle(baseStyle)
val appViewModel: AppViewModel = appViewModel() val appViewModel: AppViewModel = appViewModel()
val showBuildings by appViewModel.show3D.collectAsState() val showBuildings by appViewModel.show3D.collectAsState()
@@ -73,9 +66,8 @@ fun MapView(
NavigationInfo(step, nextStep) NavigationInfo(step, nextStep)
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
MapLibre( MapLibre(
applicationContext,
cameraState, cameraState,
rememberBaseStyle, baseStyle,
route, route,
emptyMap(), emptyMap(),
ViewStyle.VIEW, ViewStyle.VIEW,

View File

@@ -8,9 +8,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.car.permission.CAR_SPEED"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" /> <uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<!-- Various required feature settings for an automotive app. -->
<uses-feature <uses-feature
android:name="android.hardware.type.automotive" android:name="android.hardware.type.automotive"
android:required="true" /> android:required="true" />

View File

@@ -46,7 +46,6 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
implementation(libs.androidx.app.projected)
//implementation(libs.maplibre.composeMaterial3) //implementation(libs.maplibre.composeMaterial3)
implementation(project(":common:data")) implementation(project(":common:data"))

View File

@@ -49,6 +49,7 @@
<category android:name="androidx.car.app.category.WEATHER" /> <category android:name="androidx.car.app.category.WEATHER" />
<category android:name="androidx.car.app.category.POI"/> <category android:name="androidx.car.app.category.POI"/>
<category android:name="androidx.car.app.category.NAVIGATION"/> <category android:name="androidx.car.app.category.NAVIGATION"/>
<category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
</intent-filter> </intent-filter>
</service> </service>
</application> </application>

View File

@@ -13,6 +13,7 @@ import androidx.car.app.CarContext
import androidx.car.app.Screen 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.hardware.CarHardwareManager import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarValue import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener import androidx.car.app.hardware.common.OnCarDataAvailableListener
@@ -36,6 +37,7 @@ import com.kouros.navigation.car.screen.SearchScreen
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
import com.kouros.navigation.data.ObjectBox
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
@@ -48,7 +50,7 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import android.Manifest.permission
class NavigationSession : Session(), NavigationScreen.Listener { class NavigationSession : Session(), NavigationScreen.Listener {
@@ -82,14 +84,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo removeSensors()
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
carInfo.removeSpeedListener(carSpeedListener)
Log.i(TAG, "In onDestroy()") Log.i(TAG, "In onDestroy()")
val locationManager = val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@@ -139,6 +134,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
} }
fun onPermissionGranted(permission : Boolean) {
addSensors(routeModel.navState.carConnection)
}
fun onConnectionStateUpdated(connectionState: Int) {
routeModel.navState = routeModel.navState.copy(carConnection = connectionState)
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_NATIVE -> {
ObjectBox.init(carContext)
navigationScreen.checkPermission("android.car.permission.CAR_SPEED")
} // Automotive OS
CarConnection.CONNECTION_TYPE_PROJECTION -> {
"Connected to Android Auto"
navigationScreen.checkPermission("com.google.android.gms.permission.CAR_SPEED")
}
}
}
override fun onCreateScreen(intent: Intent): Screen { override fun onCreateScreen(intent: Intent): Screen {
@@ -153,18 +166,16 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
} }
// lifecycleScope.launch {
//}
navigationViewModel = getViewModel(carContext) navigationViewModel = getViewModel(carContext)
navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated) navigationViewModel.routingEngine.observe(this, ::onRoutingEngineStateUpdated)
navigationViewModel.permissionGranted.observe(this, ::onPermissionGranted)
routeModel = RouteCarModel() routeModel = RouteCarModel()
CarConnection(carContext).type.observe(this, ::onConnectionStateUpdated)
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner) surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
navigationScreen = navigationScreen =
@@ -172,7 +183,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) if ( carContext.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED == PackageManager.PERMISSION_GRANTED
&& carContext.checkSelfPermission("com.google.android.gms.permission.CAR_SPEED") == PackageManager.PERMISSION_GRANTED
&& !useContacts && !useContacts
|| (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS) || (useContacts && carContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED) == PackageManager.PERMISSION_GRANTED)
@@ -180,27 +190,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
requestLocationUpdates() requestLocationUpdates()
} else { } else {
// If we do not have the location permission, show the request permission screen. // If we do not have the location permission, show the request permission screen.
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
val screenManager = val screenManager =
carContext.getCarService(ScreenManager::class.java) carContext.getCarService(ScreenManager::class.java)
screenManager screenManager
.push(navigationScreen) .push(navigationScreen)
return RequestPermissionScreen( return RequestPermissionScreen(
carContext, carContext,
mLocationPermissionCheckCallback = { permissionCheckCallback = {
screenManager.pop() screenManager.pop()
}, },
mContactsPermissionCheckCallback = {
screenManager.pop()
}
) )
} }
addSensors()
return navigationScreen return navigationScreen
} }
fun addSensors() { fun addSensors(connectionState: Int) {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext) val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() } val useCarLocation = runBlocking { repository.carLocationFlow.first() }
@@ -215,8 +222,25 @@ class NavigationSession : Session(), NavigationScreen.Listener {
carLocationListener carLocationListener
) )
} }
if (connectionState == CarConnection.CONNECTION_TYPE_NATIVE
|| connectionState == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener) carInfo.addSpeedListener(carContext.mainExecutor, carSpeedListener)
} }
}
fun removeSensors() {
val carInfo = carContext.getCarService(CarHardwareManager::class.java).carInfo
val repository = getSettingsRepository(carContext)
val useCarLocation = runBlocking { repository.carLocationFlow.first() }
if (useCarLocation) {
val carSensors = carContext.getCarService(CarHardwareManager::class.java).carSensors
carSensors.removeCarHardwareLocationListener(carLocationListener)
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_NATIVE
|| routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
carInfo.removeSpeedListener(carSpeedListener)
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
val screenManager = carContext.getCarService(ScreenManager::class.java) val screenManager = carContext.getCarService(ScreenManager::class.java)
@@ -277,6 +301,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
} }
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
if (location.hasBearing()) {
routeModel.navState = routeModel.navState.copy(routeBearing = location.bearing)
}
if (routeModel.isNavigating()) { if (routeModel.isNavigating()) {
navigationScreen.updateTrip(location) navigationScreen.updateTrip(location)
if (!routeModel.navState.arrived) { if (!routeModel.navState.arrived) {

View File

@@ -29,7 +29,6 @@ import com.kouros.navigation.car.map.DrawNavigationImages
import com.kouros.navigation.car.map.MapLibre import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.map.rememberBaseStyle
import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.homeVogelhart import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.ObjectBox import com.kouros.navigation.data.ObjectBox
@@ -79,6 +78,7 @@ class SurfaceRenderer(
val trafficData = MutableLiveData(emptyMap<String, String>()) val trafficData = MutableLiveData(emptyMap<String, String>())
val speedCamerasData = MutableLiveData("") val speedCamerasData = MutableLiveData("")
val speed = MutableLiveData(0F) val speed = MutableLiveData(0F)
val maxSpeed = MutableLiveData(0) val maxSpeed = MutableLiveData(0)
var viewStyle = ViewStyle.VIEW var viewStyle = ViewStyle.VIEW
lateinit var centerLocation: Location lateinit var centerLocation: Location
@@ -175,14 +175,6 @@ class SurfaceRenderer(
} }
fun onConnectionStateUpdated(connectionState: Int) {
when (connectionState) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "Not connected to a head unit"
CarConnection.CONNECTION_TYPE_PROJECTION -> "Connected to Android Auto"
CarConnection.CONNECTION_TYPE_NATIVE -> ObjectBox.init(carContext) // Automotive OS
}
}
fun onBaseStyleStateUpdated(style: BaseStyle) { fun onBaseStyleStateUpdated(style: BaseStyle) {
} }
@@ -190,29 +182,24 @@ class SurfaceRenderer(
@Composable @Composable
fun MapView() { fun MapView() {
//val appViewModel: AppViewModel = appViewModel(viewModelStoreOwner)
//val darkMode by appViewModel.darkMode.collectAsState()
val darkMode = settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value val darkMode = settingsViewModel(carContext, viewModelStoreOwner).darkMode.collectAsState().value
val showBuildings = settingsViewModel(carContext, viewModelStoreOwner).show3D.collectAsState().value
val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
val position: CameraPosition? by cameraPosition.observeAsState() val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState() val route: String? by routeData.observeAsState()
val traffic: Map<String, String> ? by trafficData.observeAsState() val traffic: Map<String, String> ? by trafficData.observeAsState()
val speedCameras: String? by speedCamerasData.observeAsState() val speedCameras: String? by speedCamerasData.observeAsState()
val paddingValues = getPaddingValues(height, viewStyle) val paddingValues = getPaddingValues(height, viewStyle)
val cameraState = cameraState(paddingValues, position, tilt) val cameraState = cameraState(paddingValues, position, tilt)
val rememberBaseStyle = rememberBaseStyle(baseStyle) val baseStyle = BaseStyleModel().readStyle(carContext, darkMode, carContext.isDarkMode)
MapLibre( MapLibre(
carContext,
cameraState, cameraState,
rememberBaseStyle, baseStyle,
route, route,
traffic, traffic,
viewStyle, viewStyle,
speedCameras, speedCameras,
false showBuildings
) )
ShowPosition(cameraState, position, paddingValues) ShowPosition(cameraState, position, paddingValues)
} }
@@ -226,12 +213,12 @@ class SurfaceRenderer(
val cameraDuration = val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing) duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
val currentSpeed: Float? by speed.observeAsState() val currentSpeed: Float? by speed.observeAsState()
val maxSpeed: Int? by maxSpeed.observeAsState() val speed: Int? by maxSpeed.observeAsState()
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
DrawNavigationImages( DrawNavigationImages(
paddingValues, paddingValues,
currentSpeed, currentSpeed,
maxSpeed!!, speed!!,
width, width,
height height
) )
@@ -251,7 +238,6 @@ class SurfaceRenderer(
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
CarConnection(carContext).type.observe(owner, ::onConnectionStateUpdated)
style.observe(owner, :: onBaseStyleStateUpdated) style.observe(owner, :: onBaseStyleStateUpdated)
Log.i(TAG, "SurfaceRenderer created") Log.i(TAG, "SurfaceRenderer created")
carContext.getCarService(AppManager::class.java) carContext.getCarService(AppManager::class.java)
@@ -281,15 +267,19 @@ class SurfaceRenderer(
fun updateLocation(location: Location) { fun updateLocation(location: Location) {
synchronized(this) { synchronized(this) {
if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) { if (viewStyle == ViewStyle.VIEW || viewStyle == ViewStyle.PAN_VIEW) {
val bearing = if (carOrientation == 999F) val bearing = if (carOrientation == 999F) {
if (location.hasBearing()) {
location.bearing.toDouble()
} else {
bearing( bearing(
lastLocation, lastLocation,
location, location,
cameraPosition.value!!.bearing cameraPosition.value!!.bearing
) else { )
}
} else {
carOrientation.toDouble() carOrientation.toDouble()
} }
println("Bearing $bearing")
val zoom = if (viewStyle == ViewStyle.VIEW) { val zoom = if (viewStyle == ViewStyle.VIEW) {
calculateZoom(location.speed.toDouble()) calculateZoom(location.speed.toDouble())
} else { } else {

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.map package com.kouros.navigation.car.map
import android.content.Context
import android.location.Location import android.location.Location
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -10,8 +9,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -26,18 +23,13 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.kouros.data.R import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.RouteColor import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.datastore.DataStoreManager
import com.kouros.navigation.model.RouteModel import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
@@ -89,7 +81,6 @@ fun cameraState(
@Composable @Composable
fun MapLibre( fun MapLibre(
context: Context,
cameraState: CameraState, cameraState: CameraState,
baseStyle: BaseStyle.Json, baseStyle: BaseStyle.Json,
route: String?, route: String?,
@@ -229,15 +220,15 @@ fun trafficColor(key: String): Expression<ColorValue> {
@Composable @Composable
fun AmenityLayer(routeData: String?) { fun AmenityLayer(routeData: String?) {
if (routeData != null && routeData.isNotEmpty()) { if (!routeData.isNullOrEmpty()) {
var color = const(Color.Red) var color = const(Color.Red)
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true) var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
if (routeData.contains(Constants.CHARGING_STATION)) { if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color.Green) color = const(Color(0xFF054603))
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true) img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)) { } else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Black) color = const(Color.Blue)
img = image(painterResource(R.drawable.local_gas_station_48px), drawAsSdf = true) img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
} }
val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData)) val routes = rememberGeoJsonSource(GeoJsonData.JsonString(routeData))
SymbolLayer( SymbolLayer(
@@ -261,7 +252,7 @@ fun AmenityLayer(routeData: String?) {
@Composable @Composable
fun SpeedCameraLayer(speedCameras: String?) { fun SpeedCameraLayer(speedCameras: String?) {
if (speedCameras != null && speedCameras.isNotEmpty()) { if (!speedCameras.isNullOrEmpty()) {
val color = const(Color.Red) val color = const(Color.Red)
val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras)) val cameraSource = rememberGeoJsonSource(GeoJsonData.JsonString(speedCameras))
SymbolLayer( SymbolLayer(
@@ -497,14 +488,6 @@ fun DebugInfo(
} }
} }
@Composable
fun rememberBaseStyle(baseStyle: BaseStyle.Json): BaseStyle.Json {
val rememberBaseStyle by remember() {
mutableStateOf(baseStyle)
}
return rememberBaseStyle
}
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues { fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) { return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues( ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(

View File

@@ -76,7 +76,7 @@ class CategoriesScreen(
fun carIcon(context: CarContext, id: String): CarIcon { fun carIcon(context: CarContext, id: String): CarIcon {
val resId = when (id) { val resId = when (id) {
FUEL_STATION -> R.drawable.local_gas_station_48px FUEL_STATION -> R.drawable.local_gas_station_24
PHARMACY -> R.drawable.local_pharmacy_48px PHARMACY -> R.drawable.local_pharmacy_48px
CHARGING_STATION -> R.drawable.ev_station_48px CHARGING_STATION -> R.drawable.ev_station_48px
else -> {} else -> {}

View File

@@ -46,7 +46,7 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
) )
.addItem( .addItem(
buildRowForTemplate( buildRowForTemplate(
R.string.use_telephon_settings, R.string.use_car_settings,
) )
) )
.setOnSelectedListener { index: Int -> .setOnSelectedListener { index: Int ->
@@ -74,14 +74,6 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
private fun onSelected(index: Int) { private fun onSelected(index: Int) {
settingsViewModel.onDarkModeChanged(index) settingsViewModel.onDarkModeChanged(index)
CarToast.makeText(
carContext,
(carContext
.getString(R.string.display_settings)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
} }
private fun buildRowForTemplate(title: Int): Row { private fun buildRowForTemplate(title: Int): Row {

View File

@@ -1,11 +1,14 @@
package com.kouros.navigation.car.screen package com.kouros.navigation.car.screen
import android.Manifest
import android.content.pm.PackageManager
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler import android.os.Handler
import androidx.car.app.CarContext import androidx.car.app.CarContext
import androidx.car.app.Screen import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.model.Action import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip import androidx.car.app.model.ActionStrip
@@ -111,6 +114,9 @@ class NavigationScreen(
surfaceRenderer.speedCamerasData.value = speedData surfaceRenderer.speedCamerasData.value = speedData
} }
val maxSpeedObserver = Observer<Int> { speed ->
surfaceRenderer.maxSpeed.value = speed
}
init { init {
navigationViewModel.route.observe(this, observer) navigationViewModel.route.observe(this, observer)
@@ -118,6 +124,7 @@ class NavigationScreen(
navigationViewModel.recentPlace.observe(this, recentObserver) navigationViewModel.recentPlace.observe(this, recentObserver)
navigationViewModel.placeLocation.observe(this, placeObserver) navigationViewModel.placeLocation.observe(this, placeObserver)
navigationViewModel.speedCameras.observe(this, speedObserver) navigationViewModel.speedCameras.observe(this, speedObserver)
navigationViewModel.maxSpeed.observe(this, maxSpeedObserver)
lifecycleScope.launch { lifecycleScope.launch {
getSettingsViewModel(carContext).routingEngine.collect { getSettingsViewModel(carContext).routingEngine.collect {
} }
@@ -552,6 +559,29 @@ class NavigationScreen(
} }
} }
} }
fun checkPermission(permission: String) {
println("Car connection permission: $permission")
if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission)
val screenManager =
carContext.getCarService(ScreenManager::class.java)
screenManager
.push(
RequestPermissionScreen(
carContext,
permissionCheckCallback = {
screenManager.pop()
navigationViewModel.permissionGranted.value = true
},
permissions
)
)
} else {
navigationViewModel.permissionGranted.value = true
}
}
} }
enum class NavigationType { enum class NavigationType {

View File

@@ -16,7 +16,10 @@ import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class NavigationSettings(private val carContext: CarContext, private var navigationViewModel: NavigationViewModel) : class NavigationSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) :
Screen(carContext) { Screen(carContext) {
private var motorWayToggleState = false private var motorWayToggleState = false
@@ -79,6 +82,12 @@ class NavigationSettings(private val carContext: CarContext, private var navigat
R.string.routing_engine R.string.routing_engine
) )
) )
.addItem(
buildRowForScreenTemplate(
PasswordSettings(carContext, navigationViewModel),
R.string.tomtom_api_key
)
)
return ListTemplate.Builder() return ListTemplate.Builder()
.setSingleList(listBuilder.build()) .setSingleList(listBuilder.build())
.setHeader( .setHeader(

View File

@@ -0,0 +1,67 @@
package com.kouros.navigation.car.screen
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.InputCallback
import androidx.car.app.model.ParkedOnlyOnClickListener
import androidx.car.app.model.Template
import androidx.car.app.model.signin.InputSignInMethod
import androidx.car.app.model.signin.SignInTemplate
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.launch
class PasswordSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) : Screen(carContext) {
var errorMessage: String? = null
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.tomTomApiKey.collect {
invalidate()
}
}
}
override fun onGetTemplate(): Template {
val apiKey = settingsViewModel.tomTomApiKey.value
val callback: InputCallback = object : InputCallback {
override fun onInputSubmitted(text: String) {
settingsViewModel.onTomTomApiKeyChanged(text)
//errorMessage = carContext.getString(R.string.tomtom_api_key)
invalidate()
}
}
val builder = InputSignInMethod.Builder(callback)
.setHint(apiKey)
.setInputType(InputSignInMethod.INPUT_TYPE_PASSWORD)
if (errorMessage != null) {
// builder.setErrorMessage(errorMessage)
}
val signInMethod = builder.build()
val pinSignInAction = Action.Builder()
.setTitle(carContext.getString(R.string.stop_action_title))
.setOnClickListener(ParkedOnlyOnClickListener.create {
println("Sign")
invalidate()
})
.build()
return SignInTemplate.Builder(signInMethod)
//.addAction(pinSignInAction)
.setTitle(carContext.getString(R.string.settings_action_title))
.setInstructions(
carContext.getString(R.string.tomtom_api_key)
)
.setHeaderAction(Action.BACK)
.build()
}
}

View File

@@ -14,22 +14,26 @@ import androidx.car.app.model.Template
/** Screen for asking the user to grant location permission. */ /** Screen for asking the user to grant location permission. */
class RequestPermissionScreen( class RequestPermissionScreen(
carContext: CarContext, carContext: CarContext,
var mLocationPermissionCheckCallback: LocationPermissionCheckCallback, var permissionCheckCallback: PermissionCheckCallback,
var mContactsPermissionCheckCallback: LocationPermissionCheckCallback, //var mContactsPermissionCheckCallback: LocationPermissionCheckCallback,
val permissions: MutableList<String?> = ArrayList()
) : Screen(carContext) { ) : Screen(carContext) {
/** Callback called when the location permission is granted. */
fun interface LocationPermissionCheckCallback { /** Callback called when the permission is granted. */
/** Callback called when the location permission is granted. */ fun interface PermissionCheckCallback {
/** Callback called when the permission is granted. */
fun onPermissionGranted() fun onPermissionGranted()
} }
override fun onGetTemplate(): Template { override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add("com.google.android.gms.permission.CAR_SPEED")
//permissions.add(permission.READ_CONTACTS)
val message = "This app needs access to location and to car speed" var message = ""
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"))
message = "This app needs access to car speed"
if (permissions.contains("com.google.android.gms.permission.CAR_SPEED"))
message = "This app needs access to car speed"
val listener: OnClickListener = ParkedOnlyOnClickListener.create { val listener: OnClickListener = ParkedOnlyOnClickListener.create {
carContext.requestPermissions( carContext.requestPermissions(
@@ -41,8 +45,8 @@ class RequestPermissionScreen(
CarToast.LENGTH_LONG CarToast.LENGTH_LONG
).show() ).show()
if (!approved!!.isEmpty()) { if (!approved!!.isEmpty()) {
mLocationPermissionCheckCallback.onPermissionGranted() permissionCheckCallback.onPermissionGranted()
mContactsPermissionCheckCallback.onPermissionGranted() //mContactsPermissionCheckCallback.onPermissionGranted()
finish() finish()
} }
} }

View File

@@ -79,7 +79,7 @@ class RoutePreviewScreen(
val itemListBuilder = ItemList.Builder() val itemListBuilder = ItemList.Builder()
var i = 0 var i = 0
routeModel.route.routes.forEach { it -> routeModel.route.routes.forEach { _ ->
itemListBuilder.addItem(createRow(i++, navigateAction)) itemListBuilder.addItem(createRow(i++, navigateAction))
} }
@@ -95,7 +95,7 @@ class RoutePreviewScreen(
.build() .build()
val message = val message =
if (routeModel.isNavigating() && routeModel.curRoute.waypoints!!.isNotEmpty()) { if (routeModel.isNavigating() && routeModel.curRoute.waypoints.isNotEmpty()) {
createRouteText(0) createRouteText(0)
} else { } else {
CarText.Builder("Wait") CarText.Builder("Wait")
@@ -190,7 +190,7 @@ class RoutePreviewScreen(
private fun createRouteText(index: Int): CarText { private fun createRouteText(index: Int): CarText {
val time = routeModel.route.routes[index].summary.duration val time = routeModel.route.routes[index].summary.duration
val length = val length =
BigDecimal(routeModel.route.routes[index].summary.distance).setScale( BigDecimal((routeModel.route.routes[index].summary.distance) / 1000).setScale(
1, 1,
RoundingMode.HALF_EVEN RoundingMode.HALF_EVEN
) )
@@ -207,7 +207,7 @@ class RoutePreviewScreen(
val titleText = "$index" val titleText = "$index"
return Row.Builder() return Row.Builder()
.setTitle(route) .setTitle(route)
.setOnClickListener(OnClickListener { onRouteSelected(index) }) .setOnClickListener { onRouteSelected(index) }
.addText(titleText) .addText(titleText)
.addAction(action) .addAction(action)
.build() .build()

View File

@@ -73,14 +73,6 @@ class RoutingSettings(private val carContext: CarContext, private var navigation
private fun onSelected(index: Int) { private fun onSelected(index: Int) {
settingsViewModel.onRoutingEngineChanged(index) settingsViewModel.onRoutingEngineChanged(index)
navigationViewModel.routingEngine.value = index navigationViewModel.routingEngine.value = index
CarToast.makeText(
carContext,
(carContext
.getString(R.string.routing_engine)
+ ":"
+ " " + index), CarToast.LENGTH_LONG
)
.show()
} }
private fun buildRowForTemplate(title: Int): Row { private fun buildRowForTemplate(title: Int): Row {

View File

@@ -124,7 +124,7 @@ object Constants {
val homeVogelhart = location(11.5793748, 48.185749) val homeVogelhart = location(11.5793748, 48.185749)
val homeHohenwaldeck = location( 11.594322, 48.1164817) val homeHohenwaldeck = location( 11.594322, 48.1164817)
const val NEXT_STEP_THRESHOLD = 500.0 const val NEXT_STEP_THRESHOLD = 300.0
const val MAXIMAL_SNAP_CORRECTION = 50.0 const val MAXIMAL_SNAP_CORRECTION = 50.0

View File

@@ -33,59 +33,87 @@ import org.maplibre.geojson.FeatureCollection
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
/**
* ViewModel for navigation-related data operations.
* Handles route calculation, place search, traffic information, and local data persistence.
*/
class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() { class NavigationViewModel(private val repository: NavigationRepository) : ViewModel() {
/** LiveData containing the calculated route JSON string */
val route: MutableLiveData<String> by lazy { val route: MutableLiveData<String> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing categorized traffic incidents map */
val traffic: MutableLiveData<Map<String, String>> by lazy { val traffic: MutableLiveData<Map<String, String>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing a preview route JSON string for route preview screens */
val previewRoute: MutableLiveData<String> by lazy { val previewRoute: MutableLiveData<String> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing the most recent place used for navigation */
val recentPlace: MutableLiveData<Place> by lazy { val recentPlace: MutableLiveData<Place> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing list of recent navigation destinations */
val places: MutableLiveData<List<Place>> by lazy { val places: MutableLiveData<List<Place>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing list of favorite saved places */
val favorites: MutableLiveData<List<Place>> by lazy { val favorites: MutableLiveData<List<Place>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing search results from Nominatim geocoding */
val searchPlaces: MutableLiveData<List<SearchResult>> by lazy { val searchPlaces: MutableLiveData<List<SearchResult>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing the best matching place from address search */
val placeLocation: MutableLiveData<SearchResult> by lazy { val placeLocation: MutableLiveData<SearchResult> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing contacts with addresses */
val contactAddress: MutableLiveData<List<Place>> by lazy { val contactAddress: MutableLiveData<List<Place>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing POI elements from Overpass API */
val elements: MutableLiveData<List<Elements>> by lazy { val elements: MutableLiveData<List<Elements>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing speed camera locations */
val speedCameras: MutableLiveData<List<Elements>> by lazy { val speedCameras: MutableLiveData<List<Elements>> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing current road speed limit */
val maxSpeed: MutableLiveData<Int> by lazy { val maxSpeed: MutableLiveData<Int> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData containing selected routing engine index */
val routingEngine: MutableLiveData<Int> by lazy { val routingEngine: MutableLiveData<Int> by lazy {
MutableLiveData() MutableLiveData()
} }
/** LiveData indicating whether permission is granted */
val permissionGranted: MutableLiveData<Boolean> by lazy {
MutableLiveData()
}
/**
* Loads the most recent place from ObjectBox and calculates its distance.
* Posts the result to recentPlace LiveData if distance > 1km.
*/
fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) { fun loadRecentPlace(location: Location, carOrientation: Float, context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -116,6 +144,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Loads all recent places from ObjectBox and calculates distances.
* Posts the sorted list to places LiveData.
*/
fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) { fun loadRecentPlaces(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -146,6 +178,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Loads favorite places from ObjectBox and calculates distances.
* Posts the sorted list to favorites LiveData.
*/
fun loadFavorites(context: Context, location: Location, carOrientation: Float) { fun loadFavorites(context: Context, location: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -175,6 +211,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Calculates a route between current location and destination.
* Posts the route JSON to route LiveData.
*/
fun loadRoute( fun loadRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
@@ -198,6 +238,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Fetches traffic incident data and categorizes by severity.
* Posts categorized traffic map to traffic LiveData.
*/
fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) { fun loadTraffic(context: Context, currentLocation: Location, carOrientation: Float) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -216,6 +260,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Categorizes traffic incidents by type (queuing, stationary, slow, heavy, roadworks).
* @param data Raw traffic GeoJSON string
* @return Map of incident type to GeoJSON FeatureCollection
*/
private fun rebuildTraffic(data: String): Map<String, String> { private fun rebuildTraffic(data: String): Map<String, String> {
val featureCollection = FeatureCollection.fromJson(data) val featureCollection = FeatureCollection.fromJson(data)
val incidents = mutableMapOf<String, String>() val incidents = mutableMapOf<String, String>()
@@ -238,6 +287,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return incidents return incidents
} }
/**
* Calculates a preview route for route preview screen.
* Posts the route JSON to previewRoute LiveData.
*/
fun loadPreviewRoute( fun loadPreviewRoute(
context: Context, context: Context,
currentLocation: Location, currentLocation: Location,
@@ -261,6 +314,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Loads device contacts with addresses and converts to Place objects.
* Posts results to contactAddress LiveData.
*/
fun loadContacts(context: Context) { fun loadContacts(context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val contactList = mutableListOf<Place>() val contactList = mutableListOf<Place>()
@@ -288,7 +345,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Finds the closest matching address for a search query.
* Posts the best result to placeLocation LiveData.
*/
fun findAddress(search: String, location: Location) { fun findAddress(search: String, location: Location) {
var sortedList: List<SearchResult> var sortedList: List<SearchResult>
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -310,6 +370,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Searches for places using Nominatim geocoding API.
* Posts sorted results to searchPlaces LiveData.
*/
fun searchPlaces(search: String, location: Location) { fun searchPlaces(search: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val placesJson = repository.searchPlaces(search, location) val placesJson = repository.searchPlaces(search, location)
@@ -330,6 +394,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Performs reverse geocoding to get street name from coordinates.
* @return Street name or empty string
*/
fun reverseAddress(location: Location): String { fun reverseAddress(location: Location): String {
val address = repository.reverseAddress(location) val address = repository.reverseAddress(location)
val gson = GsonBuilder().serializeNulls().create() val gson = GsonBuilder().serializeNulls().create()
@@ -337,6 +405,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return place.address.road return place.address.road
} }
/**
* Queries Overpass API for nearby amenities of a specific category.
* Posts sorted results to elements LiveData.
*/
fun getAmenities(category: String, location: Location) { fun getAmenities(category: String, location: Location) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("amenity", category, location, 5.0) val amenities = Overpass().getAmenities("amenity", category, location, 5.0)
@@ -353,6 +425,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Queries Overpass API for speed cameras within a radius.
* Posts sorted results to speedCameras LiveData.
*/
fun getSpeedCameras(location: Location, radius: Double) { fun getSpeedCameras(location: Location, radius: Double) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius) val amenities = Overpass().getAmenities("highway", "speed_camera", location, radius)
@@ -369,6 +445,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Queries Overpass API for speed limit on current road using fuzzy matching.
* Posts speed limit to maxSpeed LiveData.
*/
fun getMaxSpeed(location: Location, street: String) { fun getMaxSpeed(location: Location, street: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val levenshtein = Levenshtein() val levenshtein = Levenshtein()
@@ -387,11 +467,18 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Saves a place as a favorite in ObjectBox.
*/
fun saveFavorite(place: Place) { fun saveFavorite(place: Place) {
place.category = Constants.FAVORITES place.category = Constants.FAVORITES
savePlace(place) savePlace(place)
} }
/**
* Saves a place to recent destinations in ObjectBox.
* Skips fuel stations, charging stations, and pharmacies.
*/
fun saveRecent(place: Place) { fun saveRecent(place: Place) {
if (place.category == Constants.FUEL_STATION if (place.category == Constants.FUEL_STATION
|| place.category == Constants.CHARGING_STATION || place.category == Constants.CHARGING_STATION
@@ -403,6 +490,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
savePlace(place) savePlace(place)
} }
/**
* Saves a place to ObjectBox, removing existing duplicates first.
* Updates the timestamp to current time.
*/
private fun savePlace(place: Place) { private fun savePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -426,16 +517,25 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Deletes a place from favorites in ObjectBox.
*/
fun deleteFavorite(place: Place) { fun deleteFavorite(place: Place) {
place.category = Constants.FAVORITES place.category = Constants.FAVORITES
deletePlace(place) deletePlace(place)
} }
/**
* Deletes a place from recent destinations in ObjectBox.
*/
fun deleteRecent(place: Place) { fun deleteRecent(place: Place) {
place.category = Constants.RECENT place.category = Constants.RECENT
deletePlace(place) deletePlace(place)
} }
/**
* Deletes a place from ObjectBox matching name and category.
*/
fun deletePlace(place: Place) { fun deletePlace(place: Place) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
@@ -456,6 +556,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
} }
/**
* Retrieves search filter settings from preferences.
* @return SearchFilter with avoid motorway/tollway flags
*/
fun getSearchFilter(context: Context): SearchFilter { fun getSearchFilter(context: Context): SearchFilter {
val repository = getSettingsRepository(context) val repository = getSettingsRepository(context)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() } val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
@@ -463,7 +567,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return SearchFilter(avoidMotorway, avoidTollway) return SearchFilter(avoidMotorway, avoidTollway)
} }
/**
* Loads recent places with calculated distances for Compose state.
* @return SnapshotStateList of recent places with distances
*/
fun loadPlaces2( fun loadPlaces2(
context: Context, context: Context,
location: Location, location: Location,
@@ -495,6 +602,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
return results.toMutableStateList() return results.toMutableStateList()
} }
/**
* Loads recent places as Compose SnapshotStateList.
* @return SnapshotStateList of recent places
*/
fun loadRecentPlace(): SnapshotStateList<Place?> { fun loadRecentPlace(): SnapshotStateList<Place?> {
val results = listOf<Place>() val results = listOf<Place>()
try { try {
@@ -511,5 +622,4 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
} }
return results.toMutableStateList() return results.toMutableStateList()
} }
} }

View File

@@ -26,9 +26,9 @@ class RouteCalculator(var routeModel: RouteModel) {
routeModel.navState.route.currentStepIndex = step.index routeModel.navState.route.currentStepIndex = step.index
step.waypointIndex = wayIndex step.waypointIndex = wayIndex
step.wayPointLocation = location(waypoint[0], waypoint[1]) step.wayPointLocation = location(waypoint[0], waypoint[1])
routeModel.navState = routeModel.navState.copy( //routeModel.navState = routeModel.navState.copy(
routeBearing = routeModel.navState.lastLocation.bearingTo(location) // routeBearing = routeModel.navState.lastLocation.bearingTo(location)
) // )
} }
} }
} }

View File

@@ -2,6 +2,8 @@ package com.kouros.navigation.model
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_NATIVE
import androidx.car.app.connection.CarConnection.CONNECTION_TYPE_PROJECTION
import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.Maneuver
import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD import com.kouros.navigation.data.Constants.NEXT_STEP_THRESHOLD
import com.kouros.navigation.data.Place import com.kouros.navigation.data.Place
@@ -33,7 +35,8 @@ open class RouteModel {
val currentLocation: Location = location(0.0, 0.0), val currentLocation: Location = location(0.0, 0.0),
val routeBearing: Float = 0F, val routeBearing: Float = 0F,
val currentRouteIndex: Int = 0, val currentRouteIndex: Int = 0,
val destination: Place = Place() val destination: Place = Place(),
val carConnection: Int = 0,
) )
var navState = NavigationState() var navState = NavigationState()
@@ -81,9 +84,8 @@ open class RouteModel {
fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) { fun updateLocation(context: Context, curLocation: Location, viewModel: NavigationViewModel) {
navState = navState.copy(currentLocation = curLocation) navState = navState.copy(currentLocation = curLocation)
routeCalculator.findStep(curLocation) routeCalculator.findStep(curLocation)
val repository = getSettingsRepository(context) if (navState.carConnection == CONNECTION_TYPE_PROJECTION
val carLocation = runBlocking { repository.carLocationFlow.first() } || navState.carConnection == CONNECTION_TYPE_NATIVE) {
if (carLocation) {
routeCalculator.updateSpeedLimit(curLocation, viewModel) routeCalculator.updateSpeedLimit(curLocation, viewModel)
} }
navState = navState.copy(lastLocation = navState.currentLocation) navState = navState.copy(lastLocation = navState.currentLocation)

View File

@@ -27,6 +27,9 @@ object GeoUtils {
val point = pointFeature.geometry() as Point val point = pointFeature.geometry() as Point
newLocation.latitude = point.latitude() newLocation.latitude = point.latitude()
newLocation.longitude = point.longitude() newLocation.longitude = point.longitude()
if (location.hasBearing()) {
newLocation.bearing = location.bearing
}
} }
return newLocation return newLocation
} }

View File

@@ -52,7 +52,7 @@ fun calculateZoom(speed: Double?): Double {
} }
fun previewZoom(previewDistance: Double): Double { fun previewZoom(previewDistance: Double): Double {
when (previewDistance) { when (previewDistance / 1000) {
in 0.0..10.0 -> return 13.5 in 0.0..10.0 -> return 13.5
in 10.0..20.0 -> return 11.5 in 10.0..20.0 -> return 11.5
in 20.0..30.0 -> return 10.5 in 20.0..30.0 -> return 10.5

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.77,7.23l0.01,-0.01 -3.72,-3.72L15,4.56l2.11,2.11c-0.94,0.36 -1.61,1.26 -1.61,2.33 0,1.38 1.12,2.5 2.5,2.5 0.36,0 0.69,-0.08 1,-0.21v7.21c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L17,14c0,-1.1 -0.9,-2 -2,-2h-1L14,5c0,-1.1 -0.9,-2 -2,-2L6,3c-1.1,0 -2,0.9 -2,2v16h10v-7.5h1.5v5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5L20.5,9c0,-0.69 -0.28,-1.32 -0.73,-1.77zM12,10L6,10L6,5h6v5zM18,10c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L600,480Q633,480 656.5,503.5Q680,527 680,560L680,740Q680,757 691.5,768.5Q703,780 720,780Q737,780 748.5,768.5Q760,757 760,740L760,452Q751,457 741,458.5Q731,460 720,460Q678,460 649,431Q620,402 620,360Q620,328 637.5,302.5Q655,277 684,266L600,182L642,140L790,284Q805,299 812.5,319Q820,339 820,360L820,740Q820,782 791,811Q762,840 720,840Q678,840 649,811Q620,782 620,740L620,540Q620,540 620,540Q620,540 620,540L560,540L560,840L160,840ZM240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM720,400Q737,400 748.5,388.5Q760,377 760,360Q760,343 748.5,331.5Q737,320 720,320Q703,320 691.5,331.5Q680,343 680,360Q680,377 691.5,388.5Q703,400 720,400ZM240,760L480,760L480,480L240,480L240,760ZM480,760L240,760L240,760L480,760Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.63,468 649.31,482.69Q664,497.37 664,518L664,737Q664,758.68 679.5,773.34Q695,788 717,788Q739,788 754.5,773.34Q770,758.68 770,737L770,442Q759,448 747,451Q735,454 723,454Q683.52,454 656.26,426.74Q629,399.48 629,360Q629,328.39 647,303.19Q665,278 695,270L600,175L636,140L789,293Q803,307 811.5,323.5Q820,340 820,360L820,737Q820,780.26 790.18,810.13Q760.37,840 717.18,840Q674,840 644,810.13Q614,780.26 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM723,404Q741,404 754,391Q767,378 767,360Q767,342 754,329Q741,316 723,316Q705,316 692,329Q679,342 679,360Q679,378 692,391Q705,404 723,404ZM220,780L489,780L489,468L220,468L220,780ZM489,780L220,780L220,780L489,780Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680ZM204,760L756,760L676,520L756,280L204,280L284,520L204,760ZM480,520L480,520L480,520L480,520L480,520L480,520Z"/>
</vector>

View File

@@ -50,5 +50,7 @@
<string name="use_car_location">Auto GPS verwenden</string> <string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string> <string name="tomtom">TomTom\t</string>
<string name="options">Optionen</string> <string name="options">Optionen</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Verwende Auto Einstellungen</string>
</resources> </resources>

View File

@@ -37,4 +37,5 @@
<string name="tomtom">TomTom\t</string> <string name="tomtom">TomTom\t</string>
<string name="options">Options</string> <string name="options">Options</string>
<string name="tomtom_api_key">TomTom ApiKey</string> <string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Use car settings</string>
</resources> </resources>

View File

@@ -38,7 +38,6 @@ material3WindowSizeClass = "1.4.0"
uiGraphics = "1.10.3" uiGraphics = "1.10.3"
window = "1.5.1" window = "1.5.1"
foundationLayout = "1.10.3" foundationLayout = "1.10.3"
foundationLayoutVersion = "1.10.3"
datastorePreferences = "1.2.0" datastorePreferences = "1.2.0"
datastoreCore = "1.2.0" datastoreCore = "1.2.0"
@@ -81,7 +80,6 @@ androidx-app-automotive = { module = "androidx.car.app:app-automotive", version.
androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "uiGraphics" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" } androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" }