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,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<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