Launcher Icons, NotificationService

This commit is contained in:
Dimitris
2026-03-24 17:04:05 +01:00
parent bc8a53a5d8
commit 5098dad9d6
76 changed files with 930 additions and 478 deletions

View File

@@ -5,3 +5,6 @@
## Simulation
adb shell dumpsys activity service com.kouros.navigation.car.NavigationCarAppService AUTO_DRIVE
## Signing
./gradlew bundleFull

View File

@@ -1,10 +1,14 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
val properties = Properties().apply {
load(File("signing.properties").reader())
}
android {
namespace = "com.kouros.navigation"
compileSdk = 36
@@ -13,8 +17,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 76
versionName = "0.2.0.76"
versionCode = 82
versionName = "0.2.0.82"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -22,23 +26,22 @@ android {
signingConfigs {
getByName("debug") {
keyAlias = "release"
keyPassword = "zeta67#gAe3aN3"
storeFile = file("/home/kouros/work/keystore/keystoreRelease")
storePassword = "zeta67#gAe3aN3"
keyPassword = properties.getProperty("keyPassword")
storeFile = file(properties.getProperty("storeFile"))
storePassword = properties.getProperty("storePassword")
}
create("release") {
keyAlias = "release"
keyPassword = "zeta67#gAe3aN3"
storeFile = file("/home/kouros/work/keystore/keystoreRelease")
storePassword = "zeta67#gAe3aN3"
keyPassword = properties.getProperty("keyPassword")
storeFile = file(properties.getProperty("storeFile"))
storePassword = properties.getProperty("storePassword")
}
}
buildTypes {
release {
// Enables code-related app optimization.
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = false
// Enables resource shrinking.
isShrinkResources = false
proguardFiles(
@@ -48,15 +51,20 @@ android {
}
}
// Specifies one flavor dimension.
flavorDimensions += "version"
flavorDimensions += "store"
productFlavors {
create("play") {
dimension = "store"
applicationIdSuffix = ".play"
versionNameSuffix = "-play"
}
create("demo") {
dimension = "version"
dimension = "store"
applicationIdSuffix = ".demo"
versionNameSuffix = "-demo"
}
create("full") {
dimension = "version"
dimension = "store"
applicationIdSuffix = ".full"
versionNameSuffix = "-full"
}
@@ -66,11 +74,20 @@ android {
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures {
compose = true
packaging {
resources {
excludes +=
setOf(
"/META-INF/{AL2.0,LGPL2.1}",
"/META-INF/*.version",
)
}
}
buildFeatures {
compose = true
buildConfig = true
}
}
dependencies {
@@ -95,7 +112,6 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.window)
implementation(libs.androidx.compose.foundation.layout)
implementation(libs.android.gpx.parser)
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.compose.foundation.layout)
@@ -107,4 +123,3 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,20 +1,12 @@
package com.kouros.navigation.model
import android.content.Context
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.utils.location
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.joda.time.DateTime
import kotlin.collections.forEach
var simulationJob: Job? = null
fun simulate(routeModel: RouteModel, mock: MockLocation) {
@@ -91,43 +83,6 @@ fun testSingleUpdate(
Thread.sleep(1_000)
}
fun gpx(context: Context, mock: MockLocation) {
CoroutineScope(Dispatchers.IO).launch {
var lastLocation = location(0.0, 0.0)
val parser = GPXParser()
val resourceId: Int = context.resources
.getIdentifier("vh", "raw", context.packageName)
val input = context.resources.openRawResource(resourceId)
val parsedGpx: Gpx? = parser.parse(input) // consider using a background thread
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val curLocation = location(p.longitude, p.latitude)
val ext = p.extensions
val speed: Double?
if (ext != null) {
speed = ext.speed
mock.curSpeed = speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val bearing = lastLocation.bearingTo(curLocation)
mock.setMockLocation(p.latitude, p.longitude, bearing)
if (duration > 0) {
delay(duration / 5)
}
lastTime = p.time
lastLocation = curLocation
}
}
}
}
}
}
enum class SimulationType {
SIMULATE, TEST, GPX, TEST_SINGLE
}

View File

@@ -50,7 +50,6 @@ import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.model.MockLocation
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.model.SimulationType
import com.kouros.navigation.model.gpx
import com.kouros.navigation.model.simulate
import com.kouros.navigation.model.simulationJob
import com.kouros.navigation.model.test
@@ -112,11 +111,10 @@ class MainActivity : ComponentActivity() {
when (type) {
SimulationType.SIMULATE -> simulate(routeModel, mock)
SimulationType.TEST -> test(applicationContext, routeModel)
SimulationType.GPX -> gpx(
context = applicationContext, mock
)
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
else -> {}
}
}
}

View File

@@ -13,10 +13,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.window.layout.WindowMetricsCalculator
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.NavigationImage
import com.kouros.navigation.data.StepData
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.ui.app.AppViewModel
import com.kouros.navigation.ui.app.appViewModel
import com.kouros.navigation.ui.navigation.NavigationInfo

View File

@@ -48,5 +48,11 @@ fun AppNavGraph(mainActivity: MainActivity) {
navController
) { navController.popBackStack() }
}
composable("car_settings") {
SettingsRoute(
"car_settings",
navController
) { navController.popBackStack() }
}
}
}

View File

@@ -0,0 +1,90 @@
package com.kouros.navigation.ui.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.kouros.data.R
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.ui.components.RadioButtonSingleSelection
import com.kouros.navigation.ui.components.SectionTitle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CarScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) {
val engineType by viewModel.engineType.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(id = R.string.car_settings),
)
},
navigationIcon = {
IconButton(onClick = navigateBack) {
Icon(
painter = painterResource(R.drawable.arrow_back_24px),
contentDescription = stringResource(id = R.string.accept_action_title),
modifier = Modifier.size(48.dp, 48.dp),
)
}
},
)
},
)
{ paddingValues ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.padding(top = 10.dp)
.verticalScroll(scrollState)
) {
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(10.dp),
) {
SectionTitle(stringResource(R.string.engine_type))
val radioOptions = listOf(
stringResource(R.string.combustion),
stringResource(R.string.electric),
)
RadioButtonSingleSelection(
modifier = Modifier.padding(),
selectedOption = engineType,
radioOptions = radioOptions,
onClick = viewModel::onEngineTypeChanged
)
}
}
}
}
}

View File

@@ -39,4 +39,7 @@ fun SettingsRoute(route: String, navController: NavHostController, function: ()
if (route == "settings_screen") {
SettingsScreen(viewModel, navController, function)
}
if (route == "car_settings") {
CarScreen (viewModel = viewModel, function)
}
}

View File

@@ -51,7 +51,14 @@ fun SettingsScreen(
name = "Navigation Settings",
description = "",
icon = R.drawable.navigation_24px
),
Item(
id = "car_settings",
name = "Car Settings",
description = "",
icon = R.drawable.electric_car_24px
)
)
Scaffold(

View File

@@ -3,7 +3,7 @@
android:height="108dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#000000">
android:tint="#1A7416">
<group android:scaleX="0.7888"
android:scaleY="0.7888"
android:translateX="101.376"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#98DABB</color>
</resources>

View File

@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -3,7 +3,7 @@
android:height="108dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="#000000">
android:tint="#1A7416">
<group android:scaleX="0.7888"
android:scaleY="0.7888"
android:translateX="101.376"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -29,6 +29,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@@ -52,7 +53,8 @@ dependencies {
implementation(libs.play.services.location)
implementation(libs.androidx.datastore.core)
implementation(libs.androidx.monitor)
implementation(libs.android.gpx.parser)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.runner)
androidTestImplementation(libs.androidx.rules)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -137,7 +137,7 @@ class CarSensorManager(
carCompassListener
)
carSensors.addCarHardwareLocationListener(
CarSensors.UPDATE_RATE_FASTEST,
CarSensors.UPDATE_RATE_NORMAL,
carContext.mainExecutor,
carLocationListener
)

View File

@@ -1,7 +1,7 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.location.Location
import android.net.Uri
import androidx.car.app.CarAppService
import androidx.car.app.Session
import androidx.car.app.SessionInfo
@@ -10,6 +10,14 @@ import androidx.car.app.validation.HostValidator
class NavigationCarAppService : CarAppService() {
val INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP =
"com.kouros.navigation.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP"
fun createDeepLinkUri(deepLinkAction: String): Uri {
return Uri.fromParts(NavigationSession.uriScheme, NavigationSession.uriHost, deepLinkAction)
}
@SuppressLint("PrivateResource")
override fun createHostValidator(): HostValidator {

View File

@@ -0,0 +1,232 @@
package com.kouros.navigation.car
import android.annotation.SuppressLint
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import androidx.car.app.notification.CarAppExtender
import androidx.car.app.notification.CarNotificationManager
import androidx.car.app.notification.CarPendingIntent
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.kouros.data.R
import java.math.RoundingMode
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
/**
* A simple foreground service that imitates a client routing service posting navigation
* notifications.
*/
class NavigationNotificationService : Service() {
/**
* The number of notifications fired so far.
*
*
* We use this number to post notifications with a repeating list of directions. See [ ][.getDirectionInfo] for details.
*
* Note: Package private for inner class reference
*/
var mNotificationCount: Int = 0
/**
* A handler that posts notifications when given the message request. See [ ] for details.
*
* Note: Package private for inner class reference
*/
val mHandler: Handler =
Handler(Looper.getMainLooper(), HandlerCallback())
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
initNotifications(this)
startForeground(
NAV_NOTIFICATION_ID,
getNavigationNotification(this, mNotificationCount).build()
)
// Start updating the notification continuously.
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION), NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return START_NOT_STICKY
}
override fun onDestroy() {
mHandler.removeMessages(MSG_SEND_NOTIFICATION)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
/**
* A [Handler.Callback] used to process the message queue for the notification service.
*/
internal inner class HandlerCallback : Handler.Callback {
override fun handleMessage(msg: Message): Boolean {
if (msg.what == MSG_SEND_NOTIFICATION) {
val context: Context = this@NavigationNotificationService
CarNotificationManager.from(context).notify(
NAV_NOTIFICATION_ID,
getNavigationNotification(context, mNotificationCount)
)
mNotificationCount++
mHandler.sendMessageDelayed(
mHandler.obtainMessage(MSG_SEND_NOTIFICATION),
NAV_NOTIFICATION_DELAY_IN_MILLIS
)
return true
}
return false
}
}
/**
* A container class that encapsulates the direction information to use in the notifications.
*/
internal class DirectionInfo(
val mTitle: String, val mDistance: String, val mIcon: Int,
val mOnlyAlertOnce: Boolean
)
companion object {
private const val MSG_SEND_NOTIFICATION = 1
private const val NAV_NOTIFICATION_CHANNEL_ID = "nav_channel_00"
private val NAV_NOTIFICATION_CHANNEL_NAME: CharSequence = "Navigation Channel"
private const val NAV_NOTIFICATION_ID = 10101
val NAV_NOTIFICATION_DELAY_IN_MILLIS: Long = TimeUnit.SECONDS.toMillis(1)
/**
* Initializes the notifications, if needed.
*
*
* [NotificationManager.IMPORTANCE_HIGH] is needed to show the alerts on top of the car
* screen. However, the rail widget at the bottom of the screen will show regardless of the
* importance setting.
*/
// Suppressing 'ObsoleteSdkInt' as this code is shared between APKs with different min SDK
// levels
@SuppressLint("ObsoleteSdkInt")
private fun initNotifications(context: Context) {
val navChannel =
NotificationChannelCompat.Builder(
NAV_NOTIFICATION_CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_HIGH
)
.setName(NAV_NOTIFICATION_CHANNEL_NAME).build()
CarNotificationManager.from(context).createNotificationChannel(navChannel)
}
/** Returns the navigation notification that corresponds to the given notification count. */
fun getNavigationNotification(
context: Context, notificationCount: Int
): NotificationCompat.Builder {
val builder =
NotificationCompat.Builder(context, NAV_NOTIFICATION_CHANNEL_ID)
val directionInfo = getDirectionInfo(context, notificationCount)
// Set an intent to open the car app. The app receives this intent when the user taps the
// heads-up notification or the rail widget.
val pendingIntent = CarPendingIntent.getCarApp(
context,
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP.hashCode(),
Intent(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
).setComponent(
ComponentName(
context,
NavigationCarAppService()::class.java
)
).setData(
NavigationCarAppService().createDeepLinkUri(
NavigationCarAppService().INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP
)
),
0
)
return builder // This title, text, and icon will be shown in both phone and car screen. These
// values can
// be overridden in the extender below, to customize notifications in the car
// screen.
.setContentTitle(directionInfo.mTitle)
.setContentText(directionInfo.mDistance)
.setSmallIcon(directionInfo.mIcon) // The notification must be set to 'ongoing' and its category must be set to
// CATEGORY_NAVIGATION in order to show it in the rail widget when the app is
// navigating on
// the background.
// These values cannot be overridden in the extender.
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION) // If set to true, the notification will only show the alert once in both phone and
// car screen. This value cannot be overridden in the extender.
.setOnlyAlertOnce(directionInfo.mOnlyAlertOnce) // This extender must be set in order to display the notification in the car screen.
// The extender also allows various customizations, such as showing different title
// or icon on the car screen.
.extend(
CarAppExtender.Builder()
.setContentIntent(pendingIntent)
.build()
)
}
/**
* Returns a [DirectionInfo] that corresponds to the given notification count.
*
*
* There are 5 directions, repeating in order. For each direction, the alert will only show
* once, but the distance will update on every count on the rail widget.
*/
private fun getDirectionInfo(context: Context, notificationCount: Int): DirectionInfo {
val formatter = DecimalFormat("#.##")
formatter.setRoundingMode(RoundingMode.DOWN)
val repeatingCount = notificationCount % 35
if (repeatingCount in 0..<10) {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((10 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.stop_action_title),
distance,
R.drawable.arrow_back_24px,
repeatingCount > 0
)
} else if (repeatingCount in 10..<20) {
// Distance decreases from 5km to 0.5km
val distance = formatter.format((20 - repeatingCount) * 0.5) + "km"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.ic_turn_normal_right, /* onlyAlertOnce= */
repeatingCount > 10
)
} else if (repeatingCount in 20..<25) {
// Distance decreases from 200m to 40m
val distance = formatter.format(((25 - repeatingCount) * 40).toLong()) + "m"
return DirectionInfo(
context.getString(R.string.route_preview),
distance,
R.drawable.navigation_48px, /* onlyAlertOnce= */
repeatingCount > 20
)
} else {
// Distance decreases from 1km to 0.1km
val distance = formatter.format((35 - repeatingCount) * 0.1) + "km"
return DirectionInfo(
context.getString(R.string.charging_station),
distance,
R.drawable.local_gas_station_24,
repeatingCount > 25
)
}
}
}
}

View File

@@ -26,6 +26,7 @@ import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.navigation.Simulation
import com.kouros.navigation.car.screen.NavigationListener
import com.kouros.navigation.car.screen.NavigationScreen
import com.kouros.navigation.car.screen.NavigationType
import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.car.screen.checkPermission
@@ -37,6 +38,7 @@ import com.kouros.navigation.data.Constants.MAXIMAL_SNAP_CORRECTION
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
@@ -229,9 +231,10 @@ class NavigationSession : Session(), NavigationListener {
override fun onStopNavigation() {
// Called when the user stops navigation in the car screen
// Stop turn-by-turn logic and clean up
routeModel.stopNavigation()
autoDriveEnabled = false
deviceLocationManager.startLocationUpdates()
stopNavigation()
if (autoDriveEnabled) {
deviceLocationManager.startLocationUpdates()
}
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
@@ -387,6 +390,7 @@ class NavigationSession : Session(), NavigationListener {
* Snaps location to route and checks for deviation requiring reroute.
*/
private fun handleNavigationLocation(location: Location) {
if (guidanceAudio == 1) {
handleGuidanceAudio()
}
@@ -420,6 +424,9 @@ class NavigationSession : Session(), NavigationListener {
simulation.stopSimulation()
autoDriveEnabled = false
}
surfaceRenderer.routeData.value = ""
surfaceRenderer.viewStyle = ViewStyle.VIEW
navigationScreen.navigationType = NavigationType.VIEW
}
/**

View File

@@ -10,12 +10,17 @@ import androidx.car.app.AppManager
import androidx.car.app.CarContext
import androidx.car.app.SurfaceCallback
import androidx.car.app.SurfaceContainer
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
@@ -29,9 +34,11 @@ import com.kouros.navigation.car.map.MapLibre
import com.kouros.navigation.car.map.cameraState
import com.kouros.navigation.car.map.getPaddingValues
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Constants.TAG
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.RouteEngine
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.BaseStyleModel
import com.kouros.navigation.utils.bearing
import com.kouros.navigation.utils.calculateTilt
@@ -45,9 +52,10 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.expressions.dsl.zoom
import org.maplibre.compose.style.BaseStyle
import org.maplibre.spatialk.geojson.Position
import java.time.Duration
import java.time.LocalDateTime
/**
@@ -123,6 +131,8 @@ class SurfaceRenderer(
// Camera tilt angle (default 60 degrees for navigation)
var tilt = TILT
var lastLocationUpdate: LocalDateTime = LocalDateTime.now()
// Map base style (day/night)
val style: MutableLiveData<BaseStyle> by lazy {
MutableLiveData()
@@ -238,7 +248,6 @@ class SurfaceRenderer(
init {
lifecycle.addObserver(this)
speed.value = 0F
}
fun onBaseStyleStateUpdated(style: BaseStyle) {
@@ -287,7 +296,7 @@ class SurfaceRenderer(
darkMode: Boolean
) {
val cameraDuration =
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing)
duration(viewStyle == ViewStyle.PREVIEW, position!!.bearing, lastBearing, lastLocationUpdate)
val currentSpeed: Float? by speed.observeAsState()
val maximumSpeed: Int? by maxSpeed.observeAsState()
val streetName: String? by street.observeAsState()
@@ -311,9 +320,10 @@ class SurfaceRenderer(
tilt = tilt,
padding = paddingValues
),
duration = cameraDuration
duration = cameraDuration,
)
}
lastLocationUpdate = LocalDateTime.now()
}
override fun onCreate(owner: LifecycleOwner) {
@@ -396,6 +406,15 @@ class SurfaceRenderer(
viewStyle = ViewStyle.VIEW
}
/**
* Activates navigation View
*/
fun activateNavigationView() {
viewStyle = ViewStyle.VIEW
tilt = TILT
updateLocation(lastLocation)
}
/**
* Updates camera position with new bearing, zoom, and target.
* Posts update to LiveData for UI observation.
@@ -414,21 +433,6 @@ class SurfaceRenderer(
}
}
/**
* Sets route data for active navigation and switches to VIEW mode.
*/
fun clearRouteData() {
updateLocation(lastLocation)
routeData.value = ""
viewStyle = ViewStyle.VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
zoom = 16.0
)
)
tilt = TILT
}
/**
* Updates traffic incident data on the map.
*/
@@ -492,9 +496,9 @@ class SurfaceRenderer(
}
/**
* Centers the map on a specific category/POI location.
* Centers the map on a specific POI location.
*/
fun setCategoryLocation(location: Location, category: String) {
fun setCategoryLocation(location: Location) {
viewStyle = ViewStyle.AMENITY_VIEW
cameraPosition.postValue(
cameraPosition.value!!.copy(
@@ -502,22 +506,4 @@ class SurfaceRenderer(
)
)
}
companion
object {
private const val TAG = "MapRenderer"
}
}
/**
* Enum representing different map view modes.
* - VIEW: Active navigation mode with follow-car camera
* - PREVIEW: Route overview before starting navigation
* - PAN_VIEW: User-controlled map panning
* - AMENITY_VIEW: Displaying POI/amenity locations
*/
enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
}

View File

@@ -26,11 +26,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.NavigationColor
import com.kouros.navigation.data.NavigationColorDark
import com.kouros.navigation.data.NavigationColorLight
import com.kouros.navigation.data.RouteColor
import com.kouros.navigation.data.SpeedColor
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.utils.isMetricSystem
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
@@ -74,9 +75,9 @@ fun cameraState(
latitude = position!!.target.latitude,
longitude = position.target.longitude
),
zoom = 15.0,
zoom = position.zoom,
tilt = tilt,
padding = padding
padding = padding,
)
)
}
@@ -317,7 +318,10 @@ fun NavigationImage(
) {
val imageSize = (height / 8)
val navigationColor = remember { NavigationColor }
val navigationColor = if (darkMode)
remember { NavigationColorDark }
else
remember { NavigationColorLight }
val textMeasurerStreet = rememberTextMeasurer()
val street = streetName.toString()
@@ -545,7 +549,7 @@ fun DebugInfo(
fun getPaddingValues(height: Int, viewStyle: ViewStyle): PaddingValues {
return when (viewStyle) {
ViewStyle.VIEW, ViewStyle.PAN_VIEW -> PaddingValues(
start = 50.dp,
start = 100.dp,
top = distanceFromTop(height).dp
)

View File

@@ -1,80 +0,0 @@
package com.kouros.navigation.car.navigation
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaPlayer
import android.media.MediaPlayer.OnCompletionListener
import android.speech.tts.TextToSpeech
import android.util.Log
import androidx.annotation.DrawableRes
import androidx.annotation.RawRes
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.model.Action
import androidx.car.app.model.CarIcon
import androidx.car.app.model.CarText
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.Constants.TAG
import java.io.IOException
import java.util.Locale
class NavigationUtils(private var carContext: CarContext) {
fun createCarIcon(@DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}
fun buildRowForTemplate(title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
}

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.car.navigation
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.util.Log
import androidx.annotation.StringRes
import androidx.car.app.AppManager
import androidx.car.app.CarContext
@@ -184,7 +185,6 @@ class RouteCarModel : RouteModel() {
return CarText.create(carContext.getString(stringRes))
}
fun createCarIcon(iconCompat: IconCompat): CarIcon {
return CarIcon.Builder(iconCompat).build()
}
@@ -235,4 +235,17 @@ class RouteCarModel : RouteModel() {
.setFlags(flags)
.build()
}
fun backGroundColor(): CarColor {
return if (isNavigating()) {
when (route.currentStep().countryCode) {
"DEU", "FRA", "AUT", "POL", "BEL", "NLD", "ESP", "PRT", "CZE", "SVK", "BGR", "HUN" -> CarColor.BLUE
else -> {
CarColor.GREEN
}
}
} else {
CarColor.GREEN
}
}
}

View File

@@ -4,47 +4,126 @@ import android.location.Location
import android.location.LocationManager
import android.os.SystemClock
import androidx.lifecycle.LifecycleCoroutineScope
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.utils.location
import com.kouros.android.cars.carappservice.BuildConfig
import com.kouros.navigation.data.tomtom.TomTomRepository
import io.ticofab.androidgpxparser.parser.GPXParser
import io.ticofab.androidgpxparser.parser.domain.Gpx
import io.ticofab.androidgpxparser.parser.domain.TrackSegment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.joda.time.DateTime
class Simulation {
private var simulationJob: Job? = null
fun startSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
if (routeModel.navState.route.isRouteValid()) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
if (BuildConfig.DEBUG) {
gpxSimulation(routeModel, lifecycleScope, updateLocation)
} else {
currentSimulation(routeModel, lifecycleScope, updateLocation)
}
return
}
}
private fun currentSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
val points = routeModel.curRoute.waypoints
if (points.isEmpty()) return
simulationJob?.cancel()
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
delay(1000)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
}
private fun gpxSimulation(
routeModel: RouteCarModel,
lifecycleScope: LifecycleCoroutineScope,
updateLocation: (Location) -> Unit
) {
var route = ""
simulationJob?.cancel()
runBlocking {
simulationJob = launch(Dispatchers.IO) {
route = TomTomRepository().fetchUrl(
"https://kouros-online.de/vh.gpx",
false
)
}
simulationJob?.join()
}
simulationJob?.cancel()
simulationJob =lifecycleScope.launch() {
var lastLocation = Location(LocationManager.FUSED_PROVIDER)
var curBearing = 0f
simulationJob = lifecycleScope.launch {
for (point in points) {
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = point[1]
longitude = point[0]
bearing = curBearing
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = 13.0f // ~50 km/h
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
val parser = GPXParser()
val parsedGpx: Gpx? =
parser.parse(route.byteInputStream())
parsedGpx?.let {
val tracks = parsedGpx.tracks
tracks.forEach { tr ->
val segments: MutableList<TrackSegment?>? = tr.trackSegments
segments!!.forEach { seg ->
var lastTime = DateTime.now()
seg!!.trackPoints.forEach { p ->
val ext = p.extensions
var curSpeed = 0F
if (ext != null) {
curSpeed = ext.speed.toFloat()
}
val duration = p.time.millis - lastTime.millis
val fakeLocation = Location(LocationManager.FUSED_PROVIDER).apply {
latitude = p.latitude
longitude = p.longitude
speedAccuracyMetersPerSecond = 1.0f // ~1 m/s
speed = curSpeed
time = System.currentTimeMillis()
elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos()
}
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
if (duration > 100) {
delay(duration / 4)
}
lastTime = p.time
lastLocation = fakeLocation
}
}
curBearing = lastLocation.bearingTo(fakeLocation)
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 1 second)
delay(500)
lastLocation = fakeLocation
}
routeModel.stopNavigation()
}
routeModel.stopNavigation()
}
}

View File

@@ -1,6 +1,5 @@
package com.kouros.navigation.car.screen
import androidx.activity.OnBackPressedCallback
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
@@ -13,18 +12,12 @@ import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.utils.GeoUtils.createPointCollection
import com.kouros.navigation.utils.location
class CategoriesScreen(
private val carContext: CarContext,
@@ -100,7 +93,7 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else {
return CarIcon.Builder(
NavigationUtils(context).createNumberIcon(
createNumberIcon(
category,
index.toString()
)

View File

@@ -21,7 +21,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.observers.CategoryObserver
import com.kouros.navigation.car.screen.observers.CategoryObserverCallback
import com.kouros.navigation.data.Constants
@@ -130,7 +129,7 @@ class CategoryScreen(
val row = Row.Builder()
.setOnClickListener {
val location = location(it.lon, it.lat)
surfaceRenderer.setCategoryLocation(location, category)
surfaceRenderer.setCategoryLocation(location)
}
.setTitle(name)
.setImage(carIcon(carContext, category, index))

View File

@@ -9,7 +9,6 @@ import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_IS_PERSISTENT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.Header
@@ -31,28 +30,25 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.car.screen.observers.NavigationObserverCallback
import com.kouros.navigation.car.screen.observers.NavigationObserverManager
import com.kouros.navigation.car.screen.settings.SettingsScreen
import com.kouros.navigation.data.Constants
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.RouteModel
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.calculateZoom
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.getSettingsViewModel
import com.kouros.navigation.utils.location
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.maplibre.spatialk.geojson.Position
import java.time.Duration
import java.time.LocalDateTime
import java.time.ZoneOffset
@@ -70,8 +66,6 @@ open class NavigationScreen(
private val navigationViewModel: NavigationViewModel
) : Screen(carContext), NavigationObserverCallback {
val backGroundColor = CarColor.GREEN
var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER)
var recentPlaces = mutableListOf<Place>()
@@ -191,7 +185,7 @@ open class NavigationScreen(
)
})
)
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.build()
}
@@ -200,7 +194,7 @@ open class NavigationScreen(
*/
private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template {
return NavigationTemplate.Builder()
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -261,7 +255,7 @@ open class NavigationScreen(
)
.build()
)
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.setActionStrip(actionStripBuilder.build())
.setMapActionStrip(
mapActionStrip(
@@ -298,7 +292,7 @@ open class NavigationScreen(
)
}
val listBuilder = ItemList.Builder()
recentPlaces.filter { it.category == Constants.RECENT }.forEach {
recentPlaces.filter { it.category == Constants.RECENT && it.distance > 300F }.forEach {
val row = Row.Builder()
.setTitle(it.name!!)
.addAction(
@@ -355,7 +349,7 @@ open class NavigationScreen(
return NavigationTemplate.Builder()
.setNavigationInfo(RoutingInfo.Builder().setLoading(true).build())
.setActionStrip(actionStripBuilder.build())
.setBackgroundColor(backGroundColor)
.setBackgroundColor(routeModel.backGroundColor())
.build()
}
@@ -421,12 +415,6 @@ open class NavigationScreen(
* Creates an action to start the settings screen.
*/
private fun settingsAction(): Action {
// return Action.Builder()
// .setIcon(createCarIcon(carContext, R.drawable.settings_48px))
// .setOnClickListener {
// screenManager.push(SettingsScreen(carContext, navigationViewModel))
// }
// .build()
return createAction(
carContext, R.drawable.settings_48px,
0,
@@ -514,13 +502,7 @@ open class NavigationScreen(
navigationViewModel.route.value = preview
}
routeModel.navState = routeModel.navState.copy(destination = place)
surfaceRenderer.viewStyle = ViewStyle.VIEW
surfaceRenderer.updateCameraPosition(
0.0,
16.0,
Position(surfaceRenderer.lastLocation.longitude, surfaceRenderer.lastLocation.latitude),
TILT
)
surfaceRenderer.activateNavigationView()
invalidate()
}
@@ -530,7 +512,6 @@ open class NavigationScreen(
fun stopNavigation() {
navigationType = NavigationType.VIEW
listener.stopNavigation()
surfaceRenderer.routeData.value = ""
lastCameraSearch = 0
invalidate()
}
@@ -573,9 +554,9 @@ open class NavigationScreen(
* Updates navigation state with the current location, checks for arrival, and traffic updates.
*/
fun updateTrip(location: Location) {
val current = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(current, location)
checkTraffic(current, location)
val currentDate = LocalDateTime.now(ZoneOffset.UTC)
checkRoute(currentDate, location)
checkTraffic(currentDate, location)
updateSpeedCamera(location)
@@ -588,11 +569,11 @@ open class NavigationScreen(
/**
* Checks if a new route is needed based on the time since the last update.
*/
private fun checkRoute(current: LocalDateTime, location: Location) {
val duration = Duration.between(current, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 6
private fun checkRoute(currentDate: LocalDateTime, location: Location) {
val duration = Duration.between(currentDate, lastRouteDate)
val routeUpdate = routeModel.curRoute.summary.duration / 4
if (duration.abs().seconds > routeUpdate) {
lastRouteDate = current
lastRouteDate = currentDate
val destination = location(
routeModel.navState.destination.longitude,
routeModel.navState.destination.latitude

View File

@@ -36,10 +36,9 @@ import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.navigation.RouteCarModel
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.route.Routes
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsRepository
@@ -324,7 +323,7 @@ class RoutePreviewScreen(
.addAction(navigateAction)
if (route.summary.trafficDelay > 60) {
row.addText(createDelay(route))
row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px))
row.setImage(createCarIcon(carContext = carContext, R.drawable.traffic_jam_48px))
}
return row.build()
}

View File

@@ -1,13 +1,63 @@
package com.kouros.navigation.car.screen
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import androidx.annotation.DrawableRes
import androidx.car.app.CarContext
import androidx.car.app.model.Action
import androidx.car.app.model.Action.FLAG_DEFAULT
import androidx.car.app.model.ActionStrip
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Row
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.IconCompat
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Constants.CHARGING_STATION
import com.kouros.navigation.data.Constants.FUEL_STATION
import com.kouros.navigation.data.Constants.PHARMACY
import com.kouros.navigation.data.ViewStyle
fun buildRowForTemplate(carContext: CarContext, title: Int, resource: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))
.setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
resource
)
)
.build()
)
.build()
}
fun createNumberIcon(category: String, number: String): IconCompat {
val size = 24
val bitmap = createBitmap(size, size)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = size * 0.7f
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
val xPos = size / 2f
val yPos = (size / 2f) - ((paint.descent() + paint.ascent()) / 2f)
val color = when (category) {
CHARGING_STATION -> Color.GREEN
FUEL_STATION -> Color.BLUE
PHARMACY -> Color.RED
else -> Color.WHITE
}
paint.color = color
canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
paint.color = Color.WHITE
canvas.drawText(number, xPos, yPos, paint)
return IconCompat.createWithBitmap(bitmap)
}
fun createActionStrip(executeAction: () -> Action): ActionStrip {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
@@ -17,7 +67,7 @@ fun createActionStrip(executeAction: () -> Action): ActionStrip {
return actionStripBuilder.build()
}
fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder {
fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder {
val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder()
actionStripBuilder.addAction(
action1()
@@ -31,7 +81,12 @@ fun createActionStrip(executeAction: () -> Action): ActionStrip {
/**
* Creates an ActionStrip builder for map-related actions like zoom and pan.
*/
fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -> Action , panAction: () -> Action): ActionStrip {
fun mapActionStrip(
viewStyle: ViewStyle,
zoomPlus: () -> Action,
zoomMinus: () -> Action,
panAction: () -> Action
): ActionStrip {
val actionStripBuilder = ActionStrip.Builder()
.addAction(zoomPlus())
.addAction(zoomMinus())
@@ -47,7 +102,12 @@ fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -
/**
* Creates an action to do something.
*/
fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int = FLAG_DEFAULT, onClickAction: () -> Unit): Action {
fun createAction(
carContext: CarContext,
@DrawableRes iconRes: Int,
flag: Int = FLAG_DEFAULT,
onClickAction: () -> Unit
): Action {
return Action.Builder()
.setIcon(createCarIcon(carContext, iconRes))
.setFlags(flag)
@@ -60,3 +120,6 @@ fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int =
fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon {
return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build()
}

View File

@@ -14,12 +14,12 @@ import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import com.kouros.data.R
import com.kouros.navigation.car.SurfaceRenderer
import com.kouros.navigation.car.ViewStyle
import com.kouros.navigation.data.Category
import com.kouros.navigation.data.Constants.CATEGORIES
import com.kouros.navigation.data.Constants.FAVORITES
import com.kouros.navigation.data.Constants.RECENT
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.ViewStyle
import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Template
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -40,19 +40,22 @@ class AudioSettings(
val radioList =
ItemList.Builder()
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.muted,
R.drawable.volume_off_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.unmuted,
R.drawable.volume_up_24px,
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(
carContext,
R.string.alerts_only,
R.drawable.warning_24px,
)

View File

@@ -0,0 +1,77 @@
package com.kouros.navigation.car.screen.settings
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.Action
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.data.EngineType
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class CarSettings(
private val carContext: CarContext,
private var navigationViewModel: NavigationViewModel
) :
Screen(carContext) {
private var engineType = EngineType.COMBUSTION.ordinal
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.engineType.first()
}
}
override fun onGetTemplate(): Template {
engineType = settingsViewModel.engineType.value
val templateBuilder = ListTemplate.Builder()
val radioList =
ItemList.Builder()
.addItem(
buildRowForTemplate(carContext,
R.string.combustion,
R.drawable.ev_station_24px
)
)
.addItem(
buildRowForTemplate(carContext,
R.string.electric,
R.drawable.electric_car_24px
)
)
.setOnSelectedListener { index: Int ->
this.onSelected(index)
}
.setSelectedIndex(engineType)
.build()
return templateBuilder
.addSectionedList(
SectionedItemList.create(
radioList,
carContext.getString(R.string.engine_type)
)
)
.setHeader(
Header.Builder()
.setTitle(carContext.getString(R.string.car_settings))
.setStartHeaderAction(Action.BACK)
.build()
)
.build()
}
private fun onSelected(index: Int) {
settingsViewModel.onEngineTypeChanged(index)
}
}

View File

@@ -11,7 +11,7 @@ import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.buildRowForTemplate
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -34,19 +34,19 @@ class DarkModeSettings(private val carContext: CarContext) : Screen(carContext)
val radioList =
ItemList.Builder()
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.off_action_title,
R.drawable.light_mode_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.on_action_title,
R.drawable.dark_mode_24px
)
)
.addItem(
NavigationUtils(carContext).buildRowForTemplate(
buildRowForTemplate(carContext,
R.string.use_car_settings,
R.drawable.directions_car_24px
)

View File

@@ -13,7 +13,7 @@ import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.navigation.NavigationUtils
import com.kouros.navigation.car.screen.createCarIcon
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
@@ -62,7 +62,7 @@ class NavigationSettings(
buildRowForTemplate(
R.string.avoid_highways_row_title,
highwayToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.baseline_add_road_24)
createCarIcon(carContext, R.drawable.baseline_add_road_24)
)
)
@@ -72,7 +72,7 @@ class NavigationSettings(
settingsViewModel.onAvoidTollway(checked)
tollWayToggleState = !tollWayToggleState
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_toll_24)))
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle, createCarIcon(carContext,R.drawable.baseline_toll_24)))
// Ferry
val ferryToggle: Toggle =
@@ -80,7 +80,7 @@ class NavigationSettings(
settingsViewModel.onAvoidFerry(checked)
ferryToggleState = !ferryToggleState
}.setChecked(ferryToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, NavigationUtils(carContext).createCarIcon(R.drawable.baseline_directions_boat_filled_24)))
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle, createCarIcon(carContext, R.drawable.baseline_directions_boat_filled_24)))
// CarLocation
val carLocationToggle: Toggle =
@@ -93,7 +93,7 @@ class NavigationSettings(
buildRowForTemplate(
R.string.use_car_location,
carLocationToggle,
NavigationUtils(carContext).createCarIcon(R.drawable.ic_place_white_24dp)
createCarIcon(carContext,R.drawable.ic_place_white_24dp)
)
)

View File

@@ -85,7 +85,7 @@ class SettingsScreen(
)
)
// Navigation --------------
// Drive settings --------------
listBuilder = ItemList.Builder()
listBuilder.addItem(
buildRowForTemplate(
@@ -94,10 +94,17 @@ class SettingsScreen(
)
)
listBuilder.addItem(
buildRowForTemplate(
CarSettings(carContext, navigationViewModel),
R.string.car_settings
)
)
templateBuilder.addSectionedList(
SectionedItemList.create(
listBuilder.build(),
carContext.getString(R.string.navigation_settings)
carContext.getString(R.string.drive_settings)
)
)
@@ -109,6 +116,8 @@ class SettingsScreen(
.setStartHeaderAction(Action.BACK)
.build())
.build()
}
private fun getTitle(): String {

View File

@@ -2,7 +2,9 @@ package com.kouros.navigation.data
import androidx.compose.ui.graphics.Color
val NavigationColor = Color(0xFF16BBB6)
val NavigationColorLight = Color(0xFF066462)
val NavigationColorDark = Color(0xFF10DED9)
val RouteColor = Color(0xFF7B06E1)

View File

@@ -77,9 +77,10 @@ data class Locations (
)
data class SearchFilter(
var avoidMotorway: Boolean = false,
var avoidTollway : Boolean = false,
var avoidFerry : Boolean = false,
val avoidMotorway: Boolean = false,
val avoidTollway : Boolean = false,
val avoidFerry : Boolean = false,
val engineType : Int = 0,
)
@@ -139,11 +140,27 @@ object Constants {
const val TILT = 60.0
}
/**
* Enum representing different map view modes.
* - VIEW: Active navigation mode with follow-car camera
* - PREVIEW: Route overview before starting navigation
* - PAN_VIEW: User-controlled map panning
* - AMENITY_VIEW: Displaying POI/amenity locations
*/
enum class ViewStyle {
VIEW, PREVIEW, PAN_VIEW, AMENITY_VIEW
}
enum class RouteEngine {
VALHALLA, OSRM, TOMTOM
}
enum class EngineType {
COMBUSTION, ELECTRIC
}
enum class NavigationThemeColor(val color: Long) {
RED(0xFFD32F2F),
ORANGE(0xFFF57C00),

View File

@@ -2,6 +2,7 @@ package com.kouros.navigation.data
import android.content.Context
import android.location.Location
import android.util.Log
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import java.net.Authenticator
import java.net.HttpURLConnection
@@ -66,7 +67,7 @@ abstract class NavigationRepository {
}
})
}
println(url)
Log.d("fetchUrl", url)
val httpURLConnection = URL(url).openConnection() as HttpURLConnection
httpURLConnection.setRequestProperty(
"Accept",
@@ -81,7 +82,7 @@ abstract class NavigationRepository {
return response
}
} catch (e: Exception) {
println("Exception ${e.message}")
Log.e("fetchUrl", e.toString())
}
return ""
}

View File

@@ -1,5 +1,6 @@
package com.kouros.navigation.data
import android.util.Log
import com.google.gson.GsonBuilder
import com.kouros.navigation.data.osrm.OsrmResponse
import com.kouros.navigation.data.osrm.OsrmRoute

View File

@@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.kouros.navigation.data.EngineType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -55,6 +56,8 @@ class DataStoreManager(private val context: Context) {
val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion")
val ENGINE_TYPE = intPreferencesKey("EngineType")
}
// Read values
@@ -136,6 +139,12 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRIP_SUGGESTION] == true
}
val engineTypeFlow: Flow<Int> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.ENGINE_TYPE]
?: EngineType.COMBUSTION.ordinal
}
// Save values
suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences ->
@@ -220,4 +229,10 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled
}
}
suspend fun setEngineType(mode: Int) {
context.dataStore.edit { prefs ->
prefs[PreferencesKeys.ENGINE_TYPE] = mode
}
}
}

View File

@@ -3,6 +3,7 @@ package com.kouros.navigation.data.tomtom
import android.content.Context
import android.location.Location
import com.kouros.data.R
import com.kouros.navigation.data.EngineType
import com.kouros.navigation.data.NavigationRepository
import com.kouros.navigation.data.SearchFilter
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
@@ -19,9 +20,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident
private const val tomtomFields =
"{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}"
const val useLocal = true
const val useLocal = false
const val useLocalTraffic = true
const val useLocalTraffic = false
class TomTomRepository : NavigationRepository() {
@@ -38,6 +39,7 @@ class TomTomRepository : NavigationRepository() {
false
)
}
var engineType = "combustion"
var filter = ""
if (searchFilter.avoidMotorway) {
filter = "$filter&avoid=motorways"
@@ -48,6 +50,9 @@ class TomTomRepository : NavigationRepository() {
if (searchFilter.avoidFerry) {
filter = "$filter&avoid=ferries"
}
if (searchFilter.engineType == EngineType.ELECTRIC.ordinal) {
engineType = "electric"
}
val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val currentLocale = Locale.getDefault()
@@ -60,7 +65,7 @@ class TomTomRepository : NavigationRepository() {
"&instructionsType=text&language=$language&sectionType=lanes" +
"&routeRepresentation=encodedPolyline" +
"&maxAlternatives=2" +
"&vehicleEngineType=combustion$filter&key=$tomtomApiKey"
"&vehicleEngineType=$engineType$filter&key=$tomtomApiKey"
return fetchUrl(
url,
false

View File

@@ -163,7 +163,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
}
}
}
recentPlaces.postValue(pl)
recentPlaces.postValue(pl.sortedBy { it.distance })
} catch (e: Exception) {
e.printStackTrace()
}
@@ -538,7 +538,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
val avoidFerry = runBlocking { repository.avoidFerryFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry)
val engineType = runBlocking { repository.engineTypeFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry, engineType)
}
/**

View File

@@ -1,6 +1,7 @@
package com.kouros.navigation.model
import android.location.Location
import android.util.Log
import androidx.car.app.navigation.model.Step
import com.kouros.navigation.data.Constants.MAXIMUM_LOCATION_DISTANCE
import com.kouros.navigation.data.Constants.NEAREST_LOCATION_DISTANCE

View File

@@ -96,6 +96,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false
)
val engineType = repository.engineTypeFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
0
)
fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) }
}
@@ -148,4 +154,9 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
fun onTripSuggestion(enabled: Boolean) {
viewModelScope.launch { repository.setTripSuggestion(enabled) }
}
fun onEngineTypeChanged(mode: Int) {
viewModelScope.launch { repository.setEngineType(mode) }
}
}

View File

@@ -47,6 +47,9 @@ class SettingsRepository(
val tripSuggestionFlow: Flow<Boolean> =
dataStoreManager.tripSuggestionFlow
val engineTypeFlow: Flow<Int> =
dataStoreManager.engineTypeFlow
suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled)
}
@@ -102,4 +105,8 @@ class SettingsRepository(
suspend fun setTripSuggestion(enabled: Boolean) {
dataStoreManager.setTripSuggestion(enabled)
}
suspend fun setEngineType(mode: Int) {
dataStoreManager.setEngineType(mode)
}
}

View File

@@ -10,7 +10,6 @@ import com.kouros.navigation.data.osrm.OsrmRepository
import com.kouros.navigation.data.tomtom.TomTomRepository
import com.kouros.navigation.data.valhalla.ValhallaRepository
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import java.lang.Math.toDegrees
@@ -29,6 +28,8 @@ import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
import kotlin.time.toDuration
object NavigationUtils {
@@ -50,7 +51,7 @@ fun calculateZoom(speed: Double?): Double {
}
val speedKmh = (speed * 3.6).toInt()
val zoom = when (speedKmh) {
in 0..10 -> 18.0
in 0..10 -> 17.0
in 11..30 -> 17.5
in 31..65 -> 17.0
in 66..70 -> 16.5
@@ -128,14 +129,20 @@ fun Double.round(numFractionDigits: Int): Double {
return (this * factor).roundToInt() / factor
}
fun duration(preview: Boolean, bearing: Double, lastBearing: Double): Duration {
fun duration(
preview: Boolean,
bearing: Double,
lastBearing: Double,
lastLocationUpdate: LocalDateTime
): Duration {
if (preview) {
return 3.seconds
}
val cameraDuration = if ((lastBearing - bearing).absoluteValue > 20.0) {
2.seconds
} else {
1.seconds
val updateDuration = java.time.Duration.between(LocalDateTime.now(), lastLocationUpdate)
((updateDuration!!.toMillis().absoluteValue * 1.2).toDuration(DurationUnit.MILLISECONDS))
}
return cameraDuration
}

View File

@@ -66,4 +66,9 @@
<string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string>
<string name="trip_suggestion">Fahrten-Vorschläge</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources>

View File

@@ -50,4 +50,9 @@
<string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string>
<string name="trip_suggestion">Προτάσεις διαδρομής</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources>

View File

@@ -50,4 +50,9 @@
<string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string>
<string name="trip_suggestion">Sugestie dotyczące podróży</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources>

View File

@@ -53,4 +53,9 @@
<string name="general">General</string>
<string name="traffic">Show traffic</string>
<string name="trip_suggestion">Trip suggestions</string>
<string name="drive_settings">Drive settings</string>
<string name="car_settings">Car settings</string>
<string name="combustion">Combustion</string>
<string name="electric">Electric</string>
<string name="engine_type">Engine type</string>
</resources>