From 9f53db8e76b77d5696ed53e231e91c9d3ecf61c0 Mon Sep 17 00:00:00 2001 From: Dimitris Date: Thu, 4 Dec 2025 08:10:03 +0100 Subject: [PATCH] Reroute --- app/build.gradle.kts | 34 +- app/src/main/AndroidManifest.xml | 5 +- .../com/kouros/navigation/MainApplication.kt | 2 + .../kouros/navigation/model/MockLocation.kt | 90 ++++ .../navigation/{ => ui}/MainActivity.kt | 383 ++++++++++-------- .../kouros/navigation/{ => ui}/Permissions.kt | 3 +- .../kouros/navigation/{ => ui}/SearchBar.kt | 2 +- common/car/build.gradle.kts | 1 + common/car/src/main/AndroidManifest.xml | 2 +- .../java/com/kouros/navigation/car/MapView.kt | 28 +- .../navigation/car/NavigationSession.kt | 68 +--- .../kouros/navigation/car/SurfaceRenderer.kt | 44 +- .../kouros/navigation/car/navigation/Gpx.kt | 77 ---- .../car/navigation/NavigationMessage.kt | 31 -- .../car/navigation/RouteCarModel.kt | 2 +- .../navigation/car/screen/DisplaySettings.kt | 2 +- .../navigation/car/screen/NavigationScreen.kt | 107 +++-- .../car/screen/NavigationSettings.kt | 55 ++- .../navigation/car/screen/PlaceListScreen.kt | 2 +- .../car/screen/RequestPermissionScreen.kt | 2 +- .../car/screen/RoutePreviewScreen.kt | 2 +- common/car/src/main/res/values-de/strings.xml | 55 +-- common/car/src/main/res/values/strings.xml | 63 +-- .../com/kouros/navigation/car/UnitTest.kt | 4 +- .../java/com/kouros/navigation/data/Data.kt | 70 ++-- .../navigation/data/NavigationRepository.kt | 16 +- .../com/kouros/navigation/model/RouteModel.kt | 18 +- .../com/kouros/navigation/model/ViewModel.kt | 37 +- gradle/libs.versions.toml | 8 + 29 files changed, 590 insertions(+), 623 deletions(-) create mode 100644 app/src/main/java/com/kouros/navigation/model/MockLocation.kt rename app/src/main/java/com/kouros/navigation/{ => ui}/MainActivity.kt (53%) rename app/src/main/java/com/kouros/navigation/{ => ui}/Permissions.kt (99%) rename app/src/main/java/com/kouros/navigation/{ => ui}/SearchBar.kt (98%) delete mode 100644 common/car/src/main/java/com/kouros/navigation/car/navigation/Gpx.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 380b8f6..aaeecc0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c9a234..29b64a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + diff --git a/app/src/main/java/com/kouros/navigation/MainApplication.kt b/app/src/main/java/com/kouros/navigation/MainApplication.kt index 327b502..02d0255 100644 --- a/app/src/main/java/com/kouros/navigation/MainApplication.kt +++ b/app/src/main/java/com/kouros/navigation/MainApplication.kt @@ -24,5 +24,7 @@ class MainApplication : Application() { companion object { var appContext: Context? = null private set + + var useContacts = true } } \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/model/MockLocation.kt b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt new file mode 100644 index 0000000..b615c62 --- /dev/null +++ b/app/src/main/java/com/kouros/navigation/model/MockLocation.kt @@ -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() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt similarity index 53% rename from app/src/main/java/com/kouros/navigation/MainActivity.kt rename to app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index 1703e13..e478680 100644 --- a/app/src/main/java/com/kouros/navigation/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -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 { 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,87 +369,108 @@ 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 - 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) // } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/kouros/navigation/Permissions.kt b/app/src/main/java/com/kouros/navigation/ui/Permissions.kt similarity index 99% rename from app/src/main/java/com/kouros/navigation/Permissions.kt rename to app/src/main/java/com/kouros/navigation/ui/Permissions.kt index d440764..705234f 100644 --- a/app/src/main/java/com/kouros/navigation/Permissions.kt +++ b/app/src/main/java/com/kouros/navigation/ui/Permissions.kt @@ -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 */ diff --git a/app/src/main/java/com/kouros/navigation/SearchBar.kt b/app/src/main/java/com/kouros/navigation/ui/SearchBar.kt similarity index 98% rename from app/src/main/java/com/kouros/navigation/SearchBar.kt rename to app/src/main/java/com/kouros/navigation/ui/SearchBar.kt index e030665..f73d188 100644 --- a/app/src/main/java/com/kouros/navigation/SearchBar.kt +++ b/app/src/main/java/com/kouros/navigation/ui/SearchBar.kt @@ -1,4 +1,4 @@ -package com.kouros.navigation +package com.kouros.navigation.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index d4f49ba..1a52ecc 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/common/car/src/main/AndroidManifest.xml b/common/car/src/main/AndroidManifest.xml index a76651a..4e51f10 100644 --- a/common/car/src/main/AndroidManifest.xml +++ b/common/car/src/main/AndroidManifest.xml @@ -32,7 +32,7 @@ - + Zoomed in @@ -57,17 +55,8 @@ Parked action Clicked More Grant location Permission to see current location - Sign-in with Google starts here Changed selection to index - Yes button pressed! - No button pressed! - Alert is timed out! - - Row with a large image and long text long text long text long text long text - Text text text - Row title - Row text Navigate @@ -78,56 +67,8 @@ Failure starting navigation Failure starting dialer - Car Hardware Demo - - Car Hardware Information - Model Information - Manufacturer unavailable - Model unavailable - Year unavailable - Energy Profile - No Energy Profile Permission - Fuel Types - Unavailable - EV Connector Types - - - Example %d - This text has a red color - This text has a green color - This text has a blue color - This text has a yellow color - This text uses the primary color - This text uses the secondary color - Color Demo - - - List Limit - Grid Limit - Pane Limit - Place List Limit - Route List Limit - Content Limits - Content Limits Demo - - - This will finish the app, and when you return it will pre-seed a permission screen - Finish App Demo - Pre-seed the Permission Screen on next run Demo - Pre-seed permission App Demo - Pre-seed the Permission Screen on next run Demo - - - Loading Demo - Loading Complete! - - - Pop to root - Pop to Misc Demo Marker - Push further in stack - Pop To - PopTo Demo + Display settings Package Not found. @@ -193,7 +134,6 @@ Take 520 Gas Station - Short route Less busy HOV friendly @@ -201,6 +141,7 @@ Continue to start navigation Continue to route Routes + New Route calculation Place List Navigation Template Demo diff --git a/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt b/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt index 45395c9..a0be751 100644 --- a/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt +++ b/common/car/src/test/java/com/kouros/navigation/car/UnitTest.kt @@ -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) } diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 53c5595..898595c 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -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, @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt index 588cd16..960ab2e 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 6512624..b19cc0a 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -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 diff --git a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt index fd21b31..628bf54 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/ViewModel.kt @@ -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() + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6070e35..3df4f51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }