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" }