From 61ce09f3934db2424a071165ae494d78d9f06cef Mon Sep 17 00:00:00 2001 From: Dimitris Date: Mon, 9 Mar 2026 18:55:13 +0100 Subject: [PATCH] Categories --- app/build.gradle.kts | 4 +- .../com/kouros/navigation/ui/MainActivity.kt | 180 ++++++++++++------ .../com/kouros/navigation/ui/SearchSheet.kt | 29 +-- .../kouros/navigation/car/RouteModelTest.kt | 6 +- .../navigation/car/NavigationSession.kt | 27 ++- .../kouros/navigation/car/SurfaceRenderer.kt | 2 +- .../navigation/car/TextToSpeechManager.kt | 80 +++++--- .../com/kouros/navigation/car/map/MapView.kt | 32 +++- .../car/navigation/NavigationUtils.kt | 35 ++++ .../navigation/car/navigation/Simulation.kt | 2 +- .../navigation/car/screen/CategoriesScreen.kt | 90 ++++++--- .../navigation/car/screen/CategoryScreen.kt | 114 ++++++----- .../navigation/car/screen/NavigationScreen.kt | 44 +++-- .../navigation/car/screen/SearchScreen.kt | 29 +-- .../car/screen/observers/CategoryObserver.kt | 54 ++++++ .../car/screen/settings/AudioSettings.kt | 1 - .../car/screen/settings/DisplaySettings.kt | 26 ++- .../car/screen/settings/DistanceSettings.kt | 2 +- .../car/screen/settings/NavigationSettings.kt | 16 ++ .../car/screen/settings/SettingsScreen.kt | 110 +++++++++-- .../screen/observers/CategoryObserverTest.kt | 103 ++++++++++ .../java/com/kouros/navigation/data/Data.kt | 8 +- .../navigation/data/NavigationRepository.kt | 13 +- .../data/datastore/DataStoreManager.kt | 25 +++ .../navigation/data/osrm/OsrmRepository.kt | 3 + .../data/tomtom/TomTomRepository.kt | 7 + .../navigation/model/NavigationViewModel.kt | 27 ++- .../navigation/model/SettingsViewModel.kt | 20 ++ .../repository/SettingsRepository.kt | 13 +- .../src/main/res/drawable/ev_station_24px.xml | 10 + .../src/main/res/drawable/ev_station_48px.xml | 10 - .../main/res/drawable/local_pharmacy_24px.xml | 10 + .../main/res/drawable/local_pharmacy_48px.xml | 10 - .../data/src/main/res/values-de/strings.xml | 9 +- .../data/src/main/res/values-el/strings.xml | 8 +- .../data/src/main/res/values-pl/strings.xml | 8 +- common/data/src/main/res/values/strings.xml | 8 +- gradle.properties | 27 +++ 38 files changed, 864 insertions(+), 338 deletions(-) create mode 100644 common/car/src/main/java/com/kouros/navigation/car/screen/observers/CategoryObserver.kt create mode 100644 common/car/src/test/java/com/kouros/navigation/car/screen/observers/CategoryObserverTest.kt create mode 100644 common/data/src/main/res/drawable/ev_station_24px.xml delete mode 100644 common/data/src/main/res/drawable/ev_station_48px.xml create mode 100644 common/data/src/main/res/drawable/local_pharmacy_24px.xml delete mode 100644 common/data/src/main/res/drawable/local_pharmacy_48px.xml create mode 100644 gradle.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b2020f..cd89d58 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt index f677b9a..6536711 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -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 by lazy { MutableLiveData() } - var lastStepIndex = -1 var lastLocation = location(0.0, 0.0) val observer = Observer { 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 + } + } diff --git a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt index 1ffabae..bd87c53 100644 --- a/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt +++ b/app/src/main/java/com/kouros/navigation/ui/SearchSheet.kt @@ -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, searchResults: List, - 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, diff --git a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt index 833db23..b189008 100644 --- a/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt +++ b/common/car/src/androidTest/java/com/kouros/navigation/car/RouteModelTest.kt @@ -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) diff --git a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt index 8f52809..b46ef32 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/NavigationSession.kt @@ -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 } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt index 6464b53..64a3b0e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/SurfaceRenderer.kt @@ -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) ) ) diff --git a/common/car/src/main/java/com/kouros/navigation/car/TextToSpeechManager.kt b/common/car/src/main/java/com/kouros/navigation/car/TextToSpeechManager.kt index f535a3f..8212274 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/TextToSpeechManager.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/TextToSpeechManager.kt @@ -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::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 } } } \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt index e4d0eef..fb2c0ab 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/map/MapView.kt @@ -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 { 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 { 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, ) ) } diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationUtils.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationUtils.kt index 69ff6b2..0d46ff7 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationUtils.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/NavigationUtils.kt @@ -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) + } } \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt index c0c317f..3cb5c9a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/Simulation.kt @@ -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() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt index df05a9a..71d2b8a 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoriesScreen.kt @@ -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 = 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, + centerLat: Double, + centerLon: Double, + coordinates: List> + ) { + 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() -} \ No newline at end of file +} + diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt index 0ee7514..7b59c55 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/CategoryScreen.kt @@ -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, ) : Screen(carContext) { - var elements = listOf() - - val observer = Observer> { newElements -> - elements = newElements - val coordinates = mutableListOf>() - 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( diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt index 3086926..f3b1fbc 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationScreen.kt @@ -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()) + } } /** diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt index 1634678..1e62609 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/SearchScreen.kt @@ -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() + } } \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/CategoryObserver.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/CategoryObserver.kt new file mode 100644 index 0000000..ce17849 --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/CategoryObserver.kt @@ -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, + centerLat: Double, + centerLon: Double, + coordinates: List> + ) + + /** 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> { + + override fun onChanged(value: List) { + if (value.isEmpty()) return + + var centerLat = 0.0 + var centerLon = 0.0 + val coordinates = mutableListOf>() + + 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() + } +} diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/AudioSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/AudioSettings.kt index f91f8d8..1d6cc45 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/AudioSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/AudioSettings.kt @@ -83,5 +83,4 @@ class AudioSettings( private fun onSelected(index: Int) { settingsViewModel.onGuidanceAudioChanged(index) } - } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DisplaySettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DisplaySettings.kt index 977a805..82911dd 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DisplaySettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DisplaySettings.kt @@ -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), diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DistanceSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DistanceSettings.kt index 0e7b773..7885b95 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DistanceSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/DistanceSettings.kt @@ -34,7 +34,7 @@ class DistanceSettings(private val carContext: CarContext) : Screen(carContext) ItemList.Builder() .addItem( buildRowForTemplate( - R.string.automaticaly, + R.string.automatically, ) ) .addItem( diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/NavigationSettings.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/NavigationSettings.kt index 84c63c6..476b1e4 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/NavigationSettings.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/NavigationSettings.kt @@ -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) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/SettingsScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/SettingsScreen.kt index 0396546..df9efa3 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/settings/SettingsScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/settings/SettingsScreen.kt @@ -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)) diff --git a/common/car/src/test/java/com/kouros/navigation/car/screen/observers/CategoryObserverTest.kt b/common/car/src/test/java/com/kouros/navigation/car/screen/observers/CategoryObserverTest.kt new file mode 100644 index 0000000..e2aa879 --- /dev/null +++ b/common/car/src/test/java/com/kouros/navigation/car/screen/observers/CategoryObserverTest.kt @@ -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().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>().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>>().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>>().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()) + } +} diff --git a/common/data/src/main/java/com/kouros/navigation/data/Data.kt b/common/data/src/main/java/com/kouros/navigation/data/Data.kt index 5da03c7..11aead2 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/Data.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/Data.kt @@ -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" diff --git a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt index 2f5df1d..4c60280 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/NavigationRepository.kt @@ -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 { diff --git a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt index d48cc51..c26ec63 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/datastore/DataStoreManager.kt @@ -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 = + context.dataStore.data.map { preferences -> + preferences[PreferencesKeys.AVOID_FERRY] == true + } + val useCarLocationFlow: Flow = context.dataStore.data.map { preferences -> preferences[PreferencesKeys.CAR_LOCATION] == true @@ -115,6 +124,11 @@ class DataStoreManager(private val context: Context) { ?: 0 } + val trafficFlow: Flow = + 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 + } + } } diff --git a/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt index a847822..46cce7f 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/osrm/OsrmRepository.kt @@ -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) } diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt index 5ca9bf8..6a81463 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRepository.kt @@ -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) diff --git a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt index 7a2a4bb..80bf786 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/NavigationViewModel.kt @@ -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() - 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 { val pl = mutableListOf() 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 } diff --git a/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt b/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt index c587700..d9cd742 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/SettingsViewModel.kt @@ -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) } + } } diff --git a/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt b/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt index 24eb41b..48cba26 100644 --- a/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt +++ b/common/data/src/main/java/com/kouros/navigation/repository/SettingsRepository.kt @@ -17,6 +17,9 @@ class SettingsRepository( val avoidTollwayFlow: Flow = dataStoreManager.avoidTollwayFlow + val avoidFerryFlow: Flow = + dataStoreManager.avoidFerryFlow + val carLocationFlow: Flow = dataStoreManager.useCarLocationFlow @@ -29,7 +32,6 @@ class SettingsRepository( val tomTomApiKeyFlow: Flow = dataStoreManager.tomTomApiKeyFlow - val recentPlacesFlow: Flow = dataStoreManager.recentPlacesFlow @@ -39,6 +41,8 @@ class SettingsRepository( val guidanceAudioFlow: Flow = dataStoreManager.guidanceAudioFlow + val trafficFlow: Flow = + 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) + } } diff --git a/common/data/src/main/res/drawable/ev_station_24px.xml b/common/data/src/main/res/drawable/ev_station_24px.xml new file mode 100644 index 0000000..7c6cf17 --- /dev/null +++ b/common/data/src/main/res/drawable/ev_station_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/drawable/ev_station_48px.xml b/common/data/src/main/res/drawable/ev_station_48px.xml deleted file mode 100644 index 4681f9c..0000000 --- a/common/data/src/main/res/drawable/ev_station_48px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/common/data/src/main/res/drawable/local_pharmacy_24px.xml b/common/data/src/main/res/drawable/local_pharmacy_24px.xml new file mode 100644 index 0000000..8351033 --- /dev/null +++ b/common/data/src/main/res/drawable/local_pharmacy_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/drawable/local_pharmacy_48px.xml b/common/data/src/main/res/drawable/local_pharmacy_48px.xml deleted file mode 100644 index c99e328..0000000 --- a/common/data/src/main/res/drawable/local_pharmacy_48px.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/common/data/src/main/res/values-de/strings.xml b/common/data/src/main/res/values-de/strings.xml index 8dcf179..0f2771a 100644 --- a/common/data/src/main/res/values-de/strings.xml +++ b/common/data/src/main/res/values-de/strings.xml @@ -38,6 +38,7 @@ Losfahren "Mautstraßen vermeiden" "Autobahnen vermeiden" + Fähren vermeiden Letzte Ziele Kontakte Route Vorschau @@ -46,20 +47,22 @@ Tankstelle Apotheke Ladestation - Speed camera + Blitzer Auto GPS verwenden - TomTom\t Optionen TomTom ApiKey Verwende Auto Einstellungen Ausfahrt nummer Navigations Icon Entfernungseinheiten - Automatisch + Automatisch Kilometer Meilen Töne Stummgeschaltet Ton an Nur Alarme + Keine Kategorien + Allgemein + Verkehr anzeigen diff --git a/common/data/src/main/res/values-el/strings.xml b/common/data/src/main/res/values-el/strings.xml index 8b5e992..be63d99 100644 --- a/common/data/src/main/res/values-el/strings.xml +++ b/common/data/src/main/res/values-el/strings.xml @@ -17,6 +17,7 @@ Διακοπή Αποφυγή αυτοκινητοδρόμων Αποφυγή διοδίων + Αποφυγή φέρι μποτ Δεν βρέθηκαν τοποθεσίες Πρόσφατοι προορισμοί Επαφές @@ -38,11 +39,14 @@ Αριθμός εξόδου Εικονίδιο πλοήγησης Μονάδες απόστασης - Αυτόματα + Αυτόματα Χιλιόμετρα Μίλια Φωνητική καθοδήγηση Σίγαση Ήχος ενεργός Μόνο ειδοποιήσεις - \ No newline at end of file + Δεν υπάρχουν κατηγορίες + Γενικά + Εμφάνιση κίνησης + diff --git a/common/data/src/main/res/values-pl/strings.xml b/common/data/src/main/res/values-pl/strings.xml index ec1a92c..c9bb914 100644 --- a/common/data/src/main/res/values-pl/strings.xml +++ b/common/data/src/main/res/values-pl/strings.xml @@ -17,6 +17,7 @@ Zatrzymaj Unikaj autostrad Unikaj opłat drogowych + Unikaj promów Brak miejsc Ostatnie cele Kontakty @@ -38,11 +39,14 @@ Numer zjazdu Ikona nawigacji Jednostki odległości - Automatycznie + Automatycznie Kilometry Mile Wskazówki głosowe Wyciszony Dźwięk włączony Tylko ostrzeżenia - \ No newline at end of file + Brak kategorii do wyświetlenia + Ogólne + Pokaż natężenie ruchu + diff --git a/common/data/src/main/res/values/strings.xml b/common/data/src/main/res/values/strings.xml index 4c78f68..e97ca2d 100644 --- a/common/data/src/main/res/values/strings.xml +++ b/common/data/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Stop Avoid highways Avoid tolls rows + Avoid ferries No places Recent destinations Contacts @@ -34,18 +35,21 @@ Osrm Routing engine Use car location - TomTom\t + TomTom Options TomTom ApiKey Use car settings Exit number Navigation icon Distance units - Automaticaly + Automatically Kilometer Miles Guidance audio Muted Unmuted Alerts only + No categories to show + General + Show traffic \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ca3b6db --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file