Categories
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -83,5 +83,4 @@ class AudioSettings(
|
||||
private fun onSelected(index: Int) {
|
||||
settingsViewModel.onGuidanceAudioChanged(index)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -34,7 +34,7 @@ class DistanceSettings(private val carContext: CarContext) : Screen(carContext)
|
||||
ItemList.Builder()
|
||||
.addItem(
|
||||
buildRowForTemplate(
|
||||
R.string.automaticaly,
|
||||
R.string.automatically,
|
||||
)
|
||||
)
|
||||
.addItem(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
10
common/data/src/main/res/drawable/ev_station_24px.xml
Normal file
10
common/data/src/main/res/drawable/ev_station_24px.xml
Normal 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>
|
||||
@@ -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>
|
||||
10
common/data/src/main/res/drawable/local_pharmacy_24px.xml
Normal file
10
common/data/src/main/res/drawable/local_pharmacy_24px.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
27
gradle.properties
Normal 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
|
||||
Reference in New Issue
Block a user