This commit is contained in:
Dimitris
2025-12-04 08:10:03 +01:00
parent cddb193260
commit 9f53db8e76
29 changed files with 590 additions and 623 deletions

View File

@@ -12,25 +12,25 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 1
versionName = "0.1.3"
versionCode = 2
versionName = "0.1.3.1"
setProperty("archivesBaseName", "navi-$versionName")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
signingConfigs {
// getByName("debug") {
// keyAlias = "alias"
// keyPassword = "alpha2000"
// storeFile = file("/home/kouros/work/keystore/keystoreRelease")
// storePassword = "alpha2000"
// }
getByName("debug") {
keyAlias = "release"
keyPassword = "zeta67#gAe3aN3"
storeFile = file("/home/kouros/work/keystore/keystoreRelease")
storePassword = "zeta67#gAe3aN3"
}
create("release") {
keyAlias = "release"
keyPassword = "zeta67#g"
keyPassword = "zeta67#gAe3aN3"
storeFile = file("/home/kouros/work/keystore/keystoreRelease")
storePassword = "zeta67#g"
storePassword = "zeta67#gAe3aN3"
}
}
@@ -46,11 +46,11 @@ android {
// Specifies one flavor dimension.
flavorDimensions += "version"
productFlavors {
// create("demo") {
// dimension = "version"
// applicationIdSuffix = ".demo"
// versionNameSuffix = "-demo"
// }
create("demo") {
dimension = "version"
applicationIdSuffix = ".demo"
versionNameSuffix = "-demo"
}
create("full") {
dimension = "version"
applicationIdSuffix = ".full"
@@ -87,11 +87,15 @@ dependencies {
implementation(project(":common:car"))
implementation(libs.play.services.location)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3.window.size.class1)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
debugImplementation(libs.androidx.compose.ui.tooling)
}

View File

@@ -6,6 +6,9 @@
<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_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="MockLocation" />
<application
android:name="com.kouros.navigation.MainApplication"
@@ -23,7 +26,7 @@
android:resource="@xml/automotive_app_desc" />
<activity
android:name="com.kouros.navigation.MainActivity"
android:name="com.kouros.navigation.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.Navigation">
<intent-filter>

View File

@@ -24,5 +24,7 @@ class MainApplication : Application() {
companion object {
var appContext: Context? = null
private set
var useContacts = true
}
}

View File

@@ -0,0 +1,90 @@
package com.kouros.navigation.model
import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
class MockLocation (private var locationManager: LocationManager) {
fun setMockLocation(latitude: Double, longitude: Double) {
try {
// Set mock location for all providers
setMockLocationForProvider(LocationManager.GPS_PROVIDER, latitude, longitude)
setMockLocationForProvider(LocationManager.NETWORK_PROVIDER, latitude, longitude)
} catch (e: NumberFormatException) {
} catch (e: SecurityException) {
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setMockLocationForProvider(provider: String, latitude: Double, longitude: Double) {
try {
// Check if provider exists
if (!locationManager.allProviders.contains(provider)) {
return
}
// Enable test provider
// For API 31+
locationManager.addTestProvider(
provider,
false, // requiresNetwork
false, // requiresSatellite
false, // requiresCell
false, // hasMonetaryCost
true, // supportsAltitude
true, // supportsSpeed
true, // supportsBearing
android.location.provider.ProviderProperties.POWER_USAGE_LOW,
android.location.provider.ProviderProperties.ACCURACY_FINE
)
locationManager.setTestProviderEnabled(provider, true)
// Create mock location
val mockLocation = Location(provider).apply {
this.latitude = latitude
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.speed = 15F
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
// Set the mock location
locationManager.setTestProviderLocation(provider, mockLocation)
} catch (e: SecurityException) {
throw e
} catch (e: IllegalArgumentException) {
// Provider already exists, just update location
try {
locationManager.setTestProviderEnabled(provider, true)
val mockLocation = Location(provider).apply {
this.latitude = latitude
this.longitude = longitude
this.altitude = 0.0
this.accuracy = 1.0f
this.time = System.currentTimeMillis()
this.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
this.bearingAccuracyDegrees = 0.0f
this.verticalAccuracyMeters = 0.0f
this.speedAccuracyMetersPerSecond = 0.0f
}
locationManager.setTestProviderLocation(provider, mockLocation)
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
}

View File

@@ -1,31 +1,30 @@
package com.kouros.navigation
package com.kouros.navigation.ui
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButtonDefaults.Icon
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -38,46 +37,40 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.kouros.navigation.ui.theme.NavigationTheme
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.MainApplication
import com.kouros.navigation.car.BuildingLayer
import com.kouros.navigation.car.Puck
import com.kouros.navigation.car.PuckState
import com.kouros.navigation.car.RouteLayer
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.StepData
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.snapLocation
import com.kouros.navigation.ui.theme.NavigationTheme
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.calculateZoom
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.LocationPuck
import org.maplibre.compose.location.LocationPuckColors
import org.maplibre.compose.location.LocationPuckSizes
import org.maplibre.compose.location.LocationTrackingEffect
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
@@ -87,10 +80,12 @@ import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.seconds
class MainActivity : ComponentActivity() {
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private val LOCATION_PERMISSION_REQUEST_CODE = 1001
private val CONTACTS_PERMISSION_REQUEST_CODE = 1002
val routeData = MutableLiveData("")
val vieModel = ViewModel(NavigationRepository())
@@ -107,6 +102,8 @@ class MainActivity : ComponentActivity() {
val observer = Observer<String> { newRoute ->
routeModel.startNavigation(newRoute)
routeData.value = routeModel.route.routeGeoJson
println("Start simulating $newRoute")
simulate()
}
val cameraPosition = MutableLiveData(
@@ -116,77 +113,104 @@ class MainActivity : ComponentActivity() {
)
)
var locationIndex = 0
var simulate = false
private lateinit var locationManager: LocationManager
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var mock: MockLocation
init {
vieModel.route.observe(this, observer)
if (simulate) {
vieModel.loadRoute(
Constants.homeLocation,
Constants.home2Location
)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkLocationPermissions()
if (MainApplication.useContacts) {
checkContactsPermissions()
}
checkMockLocationEnabled()
enableEdgeToEdge()
setContent {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
if ((checkPermissionForLocation() && !MainApplication.useContacts)
|| (checkPermissionForLocation() && MainApplication.useContacts && checkPermissionForContact())) {
Content()
} else {
NavigationTheme {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
Text("Drawer title", modifier = Modifier.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
}
}
}
@Composable
fun Content() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
mock = MockLocation(locationManager)
mock.setMockLocation(
Constants.homeLocation.latitude,
Constants.homeLocation.longitude
)
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
var simulationText by remember { mutableStateOf("Start Simulation") }
NavigationTheme {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
Text("Drawer title", modifier = Modifier.Companion.padding(16.dp))
HorizontalDivider()
NavigationDrawerItem(
label = { Text(text = "Drawer Item") },
selected = false,
onClick = { /*TODO*/ }
)
}
},
gesturesEnabled = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
gesturesEnabled = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = {
Text("Navigate")
},
icon = { Icon(true) },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
if (!routeModel.isNavigating() && lastLocation.latitude != 0.0) {
tilt = 60.0
vieModel.loadRoute(
lastLocation,
Constants.home2Location
)
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
}
floatingActionButton = {
ExtendedFloatingActionButton(
text = {
Text(simulationText)
},
icon = { SegmentedButtonDefaults.Icon(true) },
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Starte Navigation")
}
)
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
CheckPermission()
}
if (!routeModel.isNavigating()) {
tilt = 60.0
vieModel.loadRoute(
applicationContext,
lastLocation,
Constants.home2Location
)
simulationText = "Stop Simulation"
} else {
tilt = 0.0
routeModel.stopNavigation()
routeData.value = ""
println("stopNavigation")
simulationText = "Start Simulation"
}
}
)
}
) { innerPadding ->
Column(modifier = Modifier.Companion.padding(innerPadding)) {
//CheckPermission()
Map()
}
}
}
@@ -206,7 +230,7 @@ class MainActivity : ComponentActivity() {
listOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.READ_CONTACTS,
//Manifest.permission.READ_CONTACTS,
),
)
@@ -272,11 +296,7 @@ class MainActivity : ComponentActivity() {
)
val userLocationState = rememberUserLocationState(locationProvider)
val locationState = locationProvider.location.collectAsState()
if (!simulate) {
updateLocation(locationState.value)
} else {
simulate()
}
updateLocation(locationState.value)
if (locationState.value != null && lastLocation.latitude == 0.0) {
lastLocation.latitude = locationState.value?.position!!.latitude
lastLocation.longitude = locationState.value?.position!!.longitude
@@ -300,29 +320,33 @@ class MainActivity : ComponentActivity() {
baseStyle = BaseStyle.Uri(Constants.STYLE),
) {
getBaseSource(id = "openmaptiles")?.let { tiles ->
if (!getBooleanKeyValue(context = applicationContext, SHOW_THREED_BUILDING)) {
if (!NavigationUtils.getBooleanKeyValue(
context = applicationContext,
Constants.SHOW_THREED_BUILDING
)
) {
BuildingLayer(tiles)
}
RouteLayer(route, "")
}
val location = Location(LocationManager.GPS_PROVIDER)
if (userLocationState.location != null) {
val location = Location(LocationManager.GPS_PROVIDER)
location.longitude = userLocationState.location!!.position.longitude
location.latitude = userLocationState.location!!.position.latitude
PuckState(cameraState, userLocationState,)
PuckState(cameraState, userLocationState)
}
}
LocationTrackingEffect(
locationState = userLocationState,
) {
//cameraState.updateFromLocation()
cameraState.animateTo(
finalPosition = CameraPosition(
bearing = position!!.bearing,
zoom = position!!.zoom,
target = position!!.target,
tilt = tilt
tilt = tilt,
padding = PaddingValues(start = 0.dp, top = 350.dp)
),
duration = 1.seconds
)
@@ -345,86 +369,107 @@ class MainActivity : ComponentActivity() {
}
}
fun updateTestLocation(location: Location) {
var snapedLocation = location
var bearing: Double
if (routeModel.isNavigating()) {
snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
routeModel.updateLocation(location)
bearing = routeModel.currentStep().bearing
instruction.postValue(routeModel.currentStep())
} else {
bearing = cameraPosition.value!!.bearing
}
val zoom = calculateZoom(snapedLocation.speed.toDouble())
cameraPosition.postValue(
cameraPosition.value!!.copy(
bearing = bearing,
zoom = zoom,
target = Position(snapedLocation.longitude, snapedLocation.latitude)
),
private fun checkLocationPermissions() {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
fun simulate() {
if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) {
coroutineScope.launch {
delay(
100
fun checkPermissionForLocation(): Boolean {
val permissions = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (MainApplication.useContacts) {
permissions.add(Manifest.permission.READ_CONTACTS)
}
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
fun checkPermissionForContact(): Boolean {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
return permissionsToRequest.isEmpty()
}
private fun checkContactsPermissions() {
val permissions = arrayOf(
Manifest.permission.READ_CONTACTS,
)
val permissionsToRequest = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
CONTACTS_PERMISSION_REQUEST_CODE
)
}
}
private fun checkMockLocationEnabled() {
try {
// Check if mock location is enabled for this app
val appOpsManager =
getSystemService(APP_OPS_SERVICE) as AppOpsManager
val mode =
appOpsManager.unsafeCheckOp(
AppOpsManager.OPSTR_MOCK_LOCATION,
Process.myUid(),
packageName
)
val loc = routeModel.route.waypoints[locationIndex]
if (mode != AppOpsManager.MODE_ALLOWED) {
Toast.makeText(
this,
"Please select this app as mock location app in Developer Options",
Toast.LENGTH_LONG
).show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
@OptIn(DelicateCoroutinesApi::class)
fun simulate() = GlobalScope.async {
for ((i, loc) in routeModel.route.waypoints.withIndex()) {
if (routeModel.isNavigating()) {
lastLocation.longitude = loc[0]
lastLocation.latitude = loc[1]
updateTestLocation(lastLocation)
Thread.sleep(1_000)
locationIndex++
}
}
}
@Composable
fun PlaceList(viewModel: ViewModel = koinViewModel()) {
var categories: List<Category>
val places = viewModel.places.observeAsState().value ?: return
val countries = places.groupBy { it.category }.map {
Category(id = Constants.RECENT, name = it.key!!)
}
categories = countries
val context = LocalContext.current
LazyColumn {
items(categories.size) {
val place = categories[it]
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.border(
2.dp,
color = MaterialTheme.colorScheme.outline,
shape = RoundedCornerShape(8.dp)
)
.clip(RoundedCornerShape(8.dp))
//.clickable {
//context.startActivity(place.toIntent(Intent.ACTION_VIEW))
//}
.padding(8.dp)
) {
Column {
Text(
text = place.name,
style = MaterialTheme.typography.labelLarge
)
Text(
text = place.name,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
}
if (i == 20) {
mock.setMockLocation(loc[1] + 0.03, loc[0])
} else {
mock.setMockLocation(loc[1], loc[0])
}
delay(1000L) //
}
}
}

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation
package com.kouros.navigation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -26,6 +26,7 @@ import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
/**
* Simple screen that manages the location permission state
*/

View File

@@ -1,4 +1,4 @@
package com.kouros.navigation
package com.kouros.navigation.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box

View File

@@ -51,6 +51,7 @@ dependencies {
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose.ui.text)
implementation(libs.play.services.location)
androidTestImplementation(libs.androidx.junit)
testImplementation(libs.junit)
}

View File

@@ -32,7 +32,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application android:requestLegacyExternalStorage="true">
<!--

View File

@@ -47,13 +47,10 @@ import org.maplibre.spatialk.geojson.Position
@Composable
fun cameraState(
width: Int,
height: Int,
padding : PaddingValues,
position: CameraPosition?,
tilt: Double,
preview: Boolean
): CameraState {
val padding = getPaddingValues(height, preview)
return rememberCameraState(
firstPosition =
CameraPosition(
@@ -117,20 +114,18 @@ fun BuildingLayer(tiles: Source) {
}
@Composable
fun DrawImage(width: Int, height: Int, location: Location, street: String) {
NavigationImage(height, street)
fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: Int, street: String) {
NavigationImage(padding, street)
Speed(width, height, location)
}
@Composable
fun NavigationImage(height: Int, street: String) {
fun NavigationImage(padding: PaddingValues, street: String) {
val vector = ImageVector.vectorResource(id = R.drawable.assistant_navigation_48px)
val color = remember { NavigationColor }
BadgedBox(
modifier = Modifier
.padding(
start = 0.dp, top = distanceFromTop(height).dp
),
.padding(padding),
badge = {
Badge()
}
@@ -156,7 +151,7 @@ private fun Speed(
Box(
modifier = Modifier
.padding(
start = width.dp- 300.dp,
start = width.dp- 250.dp,
top = height.dp- 80.dp
),
contentAlignment = Alignment.Center
@@ -210,18 +205,17 @@ private fun Speed(
}
}
fun getPaddingValues(height: Int, preView: Boolean): PaddingValues {
val padding = PaddingValues(start = 0.dp, top = distanceFromTop(height).dp)
val prePadding = PaddingValues(start = 150.dp, bottom = 0.dp)
fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues {
return if (preView) {
prePadding
PaddingValues(start = 150.dp, bottom = 0.dp)
} else {
padding
// PaddingValues(start = width.dp, top = distanceFromTop(height).dp)
PaddingValues(start = 0.dp, top = distanceFromTop(height).dp)
}
}
fun distanceFromTop(height: Int): Int {
return height - percent(height, 20)
return height - percent(height, 25)
}
fun percent(maxValue: Int, value: Int): Int {

View File

@@ -7,9 +7,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
@@ -27,13 +25,7 @@ import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.ObjectBox
import com.kouros.navigation.utils.NavigationUtils.snapLocation
import com.kouros.navigation.utils.location
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class NavigationSession : Session(), NavigationScreen.Listener {
val uriScheme = "samples";
@@ -46,16 +38,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
lateinit var surfaceRenderer: SurfaceRenderer
var locationIndex = 0
val simulate = false
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
updateLocation(location)
updateLocation(location!!)
}
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
Log.i(TAG, "In onCreate()")
@@ -161,56 +147,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 500,
/* minDistanceM= */ 0f,
mLocationListener
)
}
fun updateLocation(location: Location?) {
if (location != null) {
if (simulate) {
simulate(location)
} else {
update(location)
}
updateLocation(location)
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
/* minTimeMs= */ 500,
/* minDistanceM= */ 0f,
mLocationListener
)
}
}
fun simulate(location: Location?) {
if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) {
coroutineScope.launch {
if (locationIndex >= routeModel.route.waypoints.size) {
return@launch
}
val loc = routeModel.route.waypoints[locationIndex]
val curLocation = Location(LocationManager.GPS_PROVIDER)
curLocation.longitude = loc[0]// + 0.00001 * locationIndex
curLocation.latitude = loc[1] //+ 0.00001 * locationIndex
curLocation.speed = 15F
update(curLocation)
locationIndex += 1
if (locationIndex > routeModel.route.waypoints.size) {
val locationManager =
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
locationManager.removeUpdates(mLocationListener)
}
}
} else {
update(location = location!!)
}
}
fun update(location: Location) {
fun updateLocation(location: Location) {
if (routeModel.isNavigating()) {
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
val distance = location.distanceTo(snapedLocation)
if (distance > MAXIMAL_ROUTE_DEVIATION) {
// navigationScreen.calculateNewRoute()
//return
navigationScreen.calculateNewRoute(routeModel.destination)
return
}
routeModel.updateLocation(location)
navigationScreen.updateTrip()

View File

@@ -6,13 +6,13 @@ import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -54,7 +54,7 @@ class SurfaceRenderer(
)
)
var visibleArea = MutableLiveData(
Rect(0,0,0,0)
Rect(0, 0, 0, 0)
)
var stableArea = Rect()
@@ -162,16 +162,17 @@ class SurfaceRenderer(
@Composable
fun MapView() {
val stateWidth = visibleArea.observeAsState()
val position: CameraPosition? by cameraPosition.observeAsState()
val route: String? by routeData.observeAsState()
val previewRoute: String? by previewRouteData.observeAsState()
val cameraState = cameraState(width, height, position, tilt, preview)
val paddingValues = getPaddingValues( width - stateWidth.value!!.width(), height, preview)
val cameraState = cameraState(paddingValues, position, tilt)
val baseStyle = BaseStyle.Uri(Constants.STYLE)
// if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
// Constants.STYLE
// )
if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
Constants.STYLE
)
MaplibreMap(
cameraState = cameraState,
baseStyle = baseStyle,
@@ -184,11 +185,11 @@ class SurfaceRenderer(
}
//Puck(cameraState, lastLocation)
}
ShowPosition(cameraState, position)
ShowPosition(cameraState, position, paddingValues)
}
@Composable
fun ShowPosition(cameraState: CameraState, position: CameraPosition?) {
fun ShowPosition(cameraState: CameraState, position: CameraPosition?, paddingValues: PaddingValues) {
val cameraDuration = duration(position)
var bearing = position!!.bearing
var zoom = position.zoom
@@ -196,9 +197,9 @@ class SurfaceRenderer(
var localTilt = tilt
if (!preview) {
if (routeModel.isNavigating()) {
DrawImage(width, height, lastLocation, "")
DrawImage(paddingValues, lastLocation, width, height,"")
} else {
DrawImage(width, height, lastLocation, "")
DrawImage(paddingValues, lastLocation,width, height, "")
}
} else {
bearing = 0.0
@@ -213,7 +214,7 @@ class SurfaceRenderer(
zoom = zoom,
target = target,
tilt = localTilt,
padding = getPaddingValues(height, preview)
padding = paddingValues
),
duration = cameraDuration
)
@@ -259,16 +260,21 @@ class SurfaceRenderer(
fun updateLocation(location: Location) {
synchronized(this) {
if (!preview) {
var bearing = cameraPosition.value!!.bearing
if (routeModel.isNavigating()) {
bearing = routeModel.currentStep().bearing
val bearing = if (routeModel.isNavigating()) {
routeModel.currentStep().bearing
} else {
lastLocation.bearingTo(location).toInt().toDouble().absoluteValue
}
val zoom = if (!panView) {
calculateZoom(location.speed.toDouble())
} else {
cameraPosition.value!!.zoom
}
updateCameraPosition(bearing, zoom, Position(location.longitude, location.latitude))
updateCameraPosition(
bearing,
zoom,
Position(location.longitude, location.latitude)
)
lastBearing = cameraPosition.value!!.bearing
lastLocation = location
} else {
@@ -289,7 +295,7 @@ class SurfaceRenderer(
bearing = bearing,
zoom = zoom,
tilt = 0.0,
padding = getPaddingValues(height, preview),
padding = getPaddingValues(width-visibleArea.value!!.width(), height, preview),
target = target
)
)
@@ -315,9 +321,11 @@ class SurfaceRenderer(
in 0.0..10.0 -> {
return 13.0
}
in 10.0..20.0 -> {
return 11.0
}
in 20.0..30.0 -> {
return 10.0
}

View File

@@ -1,77 +0,0 @@
package com.kouros.navigation.car.navigation
import android.location.Location
import android.os.Environment
import org.xml.sax.SAXException
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
class Gpx {
fun loadGPX() {
val path = Environment.getExternalStorageDirectory()
.toString() + "/Download/VogelHohen.gpx"
var info = ""
val gpxFile = File(path)
info = info + gpxFile.path + "\n\n"
val gpxList = decodeGPX(gpxFile)
print(gpxList)
}
private fun decodeGPX(file: File): MutableList<Location?> {
val list: MutableList<Location?> = ArrayList()
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
try {
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val fileInputStream = FileInputStream(file)
val document = documentBuilder.parse(fileInputStream)
val elementRoot = document.documentElement
val nodelist_trkpt = elementRoot.getElementsByTagName("trkpt")
for (i in 0..<nodelist_trkpt.getLength()) {
val node = nodelist_trkpt.item(i)
val attributes = node.getAttributes()
val newLatitude = attributes.getNamedItem("lat").getTextContent()
val newLatitude_double = newLatitude.toDouble()
val newLongitude = attributes.getNamedItem("lon").getTextContent()
val newLongitude_double = newLongitude.toDouble()
val newLocationName = newLatitude + ":" + newLongitude
val newLocation = Location(newLocationName)
newLocation.setLatitude(newLatitude_double)
newLocation.setLongitude(newLongitude_double)
list.add(newLocation)
}
fileInputStream.close()
} catch (e: ParserConfigurationException) {
// TODO Auto-generated catch block
e.printStackTrace()
} catch (e: FileNotFoundException) {
// TODO Auto-generated catch block
e.printStackTrace()
} catch (e: SAXException) {
// TODO Auto-generated catch block
e.printStackTrace()
} catch (e: IOException) {
// TODO Auto-generated catch block
e.printStackTrace()
}
return list
}
}

View File

@@ -15,37 +15,6 @@ import com.kouros.android.cars.carappservice.R
class NavigationMessage (private var carContext: CarContext) {
/** Returns a sample [Alert]. */
fun createAlert(): Alert {
val title: CarText = createCarText(R.string.navigation_alert_title)
val subtitle: CarText = createCarText(R.string.navigation_alert_subtitle)
val icon = CarIcon.ALERT
val yesAction: Action = createToastAction(
R.string.yes_action_title,
R.string.yes_action_toast_msg, Action.FLAG_PRIMARY
)
val noAction: Action = createToastAction(
R.string.no_action_title, R.string.no_action_toast_msg,
Action.FLAG_DEFAULT
)
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
.setSubtitle(subtitle)
.setIcon(icon)
.addAction(yesAction)
.addAction(noAction).setCallback(object : AlertCallback {
override fun onCancel(reason: Int) {
if (reason == AlertCallback.REASON_TIMEOUT) {
showToast(R.string.alert_timeout_toast_msg)
}
}
override fun onDismiss() {
}
}).build()
}
private fun createToastAction(
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
flags: Int

View File

@@ -214,7 +214,7 @@ class RouteCarModel() : RouteModel() {
.build()
}
private fun createString(
fun createString(
text: String
): SpannableString {
val spannableString = SpannableString(text)

View File

@@ -40,7 +40,7 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
.setSingleList(listBuilder.build())
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.content_limits))
.setTitle(carContext.getString(R.string.display_settings))
.setStartHeaderAction(Action.BACK)
.build()
)

View File

@@ -17,7 +17,6 @@ import androidx.car.app.model.Header
import androidx.car.app.model.MessageTemplate
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.Maneuver
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.car.app.navigation.model.MessageInfo
import androidx.car.app.navigation.model.NavigationTemplate
@@ -86,18 +85,17 @@ class NavigationScreen(
override fun onGetTemplate(): Template {
val actionStripBuilder = createActionStripBuilder()
if (calculateNewRoute) {
return navigationRerouteTemplate(actionStripBuilder)
}
return if (routeModel.isNavigating()) {
if (calculateNewRoute) {
getNavigationLoadingTemplate(actionStripBuilder)
} else {
getNavigationTemplate(actionStripBuilder)
}
navigationTemplate(actionStripBuilder)
} else {
getNavigationEndTemplate(actionStripBuilder)
navigationEndTemplate(actionStripBuilder)
}
}
private fun getNavigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
actionStripBuilder.addAction(
stopAction()
)
@@ -112,7 +110,7 @@ class NavigationScreen(
.build()
}
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
if (routeModel.isArrived()) {
val timer = object : CountDownTimer(10000, 10000) {
override fun onTick(millisUntilFinished: Long) {}
@@ -122,30 +120,10 @@ class NavigationScreen(
}
}
timer.start()
return NavigationTemplate.Builder()
.setNavigationInfo(
MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg)
)
.setText(routeModel.destination.street!!)
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_place_white_24dp
)
)
.build()
)
.build()
)
.setBackgroundColor(CarColor.GREEN)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
return navigationArrivedTemplate(actionStripBuilder)
} else {
return if (recentPlaceFound && recentPlaceActive) {
return getRecentPlaceTemplate()
return recentPlaceTemplate()
} else {
NavigationTemplate.Builder()
.setBackgroundColor(CarColor.SECONDARY)
@@ -156,7 +134,31 @@ class NavigationScreen(
}
}
fun getRecentPlaceTemplate(): Template {
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
return NavigationTemplate.Builder()
.setNavigationInfo(
MessageInfo.Builder(
carContext.getString(R.string.arrived_exclamation_msg)
)
.setText(routeModel.destination.street!!)
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_place_white_24dp
)
)
.build()
)
.build()
)
.setBackgroundColor(CarColor.GREEN)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
}
fun recentPlaceTemplate(): Template {
val messageTemplate = MessageTemplate.Builder(
recentPlace.name + "\n"
+ recentPlace.city
@@ -181,11 +183,27 @@ class NavigationScreen(
return builder.build()
}
fun getNavigationLoadingTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build())
.setNavigationInfo(
MessageInfo.Builder(
carContext.getString(R.string.new_route)
)
.setText(routeModel.destination.street.toString())
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.navigation_48px
)
)
.build()
)
.build()
)
.setBackgroundColor(CarColor.SECONDARY)
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(mapActionStripBuilder().build())
.build()
}
@@ -271,7 +289,7 @@ class NavigationScreen(
)
.setOnClickListener {
val navigateTo = location(recentPlace.latitude, recentPlace.longitude)
viewModel.loadRoute(surfaceRenderer.lastLocation, navigateTo)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
routeModel.destination = recentPlace
}
.build()
@@ -394,7 +412,7 @@ class NavigationScreen(
location.latitude = place.latitude
location.longitude = place.longitude
viewModel.saveRecent(place)
viewModel.loadRoute(surfaceRenderer.lastLocation, location)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
currentNavigationLocation = location
routeModel.destination = place
invalidate()
@@ -408,24 +426,25 @@ class NavigationScreen(
invalidate()
}
fun calculateNewRoute() {
fun calculateNewRoute(destination: Place) {
calculateNewRoute = true
stopNavigation()
invalidate()
val mainThreadhandler = Handler(carContext.mainLooper)
mainThreadhandler.post {
val mainThreadHandler = Handler(carContext.mainLooper)
mainThreadHandler.post {
object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
calculateNewRoute = false
stopNavigation()
reRoute(destination)
}
}.start()
}
}
fun reRoute() {
NavigationMessage(carContext).createAlert()
viewModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
fun reRoute(destination: Place) {
val dest = location(destination.latitude, destination.longitude)
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
}
fun updateTrip() {

View File

@@ -1,9 +1,7 @@
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.constraints.ConstraintManager
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
@@ -11,48 +9,65 @@ import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.lifecycle.DefaultLifecycleObserver
import com.kouros.android.cars.carappservice.R
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
class NavigationSettings(private val carContext: CarContext) : Screen(carContext) {
private var motorWayToggleState = false
private var tollWayToggleState = false
init {
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
}
override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder()
val highwayToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, AVOID_MOTORWAY)
} else {
setBooleanKeyValue(carContext, false, AVOID_MOTORWAY)
}
motorWayToggleState = !motorWayToggleState
}.setChecked(motorWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_highways_row_title, highwayToggle))
listBuilder.addItem(
buildRowForTemplate(
R.string.list_limit,
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
// Tollway
val tollwayToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
setBooleanKeyValue(carContext, true, AVOID_TOLLWAY)
} else {
setBooleanKeyValue(carContext, false, AVOID_TOLLWAY)
}
tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
return ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.content_limits))
.setTitle(carContext.getString(R.string.display_settings))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun buildRowForTemplate(title: Int, contentLimitType: Int): Row {
private fun buildRowForTemplate(title: Int, toggle: Toggle): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.addText(
carContext
.getCarService(ConstraintManager::class.java)
.getContentLimit(contentLimitType).toString()
)
.setToggle(toggle)
.build()
}
}

View File

@@ -53,7 +53,7 @@ class PlaceListScreen(
init {
if (category == Constants.RECENT) {
viewModel.places.observe(this, observer)
viewModel.loadPlaces(location)
viewModel.loadPlaces(carContext, location)
}
if (category == Constants.CONTACTS) {
viewModel.contactAddress.observe(this, observerAddress)

View File

@@ -26,7 +26,7 @@ class RequestPermissionScreen(
override fun onGetTemplate(): Template {
val permissions: MutableList<String?> = ArrayList()
permissions.add(permission.ACCESS_FINE_LOCATION)
permissions.add(permission.READ_CONTACTS)
//permissions.add(permission.READ_CONTACTS)
val message = "This app needs access to location in order to show the map around you"

View File

@@ -77,7 +77,7 @@ class RoutePreviewScreen(
val location = Location(LocationManager.GPS_PROVIDER)
location.latitude = destination.latitude
location.longitude = destination.longitude
vieModel.loadPreviewRoute(surfaceRenderer.lastLocation, location)
vieModel.loadPreviewRoute(carContext,surfaceRenderer.lastLocation, location)
}
override fun onGetTemplate(): Template {

View File

@@ -41,8 +41,6 @@
<string name="sign_out_action_title" msgid="1653943000866713010">"Abmelden"</string>
<string name="yes_action_title" msgid="5507096013762092189">"Ja"</string>
<string name="no_action_title" msgid="1452124604210014010">"Nein"</string>
<string name="disable_all_rows" msgid="3003225080532928046">"Alle Zeilen deaktivieren"</string>
<string name="enable_all_rows" msgid="7274285275711872091">"Alle Zeilen aktivieren"</string>
<string name="zoomed_in_toast_msg" msgid="8915301497303842649">"Herangezoomt"</string>
<string name="zoomed_out_toast_msg" msgid="6260981223227212493">"Herausgezoomt"</string>
<string name="triggered_toast_msg" msgid="3396166539208366382">"Ausgelöst"</string>
@@ -55,59 +53,13 @@
<string name="parked_toast_msg" msgid="2532422265890824446">"Aktion „Geparkt“"</string>
<string name="more_toast_msg" msgid="5938288138225509885">"„Mehr“ angeklickt"</string>
<string name="grant_location_permission_toast_msg" msgid="268046297444808010">"Standortermittlung erlauben, um aktuellen Standort anzuzeigen"</string>
<string name="sign_in_with_google_toast_msg" msgid="5720947549233124775">"Über Google anmelden beginnt hier"</string>
<string name="changes_selection_to_index_toast_msg_prefix" msgid="957766225794389167">"Auswahl auf Index geändert"</string>
<string name="yes_action_toast_msg" msgid="6216215197177241247">"Schaltfläche „Ja“ gedrückt."</string>
<string name="no_action_toast_msg" msgid="6165492423831023809">"Schaltfläche „Nein“ gedrückt."</string>
<string name="alert_timeout_toast_msg" msgid="5568380708832805374">"Zeitüberschreitung bei Benachrichtigung."</string>
<string name="first_row_title" msgid="219428344573165351">"Zeile mit großem Bild und langem Text langem Text langem Text langem Text langem Text"</string>
<string name="first_row_text" msgid="3887390298628338716">"Text Text Text"</string>
<string name="other_row_title_prefix" msgid="4702355788835253197">"Zeilentitel"</string>
<string name="other_row_text" msgid="7510279447493169945">"Zeilentext"</string>
<string name="navigate" msgid="2713090390373996139">"Navigieren"</string>
<string name="dial" msgid="3145707439707628311">"Wählen"</string>
<string name="address" msgid="9010635942573581302">"Adresse"</string>
<string name="phone" msgid="2504766809811627577">"Smartphone"</string>
<string name="fail_start_nav" msgid="6921321606009212189">"Fehler beim Starten der Navigation"</string>
<string name="fail_start_dialer" msgid="1471602619507306261">"Fehler beim Starten des Telefons"</string>
<string name="car_hardware_demo_title" msgid="3679106197233262689">"Demo der Auto-Hardware"</string>
<string name="car_hardware_info" msgid="1244783247616395012">"Informationen zur Auto-Hardware"</string>
<string name="model_info" msgid="494224423025683030">"Modellinformationen"</string>
<string name="manufacturer_unavailable" msgid="4978995415869838056">"Hersteller nicht verfügbar"</string>
<string name="model_unavailable" msgid="4075463010215406573">"Modell nicht verfügbar"</string>
<string name="year_unavailable" msgid="994338773299644607">"Jahr nicht verfügbar"</string>
<string name="energy_profile" msgid="81415433590192158">"Energieprofil"</string>
<string name="no_energy_profile_permission" msgid="4662285713731308888">"Keine Berechtigung für Energieprofil"</string>
<string name="fuel_types" msgid="6811375173343218212">"Kraftstofftypen"</string>
<string name="unavailable" msgid="3636401138255192934">"Nicht verfügbar"</string>
<string name="ev_connector_types" msgid="735458637011996125">"Elektrofahrzeug-Anschlusssteckertypen"</string>
<string name="example_title" msgid="530257630320010494">"Beispiel: %d"</string>
<string name="example_1_text" msgid="8631503055894800688">"Dieser Text ist "<annotation color="red">"rot"</annotation></string>
<string name="example_2_text" msgid="1359373957397219102">"Dieser Text ist "<annotation color="green">"grün"</annotation></string>
<string name="example_3_text" msgid="2409207170762049673">"Dieser Text ist "<annotation color="blue">"blau"</annotation></string>
<string name="example_4_text" msgid="9055989886645433000">"Dieser Text ist "<annotation color="yellow">"gelb"</annotation></string>
<string name="example_5_text" msgid="8828804968749423500">"Für diesen Text wird die Primärfarbe verwendet"</string>
<string name="example_6_text" msgid="7991523168517599600">"Dieser Text verwendet die Sekundärfarbe"</string>
<string name="color_demo" msgid="1822427636476178993">"Farbdemo"</string>
<string name="list_limit" msgid="3023536401535417286">"Listenbeschränkung"</string>
<string name="grid_limit" msgid="1350116012893549206">"Rasterbegrenzung"</string>
<string name="pane_limit" msgid="981518409516855230">"Bereichsbegrenzung"</string>
<string name="place_list_limit" msgid="6785181191763056582">"Limit für Ortsliste"</string>
<string name="route_list_limit" msgid="505793441615134116">"Limit für Routenliste"</string>
<string name="content_limits" msgid="5726880972110281095">"Beschränkungen für Inhalte"</string>
<string name="content_limits_demo_title" msgid="3207211638386727610">"Demo für „Beschränkungen für Inhalte“"</string>
<string name="finish_app_msg" msgid="8354334557053141891">"Dadurch wird die App geschlossen und beim nächsten Ausführen eine Berechtigungsanfrage eingeblendet"</string>
<string name="finish_app_title" msgid="9013328479438745074">"App-Demo beenden"</string>
<string name="finish_app_demo_title" msgid="8223819062053448384">"Beim nächsten Ausführen der Demo Berechtigungsbildschirm voranstellen"</string>
<string name="preseed_permission_app_title" msgid="182847662545676962">"Beim nächsten Ausführen Demo zu App-Berechtigungen voranstellen"</string>
<string name="preseed_permission_demo_title" msgid="5476541421753978071">"Beim nächsten Ausführen der Demo Berechtigungsbildschirm voranstellen"</string>
<string name="loading_demo_title" msgid="1086529475809143517">"Demo wird geladen"</string>
<string name="loading_demo_row_title" msgid="8933049915126088142">"Ladevorgang abgeschlossen!"</string>
<string name="pop_to_root" msgid="2078277386355064198">"Zu Stammverzeichnis wechseln"</string>
<string name="pop_to_marker" msgid="5007078308762725207">"Zur Markierung für verschiedene Demos wechseln"</string>
<string name="push_stack" msgid="2433062141810168976">"Weiter in Stack verschieben"</string>
<string name="pop_to_prefix" msgid="4288884615669751608">"Wechseln zu"</string>
<string name="pop_to_title" msgid="3924696281273379455">"Demo für „Wechseln zu“"</string>
<string name="display_settings" msgid="5726880972110281095">"Einstellungen für die Anzeige"</string>
<string name="package_not_found_error_msg" msgid="7525619456883627939">"Paket wurde nicht gefunden."</string>
<string name="permissions_granted_msg" msgid="2348556088141992714">"Alle Berechtigungen wurden erteilt. Du kannst sie in den Einstellungen deaktivieren."</string>
<string name="needs_access_msg_prefix" msgid="2204136858798832382">"Die App benötigt Zugriff auf die folgenden Berechtigungen:\n"</string>
@@ -154,7 +106,8 @@
<string name="long_route" msgid="4737969235741057506">"Lange Route"</string>
<string name="continue_start_nav" msgid="6231797535084469163">"Weiter, um die Navigation zu starten"</string>
<string name="continue_route" msgid="5172258139245088080">"Weiter zur Route"</string>
<string name="routes_title" msgid="7799772149932075357">"Routen"</string>
<string name="routes_title" msgid="7799772149932075357">"Route"</string>
<string name="new_route">Neue Route Berechnung</string>
<string name="place_list_nav_template_demo_title" msgid="8019588508812955290">"Demo der Navigationsvorlage für Ortslisten"</string>
<string name="route_preview_template_demo_title" msgid="7878704357953167555">"Demo der Routenvorschauvorlage"</string>
<string name="notification_template_demo_title" msgid="5076051497316030274">"Demo der Benachrichtigungsvorlage"</string>
@@ -355,7 +308,7 @@
<string name="map_template_toggle_demo_title" msgid="6510798293640092611">"Kartenvorlage mit Ein-/Aus-Schaltflächen"</string>
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
<string name="route_options_demo_title" msgid="4599699012716426514">"Routenoptionen"</string>
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen meiden"</string>
<string name="avoid_ferries_row_title" msgid="8232883866013711974">"Fähren vermeiden"</string>
<string name="map_demos_title" msgid="2169766615521476592">"Kartenbezogene Demos"</string>
<string name="map_with_content_demo_title" msgid="1032610482145018739">"Demos von Karten mit Inhalten"</string>

View File

@@ -41,8 +41,6 @@
<string name="sign_out_action_title">Sign out</string>
<string name="yes_action_title">Yes</string>
<string name="no_action_title">No</string>
<string name="disable_all_rows">Disable All Rows</string>
<string name="enable_all_rows">Enable All Rows</string>
<!-- Toast Messages -->
<string name="zoomed_in_toast_msg">Zoomed in</string>
@@ -57,17 +55,8 @@
<string name="parked_toast_msg">Parked action</string>
<string name="more_toast_msg">Clicked More</string>
<string name="grant_location_permission_toast_msg">Grant location Permission to see current location</string>
<string name="sign_in_with_google_toast_msg">Sign-in with Google starts here</string>
<string name="changes_selection_to_index_toast_msg_prefix">Changed selection to index</string>
<string name="yes_action_toast_msg">Yes button pressed!</string>
<string name="no_action_toast_msg">No button pressed!</string>
<string name="alert_timeout_toast_msg">Alert is timed out!</string>
<!-- Row text -->
<string name="first_row_title">Row with a large image and long text long text long text long text long text</string>
<string name="first_row_text">Text text text</string>
<string name="other_row_title_prefix">Row title </string>
<string name="other_row_text">Row text</string>
<!-- Place Details Screen -->
<string name="navigate">Navigate</string>
@@ -78,56 +67,8 @@
<!-- CarHardwareDemoScreen -->
<string name="fail_start_nav">Failure starting navigation</string>
<string name="fail_start_dialer">Failure starting dialer</string>
<string name="car_hardware_demo_title">Car Hardware Demo</string>
<!-- CarHardwareInfoScreen -->
<string name="car_hardware_info">Car Hardware Information</string>
<string name="model_info">Model Information</string>
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
<string name="model_unavailable">Model unavailable</string>
<string name="year_unavailable">Year unavailable</string>
<string name="energy_profile">Energy Profile</string>
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
<string name="fuel_types">Fuel Types</string>
<string name="unavailable">Unavailable</string>
<string name="ev_connector_types">EV Connector Types</string>
<!-- ColorDemoScreen -->
<string name="example_title">Example %d</string>
<string name="example_1_text">This text has a <annotation color="red">red</annotation> color</string>
<string name="example_2_text">This text has a <annotation color="green">green</annotation> color</string>
<string name="example_3_text">This text has a <annotation color="blue">blue</annotation> color</string>
<string name="example_4_text">This text has a <annotation color="yellow">yellow</annotation> color</string>
<string name="example_5_text">This text uses the primary color</string>
<string name="example_6_text">This text uses the secondary color</string>
<string name="color_demo">Color Demo</string>
<!-- ContentLimitsDemoScreen -->
<string name="list_limit">List Limit</string>
<string name="grid_limit">Grid Limit</string>
<string name="pane_limit">Pane Limit</string>
<string name="place_list_limit">Place List Limit</string>
<string name="route_list_limit">Route List Limit</string>
<string name="content_limits">Content Limits</string>
<string name="content_limits_demo_title">Content Limits Demo</string>
<!-- FinishAppScreen -->
<string name="finish_app_msg">This will finish the app, and when you return it will pre-seed a permission screen</string>
<string name="finish_app_title">Finish App Demo</string>
<string name="finish_app_demo_title">Pre-seed the Permission Screen on next run Demo</string>
<string name="preseed_permission_app_title">Pre-seed permission App Demo</string>
<string name="preseed_permission_demo_title">Pre-seed the Permission Screen on next run Demo</string>
<!-- LoadingDemoScreen -->
<string name="loading_demo_title">Loading Demo</string>
<string name="loading_demo_row_title">Loading Complete!</string>
<!-- PopToDemoScreen -->
<string name="pop_to_root">Pop to root</string>
<string name="pop_to_marker">Pop to Misc Demo Marker</string>
<string name="push_stack">Push further in stack</string>
<string name="pop_to_prefix">Pop To </string>
<string name="pop_to_title">PopTo Demo</string>
<string name="display_settings">Display settings</string>
<!-- RequestPermissionScreen -->
<string name="package_not_found_error_msg">Package Not found.</string>
@@ -193,7 +134,6 @@
<string name="take_520">Take 520</string>
<string name="gas_station">Gas Station</string>
<!-- RoutePreviewDemoScreen -->
<string name="short_route">Short route</string>
<string name="less_busy">Less busy</string>
<string name="hov_friendly">HOV friendly</string>
@@ -201,6 +141,7 @@
<string name="continue_start_nav">Continue to start navigation</string>
<string name="continue_route">Continue to route</string>
<string name="routes_title">Routes</string>
<string name="new_route">New Route calculation</string>
<!-- NavigationDemosScreen -->
<string name="place_list_nav_template_demo_title">Place List Navigation Template Demo</string>

View File

@@ -5,9 +5,9 @@ import android.location.LocationManager
import com.kouros.navigation.data.Constants.home2Location
import com.kouros.navigation.data.Constants.homeLocation
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.ViewModel
import org.junit.Assert.assertEquals
import org.junit.Test
/**
@@ -30,7 +30,7 @@ class ViewModelTest {
toLocation.latitude = home2Location.latitude
toLocation.longitude = home2Location.longitude
val route = repo.getRoute(fromLocation, toLocation)
val route = repo.getRoute(fromLocation, toLocation, SearchFilter())
model.startNavigation(route)
println(route)
}

View File

@@ -16,14 +16,19 @@
package com.kouros.navigation.data
import android.R
import android.location.Location
import android.location.LocationManager
import android.net.Uri
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.valhalla.Maneuvers
import com.kouros.navigation.data.valhalla.ValhallaJson
import com.kouros.navigation.utils.NavigationUtils.createGeoJson
import com.kouros.navigation.utils.NavigationUtils.decodePolyline
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import kotlinx.serialization.Serializable
import java.time.LocalDate
import java.util.Date
import org.maplibre.geojson.Point
data class Category(
val id: String,
@@ -60,29 +65,6 @@ data class StepData (
var bearing: Double
)
val dataPlaces = listOf(
Place(
id = 0,
name = "Vogelhartstr. 17",
category = "Favorites",
latitude = 48.1857475,
longitude = 11.5793627,
postalCode = "80807",
city = "München",
street = "Vogelhartstr. 17"
),
Place(
id = 0,
name = "Hohenwaldeckstr. 27",
category = "Recent",
latitude = 48.1165005,
longitude = 11.594349,
postalCode = "81541",
city = "München",
street = "Hohenwaldeckstr. 27",
)
)
// GeoJSON data classes
@Serializable
@@ -107,9 +89,41 @@ data class GeoJsonFeatureCollection(
data class Locations (
var lat : Double,
var lon : Double,
var street : String = ""
var street : String = "",
val search_filter: SearchFilter,
)
@Serializable
data class SearchFilter(
var max_road_class: String = "",
var exclude_toll : Boolean = false
) {
class Builder {
private var avoidMotorway = false
private var avoidTollway = false
fun avoidMotorway (value: Boolean ) = apply {
avoidMotorway = value
}
fun avoidTollway (value: Boolean ) = apply {
avoidTollway = value
}
fun build(): SearchFilter {
val filter = SearchFilter()
if (avoidMotorway) {
filter.max_road_class = "trunk"
}
if (avoidTollway) {
filter.exclude_toll = true
}
return filter
}
}
}
@Serializable
data class ValhallaLocation (
var locations: List<Locations>,
@@ -149,6 +163,10 @@ object Constants {
const val SHOW_THREED_BUILDING = "Show3D"
const val AVOID_MOTORWAY = "AvoidMotorway"
const val AVOID_TOLLWAY = "AvoidTollway"
const val NEXT_STEP_THRESHOLD = 100.0
const val MAXIMAL_SNAP_CORRECTION = 50.0

View File

@@ -34,10 +34,15 @@ class NavigationRepository {
private val nominatimUrl = "https://nominatim.openstreetmap.org/"
fun getRoute(currentLocation : Location, location: Location): String {
// Road classes from highest to lowest are:
// motorway, trunk, primary, secondary, tertiary, unclassified, residential, service_other.
// exclude_toll
fun getRoute(currentLocation: Location, location: Location, SearchFilter: SearchFilter): String {
SearchFilter
val vLocation = listOf(
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude),
Locations(lat = location.latitude, lon = location.longitude)
Locations(lat = currentLocation.latitude, lon = currentLocation.longitude, search_filter = SearchFilter),
Locations(lat = location.latitude, lon = location.longitude, search_filter = SearchFilter)
)
val valhallaLocation = ValhallaLocation(
locations = vLocation,
@@ -50,8 +55,8 @@ class NavigationRepository {
return fetchUrl(routeUrl + routeLocation, true)
}
fun getRouteDistance(currentLocation : Location, location: Location): Double {
val route = getRoute(currentLocation, location)
fun getRouteDistance(currentLocation: Location, location: Location, searchFilter: SearchFilter): Double {
val route = getRoute(currentLocation, location, searchFilter)
val routeModel = RouteModel()
routeModel.startNavigation(route)
return routeModel.route.distance
@@ -108,7 +113,6 @@ class NavigationRepository {
httpURLConnection.setRequestProperty("User-Agent", "email=nominatim@kouros-online.de");
httpURLConnection.requestMethod = "GET"
val responseCode = httpURLConnection.responseCode
println(responseCode)
if (responseCode == HttpURLConnection.HTTP_OK) {
val response = httpURLConnection.inputStream.bufferedReader()
.use { it.readText() } // defaults to UTF-8

View File

@@ -10,6 +10,7 @@ import com.kouros.navigation.utils.location
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point
import org.maplibre.turf.TurfMeasurement
import org.maplibre.turf.TurfMisc
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
@@ -72,7 +73,7 @@ open class RouteModel() {
if (distance < nearestDistance) {
nearestDistance = distance
route.currentManeuverIndex = i
calculateCurrentIndex(beginShapeIndex, endShapeIndex, location)
calculateCurrentShapeIndex(beginShapeIndex, endShapeIndex, location)
}
}
}
@@ -83,16 +84,17 @@ open class RouteModel() {
if (maneuver.streetNames != null && maneuver.streetNames.isNotEmpty()) {
text = maneuver.streetNames[0]
}
// TODO: +1 check
val curLocation = location(
route.pointLocations[currentShapeIndex].latitude(),
route.pointLocations[currentShapeIndex].longitude()
)
val nextLocation = location(
route.pointLocations[currentShapeIndex + 1].latitude(),
route.pointLocations[currentShapeIndex + 1].longitude()
)
bearing = curLocation.bearingTo(nextLocation)
if (currentShapeIndex < route.pointLocations.size) {
val nextLocation = location(
route.pointLocations[currentShapeIndex + 1].latitude(),
route.pointLocations[currentShapeIndex + 1].longitude()
)
bearing = curLocation.bearingTo(nextLocation).absoluteValue
}
val distanceStepLeft = leftStepDistance() * 1000
when (distanceStepLeft) {
in 0.0..NEXT_STEP_THRESHOLD -> {
@@ -108,7 +110,7 @@ open class RouteModel() {
}
/** Calculates the index in a maneuver. */
private fun calculateCurrentIndex(
private fun calculateCurrentShapeIndex(
beginShapeIndex: Int,
endShapeIndex: Int,
location: Location

View File

@@ -3,8 +3,6 @@ package com.kouros.navigation.model
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -14,13 +12,14 @@ import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.ObjectBox.boxStore
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.Place_
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.data.nominatim.Search
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.utils.NavigationUtils
import com.kouros.navigation.utils.location
import io.objectbox.kotlin.boxFor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -66,7 +65,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
//place.distance = distance.toFloat()
if (place.distance == 0F) {
recentPlace.postValue(place)
println("RecentPlace $recentPlace")
return@launch
}
}
@@ -75,7 +73,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
}
fun loadPlaces(location: Location) {
fun loadPlaces(context: Context, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
val placeBox = boxStore.boxFor(Place::class)
@@ -87,7 +85,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
query.close()
for (place in results) {
val plLocation = location(place.latitude, place.longitude)
val distance = repository.getRouteDistance(location, plLocation)
val distance = repository.getRouteDistance(location, plLocation, getSearchFilter(context))
place.distance = distance.toFloat()
}
places.postValue(results)
@@ -97,20 +95,20 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
fun loadRoute(currentLocation: Location, location: Location) {
fun loadRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
route.postValue(repository.getRoute(currentLocation, location))
route.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context)))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun loadPreviewRoute(currentLocation: Location, location: Location) {
fun loadPreviewRoute(context: Context, currentLocation: Location, location: Location) {
viewModelScope.launch(Dispatchers.IO) {
try {
previewRoute.postValue(repository.getRoute(currentLocation, location))
previewRoute.postValue(repository.getRoute(currentLocation, location, getSearchFilter(context)))
} catch (e: Exception) {
e.printStackTrace()
}
@@ -133,7 +131,7 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
if (addressLines.size > 1) {
val plLocation = location(adr.latitude, adr.longitude)
val distance =
repository.getRouteDistance(currentLocation, plLocation)
repository.getRouteDistance(currentLocation, plLocation, getSearchFilter(context))
contactList.add(
Place(
id = address.contactId,
@@ -179,7 +177,6 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
fun reverseAddress(location: Location ): String {
val address = repository.reverseAddress(location)
println(address)
val gson = GsonBuilder().serializeNulls().create()
val place = gson.fromJson(address, SearchResult::class.java)
println(place.address.road)
@@ -226,5 +223,21 @@ class ViewModel(private val repository: NavigationRepository) : ViewModel() {
}
}
}
fun getSearchFilter(context: Context): SearchFilter {
val avoidMotorway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_MOTORWAY
)
val avoidTollway = NavigationUtils.getBooleanKeyValue(
context = context,
Constants.AVOID_TOLLWAY
)
return SearchFilter.Builder()
.avoidMotorway(avoidMotorway)
.avoidTollway(avoidTollway)
.build()
}
}

View File

@@ -30,6 +30,10 @@ runtime = "1.9.5"
accompanist = "0.37.3"
uiVersion = "1.9.5"
uiText = "1.9.5"
navigationCompose = "2.9.6"
uiToolingPreview = "1.9.5"
uiTooling = "1.9.5"
material3WindowSizeClass = "1.4.0"
[libraries]
@@ -63,6 +67,10 @@ androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "uiVersion" }
androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
androidx-compose-material3-window-size-class1 = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }