Reroute
This commit is contained in:
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,5 +24,7 @@ class MainApplication : Application() {
|
||||
companion object {
|
||||
var appContext: Context? = null
|
||||
private set
|
||||
|
||||
var useContacts = true
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) //
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.kouros.navigation
|
||||
package com.kouros.navigation.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -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)
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
|
||||
<application android:requestLegacyExternalStorage="true">
|
||||
<!--
|
||||
|
||||
@@ -47,13 +47,10 @@ import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
@Composable
|
||||
fun cameraState(
|
||||
width: Int,
|
||||
height: Int,
|
||||
padding : PaddingValues,
|
||||
position: CameraPosition?,
|
||||
tilt: Double,
|
||||
preview: Boolean
|
||||
): CameraState {
|
||||
val padding = getPaddingValues(height, preview)
|
||||
return rememberCameraState(
|
||||
firstPosition =
|
||||
CameraPosition(
|
||||
@@ -117,20 +114,18 @@ fun BuildingLayer(tiles: Source) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DrawImage(width: Int, height: Int, location: Location, street: String) {
|
||||
NavigationImage(height, street)
|
||||
fun DrawImage(padding: PaddingValues, location: Location, width: Int, height: Int, street: String) {
|
||||
NavigationImage(padding, street)
|
||||
Speed(width, height, location)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationImage(height: Int, street: String) {
|
||||
fun NavigationImage(padding: PaddingValues, street: String) {
|
||||
val vector = ImageVector.vectorResource(id = R.drawable.assistant_navigation_48px)
|
||||
val color = remember { NavigationColor }
|
||||
BadgedBox(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = 0.dp, top = distanceFromTop(height).dp
|
||||
),
|
||||
.padding(padding),
|
||||
badge = {
|
||||
Badge()
|
||||
}
|
||||
@@ -156,7 +151,7 @@ private fun Speed(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = width.dp- 300.dp,
|
||||
start = width.dp- 250.dp,
|
||||
top = height.dp- 80.dp
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -210,18 +205,17 @@ private fun Speed(
|
||||
}
|
||||
}
|
||||
|
||||
fun getPaddingValues(height: Int, preView: Boolean): PaddingValues {
|
||||
val padding = PaddingValues(start = 0.dp, top = distanceFromTop(height).dp)
|
||||
val prePadding = PaddingValues(start = 150.dp, bottom = 0.dp)
|
||||
fun getPaddingValues(width: Int, height: Int, preView: Boolean): PaddingValues {
|
||||
return if (preView) {
|
||||
prePadding
|
||||
PaddingValues(start = 150.dp, bottom = 0.dp)
|
||||
} else {
|
||||
padding
|
||||
// PaddingValues(start = width.dp, top = distanceFromTop(height).dp)
|
||||
PaddingValues(start = 0.dp, top = distanceFromTop(height).dp)
|
||||
}
|
||||
}
|
||||
|
||||
fun distanceFromTop(height: Int): Int {
|
||||
return height - percent(height, 20)
|
||||
return height - percent(height, 25)
|
||||
}
|
||||
|
||||
fun percent(maxValue: Int, value: Int): Int {
|
||||
|
||||
@@ -7,9 +7,7 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.ScreenManager
|
||||
@@ -27,13 +25,7 @@ import com.kouros.navigation.car.screen.SearchScreen
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_ROUTE_DEVIATION
|
||||
import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
|
||||
import com.kouros.navigation.data.Constants.TAG
|
||||
import com.kouros.navigation.data.ObjectBox
|
||||
import com.kouros.navigation.utils.NavigationUtils.snapLocation
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
val uriScheme = "samples";
|
||||
@@ -46,16 +38,10 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
|
||||
lateinit var surfaceRenderer: SurfaceRenderer
|
||||
|
||||
var locationIndex = 0
|
||||
|
||||
val simulate = false
|
||||
|
||||
var mLocationListener: LocationListenerCompat = LocationListenerCompat { location: Location? ->
|
||||
updateLocation(location)
|
||||
updateLocation(location!!)
|
||||
}
|
||||
|
||||
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val mLifeCycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
Log.i(TAG, "In onCreate()")
|
||||
@@ -161,56 +147,24 @@ class NavigationSession : Session(), NavigationScreen.Listener {
|
||||
val locationManager =
|
||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
|
||||
updateLocation(location)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
/* minTimeMs= */ 500,
|
||||
/* minDistanceM= */ 0f,
|
||||
mLocationListener
|
||||
)
|
||||
}
|
||||
|
||||
fun updateLocation(location: Location?) {
|
||||
if (location != null) {
|
||||
if (simulate) {
|
||||
simulate(location)
|
||||
} else {
|
||||
update(location)
|
||||
}
|
||||
updateLocation(location)
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
/* minTimeMs= */ 500,
|
||||
/* minDistanceM= */ 0f,
|
||||
mLocationListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun simulate(location: Location?) {
|
||||
if (routeModel.isNavigating() && locationIndex < routeModel.route.waypoints.size) {
|
||||
coroutineScope.launch {
|
||||
if (locationIndex >= routeModel.route.waypoints.size) {
|
||||
return@launch
|
||||
}
|
||||
val loc = routeModel.route.waypoints[locationIndex]
|
||||
val curLocation = Location(LocationManager.GPS_PROVIDER)
|
||||
curLocation.longitude = loc[0]// + 0.00001 * locationIndex
|
||||
curLocation.latitude = loc[1] //+ 0.00001 * locationIndex
|
||||
curLocation.speed = 15F
|
||||
update(curLocation)
|
||||
locationIndex += 1
|
||||
if (locationIndex > routeModel.route.waypoints.size) {
|
||||
val locationManager =
|
||||
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
locationManager.removeUpdates(mLocationListener)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
update(location = location!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(location: Location) {
|
||||
fun updateLocation(location: Location) {
|
||||
if (routeModel.isNavigating()) {
|
||||
val snapedLocation = snapLocation(location, routeModel.route.maneuverLocations())
|
||||
val distance = location.distanceTo(snapedLocation)
|
||||
if (distance > MAXIMAL_ROUTE_DEVIATION) {
|
||||
// navigationScreen.calculateNewRoute()
|
||||
//return
|
||||
navigationScreen.calculateNewRoute(routeModel.destination)
|
||||
return
|
||||
}
|
||||
routeModel.updateLocation(location)
|
||||
navigationScreen.updateTrip()
|
||||
|
||||
@@ -6,13 +6,13 @@ import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.car.app.AppManager
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.SurfaceCallback
|
||||
import androidx.car.app.SurfaceContainer
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -54,7 +54,7 @@ class SurfaceRenderer(
|
||||
)
|
||||
)
|
||||
var visibleArea = MutableLiveData(
|
||||
Rect(0,0,0,0)
|
||||
Rect(0, 0, 0, 0)
|
||||
)
|
||||
|
||||
var stableArea = Rect()
|
||||
@@ -162,16 +162,17 @@ class SurfaceRenderer(
|
||||
|
||||
@Composable
|
||||
fun MapView() {
|
||||
val stateWidth = visibleArea.observeAsState()
|
||||
val position: CameraPosition? by cameraPosition.observeAsState()
|
||||
val route: String? by routeData.observeAsState()
|
||||
val previewRoute: String? by previewRouteData.observeAsState()
|
||||
val cameraState = cameraState(width, height, position, tilt, preview)
|
||||
val paddingValues = getPaddingValues( width - stateWidth.value!!.width(), height, preview)
|
||||
val cameraState = cameraState(paddingValues, position, tilt)
|
||||
|
||||
val baseStyle = BaseStyle.Uri(Constants.STYLE)
|
||||
// if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
|
||||
// Constants.STYLE
|
||||
// )
|
||||
|
||||
if (isSystemInDarkTheme()) BaseStyle.Uri(Constants.STYLE_DARK) else BaseStyle.Uri(
|
||||
Constants.STYLE
|
||||
)
|
||||
MaplibreMap(
|
||||
cameraState = cameraState,
|
||||
baseStyle = baseStyle,
|
||||
@@ -184,11 +185,11 @@ class SurfaceRenderer(
|
||||
}
|
||||
//Puck(cameraState, lastLocation)
|
||||
}
|
||||
ShowPosition(cameraState, position)
|
||||
ShowPosition(cameraState, position, paddingValues)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowPosition(cameraState: CameraState, position: CameraPosition?) {
|
||||
fun ShowPosition(cameraState: CameraState, position: CameraPosition?, paddingValues: PaddingValues) {
|
||||
val cameraDuration = duration(position)
|
||||
var bearing = position!!.bearing
|
||||
var zoom = position.zoom
|
||||
@@ -196,9 +197,9 @@ class SurfaceRenderer(
|
||||
var localTilt = tilt
|
||||
if (!preview) {
|
||||
if (routeModel.isNavigating()) {
|
||||
DrawImage(width, height, lastLocation, "")
|
||||
DrawImage(paddingValues, lastLocation, width, height,"")
|
||||
} else {
|
||||
DrawImage(width, height, lastLocation, "")
|
||||
DrawImage(paddingValues, lastLocation,width, height, "")
|
||||
}
|
||||
} else {
|
||||
bearing = 0.0
|
||||
@@ -213,7 +214,7 @@ class SurfaceRenderer(
|
||||
zoom = zoom,
|
||||
target = target,
|
||||
tilt = localTilt,
|
||||
padding = getPaddingValues(height, preview)
|
||||
padding = paddingValues
|
||||
),
|
||||
duration = cameraDuration
|
||||
)
|
||||
@@ -259,16 +260,21 @@ class SurfaceRenderer(
|
||||
fun updateLocation(location: Location) {
|
||||
synchronized(this) {
|
||||
if (!preview) {
|
||||
var bearing = cameraPosition.value!!.bearing
|
||||
if (routeModel.isNavigating()) {
|
||||
bearing = routeModel.currentStep().bearing
|
||||
val bearing = if (routeModel.isNavigating()) {
|
||||
routeModel.currentStep().bearing
|
||||
} else {
|
||||
lastLocation.bearingTo(location).toInt().toDouble().absoluteValue
|
||||
}
|
||||
val zoom = if (!panView) {
|
||||
calculateZoom(location.speed.toDouble())
|
||||
} else {
|
||||
cameraPosition.value!!.zoom
|
||||
}
|
||||
updateCameraPosition(bearing, zoom, Position(location.longitude, location.latitude))
|
||||
updateCameraPosition(
|
||||
bearing,
|
||||
zoom,
|
||||
Position(location.longitude, location.latitude)
|
||||
)
|
||||
lastBearing = cameraPosition.value!!.bearing
|
||||
lastLocation = location
|
||||
} else {
|
||||
@@ -289,7 +295,7 @@ class SurfaceRenderer(
|
||||
bearing = bearing,
|
||||
zoom = zoom,
|
||||
tilt = 0.0,
|
||||
padding = getPaddingValues(height, preview),
|
||||
padding = getPaddingValues(width-visibleArea.value!!.width(), height, preview),
|
||||
target = target
|
||||
)
|
||||
)
|
||||
@@ -315,9 +321,11 @@ class SurfaceRenderer(
|
||||
in 0.0..10.0 -> {
|
||||
return 13.0
|
||||
}
|
||||
|
||||
in 10.0..20.0 -> {
|
||||
return 11.0
|
||||
}
|
||||
|
||||
in 20.0..30.0 -> {
|
||||
return 10.0
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package com.kouros.navigation.car.navigation
|
||||
|
||||
import android.location.Location
|
||||
import android.os.Environment
|
||||
import org.xml.sax.SAXException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
|
||||
class Gpx {
|
||||
|
||||
fun loadGPX() {
|
||||
val path = Environment.getExternalStorageDirectory()
|
||||
.toString() + "/Download/VogelHohen.gpx"
|
||||
var info = ""
|
||||
val gpxFile = File(path)
|
||||
|
||||
info = info + gpxFile.path + "\n\n"
|
||||
|
||||
val gpxList = decodeGPX(gpxFile)
|
||||
print(gpxList)
|
||||
}
|
||||
|
||||
|
||||
private fun decodeGPX(file: File): MutableList<Location?> {
|
||||
val list: MutableList<Location?> = ArrayList()
|
||||
|
||||
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
|
||||
try {
|
||||
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
|
||||
val fileInputStream = FileInputStream(file)
|
||||
val document = documentBuilder.parse(fileInputStream)
|
||||
val elementRoot = document.documentElement
|
||||
|
||||
val nodelist_trkpt = elementRoot.getElementsByTagName("trkpt")
|
||||
|
||||
for (i in 0..<nodelist_trkpt.getLength()) {
|
||||
val node = nodelist_trkpt.item(i)
|
||||
val attributes = node.getAttributes()
|
||||
|
||||
val newLatitude = attributes.getNamedItem("lat").getTextContent()
|
||||
val newLatitude_double = newLatitude.toDouble()
|
||||
|
||||
val newLongitude = attributes.getNamedItem("lon").getTextContent()
|
||||
val newLongitude_double = newLongitude.toDouble()
|
||||
|
||||
val newLocationName = newLatitude + ":" + newLongitude
|
||||
val newLocation = Location(newLocationName)
|
||||
newLocation.setLatitude(newLatitude_double)
|
||||
newLocation.setLongitude(newLongitude_double)
|
||||
|
||||
list.add(newLocation)
|
||||
}
|
||||
|
||||
fileInputStream.close()
|
||||
} catch (e: ParserConfigurationException) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace()
|
||||
} catch (e: FileNotFoundException) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace()
|
||||
} catch (e: SAXException) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,6 @@ import com.kouros.android.cars.carappservice.R
|
||||
|
||||
class NavigationMessage (private var carContext: CarContext) {
|
||||
|
||||
/** Returns a sample [Alert]. */
|
||||
fun createAlert(): Alert {
|
||||
val title: CarText = createCarText(R.string.navigation_alert_title)
|
||||
val subtitle: CarText = createCarText(R.string.navigation_alert_subtitle)
|
||||
val icon = CarIcon.ALERT
|
||||
|
||||
val yesAction: Action = createToastAction(
|
||||
R.string.yes_action_title,
|
||||
R.string.yes_action_toast_msg, Action.FLAG_PRIMARY
|
||||
)
|
||||
val noAction: Action = createToastAction(
|
||||
R.string.no_action_title, R.string.no_action_toast_msg,
|
||||
Action.FLAG_DEFAULT
|
||||
)
|
||||
|
||||
return Alert.Builder( /* alertId: */0, title, /* durationMillis: */10000)
|
||||
.setSubtitle(subtitle)
|
||||
.setIcon(icon)
|
||||
.addAction(yesAction)
|
||||
.addAction(noAction).setCallback(object : AlertCallback {
|
||||
override fun onCancel(reason: Int) {
|
||||
if (reason == AlertCallback.REASON_TIMEOUT) {
|
||||
showToast(R.string.alert_timeout_toast_msg)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss() {
|
||||
}
|
||||
}).build()
|
||||
}
|
||||
|
||||
private fun createToastAction(
|
||||
@StringRes titleRes: Int, @StringRes toastStringRes: Int,
|
||||
flags: Int
|
||||
|
||||
@@ -214,7 +214,7 @@ class RouteCarModel() : RouteModel() {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createString(
|
||||
fun createString(
|
||||
text: String
|
||||
): SpannableString {
|
||||
val spannableString = SpannableString(text)
|
||||
|
||||
@@ -40,7 +40,7 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
.setSingleList(listBuilder.build())
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setTitle(carContext.getString(R.string.content_limits))
|
||||
.setTitle(carContext.getString(R.string.display_settings))
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.build()
|
||||
)
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.MapController
|
||||
import androidx.car.app.navigation.model.MapWithContentTemplate
|
||||
import androidx.car.app.navigation.model.MessageInfo
|
||||
import androidx.car.app.navigation.model.NavigationTemplate
|
||||
@@ -86,18 +85,17 @@ class NavigationScreen(
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val actionStripBuilder = createActionStripBuilder()
|
||||
if (calculateNewRoute) {
|
||||
return navigationRerouteTemplate(actionStripBuilder)
|
||||
}
|
||||
return if (routeModel.isNavigating()) {
|
||||
if (calculateNewRoute) {
|
||||
getNavigationLoadingTemplate(actionStripBuilder)
|
||||
} else {
|
||||
getNavigationTemplate(actionStripBuilder)
|
||||
}
|
||||
navigationTemplate(actionStripBuilder)
|
||||
} else {
|
||||
getNavigationEndTemplate(actionStripBuilder)
|
||||
navigationEndTemplate(actionStripBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNavigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
actionStripBuilder.addAction(
|
||||
stopAction()
|
||||
)
|
||||
@@ -112,7 +110,7 @@ class NavigationScreen(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getNavigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template {
|
||||
if (routeModel.isArrived()) {
|
||||
val timer = object : CountDownTimer(10000, 10000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
@@ -122,30 +120,10 @@ class NavigationScreen(
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(
|
||||
MessageInfo.Builder(
|
||||
carContext.getString(R.string.arrived_exclamation_msg)
|
||||
)
|
||||
.setText(routeModel.destination.street!!)
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_place_white_24dp
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setBackgroundColor(CarColor.GREEN)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(mapActionStripBuilder().build())
|
||||
.build()
|
||||
return navigationArrivedTemplate(actionStripBuilder)
|
||||
} else {
|
||||
return if (recentPlaceFound && recentPlaceActive) {
|
||||
return getRecentPlaceTemplate()
|
||||
return recentPlaceTemplate()
|
||||
} else {
|
||||
NavigationTemplate.Builder()
|
||||
.setBackgroundColor(CarColor.SECONDARY)
|
||||
@@ -156,7 +134,31 @@ class NavigationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecentPlaceTemplate(): Template {
|
||||
fun navigationArrivedTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(
|
||||
MessageInfo.Builder(
|
||||
carContext.getString(R.string.arrived_exclamation_msg)
|
||||
)
|
||||
.setText(routeModel.destination.street!!)
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_place_white_24dp
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setBackgroundColor(CarColor.GREEN)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(mapActionStripBuilder().build())
|
||||
.build()
|
||||
}
|
||||
|
||||
fun recentPlaceTemplate(): Template {
|
||||
val messageTemplate = MessageTemplate.Builder(
|
||||
recentPlace.name + "\n"
|
||||
+ recentPlace.city
|
||||
@@ -181,11 +183,27 @@ class NavigationScreen(
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun getNavigationLoadingTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
fun navigationRerouteTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate {
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setNavigationInfo(
|
||||
MessageInfo.Builder(
|
||||
carContext.getString(R.string.new_route)
|
||||
)
|
||||
.setText(routeModel.destination.street.toString())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.navigation_48px
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.setBackgroundColor(CarColor.SECONDARY)
|
||||
.setActionStrip(actionStripBuilder.build())
|
||||
.setMapActionStrip(mapActionStripBuilder().build())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -271,7 +289,7 @@ class NavigationScreen(
|
||||
)
|
||||
.setOnClickListener {
|
||||
val navigateTo = location(recentPlace.latitude, recentPlace.longitude)
|
||||
viewModel.loadRoute(surfaceRenderer.lastLocation, navigateTo)
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, navigateTo)
|
||||
routeModel.destination = recentPlace
|
||||
}
|
||||
.build()
|
||||
@@ -394,7 +412,7 @@ class NavigationScreen(
|
||||
location.latitude = place.latitude
|
||||
location.longitude = place.longitude
|
||||
viewModel.saveRecent(place)
|
||||
viewModel.loadRoute(surfaceRenderer.lastLocation, location)
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, location)
|
||||
currentNavigationLocation = location
|
||||
routeModel.destination = place
|
||||
invalidate()
|
||||
@@ -408,24 +426,25 @@ class NavigationScreen(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun calculateNewRoute() {
|
||||
fun calculateNewRoute(destination: Place) {
|
||||
calculateNewRoute = true
|
||||
stopNavigation()
|
||||
invalidate()
|
||||
val mainThreadhandler = Handler(carContext.mainLooper)
|
||||
mainThreadhandler.post {
|
||||
val mainThreadHandler = Handler(carContext.mainLooper)
|
||||
mainThreadHandler.post {
|
||||
object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
calculateNewRoute = false
|
||||
stopNavigation()
|
||||
reRoute(destination)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun reRoute() {
|
||||
NavigationMessage(carContext).createAlert()
|
||||
viewModel.loadRoute(surfaceRenderer.lastLocation, currentNavigationLocation)
|
||||
fun reRoute(destination: Place) {
|
||||
val dest = location(destination.latitude, destination.longitude)
|
||||
viewModel.loadRoute(carContext, surfaceRenderer.lastLocation, dest)
|
||||
}
|
||||
|
||||
fun updateTrip() {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.kouros.navigation.car.screen
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.ItemList
|
||||
@@ -11,48 +9,65 @@ import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import com.kouros.android.cars.carappservice.R
|
||||
import com.kouros.navigation.data.Constants.SHOW_THREED_BUILDING
|
||||
import com.kouros.navigation.data.Constants.AVOID_MOTORWAY
|
||||
import com.kouros.navigation.data.Constants.AVOID_TOLLWAY
|
||||
import com.kouros.navigation.utils.NavigationUtils.getBooleanKeyValue
|
||||
import com.kouros.navigation.utils.NavigationUtils.setBooleanKeyValue
|
||||
|
||||
|
||||
class NavigationSettings(private val carContext: CarContext) : Screen(carContext) {
|
||||
|
||||
private var motorWayToggleState = false
|
||||
|
||||
private var tollWayToggleState = false
|
||||
|
||||
init {
|
||||
motorWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||
|
||||
tollWayToggleState = getBooleanKeyValue(carContext, AVOID_MOTORWAY)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
val listBuilder = ItemList.Builder()
|
||||
val highwayToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
if (checked) {
|
||||
setBooleanKeyValue(carContext, true, AVOID_MOTORWAY)
|
||||
} else {
|
||||
setBooleanKeyValue(carContext, false, AVOID_MOTORWAY)
|
||||
}
|
||||
motorWayToggleState = !motorWayToggleState
|
||||
}.setChecked(motorWayToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_highways_row_title, highwayToggle))
|
||||
|
||||
|
||||
listBuilder.addItem(
|
||||
buildRowForTemplate(
|
||||
R.string.list_limit,
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
|
||||
)
|
||||
)
|
||||
// Tollway
|
||||
val tollwayToggle: Toggle =
|
||||
Toggle.Builder { checked: Boolean ->
|
||||
if (checked) {
|
||||
setBooleanKeyValue(carContext, true, AVOID_TOLLWAY)
|
||||
} else {
|
||||
setBooleanKeyValue(carContext, false, AVOID_TOLLWAY)
|
||||
}
|
||||
tollWayToggleState = !tollWayToggleState
|
||||
}.setChecked(tollWayToggleState).build()
|
||||
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
|
||||
|
||||
return ListTemplate.Builder()
|
||||
.setSingleList(listBuilder.build())
|
||||
.setHeader(
|
||||
Header.Builder()
|
||||
.setTitle(carContext.getString(R.string.content_limits))
|
||||
.setTitle(carContext.getString(R.string.display_settings))
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildRowForTemplate(title: Int, contentLimitType: Int): Row {
|
||||
private fun buildRowForTemplate(title: Int, toggle: Toggle): Row {
|
||||
return Row.Builder()
|
||||
.setTitle(carContext.getString(title))
|
||||
.addText(
|
||||
carContext
|
||||
.getCarService(ConstraintManager::class.java)
|
||||
.getContentLimit(contentLimitType).toString()
|
||||
)
|
||||
.setToggle(toggle)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class PlaceListScreen(
|
||||
init {
|
||||
if (category == Constants.RECENT) {
|
||||
viewModel.places.observe(this, observer)
|
||||
viewModel.loadPlaces(location)
|
||||
viewModel.loadPlaces(carContext, location)
|
||||
}
|
||||
if (category == Constants.CONTACTS) {
|
||||
viewModel.contactAddress.observe(this, observerAddress)
|
||||
|
||||
@@ -26,7 +26,7 @@ class RequestPermissionScreen(
|
||||
override fun onGetTemplate(): Template {
|
||||
val permissions: MutableList<String?> = ArrayList()
|
||||
permissions.add(permission.ACCESS_FINE_LOCATION)
|
||||
permissions.add(permission.READ_CONTACTS)
|
||||
//permissions.add(permission.READ_CONTACTS)
|
||||
|
||||
val message = "This app needs access to location in order to show the map around you"
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class RoutePreviewScreen(
|
||||
val location = Location(LocationManager.GPS_PROVIDER)
|
||||
location.latitude = destination.latitude
|
||||
location.longitude = destination.longitude
|
||||
vieModel.loadPreviewRoute(surfaceRenderer.lastLocation, location)
|
||||
vieModel.loadPreviewRoute(carContext,surfaceRenderer.lastLocation, location)
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
|
||||
@@ -41,8 +41,6 @@
|
||||
<string name="sign_out_action_title" msgid="1653943000866713010">"Abmelden"</string>
|
||||
<string name="yes_action_title" msgid="5507096013762092189">"Ja"</string>
|
||||
<string name="no_action_title" msgid="1452124604210014010">"Nein"</string>
|
||||
<string name="disable_all_rows" msgid="3003225080532928046">"Alle Zeilen deaktivieren"</string>
|
||||
<string name="enable_all_rows" msgid="7274285275711872091">"Alle Zeilen aktivieren"</string>
|
||||
<string name="zoomed_in_toast_msg" msgid="8915301497303842649">"Herangezoomt"</string>
|
||||
<string name="zoomed_out_toast_msg" msgid="6260981223227212493">"Herausgezoomt"</string>
|
||||
<string name="triggered_toast_msg" msgid="3396166539208366382">"Ausgelöst"</string>
|
||||
@@ -55,59 +53,13 @@
|
||||
<string name="parked_toast_msg" msgid="2532422265890824446">"Aktion „Geparkt“"</string>
|
||||
<string name="more_toast_msg" msgid="5938288138225509885">"„Mehr“ angeklickt"</string>
|
||||
<string name="grant_location_permission_toast_msg" msgid="268046297444808010">"Standortermittlung erlauben, um aktuellen Standort anzuzeigen"</string>
|
||||
<string name="sign_in_with_google_toast_msg" msgid="5720947549233124775">"Über Google anmelden beginnt hier"</string>
|
||||
<string name="changes_selection_to_index_toast_msg_prefix" msgid="957766225794389167">"Auswahl auf Index geändert"</string>
|
||||
<string name="yes_action_toast_msg" msgid="6216215197177241247">"Schaltfläche „Ja“ gedrückt."</string>
|
||||
<string name="no_action_toast_msg" msgid="6165492423831023809">"Schaltfläche „Nein“ gedrückt."</string>
|
||||
<string name="alert_timeout_toast_msg" msgid="5568380708832805374">"Zeitüberschreitung bei Benachrichtigung."</string>
|
||||
<string name="first_row_title" msgid="219428344573165351">"Zeile mit großem Bild und langem Text langem Text langem Text langem Text langem Text"</string>
|
||||
<string name="first_row_text" msgid="3887390298628338716">"Text Text Text"</string>
|
||||
<string name="other_row_title_prefix" msgid="4702355788835253197">"Zeilentitel"</string>
|
||||
<string name="other_row_text" msgid="7510279447493169945">"Zeilentext"</string>
|
||||
<string name="navigate" msgid="2713090390373996139">"Navigieren"</string>
|
||||
<string name="dial" msgid="3145707439707628311">"Wählen"</string>
|
||||
<string name="address" msgid="9010635942573581302">"Adresse"</string>
|
||||
<string name="phone" msgid="2504766809811627577">"Smartphone"</string>
|
||||
<string name="fail_start_nav" msgid="6921321606009212189">"Fehler beim Starten der Navigation"</string>
|
||||
<string name="fail_start_dialer" msgid="1471602619507306261">"Fehler beim Starten des Telefons"</string>
|
||||
<string name="car_hardware_demo_title" msgid="3679106197233262689">"Demo der Auto-Hardware"</string>
|
||||
<string name="car_hardware_info" msgid="1244783247616395012">"Informationen zur Auto-Hardware"</string>
|
||||
<string name="model_info" msgid="494224423025683030">"Modellinformationen"</string>
|
||||
<string name="manufacturer_unavailable" msgid="4978995415869838056">"Hersteller nicht verfügbar"</string>
|
||||
<string name="model_unavailable" msgid="4075463010215406573">"Modell nicht verfügbar"</string>
|
||||
<string name="year_unavailable" msgid="994338773299644607">"Jahr nicht verfügbar"</string>
|
||||
<string name="energy_profile" msgid="81415433590192158">"Energieprofil"</string>
|
||||
<string name="no_energy_profile_permission" msgid="4662285713731308888">"Keine Berechtigung für Energieprofil"</string>
|
||||
<string name="fuel_types" msgid="6811375173343218212">"Kraftstofftypen"</string>
|
||||
<string name="unavailable" msgid="3636401138255192934">"Nicht verfügbar"</string>
|
||||
<string name="ev_connector_types" msgid="735458637011996125">"Elektrofahrzeug-Anschlusssteckertypen"</string>
|
||||
<string name="example_title" msgid="530257630320010494">"Beispiel: %d"</string>
|
||||
<string name="example_1_text" msgid="8631503055894800688">"Dieser Text ist "<annotation color="red">"rot"</annotation></string>
|
||||
<string name="example_2_text" msgid="1359373957397219102">"Dieser Text ist "<annotation color="green">"grün"</annotation></string>
|
||||
<string name="example_3_text" msgid="2409207170762049673">"Dieser Text ist "<annotation color="blue">"blau"</annotation></string>
|
||||
<string name="example_4_text" msgid="9055989886645433000">"Dieser Text ist "<annotation color="yellow">"gelb"</annotation></string>
|
||||
<string name="example_5_text" msgid="8828804968749423500">"Für diesen Text wird die Primärfarbe verwendet"</string>
|
||||
<string name="example_6_text" msgid="7991523168517599600">"Dieser Text verwendet die Sekundärfarbe"</string>
|
||||
<string name="color_demo" msgid="1822427636476178993">"Farbdemo"</string>
|
||||
<string name="list_limit" msgid="3023536401535417286">"Listenbeschränkung"</string>
|
||||
<string name="grid_limit" msgid="1350116012893549206">"Rasterbegrenzung"</string>
|
||||
<string name="pane_limit" msgid="981518409516855230">"Bereichsbegrenzung"</string>
|
||||
<string name="place_list_limit" msgid="6785181191763056582">"Limit für Ortsliste"</string>
|
||||
<string name="route_list_limit" msgid="505793441615134116">"Limit für Routenliste"</string>
|
||||
<string name="content_limits" msgid="5726880972110281095">"Beschränkungen für Inhalte"</string>
|
||||
<string name="content_limits_demo_title" msgid="3207211638386727610">"Demo für „Beschränkungen für Inhalte“"</string>
|
||||
<string name="finish_app_msg" msgid="8354334557053141891">"Dadurch wird die App geschlossen und beim nächsten Ausführen eine Berechtigungsanfrage eingeblendet"</string>
|
||||
<string name="finish_app_title" msgid="9013328479438745074">"App-Demo beenden"</string>
|
||||
<string name="finish_app_demo_title" msgid="8223819062053448384">"Beim nächsten Ausführen der Demo Berechtigungsbildschirm voranstellen"</string>
|
||||
<string name="preseed_permission_app_title" msgid="182847662545676962">"Beim nächsten Ausführen Demo zu App-Berechtigungen voranstellen"</string>
|
||||
<string name="preseed_permission_demo_title" msgid="5476541421753978071">"Beim nächsten Ausführen der Demo Berechtigungsbildschirm voranstellen"</string>
|
||||
<string name="loading_demo_title" msgid="1086529475809143517">"Demo wird geladen"</string>
|
||||
<string name="loading_demo_row_title" msgid="8933049915126088142">"Ladevorgang abgeschlossen!"</string>
|
||||
<string name="pop_to_root" msgid="2078277386355064198">"Zu Stammverzeichnis wechseln"</string>
|
||||
<string name="pop_to_marker" msgid="5007078308762725207">"Zur Markierung für verschiedene Demos wechseln"</string>
|
||||
<string name="push_stack" msgid="2433062141810168976">"Weiter in Stack verschieben"</string>
|
||||
<string name="pop_to_prefix" msgid="4288884615669751608">"Wechseln zu"</string>
|
||||
<string name="pop_to_title" msgid="3924696281273379455">"Demo für „Wechseln zu“"</string>
|
||||
<string name="display_settings" msgid="5726880972110281095">"Einstellungen für die Anzeige"</string>
|
||||
<string name="package_not_found_error_msg" msgid="7525619456883627939">"Paket wurde nicht gefunden."</string>
|
||||
<string name="permissions_granted_msg" msgid="2348556088141992714">"Alle Berechtigungen wurden erteilt. Du kannst sie in den Einstellungen deaktivieren."</string>
|
||||
<string name="needs_access_msg_prefix" msgid="2204136858798832382">"Die App benötigt Zugriff auf die folgenden Berechtigungen:\n"</string>
|
||||
@@ -154,7 +106,8 @@
|
||||
<string name="long_route" msgid="4737969235741057506">"Lange Route"</string>
|
||||
<string name="continue_start_nav" msgid="6231797535084469163">"Weiter, um die Navigation zu starten"</string>
|
||||
<string name="continue_route" msgid="5172258139245088080">"Weiter zur Route"</string>
|
||||
<string name="routes_title" msgid="7799772149932075357">"Routen"</string>
|
||||
<string name="routes_title" msgid="7799772149932075357">"Route"</string>
|
||||
<string name="new_route">Neue Route Berechnung</string>
|
||||
<string name="place_list_nav_template_demo_title" msgid="8019588508812955290">"Demo der Navigationsvorlage für Ortslisten"</string>
|
||||
<string name="route_preview_template_demo_title" msgid="7878704357953167555">"Demo der Routenvorschauvorlage"</string>
|
||||
<string name="notification_template_demo_title" msgid="5076051497316030274">"Demo der Benachrichtigungsvorlage"</string>
|
||||
@@ -355,7 +308,7 @@
|
||||
<string name="map_template_toggle_demo_title" msgid="6510798293640092611">"Kartenvorlage mit Ein-/Aus-Schaltflächen"</string>
|
||||
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
|
||||
<string name="route_options_demo_title" msgid="4599699012716426514">"Routenoptionen"</string>
|
||||
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
|
||||
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen meiden"</string>
|
||||
<string name="avoid_ferries_row_title" msgid="8232883866013711974">"Fähren vermeiden"</string>
|
||||
<string name="map_demos_title" msgid="2169766615521476592">"Kartenbezogene Demos"</string>
|
||||
<string name="map_with_content_demo_title" msgid="1032610482145018739">"Demos von Karten mit Inhalten"</string>
|
||||
|
||||
@@ -41,8 +41,6 @@
|
||||
<string name="sign_out_action_title">Sign out</string>
|
||||
<string name="yes_action_title">Yes</string>
|
||||
<string name="no_action_title">No</string>
|
||||
<string name="disable_all_rows">Disable All Rows</string>
|
||||
<string name="enable_all_rows">Enable All Rows</string>
|
||||
|
||||
<!-- Toast Messages -->
|
||||
<string name="zoomed_in_toast_msg">Zoomed in</string>
|
||||
@@ -57,17 +55,8 @@
|
||||
<string name="parked_toast_msg">Parked action</string>
|
||||
<string name="more_toast_msg">Clicked More</string>
|
||||
<string name="grant_location_permission_toast_msg">Grant location Permission to see current location</string>
|
||||
<string name="sign_in_with_google_toast_msg">Sign-in with Google starts here</string>
|
||||
<string name="changes_selection_to_index_toast_msg_prefix">Changed selection to index</string>
|
||||
<string name="yes_action_toast_msg">Yes button pressed!</string>
|
||||
<string name="no_action_toast_msg">No button pressed!</string>
|
||||
<string name="alert_timeout_toast_msg">Alert is timed out!</string>
|
||||
|
||||
<!-- Row text -->
|
||||
<string name="first_row_title">Row with a large image and long text long text long text long text long text</string>
|
||||
<string name="first_row_text">Text text text</string>
|
||||
<string name="other_row_title_prefix">Row title </string>
|
||||
<string name="other_row_text">Row text</string>
|
||||
|
||||
<!-- Place Details Screen -->
|
||||
<string name="navigate">Navigate</string>
|
||||
@@ -78,56 +67,8 @@
|
||||
<!-- CarHardwareDemoScreen -->
|
||||
<string name="fail_start_nav">Failure starting navigation</string>
|
||||
<string name="fail_start_dialer">Failure starting dialer</string>
|
||||
<string name="car_hardware_demo_title">Car Hardware Demo</string>
|
||||
|
||||
<!-- CarHardwareInfoScreen -->
|
||||
<string name="car_hardware_info">Car Hardware Information</string>
|
||||
<string name="model_info">Model Information</string>
|
||||
<string name="manufacturer_unavailable">Manufacturer unavailable</string>
|
||||
<string name="model_unavailable">Model unavailable</string>
|
||||
<string name="year_unavailable">Year unavailable</string>
|
||||
<string name="energy_profile">Energy Profile</string>
|
||||
<string name="no_energy_profile_permission">No Energy Profile Permission</string>
|
||||
<string name="fuel_types">Fuel Types</string>
|
||||
<string name="unavailable">Unavailable</string>
|
||||
<string name="ev_connector_types">EV Connector Types</string>
|
||||
|
||||
<!-- ColorDemoScreen -->
|
||||
<string name="example_title">Example %d</string>
|
||||
<string name="example_1_text">This text has a <annotation color="red">red</annotation> color</string>
|
||||
<string name="example_2_text">This text has a <annotation color="green">green</annotation> color</string>
|
||||
<string name="example_3_text">This text has a <annotation color="blue">blue</annotation> color</string>
|
||||
<string name="example_4_text">This text has a <annotation color="yellow">yellow</annotation> color</string>
|
||||
<string name="example_5_text">This text uses the primary color</string>
|
||||
<string name="example_6_text">This text uses the secondary color</string>
|
||||
<string name="color_demo">Color Demo</string>
|
||||
|
||||
<!-- ContentLimitsDemoScreen -->
|
||||
<string name="list_limit">List Limit</string>
|
||||
<string name="grid_limit">Grid Limit</string>
|
||||
<string name="pane_limit">Pane Limit</string>
|
||||
<string name="place_list_limit">Place List Limit</string>
|
||||
<string name="route_list_limit">Route List Limit</string>
|
||||
<string name="content_limits">Content Limits</string>
|
||||
<string name="content_limits_demo_title">Content Limits Demo</string>
|
||||
|
||||
<!-- FinishAppScreen -->
|
||||
<string name="finish_app_msg">This will finish the app, and when you return it will pre-seed a permission screen</string>
|
||||
<string name="finish_app_title">Finish App Demo</string>
|
||||
<string name="finish_app_demo_title">Pre-seed the Permission Screen on next run Demo</string>
|
||||
<string name="preseed_permission_app_title">Pre-seed permission App Demo</string>
|
||||
<string name="preseed_permission_demo_title">Pre-seed the Permission Screen on next run Demo</string>
|
||||
|
||||
<!-- LoadingDemoScreen -->
|
||||
<string name="loading_demo_title">Loading Demo</string>
|
||||
<string name="loading_demo_row_title">Loading Complete!</string>
|
||||
|
||||
<!-- PopToDemoScreen -->
|
||||
<string name="pop_to_root">Pop to root</string>
|
||||
<string name="pop_to_marker">Pop to Misc Demo Marker</string>
|
||||
<string name="push_stack">Push further in stack</string>
|
||||
<string name="pop_to_prefix">Pop To </string>
|
||||
<string name="pop_to_title">PopTo Demo</string>
|
||||
<string name="display_settings">Display settings</string>
|
||||
|
||||
<!-- RequestPermissionScreen -->
|
||||
<string name="package_not_found_error_msg">Package Not found.</string>
|
||||
@@ -193,7 +134,6 @@
|
||||
<string name="take_520">Take 520</string>
|
||||
<string name="gas_station">Gas Station</string>
|
||||
|
||||
<!-- RoutePreviewDemoScreen -->
|
||||
<string name="short_route">Short route</string>
|
||||
<string name="less_busy">Less busy</string>
|
||||
<string name="hov_friendly">HOV friendly</string>
|
||||
@@ -201,6 +141,7 @@
|
||||
<string name="continue_start_nav">Continue to start navigation</string>
|
||||
<string name="continue_route">Continue to route</string>
|
||||
<string name="routes_title">Routes</string>
|
||||
<string name="new_route">New Route calculation</string>
|
||||
|
||||
<!-- NavigationDemosScreen -->
|
||||
<string name="place_list_nav_template_demo_title">Place List Navigation Template Demo</string>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -16,14 +16,19 @@
|
||||
|
||||
package com.kouros.navigation.data
|
||||
|
||||
import android.R
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.kouros.navigation.data.valhalla.Maneuvers
|
||||
import com.kouros.navigation.data.valhalla.ValhallaJson
|
||||
import com.kouros.navigation.utils.NavigationUtils.createGeoJson
|
||||
import com.kouros.navigation.utils.NavigationUtils.decodePolyline
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
import org.maplibre.geojson.Point
|
||||
|
||||
data class Category(
|
||||
val id: String,
|
||||
@@ -60,29 +65,6 @@ data class StepData (
|
||||
var bearing: Double
|
||||
)
|
||||
|
||||
val dataPlaces = listOf(
|
||||
Place(
|
||||
id = 0,
|
||||
name = "Vogelhartstr. 17",
|
||||
category = "Favorites",
|
||||
latitude = 48.1857475,
|
||||
longitude = 11.5793627,
|
||||
postalCode = "80807",
|
||||
city = "München",
|
||||
street = "Vogelhartstr. 17"
|
||||
|
||||
),
|
||||
Place(
|
||||
id = 0,
|
||||
name = "Hohenwaldeckstr. 27",
|
||||
category = "Recent",
|
||||
latitude = 48.1165005,
|
||||
longitude = 11.594349,
|
||||
postalCode = "81541",
|
||||
city = "München",
|
||||
street = "Hohenwaldeckstr. 27",
|
||||
)
|
||||
)
|
||||
|
||||
// GeoJSON data classes
|
||||
@Serializable
|
||||
@@ -107,9 +89,41 @@ data class GeoJsonFeatureCollection(
|
||||
data class Locations (
|
||||
var lat : Double,
|
||||
var lon : Double,
|
||||
var street : String = ""
|
||||
var street : String = "",
|
||||
val search_filter: SearchFilter,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchFilter(
|
||||
var max_road_class: String = "",
|
||||
var exclude_toll : Boolean = false
|
||||
) {
|
||||
|
||||
class Builder {
|
||||
private var avoidMotorway = false
|
||||
private var avoidTollway = false
|
||||
|
||||
fun avoidMotorway (value: Boolean ) = apply {
|
||||
avoidMotorway = value
|
||||
}
|
||||
|
||||
fun avoidTollway (value: Boolean ) = apply {
|
||||
avoidTollway = value
|
||||
}
|
||||
|
||||
fun build(): SearchFilter {
|
||||
val filter = SearchFilter()
|
||||
if (avoidMotorway) {
|
||||
filter.max_road_class = "trunk"
|
||||
}
|
||||
if (avoidTollway) {
|
||||
filter.exclude_toll = true
|
||||
}
|
||||
return filter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ValhallaLocation (
|
||||
var locations: List<Locations>,
|
||||
@@ -149,6 +163,10 @@ object Constants {
|
||||
|
||||
const val SHOW_THREED_BUILDING = "Show3D"
|
||||
|
||||
const val AVOID_MOTORWAY = "AvoidMotorway"
|
||||
|
||||
const val AVOID_TOLLWAY = "AvoidTollway"
|
||||
|
||||
const val NEXT_STEP_THRESHOLD = 100.0
|
||||
|
||||
const val MAXIMAL_SNAP_CORRECTION = 50.0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user