Categories

This commit is contained in:
Dimitris
2026-03-09 18:55:13 +01:00
parent 8c103a1f96
commit 61ce09f393
38 changed files with 864 additions and 338 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId = "com.kouros.navigation"
minSdk = 33
targetSdk = 36
versionCode = 61
versionName = "0.2.0.61"
versionCode = 64
versionName = "0.2.0.64"
base.archivesName = "navi-$versionName"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -6,39 +6,43 @@ import android.app.AppOpsManager
import android.location.LocationManager
import android.os.Bundle
import android.os.Process
import android.speech.tts.TextToSpeech
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresPermission
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
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.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavHostController
@@ -46,7 +50,9 @@ import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.kouros.data.R
import com.kouros.navigation.MainApplication.Companion.navigationViewModel
import com.kouros.navigation.car.TextToSpeechManager
import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
import com.kouros.navigation.data.Constants.TILT
import com.kouros.navigation.data.Constants.homeVogelhart
import com.kouros.navigation.data.StepData
@@ -77,7 +83,6 @@ import org.maplibre.compose.location.Location
import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.spatialk.geojson.Position
import java.util.Locale
import kotlin.time.Duration.Companion.seconds
@@ -94,7 +99,6 @@ class MainActivity : ComponentActivity() {
val nextStepData: MutableLiveData<StepData> by lazy {
MutableLiveData()
}
var lastStepIndex = -1
var lastLocation = location(0.0, 0.0)
val observer = Observer<String> { newRoute ->
@@ -108,7 +112,6 @@ class MainActivity : ComponentActivity() {
}
}
lateinit var textToSpeech: TextToSpeech
private fun checkMock() {
if (useMock) {
@@ -118,7 +121,6 @@ class MainActivity : ComponentActivity() {
SimulationType.GPX -> gpx(
context = applicationContext, mock
)
SimulationType.TEST_SINGLE -> testSingle(applicationContext, routeModel, mock)
}
}
@@ -135,6 +137,10 @@ class MainActivity : ComponentActivity() {
private lateinit var mock: MockLocation
private var loadRecentPlaces = false
lateinit var textToSpeechManager: TextToSpeechManager
var guidanceAudio = 0
override fun onDestroy() {
if (simulationJob != null) {
simulationJob?.cancel()
@@ -146,11 +152,11 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textToSpeech = TextToSpeech(applicationContext) { status ->
if (status == TextToSpeech.SUCCESS) {
textToSpeech.language = Locale.getDefault()
}
}
textToSpeechManager = TextToSpeechManager(applicationContext)
val repository = getSettingsRepository(applicationContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
guidanceAudio = it
})
if (useMock) {
checkMockLocationEnabled()
@@ -158,12 +164,12 @@ class MainActivity : ComponentActivity() {
locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation.addOnSuccessListener { _: android.location.Location? ->
navigationViewModel.route.observe(this, observer)
if (useMock) {
mock = MockLocation(locationManager)
mock.setMockLocation(
homeVogelhart.latitude, homeVogelhart.longitude, 0F
)
navigationViewModel.route.observe(this, observer)
}
}
lifecycleScope.launch {
@@ -195,19 +201,11 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StartScreen(
navController: NavHostController
) {
navController: NavHostController) {
val appViewModel: AppViewModel = appViewModel()
val darkMode by appViewModel.darkMode.collectAsState()
val baseStyle = BaseStyleModel().readStyle(applicationContext, darkMode, darkMode == 1)
val scaffoldState = rememberBottomSheetScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val sheetPeekHeight = 180.dp
val sheetPeekHeightState = remember { mutableStateOf(sheetPeekHeight) }
val locationProvider = rememberDefaultLocationProvider(
updateInterval = 0.5.seconds, desiredAccuracy = DesiredAccuracy.Highest
)
@@ -218,40 +216,98 @@ class MainActivity : ComponentActivity() {
}
val step: StepData? by stepData.observeAsState()
val nextStep: StepData? by nextStepData.observeAsState()
var collapsedHeight by remember {
mutableIntStateOf(300)
}
val scope = rememberCoroutineScope()
fun closeSheet() {
scope.launch {
scaffoldState.bottomSheetState.partialExpand()
sheetPeekHeightState.value = 50.dp
collapsedHeight = if (routeModel.isNavigating()) {
150
} else {
300
}
}
}
val configuration = LocalConfiguration.current
val screenHeight = configuration.screenHeightDp
var expandedType by remember {
mutableStateOf(ExpandedType.COLLAPSED)
}
val height by animateIntAsState(
when (expandedType) {
ExpandedType.HALF -> screenHeight / 2
ExpandedType.FULL -> screenHeight
ExpandedType.COLLAPSED -> collapsedHeight
}
)
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState()
NavigationTheme(useDarkTheme = darkMode == 1) {
BottomSheetScaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
scaffoldState = scaffoldState,
sheetPeekHeight = sheetPeekHeightState.value,
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
bottomStart = 0.dp,
bottomEnd = 0.dp,
topStart = 12.dp,
topEnd = 12.dp
),
sheetContent = {
SheetContent(step, nextStep) { closeSheet() }
var isUpdated = false
Box(
Modifier
.fillMaxWidth()
.height(height.dp)
.pointerInput(Unit) {
detectVerticalDragGestures(
onVerticalDrag = { change, dragAmount ->
change.consume()
if (!isUpdated) {
expandedType = when {
dragAmount < 0 && expandedType == ExpandedType.COLLAPSED -> {
ExpandedType.HALF
}
dragAmount < 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.FULL
}
dragAmount > 0 && expandedType == ExpandedType.FULL -> {
ExpandedType.HALF
}
dragAmount > 0 && expandedType == ExpandedType.HALF -> {
ExpandedType.COLLAPSED
}
else -> {
ExpandedType.FULL
}
}
isUpdated = true
}
},
onDragEnd = {
isUpdated = false
}
)
}
) {
SheetContent(step, nextStep) { closeSheet() }
}
},
) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center,
) {
MapView(
applicationContext,
userLocationState,
step,
nextStep,
cameraPosition,
routeData,
tilt,
baseStyle,
)
}
sheetPeekHeight = height.dp
) {
MapView(
applicationContext,
userLocationState,
step,
nextStep,
cameraPosition,
routeData,
tilt,
baseStyle,
)
if (!routeModel.isNavigating()) {
Settings(navController, modifier = Modifier.fillMaxWidth())
}
@@ -338,7 +394,9 @@ class MainActivity : ComponentActivity() {
if (isNavigating()) {
updateLocation(currentLocation, navigationViewModel)
stepData.value = currentStep()
//textToSpeech(stepData.value!!.instruction)
if (guidanceAudio == 1) {
textToSpeech()
}
if (navState.nextStep) {
nextStepData.value = nextStep()
}
@@ -377,19 +435,11 @@ class MainActivity : ComponentActivity() {
stepData.value = StepData("", "", 0.0, 0, 0, 0, 0.0)
}
fun textToSpeech(text: String) {
fun textToSpeech() {
val currentStep = routeModel.route.currentStep()
val stepData = routeModel.currentStep()
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
lifecycleScope.launch {
try {
val cs: CharSequence = stepData.instruction
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
Log.d("TTS", "speak $cs")
} catch (e: Throwable) {
Log.d("TTS", "speak error", e)
}
}
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index
}
}
@@ -420,5 +470,11 @@ class MainActivity : ComponentActivity() {
e.printStackTrace()
}
}
enum class ExpandedType {
HALF, FULL, COLLAPSED
}
}

View File

@@ -4,11 +4,14 @@ import android.content.Context
import android.location.Location
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.captionBar
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.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
@@ -42,6 +45,7 @@ import com.kouros.navigation.data.nominatim.SearchResult
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.location
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchSheet(
applicationContext: Context,
@@ -61,7 +65,6 @@ fun SearchSheet(
.fillMaxWidth()
.wrapContentHeight()
) {
// Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
SearchBar(
textFieldState = textFieldState,
searchPlaces = emptyList(),
@@ -72,7 +75,7 @@ fun SearchSheet(
closeSheet = { closeSheet() }
)
Home(applicationContext, viewModel, location, closeSheet = { closeSheet() })
if (recentPlaces.value != null) {
val items = listOf(recentPlaces)
if (items.isNotEmpty()) {
@@ -121,29 +124,33 @@ fun Home(
}
}
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
textFieldState: TextFieldState,
searchPlaces: List<Place>,
searchResults: List<SearchResult>,
modifier: Modifier = Modifier,
viewModel: NavigationViewModel,
context: Context,
location: Location,
closeSheet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
SearchBar(
windowInsets = WindowInsets.captionBar,
colors = SearchBarDefaults.colors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = modifier,
inputField = {
SearchBarDefaults.InputField(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.speed_camera_24px),
painter = painterResource(id = R.drawable.search_48px),
"Search",
modifier = Modifier.size(24.dp, 24.dp),
)
@@ -160,23 +167,19 @@ fun SearchBar(
)
},
expanded = expanded,
onExpandedChange = { expanded = it },
onExpandedChange = { },
) {
if (searchPlaces.isNotEmpty()) {
Text(context.getString(R.string.recent_destinations))
RecentPlaces(searchPlaces, viewModel, context, location, closeSheet)
}
if (searchResults.isNotEmpty()) {
Text("Search places")
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
Text("Search places")
SearchPlaces( searchResults, viewModel, context, location, closeSheet)
}
}
}
private fun searchPlaces(viewModel: NavigationViewModel, location: Location, it: String) {
viewModel.searchPlaces(it, location)
}
@Composable
private fun SearchPlaces(
searchResults: List<SearchResult>,

View File

@@ -55,7 +55,7 @@ class RouteModelTest {
fun checkRoute() {
assertEquals(true, routeModel.isNavigating())
assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1148.0, 10.0)
assertEquals(routeModel.curRoute.summary.duration, 1483.0, 10.0)
}
@Test
@@ -66,7 +66,7 @@ class RouteModelTest {
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(stepData.instruction, "Silcherstraße")
assertEquals(stepData.leftStepDistance, 30.0, 1.0)
assertEquals(stepData.leftStepDistance, 25.0, 1.0)
val nextStepData = routeModel.nextStep()
assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT)
assertEquals(nextStepData.instruction, "Schmalkaldener Straße")
@@ -114,7 +114,7 @@ class RouteModelTest {
val stepData = routeModel.currentStep()
assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_LEFT)
assertEquals(stepData.instruction, "Schenkendorfstraße")
assertEquals(stepData.leftStepDistance, 170.0, 1.0)
assertEquals(stepData.leftStepDistance, 170.0, 10.0)
assertEquals(stepData.lane.size, 4)
assertEquals(stepData.lane.first().valid, true)
assertEquals(stepData.lane.last().valid, false)

View File

@@ -29,6 +29,7 @@ import com.kouros.navigation.car.screen.RequestPermissionScreen
import com.kouros.navigation.car.screen.SearchScreen
import com.kouros.navigation.data.Constants.AUTOMOTIVE_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.GMS_CAR_SPEED_PERMISSION
import com.kouros.navigation.data.Constants.INSTRUCTION_DISTANCE
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
@@ -40,6 +41,7 @@ import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.GeoUtils.snapLocation
import com.kouros.navigation.utils.NavigationUtils.getViewModel
import com.kouros.navigation.utils.getSettingsRepository
import com.kouros.navigation.utils.location
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
@@ -77,6 +79,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
var autoDriveEnabled = false
val simulation = Simulation()
/**
* Lifecycle observer for managing session lifecycle events.
* Cleans up resources when the session is destroyed.
@@ -205,8 +208,13 @@ class NavigationSession : Session(), NavigationScreen.Listener {
navigationManager.setNavigationManagerCallback(object : NavigationManagerCallback {
override fun onAutoDriveEnabled() {
// Called when the app should simulate navigation (e.g., for testing)
// Implement your simulation logic here
deviceLocationManager.stopLocationUpdates()
autoDriveEnabled = true
simulation.startSimulation(
routeModel, lifecycle.coroutineScope
) { location ->
updateLocation(location)
}
}
override fun onStopNavigation() {
@@ -214,6 +222,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
// Stop turn-by-turn logic and clean up
routeModel.stopNavigation()
autoDriveEnabled = false
deviceLocationManager.startLocationUpdates()
}
})
surfaceRenderer = SurfaceRenderer(carContext, lifecycle, routeModel, viewModelStoreOwner)
@@ -240,6 +249,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
)
textToSpeechManager = TextToSpeechManager(carContext)
val repository = getSettingsRepository(carContext)
repository.guidanceAudioFlow.asLiveData().observe(this, Observer {
@@ -342,9 +352,9 @@ class NavigationSession : Session(), NavigationScreen.Listener {
* Handles route snapping, deviation detection for rerouting, and map updates.
*/
fun updateLocation(location: Location) {
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION ) {
surfaceRenderer.updateCarSpeed(location.speed)
}
if (routeModel.navState.carConnection == CarConnection.CONNECTION_TYPE_PROJECTION) {
surfaceRenderer.updateCarSpeed(location.speed)
}
updateBearing(location)
if (routeModel.isNavigating()) {
handleNavigationLocation(location)
@@ -410,7 +420,7 @@ class NavigationSession : Session(), NavigationScreen.Listener {
navigationManager.navigationStarted()
if (autoDriveEnabled) {
simulation.startSimulation(
routeModel, lifecycle.coroutineScope
routeModel, lifecycle.coroutineScope
) { location ->
updateLocation(location)
}
@@ -418,7 +428,6 @@ class NavigationSession : Session(), NavigationScreen.Listener {
}
override fun updateTrip(trip: Trip) {
Log.d("Trip", trip.toString())
navigationManager.updateTrip(trip)
}
@@ -429,10 +438,8 @@ class NavigationSession : Session(), NavigationScreen.Listener {
private fun handleGuidanceAudio() {
val currentStep = routeModel.route.currentStep()
val stepData = routeModel.currentStep()
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < 50) {
if (textToSpeechManager.initialized) {
textToSpeechManager.speak(stepData.message)
}
if (currentStep.index > lastStepIndex && stepData.leftStepDistance < INSTRUCTION_DISTANCE) {
textToSpeechManager.speak(stepData.message)
lastStepIndex = currentStep.index
}
}

View File

@@ -70,7 +70,7 @@ class SurfaceRenderer(
// Current camera position state for the map
private val cameraPosition = MutableLiveData(
CameraPosition(
zoom = 15.0,
zoom = 16.0,
target = Position(latitude = homeVogelhart.latitude, longitude = homeVogelhart.longitude)
)
)

View File

@@ -1,61 +1,81 @@
package com.kouros.navigation.car
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import androidx.car.app.CarContext
import java.util.Locale
class TextToSpeechManager(private val carContext: CarContext) {
class TextToSpeechManager(private val carContext: Context) {
var textToSpeech: TextToSpeech
private var textToSpeech: TextToSpeech? = null
@Volatile private var initialized = false
var initialized = false
private val audioManager: AudioManager by lazy {
carContext.getSystemService(AudioManager::class.java)!!
}
private val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.build()
private val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener { /* Handle focus changes if needed */ }
.build()
init {
textToSpeech = TextToSpeech(carContext) { status ->
if (status == TextToSpeech.SUCCESS) {
Log.d("TTS", "Initialization Success")
val audioAttributes =
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.build()
val request =
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(audioAttributes)
.build()
val audioManager: AudioManager =
carContext.getSystemService<AudioManager?>(AudioManager::class.java)!!
// Requesting the audio focus.
if (audioManager.requestAudioFocus(request) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
textToSpeech.setAudioAttributes(audioAttributes)
textToSpeech?.apply {
setAudioAttributes(audioAttributes)
setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) {}
override fun onDone(utteranceId: String?) {
// Release focus ONLY after speech is finished
audioManager.abandonAudioFocusRequest(focusRequest)
}
override fun onError(utteranceId: String) {
audioManager.abandonAudioFocusRequest(focusRequest)
}
})
}
initialized = true
Log.d("TTS", "Initialization Success")
} else {
Log.d("TTS", "Initialization Failed")
Log.e("TTS", "Initialization Failed")
}
}
}
fun speak(text: String) {
try {
val cs: CharSequence = text
textToSpeech.speak(cs, TextToSpeech.QUEUE_FLUSH, null, "1233455")
} catch (e: Throwable) {
Log.d("TTS", "speak error", e)
if (!initialized) {
Log.w("TTS", "Ignore speak: Not initialized yet")
return
}
// 1. Request focus
val result = audioManager.requestAudioFocus(focusRequest)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 2. Speak with a unique ID to trigger the listener
val utteranceId = System.currentTimeMillis().toString()
textToSpeech?.speak(text, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
}
}
/**
* Cleans up manager.
* Should be called when the session is destroyed.
*/
fun cleanup() {
if (initialized) {
textToSpeech.shutdown()
textToSpeech?.stop()
textToSpeech?.shutdown()
initialized = false
}
}
}

View File

@@ -14,7 +14,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.res.painterResource
@@ -22,6 +24,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kouros.data.R
@@ -210,10 +213,10 @@ fun RouteLayerPoint(routeData: String?) {
fun trafficColor(key: String): Expression<ColorValue> {
when (key) {
"queuing" -> return const(Color(0xFFC46E53))
"stationary" -> return const(Color(0xFFFF0000))
"slow" -> return const(Color(0xFFC43E3E))
"stationary" -> return const(Color(0xFF910A0A))
"heavy" -> return const(Color(0xFF6B0404))
"slow" -> return const(Color(0xFFBD2525))
"roadworks" -> return const(Color(0xFF725A0F))
"roadworks" -> return const(Color(0xFF443506))
}
return const(Color.Blue)
}
@@ -222,10 +225,10 @@ fun trafficColor(key: String): Expression<ColorValue> {
fun AmenityLayer(routeData: String?) {
if (!routeData.isNullOrEmpty()) {
var color = const(Color.Red)
var img = image(painterResource(R.drawable.local_pharmacy_48px), drawAsSdf = true)
var img = image(painterResource(R.drawable.local_pharmacy_24px), drawAsSdf = true)
if (routeData.contains(Constants.CHARGING_STATION)) {
color = const(Color(0xFF054603))
img = image(painterResource(R.drawable.ev_station_48px), drawAsSdf = true)
img = image(painterResource(R.drawable.ev_station_24px), drawAsSdf = true)
} else if (routeData.contains(Constants.FUEL_STATION)) {
color = const(Color.Blue)
img = image(painterResource(R.drawable.local_gas_station_24), drawAsSdf = true)
@@ -321,10 +324,11 @@ fun NavigationImage(
val street = streetName.toString()
val styleStreet = TextStyle(
fontSize = 14.sp,
color = if (darkMode == 1) Color.Yellow else Color.Red,
fontWeight = FontWeight.Bold,
color = if (darkMode == 1) Color.White else navigationColor,
)
val textLayoutStreet = remember(street) {
textMeasurerStreet.measure(street, styleStreet)
textMeasurerStreet.measure(street, styleStreet, overflow = TextOverflow.Ellipsis)
}
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding)) {
@@ -346,16 +350,26 @@ fun NavigationImage(
)
Canvas(
modifier = Modifier
.size(imageSize.dp + 100.dp, imageSize.dp + 80.dp)
.size((width / 5).dp, (height/4).dp)
) {
if (!streetName.isNullOrEmpty()) {
drawRoundRect(
topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2 ,
y = center.y + textLayoutStreet.size.height,
),
color = if (darkMode == 1) navigationColor else Color.White,
cornerRadius = CornerRadius(x = 10f, y = 10f),
)
drawText(
textMeasurer = textMeasurerStreet,
text = streetName,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = styleStreet,
topLeft = Offset(
x = center.x - textLayoutStreet.size.width / 2,
y = center.y + textLayoutStreet.size.height,
y = center.y + textLayoutStreet.size.height + 10,
)
)
}

View File

@@ -1,5 +1,8 @@
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
@@ -16,7 +19,12 @@ 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
@@ -66,4 +74,31 @@ class NavigationUtils(private var carContext: CarContext) {
)
.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

@@ -37,7 +37,7 @@ class Simulation {
// Update your app's state as if a real GPS update occurred
updateLocation(fakeLocation)
// Wait before moving to the next point (e.g., every 2 seconds)
delay(500)
delay(1000)
lastLocation = fakeLocation
}
routeModel.stopNavigation()

View File

@@ -18,19 +18,32 @@ 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.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,
private val surfaceRenderer: SurfaceRenderer,
private val navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
) : Screen(carContext), CategoryObserverCallback {
private val categoryObserver = CategoryObserver(this)
private var category = ""
var categories: List<Category> = listOf(
Category(id = FUEL_STATION, name = carContext.getString(R.string.fuel_station)),
Category(id = PHARMACY, name = carContext.getString(R.string.pharmacy)),
Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station))
)
init {
navigationViewModel.elements.observe(this, categoryObserver)
}
override fun onGetTemplate(): Template {
val itemListBuilder = ItemList.Builder()
.setNoItemsMessage("No categories to show")
@@ -38,22 +51,10 @@ class CategoriesScreen(
itemListBuilder.addItem(
Row.Builder()
.setTitle(it.name)
.setImage(carIcon(carContext,it.id))
.setImage(carIcon(carContext,it.id, -1))
.setOnClickListener {
screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
it.id,
navigationViewModel
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
category = it.id
navigationViewModel.getAmenities(it.id, surfaceRenderer.lastLocation)
}
.setBrowsable(true)
.build()
@@ -72,20 +73,49 @@ class CategoriesScreen(
.setSingleList(itemListBuilder.build())
.build()
}
override fun onCategoryElementsReady(
elements: List<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
) {
val loc = location(centerLon, centerLat)
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
category,
navigationViewModel,
elements
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
override fun invalidateScreen() {
}
}
fun carIcon(context: CarContext, id: String): CarIcon {
val resId = when (id) {
FUEL_STATION -> R.drawable.local_gas_station_24
PHARMACY -> R.drawable.local_pharmacy_48px
CHARGING_STATION -> R.drawable.ev_station_48px
else -> {}
fun carIcon(context: CarContext, category: String, index: Int): CarIcon {
if (index == -1) {
val resId = when (category) {
CHARGING_STATION -> R.drawable.ev_station_24px
FUEL_STATION -> R.drawable.local_gas_station_24
PHARMACY -> R.drawable.local_pharmacy_24px
else -> R.drawable.ic_place_white_24dp
}
return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build()
} else {
return CarIcon.Builder(NavigationUtils(context).createNumberIcon(category, index.toString())).build()
}
return CarIcon.Builder(
IconCompat.createWithResource(
context,
resId as Int
)
)
.build()
}
}

View File

@@ -14,11 +14,16 @@ import androidx.car.app.model.Row
import androidx.car.app.model.Template
import androidx.car.app.navigation.model.MapController
import androidx.car.app.navigation.model.MapWithContentTemplate
import androidx.lifecycle.Observer
import androidx.car.app.versioning.CarAppApiLevels
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
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.Place
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
@@ -32,57 +37,39 @@ class CategoryScreen(
private val surfaceRenderer: SurfaceRenderer,
private val category: String,
private val navigationViewModel: NavigationViewModel,
private var elements: List<Elements>,
) : Screen(carContext) {
var elements = listOf<Elements>()
val observer = Observer<List<Elements>> { newElements ->
elements = newElements
val coordinates = mutableListOf<List<Double>>()
val loc = location(0.0, 0.0)
elements.forEach {
if (loc.latitude == 0.0) {
loc.longitude = it.lon
loc.latitude = it.lat
}
coordinates.add(listOf(it.lon, it.lat))
}
if (elements.isNotEmpty()) {
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
invalidate()
}
}
init {
navigationViewModel.elements.observe(this, observer)
navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation)
}
val maxListItems: Int = 30
override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder()
var index = 0
val listLimit = min(
50,
carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
elements.forEach {
if (index++ < listLimit) {
if (it.tags.operator != null) {
listBuilder.addItem(
createItem(it, category)
// Some hosts may allow more items in the list than others, so create more.
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
val listLimit = min(
maxListItems,
carContext.getCarService(ConstraintManager::class.java)
.getContentLimit(
ConstraintManager.CONTENT_LIMIT_TYPE_LIST
)
)
elements.forEach {
if (it.tags.operator != null) {
if (index++ < listLimit) {
listBuilder.addItem(
createItem(it, category, index)
)
}
}
}
}
val header = Header.Builder()
.setStartHeaderAction(Action.BACK)
.setTitle(carContext.getString(R.string.charging_station))
.setTitle(getTitle(carContext, category))
.build()
val builder = MapWithContentTemplate.Builder()
.setContentTemplate(
@@ -99,7 +86,17 @@ class CategoryScreen(
return builder.build()
}
private fun createItem(it: Elements, category: String): Row {
private fun getTitle(carContext: CarContext, category: String): String {
val resId = when (category) {
CHARGING_STATION -> R.string.charging_station
FUEL_STATION -> R.string.fuel_station
PHARMACY -> R.string.pharmacy
else -> R.string.no_places
}
return carContext.getString(resId)
}
private fun createItem(it: Elements, category: String, index: Int): Row {
var name = ""
if (it.tags.name != null) {
name = it.tags.name.toString()
@@ -113,7 +110,7 @@ class CategoryScreen(
surfaceRenderer.setCategoryLocation(location, category)
}
.setTitle(name)
.setImage(carIcon(carContext, category))
.setImage(carIcon(carContext, category, index))
if (it.distance < 1000) {
row.addText("${(it.distance).toInt()} m")
} else {
@@ -127,28 +124,29 @@ class CategoryScreen(
val navigationUtils = NavigationUtils(carContext)
row.addAction(
Action.Builder()
.setOnClickListener {
navigationViewModel.loadRoute(
carContext,
currentLocation = surfaceRenderer.lastLocation,
location(it.lon!!, it.lat!!),
surfaceRenderer.carOrientation
)
setResult(
Place(
name = name,
category = Constants.CHARGING_STATION,
latitude = it.lat!!,
longitude = it.lon!!
.setOnClickListener {
navigationViewModel.loadRoute(
carContext,
currentLocation = surfaceRenderer.lastLocation,
location(it.lon, it.lat),
surfaceRenderer.carOrientation
)
)
finish()
}
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
.build())
setResult(
Place(
name = name,
category = Constants.CHARGING_STATION,
latitude = it.lat,
longitude = it.lon
)
)
finish()
}
.setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px))
.build())
return row.build()
}
private fun carText(sText: String): CarText {
val secondText =
CarText.Builder(

View File

@@ -40,8 +40,6 @@ import com.kouros.navigation.data.Constants.TRAFFIC_UPDATE
import com.kouros.navigation.data.Place
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.model.SettingsViewModel
import com.kouros.navigation.repository.SettingsRepository
import com.kouros.navigation.utils.GeoUtils
import com.kouros.navigation.utils.formattedDistance
import com.kouros.navigation.utils.getSettingsRepository
@@ -74,7 +72,7 @@ class NavigationScreen(
/** Starts navigation. */
fun startNavigation()
/** Updates trip information. */
fun updateTrip(trip: Trip)
}
@@ -102,8 +100,10 @@ class NavigationScreen(
lifecycleScope.launch {
settingsViewModel.routingEngine.first()
settingsViewModel.recentPlaces.first()
distanceMode = repository.distanceModeFlow.first()
}
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
}
/**
@@ -112,7 +112,7 @@ class NavigationScreen(
*/
override fun onRouteReceived(route: String) {
if (route.isNotEmpty()) {
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
val routingEngine = runBlocking { repository.routingEngineFlow.first() }
routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine)
navigationType = NavigationType.NAVIGATION
routeModel.startNavigation(route)
@@ -187,9 +187,7 @@ class NavigationScreen(
* Returns the appropriate template based on the current navigation state.
*/
override fun onGetTemplate(): Template {
repository.distanceModeFlow.asLiveData().observe(this, Observer {
distanceMode = it
})
val actionStripBuilder = createActionStripBuilder()
return when (navigationType) {
NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder)
@@ -330,8 +328,9 @@ class NavigationScreen(
* Builds and returns RoutingInfo based on the current step and distance.
*/
fun getRoutingInfo(): RoutingInfo {
val distance = formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val routingInfo = RoutingInfo.Builder()
val distance =
formattedDistance(distanceMode, routeModel.routeCalculator.leftStepDistance())
val routingInfo = RoutingInfo.Builder()
.setCurrentStep(
routeModel.currentStep(carContext = carContext),
Distance.create(distance.first, distance.second)
@@ -460,7 +459,7 @@ class NavigationScreen(
* Creates an action to start the settings screen.
*/
private fun settingsAction(): Action {
return Action.Builder()
return Action.Builder()
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
.setOnClickListener {
screenManager.push(SettingsScreen(carContext, navigationViewModel))
@@ -628,7 +627,7 @@ class NavigationScreen(
}
updateSpeedCamera(location)
with(routeModel) {
updateLocation( location, navigationViewModel)
updateLocation(location, navigationViewModel)
checkArrival()
}
invalidate()
@@ -658,14 +657,19 @@ class NavigationScreen(
* This includes destination name, address, travel estimate, and loading status.
*/
private fun updateTrip() {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")
.setAddress(routeModel.navState.destination.street ?: "")
.build()
tripBuilder.addDestination(destination, routeModel.travelEstimate(carContext, distanceMode))
tripBuilder.setLoading(false)
listener.updateTrip(tripBuilder.build())
if (routeModel.isNavigating()) {
val tripBuilder = Trip.Builder()
val destination = Destination.Builder()
.setName(routeModel.navState.destination.name ?: "")
.setAddress(routeModel.navState.destination.street ?: "")
.build()
tripBuilder.addDestination(
destination,
routeModel.travelEstimate(carContext, distanceMode)
)
tripBuilder.setLoading(false)
listener.updateTrip(tripBuilder.build())
}
}
/**

View File

@@ -142,22 +142,15 @@ class SearchScreen(
@SuppressLint("DefaultLocale")
fun doSearch(searchItemListBuilder: ItemList.Builder) {
if (searchResult.size == 1) {
navigateToPlace(searchResult.first())
}
searchResult.forEach {
searchItemListBuilder.addItem(
Row.Builder()
.setTitle("${(it.distance/1000).toInt()} km ${it.displayName} ")
.setOnClickListener {
val place = Place(
name = it.displayName,
latitude = it.lat.toDouble(),
longitude = it.lon.toDouble(),
street = it.address.road,
city = it.address.city,
postalCode = it.address.postcode,
distance = it.distance
)
setResult(place)
finish()
navigateToPlace(it)
}
.setBrowsable(false)
.build()
@@ -165,4 +158,18 @@ class SearchScreen(
}
invalidate()
}
private fun navigateToPlace(result: SearchResult) {
val place = Place(
name = result.displayName,
latitude = result.lat.toDouble(),
longitude = result.lon.toDouble(),
street = result.address.road,
city = result.address.city,
postalCode = result.address.postcode,
distance = result.distance
)
setResult(place)
finish()
}
}

View File

@@ -0,0 +1,54 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
import com.kouros.navigation.data.overpass.Elements
/**
* Callback interface for category element updates in CategoryScreen.
*/
interface CategoryObserverCallback {
/**
* Called when category elements are ready to display.
* @param elements the full list of elements
* @param centerLat latitude of the first element (used to center the map)
* @param centerLon longitude of the first element (used to center the map)
* @param coordinates all element coordinates as [lon, lat] pairs
*/
fun onCategoryElementsReady(
elements: List<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
)
/** Called to request UI invalidation after elements are updated */
fun invalidateScreen()
}
/**
* Observer for POI/amenity element lists. Extracts coordinates and notifies the screen
* when new data is ready to display.
*/
class CategoryObserver(
private val callback: CategoryObserverCallback
) : Observer<List<Elements>> {
override fun onChanged(value: List<Elements>) {
if (value.isEmpty()) return
var centerLat = 0.0
var centerLon = 0.0
val coordinates = mutableListOf<List<Double>>()
value.forEach { element ->
if (centerLat == 0.0) {
centerLat = element.lat
centerLon = element.lon
}
coordinates.add(listOf(element.lon, element.lat))
}
callback.onCategoryElementsReady(value, centerLat, centerLon, coordinates)
callback.invalidateScreen()
}
}

View File

@@ -83,5 +83,4 @@ class AudioSettings(
private fun onSelected(index: Int) {
settingsViewModel.onGuidanceAudioChanged(index)
}
}

View File

@@ -9,6 +9,7 @@ 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.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.car.screen.settings.DistanceSettings
@@ -20,16 +21,19 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
private var buildingToggleState = false
private var showTraffic = false
val settingsViewModel = getSettingsViewModel(carContext)
init {
lifecycleScope.launch {
settingsViewModel.show3D.first()
}
}
override fun onGetTemplate(): Template {
buildingToggleState = settingsViewModel.show3D.value
settingsViewModel.traffic.asLiveData().observe(this) {
showTraffic = it
invalidate()
}
settingsViewModel.show3D.asLiveData().observe(this) {
buildingToggleState = it
invalidate()
}
val listBuilder = ItemList.Builder()
val buildingToggle: Toggle =
Toggle.Builder { checked: Boolean ->
@@ -37,6 +41,14 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) {
buildingToggleState = !buildingToggleState
}.setChecked(buildingToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.threed_building, buildingToggle))
val trafficToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onTraffic(checked)
showTraffic = !showTraffic
}.setChecked(showTraffic).build()
listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle))
listBuilder.addItem(
buildRowForScreenTemplate(
DarkModeSettings(carContext),

View File

@@ -34,7 +34,7 @@ class DistanceSettings(private val carContext: CarContext) : Screen(carContext)
ItemList.Builder()
.addItem(
buildRowForTemplate(
R.string.automaticaly,
R.string.automatically,
)
)
.addItem(

View File

@@ -28,6 +28,9 @@ class NavigationSettings(
private var tollWayToggleState = false
private var ferryToggleState = false
private var carLocationToggleState = false
val settingsViewModel = getSettingsViewModel(carContext)
@@ -36,6 +39,7 @@ class NavigationSettings(
lifecycleScope.launch {
settingsViewModel.avoidTollway.first()
settingsViewModel.avoidMotorway.first()
settingsViewModel.avoidFerry.first()
settingsViewModel.carLocation.first()
}
}
@@ -43,9 +47,12 @@ class NavigationSettings(
override fun onGetTemplate(): Template {
motorWayToggleState = settingsViewModel.avoidMotorway.value
tollWayToggleState = settingsViewModel.avoidTollway.value
ferryToggleState = settingsViewModel.avoidFerry.value
carLocationToggleState = settingsViewModel.carLocation.value
val listBuilder = ItemList.Builder()
// Motorway
val highwayToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onAvoidMotorway(checked)
@@ -66,6 +73,15 @@ class NavigationSettings(
}.setChecked(tollWayToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_tolls_row_title, tollwayToggle))
// Ferry
val ferryToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onAvoidFerry(checked)
ferryToggleState = !ferryToggleState
}.setChecked(ferryToggleState).build()
listBuilder.addItem(buildRowForTemplate(R.string.avoid_ferries, ferryToggle))
// CarLocation
val carLocationToggle: Toggle =
Toggle.Builder { checked: Boolean ->
settingsViewModel.onCarLocation(checked)

View File

@@ -3,13 +3,23 @@ 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.CarIcon
import androidx.car.app.model.Header
import androidx.car.app.model.ItemList
import androidx.car.app.model.ListTemplate
import androidx.car.app.model.Row
import androidx.car.app.model.SectionedItemList
import androidx.car.app.model.Template
import androidx.car.app.model.Toggle
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import com.kouros.data.R
import com.kouros.navigation.model.NavigationViewModel
import com.kouros.navigation.utils.getSettingsViewModel
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/** A screen demonstrating selectable lists. */
class SettingsScreen(
@@ -17,20 +27,66 @@ class SettingsScreen(
private var navigationViewModel: NavigationViewModel,
) : Screen(carContext) {
val settingsViewModel = getSettingsViewModel(carContext)
private var audioToggleState = false
init {
lifecycleScope.launch {
settingsViewModel.guidanceAudio.first()
}
}
override fun onGetTemplate(): Template {
val listBuilder = ItemList.Builder()
listBuilder.addItem(
buildRowForTemplate(
AudioSettings(carContext),
R.string.audio_settings
)
)
settingsViewModel.guidanceAudio.asLiveData().observe(this, Observer {
audioToggleState = settingsViewModel.guidanceAudio.value == 1
invalidate()
})
val templateBuilder = ListTemplate.Builder()
val audioToggle: Toggle =
Toggle.Builder { checked: Boolean ->
if (checked) {
settingsViewModel.onGuidanceAudioChanged(1)
} else {
settingsViewModel.onGuidanceAudioChanged(0)
}
audioToggleState = !audioToggleState
}.setChecked(audioToggleState).build()
var listBuilder = ItemList.Builder()
listBuilder.addItem(Row.Builder()
.setTitle(getTitle())
.setImage(getImage())
.setToggle(audioToggle)
.build())
listBuilder.addItem(
buildRowForTemplate(
DisplaySettings(carContext),
R.string.display
)
)
listBuilder.addItem(
buildRowForTemplate(
AudioSettings(carContext),
R.string.audio_settings
)
)
templateBuilder.addSectionedList(
SectionedItemList.create(
listBuilder.build(),
carContext.getString(R.string.general)
)
)
// Navigation --------------
listBuilder = ItemList.Builder()
listBuilder.addItem(
buildRowForTemplate(
NavigationSettings(carContext, navigationViewModel),
@@ -38,19 +94,41 @@ class SettingsScreen(
)
)
return ListTemplate.Builder()
.setSingleList(listBuilder.build())
.setHeader(
Header.Builder()
.setTitle(
(carContext.getString(R.string.settings_action_title))
)
.setStartHeaderAction(Action.BACK)
.build()
templateBuilder.addSectionedList(
SectionedItemList.create(
listBuilder.build(),
carContext.getString(R.string.navigation_settings)
)
)
return templateBuilder
.setHeader( Header.Builder()
.setTitle(
(carContext.getString(R.string.settings_action_title))
)
.setStartHeaderAction(Action.BACK)
.build())
.build()
}
private fun getTitle(): String {
return if (audioToggleState) {
carContext.getString(R.string.on_action_title)
} else {
carContext.getString(R.string.off_action_title)
}
}
private fun getImage(): CarIcon {
return if (audioToggleState) {
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_up_24px))
.build()
} else {
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.volume_off_24px))
.build()
}
}
private fun buildRowForTemplate(screen: Screen, title: Int): Row {
return Row.Builder()
.setTitle(carContext.getString(title))

View File

@@ -0,0 +1,103 @@
package com.kouros.navigation.car.screen.observers
import com.kouros.navigation.data.overpass.Elements
import com.kouros.navigation.data.overpass.Tags
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.*
class CategoryObserverTest {
private lateinit var mockCallback: CategoryObserverCallback
private lateinit var observer: CategoryObserver
@Before
fun setup() {
mockCallback = mock()
observer = CategoryObserver(mockCallback)
}
@Test
fun `onChanged with empty list does not invoke callback`() {
observer.onChanged(emptyList())
verify(mockCallback, never()).onCategoryElementsReady(any(), any(), any(), any())
verify(mockCallback, never()).invalidateScreen()
}
@Test
fun `onChanged with single element invokes both callbacks`() {
val element = createElement(lon = 10.5, lat = 52.3)
observer.onChanged(listOf(element))
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), any())
verify(mockCallback).invalidateScreen()
}
@Test
fun `onChanged uses first element coordinates as map center`() {
val first = createElement(lon = 10.5, lat = 52.3)
val second = createElement(lon = 11.0, lat = 53.0)
observer.onChanged(listOf(first, second))
argumentCaptor<Double>().apply {
verify(mockCallback).onCategoryElementsReady(any(), capture(), capture(), any())
val capturedLat = firstValue
val capturedLon = secondValue
assert(capturedLat == 52.3) { "Expected centerLat=52.3 but was $capturedLat" }
assert(capturedLon == 10.5) { "Expected centerLon=10.5 but was $capturedLon" }
}
}
@Test
fun `onChanged passes all elements to callback`() {
val elements = listOf(
createElement(lon = 10.0, lat = 52.0),
createElement(lon = 11.0, lat = 53.0),
createElement(lon = 12.0, lat = 54.0)
)
observer.onChanged(elements)
argumentCaptor<List<Elements>>().apply {
verify(mockCallback).onCategoryElementsReady(capture(), any(), any(), any())
assert(firstValue.size == 3)
}
}
@Test
fun `onChanged builds coordinates list with lon first then lat`() {
val element = createElement(lon = 10.5, lat = 52.3)
observer.onChanged(listOf(element))
argumentCaptor<List<List<Double>>>().apply {
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
val coords = firstValue
assert(coords.size == 1)
assert(coords[0][0] == 10.5) { "Expected lon=10.5 at index 0 but was ${coords[0][0]}" }
assert(coords[0][1] == 52.3) { "Expected lat=52.3 at index 1 but was ${coords[0][1]}" }
}
}
@Test
fun `onChanged collects coordinates for all elements`() {
val elements = listOf(
createElement(lon = 10.0, lat = 52.0),
createElement(lon = 11.0, lat = 53.0)
)
observer.onChanged(elements)
argumentCaptor<List<List<Double>>>().apply {
verify(mockCallback).onCategoryElementsReady(any(), any(), any(), capture())
assert(firstValue.size == 2)
}
}
private fun createElement(lon: Double, lat: Double): Elements {
return Elements(lon = lon, lat = lat, tags = Tags())
}
}

View File

@@ -78,7 +78,7 @@ data class Locations (
data class SearchFilter(
var avoidMotorway: Boolean = false,
var avoidTollway : Boolean = false,
var avoidFerry : Boolean = false,
)
@@ -93,10 +93,6 @@ data class ValhallaLocation (
object Constants {
//const val STYLE: String = "https://kouros-online.de/liberty.json"
//const val STYLE_DARK: String = "https://kouros-online.de/liberty_night.json"
const val TAG: String = "Navigation"
const val CATEGORIES: String = "Categories"
@@ -132,6 +128,8 @@ object Constants {
const val MAXIMUM_LOCATION_DISTANCE = 100000F
const val TRAFFIC_UPDATE = 300
const val INSTRUCTION_DISTANCE = 50
const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.permission.CAR_SPEED"
const val AUTOMOTIVE_CAR_SPEED_PERMISSION = "android.car.permission.CAR_SPEED"

View File

@@ -55,18 +55,7 @@ abstract class NavigationRepository {
): Double {
if (currentLocation.latitude == 0.0)
return 0.0
val osrm = OsrmRepository()
val route = osrm.getRoute(context, currentLocation, location, carOrientation, SearchFilter())
val gson = GsonBuilder().serializeNulls().create()
val osrmJson = gson.fromJson(route, OsrmResponse::class.java)
if (osrmJson.routes.isEmpty()) {
return 0.0
}
return osrmJson.routes.first().distance
// return osrmJson.destinations.first().distance?.toDouble() ?: 0.0
///val routeModel = RouteModel()
//routeModel.startNavigation(route, context)
//return routeModel.curRoute.summary.distance
return currentLocation.distanceTo(location).toDouble()
}
fun searchPlaces(search: String, location: Location): String {

View File

@@ -35,6 +35,8 @@ class DataStoreManager(private val context: Context) {
val AVOID_TOLLWAY = booleanPreferencesKey("AvoidTollway")
val AVOID_FERRY = booleanPreferencesKey("AvoidFerry")
val CAR_LOCATION = booleanPreferencesKey("CarLocation")
val ROUTING_ENGINE = intPreferencesKey("RoutingEngine")
@@ -49,6 +51,8 @@ class DataStoreManager(private val context: Context) {
val GUIDANCE_AUDIO = intPreferencesKey("GuidanceAudio")
val TRAFFIC = booleanPreferencesKey("Traffic")
}
// Read values
@@ -73,6 +77,11 @@ class DataStoreManager(private val context: Context) {
preferences[PreferencesKeys.AVOID_TOLLWAY] == true
}
val avoidFerryFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] == true
}
val useCarLocationFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] == true
@@ -115,6 +124,11 @@ class DataStoreManager(private val context: Context) {
?: 0
}
val trafficFlow: Flow<Boolean> =
context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.TRAFFIC] == true
}
// Save values
suspend fun setShow3D(enabled: Boolean) {
context.dataStore.edit { preferences ->
@@ -140,6 +154,12 @@ class DataStoreManager(private val context: Context) {
}
}
suspend fun setAvoidFerry(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.AVOID_FERRY] = enabled
}
}
suspend fun setCarLocation(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.CAR_LOCATION] = enabled
@@ -182,4 +202,9 @@ class DataStoreManager(private val context: Context) {
}
}
suspend fun setTraffic(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.TRAFFIC] = enabled
}
}
}

View File

@@ -25,6 +25,9 @@ class OsrmRepository : NavigationRepository() {
if (searchFilter.avoidTollway) {
exclude = "$exclude&exclude=toll"
}
if (searchFilter.avoidFerry) {
exclude = "$exclude&exclude=ferry"
}
val routeLocation = "${currentLocation.longitude},${currentLocation.latitude};${location.longitude},${location.latitude}?steps=true&alternatives=false"
return fetchUrl(routeUrl + routeLocation + exclude, true)
}

View File

@@ -44,6 +44,9 @@ class TomTomRepository : NavigationRepository() {
if (searchFilter.avoidTollway) {
filter = "$filter&avoid=tollRoads"
}
if (searchFilter.avoidFerry) {
filter = "$filter&avoid=ferries"
}
val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val currentLocale = Locale.getDefault()
@@ -65,6 +68,10 @@ class TomTomRepository : NavigationRepository() {
override fun getTraffic(context: Context, location: Location, carOrientation: Float): String {
val repository = getSettingsRepository(context)
val tomtomApiKey = runBlocking { repository.tomTomApiKeyFlow.first() }
val showTraffic = runBlocking { repository.trafficFlow.first() }
if (!showTraffic) {
return ""
}
val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0)
return if (useAssetTraffic) {
val trafficJson = context.resources.openRawResource(R.raw.tomtom_traffic)

View File

@@ -119,17 +119,11 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val recentPlaces = settingsRepository.recentPlacesFlow.first()
val gson = GsonBuilder().serializeNulls().create()
val places = gson.fromJson(recentPlaces, Places::class.java)
val place = places.places.minByOrNull { it.lastDate.dec() }
if (place != null) {
for (place in places.places.sortedBy { it.lastDate }) {
val plLocation = location(place.longitude, place.latitude)
val distance = repository.getRouteDistance(
location,
plLocation,
carOrientation,
context
)
place.distance = distance.toFloat()
if (place.distance > 1F) {
val distance = plLocation.distanceTo(location)
place.distance = distance
if (place.distance > 200F) {
recentPlace.postValue(place)
return@launch
}
@@ -152,7 +146,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java)
val pl = mutableListOf<Place>()
var id : Long = 0
var id: Long = 0
if (rp.isNotEmpty()) {
for (place in recentPlaces.places) {
if (place.category.equals(Constants.RECENT)) {
@@ -509,7 +503,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
val gson = GsonBuilder().serializeNulls().create()
val settingsRepository = getSettingsRepository(context)
val rp = settingsRepository.recentPlacesFlow.first()
var id : Long = 0
var id: Long = 0
if (rp.isNotEmpty()) {
val recentPlaces =
gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }
@@ -579,9 +573,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
*/
fun getSearchFilter(context: Context): SearchFilter {
val repository = getSettingsRepository(context)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway)
val avoidMotorway = runBlocking { repository.avoidMotorwayFlow.first() }
val avoidTollway = runBlocking { repository.avoidTollwayFlow.first() }
val avoidFerry = runBlocking { repository.avoidFerryFlow.first() }
return SearchFilter(avoidMotorway, avoidTollway, avoidFerry)
}
/**
@@ -591,7 +586,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo
fun loadRecentPlace(context: Context): SnapshotStateList<Place?> {
val pl = mutableListOf<Place>()
val settingsRepository = getSettingsRepository(context)
val rp = runBlocking { settingsRepository.recentPlacesFlow.first()}
val rp = runBlocking { settingsRepository.recentPlacesFlow.first() }
if (rp.isNotEmpty()) {
val gson = GsonBuilder().serializeNulls().create()
val recentPlaces = gson.fromJson(rp, Places::class.java).places.sortedBy { it.lastDate }

View File

@@ -36,6 +36,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
false
)
val avoidFerry = repository.avoidFerryFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
val carLocation = repository.carLocationFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
@@ -78,6 +84,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
0
)
val traffic = repository.trafficFlow.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
false
)
fun onShow3DChanged(enabled: Boolean) {
viewModelScope.launch { repository.setShow3D(enabled) }
}
@@ -94,6 +106,11 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
viewModelScope.launch { repository.setAvoidTollway(enabled) }
}
fun onAvoidFerry(enabled: Boolean) {
viewModelScope.launch { repository.setAvoidFerry(enabled) }
}
fun onCarLocation(enabled: Boolean) {
viewModelScope.launch { repository.setCarLocation(enabled) }
}
@@ -118,4 +135,7 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel(
viewModelScope.launch { repository.setGuidanceAudio(mode) }
}
fun onTraffic(enabled: Boolean) {
viewModelScope.launch { repository.setTraffic(enabled) }
}
}

View File

@@ -17,6 +17,9 @@ class SettingsRepository(
val avoidTollwayFlow: Flow<Boolean> =
dataStoreManager.avoidTollwayFlow
val avoidFerryFlow: Flow<Boolean> =
dataStoreManager.avoidFerryFlow
val carLocationFlow: Flow<Boolean> =
dataStoreManager.useCarLocationFlow
@@ -29,7 +32,6 @@ class SettingsRepository(
val tomTomApiKeyFlow: Flow<String> =
dataStoreManager.tomTomApiKeyFlow
val recentPlacesFlow: Flow<String> =
dataStoreManager.recentPlacesFlow
@@ -39,6 +41,8 @@ class SettingsRepository(
val guidanceAudioFlow: Flow<Int> =
dataStoreManager.guidanceAudioFlow
val trafficFlow: Flow<Boolean> =
dataStoreManager.trafficFlow
suspend fun setShow3D(enabled: Boolean) {
dataStoreManager.setShow3D(enabled)
@@ -56,6 +60,10 @@ class SettingsRepository(
dataStoreManager.setAvoidTollway(enabled)
}
suspend fun setAvoidFerry(enabled: Boolean) {
dataStoreManager.setAvoidFerry(enabled)
}
suspend fun setCarLocation(enabled: Boolean) {
dataStoreManager.setCarLocation(enabled)
}
@@ -84,4 +92,7 @@ class SettingsRepository(
dataStoreManager.setGuidanceAudio(mode)
}
suspend fun setTraffic(enabled: Boolean) {
dataStoreManager.setTraffic(enabled)
}
}

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M240,400L480,400L480,200Q480,200 480,200Q480,200 480,200L240,200Q240,200 240,200Q240,200 240,200L240,400ZM160,840L160,200Q160,167 183.5,143.5Q207,120 240,120L480,120Q513,120 536.5,143.5Q560,167 560,200L560,480L610,480Q639,480 659.5,500.5Q680,521 680,550L680,735Q680,752 694,766Q708,780 725,780Q743,780 756.5,766Q770,752 770,735L770,360L760,360Q743,360 731.5,348.5Q720,337 720,320L720,240L740,240L740,180L780,180L780,240L820,240L820,180L860,180L860,240L880,240L880,320Q880,337 868.5,348.5Q857,360 840,360L830,360L830,735Q830,777 799.5,808.5Q769,840 725,840Q682,840 651,808.5Q620,777 620,735L620,550Q620,545 617.5,542.5Q615,540 610,540L560,540L560,840L160,840ZM340,760L440,600L380,600L380,480L280,640L340,640L340,760Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M220,408L489,408L489,180Q489,180 489,180Q489,180 489,180L220,180Q220,180 220,180Q220,180 220,180L220,408ZM160,840L160,180Q160,156 178,138Q196,120 220,120L489,120Q513,120 531,138Q549,156 549,180L549,468L614,468Q634.71,468 649.36,482.64Q664,497.29 664,518L664,737Q664,759 681.5,773.5Q699,788 722,788Q745,788 765,773.5Q785,759 785,737L785,350L770,350Q757.25,350 748.63,341.37Q740,332.75 740,320L740,230L760,230L760,180L790,180L790,230L830,230L830,180L860,180L860,230L880,230L880,320Q880,332.75 871.38,341.37Q862.75,350 850,350L835,350L835,736.69Q835,780 801,810Q767,840 721.82,840Q677.66,840 645.83,810Q614,780 614,737L614,518Q614,518 614,518Q614,518 614,518L549,518L549,840L160,840ZM337,746L425,606L372,606L372,501L285,641L337,641L337,746Z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,760L200,520L120,280L120,200L628,200L686,40L780,74L734,200L840,200L840,280L760,520L840,760L840,840L120,840ZM440,680L520,680L520,560L640,560L640,480L520,480L520,360L440,360L440,480L320,480L320,560L440,560L440,680Z"/>
</vector>

View File

@@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M120,840L120,780L207,525L120,270L120,210L647,210L709,40L777,67L725,210L840,210L840,270L752,525L840,780L840,840L120,840ZM452,679L512,679L512,555L636,555L636,495L512,495L512,371L452,371L452,495L328,495L328,555L452,555L452,679ZM182,780L778,780L690,525L778,270L182,270L270,525L182,780ZM480,525L480,525L480,525L480,525L480,525L480,525Z"/>
</vector>

View File

@@ -38,6 +38,7 @@
<string name="drive_now">Losfahren</string>
<string name="avoid_tolls_row_title" msgid="5194057244144831024">"Mautstraßen vermeiden"</string>
<string name="avoid_highways_row_title" msgid="4711913426200490304">"Autobahnen vermeiden"</string>
<string name="avoid_ferries">Fähren vermeiden</string>
<string name="recent_destinations">Letzte Ziele</string>
<string name="contacts">Kontakte</string>
<string name="route_preview">Route Vorschau</string>
@@ -46,20 +47,22 @@
<string name="fuel_station">Tankstelle</string>
<string name="pharmacy">Apotheke</string>
<string name="charging_station">Ladestation</string>
<string name="speed_camera">Speed camera</string>
<string name="speed_camera">Blitzer</string>
<string name="use_car_location">Auto GPS verwenden</string>
<string name="tomtom">TomTom\t</string>
<string name="options">Optionen</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Verwende Auto Einstellungen</string>
<string name="exit_number">Ausfahrt nummer</string>
<string name="navigation_icon_description">Navigations Icon</string>
<string name="distance_units">Entfernungseinheiten</string>
<string name="automaticaly">Automatisch</string>
<string name="automatically">Automatisch</string>
<string name="kilometer">Kilometer</string>
<string name="miles">Meilen</string>
<string name="audio_settings">Töne</string>
<string name="muted">Stummgeschaltet</string>
<string name="unmuted">Ton an</string>
<string name="alerts_only">Nur Alarme</string>
<string name="no_categories">Keine Kategorien</string>
<string name="general">Allgemein</string>
<string name="traffic">Verkehr anzeigen</string>
</resources>

View File

@@ -17,6 +17,7 @@
<string name="stop_action_title">Διακοπή</string>
<string name="avoid_highways_row_title">Αποφυγή αυτοκινητοδρόμων</string>
<string name="avoid_tolls_row_title">Αποφυγή διοδίων</string>
<string name="avoid_ferries">Αποφυγή φέρι μποτ</string>
<string name="no_places">Δεν βρέθηκαν τοποθεσίες</string>
<string name="recent_destinations">Πρόσφατοι προορισμοί</string>
<string name="contacts">Επαφές</string>
@@ -38,11 +39,14 @@
<string name="exit_number">Αριθμός εξόδου</string>
<string name="navigation_icon_description">Εικονίδιο πλοήγησης</string>
<string name="distance_units">Μονάδες απόστασης</string>
<string name="automaticaly">Αυτόματα</string>
<string name="automatically">Αυτόματα</string>
<string name="kilometer">Χιλιόμετρα</string>
<string name="miles">Μίλια</string>
<string name="audio_settings">Φωνητική καθοδήγηση</string>
<string name="muted">Σίγαση</string>
<string name="unmuted">Ήχος ενεργός</string>
<string name="alerts_only">Μόνο ειδοποιήσεις</string>
</resources>
<string name="no_categories">Δεν υπάρχουν κατηγορίες</string>
<string name="general">Γενικά</string>
<string name="traffic">Εμφάνιση κίνησης</string>
</resources>

View File

@@ -17,6 +17,7 @@
<string name="stop_action_title">Zatrzymaj</string>
<string name="avoid_highways_row_title">Unikaj autostrad</string>
<string name="avoid_tolls_row_title">Unikaj opłat drogowych</string>
<string name="avoid_ferries">Unikaj promów</string>
<string name="no_places">Brak miejsc</string>
<string name="recent_destinations">Ostatnie cele</string>
<string name="contacts">Kontakty</string>
@@ -38,11 +39,14 @@
<string name="exit_number">Numer zjazdu</string>
<string name="navigation_icon_description">Ikona nawigacji</string>
<string name="distance_units">Jednostki odległości</string>
<string name="automaticaly">Automatycznie</string>
<string name="automatically">Automatycznie</string>
<string name="kilometer">Kilometry</string>
<string name="miles">Mile</string>
<string name="audio_settings">Wskazówki głosowe</string>
<string name="muted">Wyciszony</string>
<string name="unmuted">Dźwięk włączony</string>
<string name="alerts_only">Tylko ostrzeżenia</string>
</resources>
<string name="no_categories">Brak kategorii do wyświetlenia</string>
<string name="general">Ogólne</string>
<string name="traffic">Pokaż natężenie ruchu</string>
</resources>

View File

@@ -17,6 +17,7 @@
<string name="stop_action_title">Stop</string>
<string name="avoid_highways_row_title">Avoid highways</string>
<string name="avoid_tolls_row_title">Avoid tolls rows</string>
<string name="avoid_ferries">Avoid ferries</string>
<string name="no_places">No places</string>
<string name="recent_destinations">Recent destinations</string>
<string name="contacts">Contacts</string>
@@ -34,18 +35,21 @@
<string name="osrm" translatable="false">Osrm</string>
<string name="routing_engine" translatable="false">Routing engine</string>
<string name="use_car_location">Use car location</string>
<string name="tomtom" translatable="false">TomTom\t</string>
<string name="tomtom" translatable="false">TomTom</string>
<string name="options">Options</string>
<string name="tomtom_api_key">TomTom ApiKey</string>
<string name="use_car_settings">Use car settings</string>
<string name="exit_number">Exit number</string>
<string name="navigation_icon_description">Navigation icon</string>
<string name="distance_units">Distance units</string>
<string name="automaticaly">Automaticaly</string>
<string name="automatically">Automatically</string>
<string name="kilometer">Kilometer</string>
<string name="miles">Miles</string>
<string name="audio_settings">Guidance audio</string>
<string name="muted">Muted</string>
<string name="unmuted">Unmuted</string>
<string name="alerts_only">Alerts only</string>
<string name="no_categories">No categories to show</string>
<string name="general">General</string>
<string name="traffic">Show traffic</string>
</resources>

27
gradle.properties Normal file
View File

@@ -0,0 +1,27 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn