From d1968cfa68d479de6a9392179188df9db34885aa Mon Sep 17 00:00:00 2001 From: Dimitris Date: Sat, 21 Mar 2026 12:24:53 +0100 Subject: [PATCH] Preview --- app/build.gradle.kts | 4 +- .../com/kouros/navigation/ui/MainActivity.kt | 2 +- .../navigation/ui/settings/DisplayScreen.kt | 14 +- common/car/build.gradle.kts | 19 +- .../kouros/navigation/car/RouteModelTest.kt | 14 +- .../navigation/car/NavigationSession.kt | 29 +- .../kouros/navigation/car/SurfaceRenderer.kt | 6 + .../car/navigation/NavigationUtils.kt | 24 - .../car/navigation/RouteCarModel.kt | 34 +- .../navigation/car/navigation/Simulation.kt | 4 - .../navigation/car/screen/CategoriesScreen.kt | 67 +- .../navigation/car/screen/CategoryScreen.kt | 100 ++- .../car/screen/NavigationListener.kt | 17 + .../navigation/car/screen/NavigationScreen.kt | 735 +++++++++--------- .../navigation/car/screen/PlaceListScreen.kt | 94 +-- .../car/screen/RequestPermissionScreen.kt | 30 +- .../car/screen/RoutePreviewScreen.kt | 245 +++--- .../navigation/car/screen/ScreenUtils.kt | 62 ++ .../navigation/car/screen/SearchScreen.kt | 77 +- .../observers/NavigationObserverCallback.kt | 8 +- .../observers/NavigationObserverManager.kt | 4 - .../screen/observers/PreviewRouteObserver.kt | 14 - .../screen/observers/RecentPlaceObserver.kt | 18 - .../car/screen/settings/DisplaySettings.kt | 14 + .../car/screen/NavigationScreenTest.kt | 133 ++++ .../car/screen/observers/ObserversTest.kt | 30 +- .../java/com/kouros/navigation/data/Data.kt | 2 + .../navigation/data/NavigationRepository.kt | 26 +- .../data/datastore/DataStoreManager.kt | 13 + .../com/kouros/navigation/data/route/Step.kt | 1 + .../data/tomtom/TomTomRepository.kt | 25 +- .../navigation/data/tomtom/TomTomRoute.kt | 38 +- .../com/kouros/navigation/model/IconMapper.kt | 2 +- .../navigation/model/NavigationViewModel.kt | 55 +- .../com/kouros/navigation/model/RouteModel.kt | 18 +- .../navigation/model/SettingsViewModel.kt | 10 + .../repository/SettingsRepository.kt | 7 + .../main/res/drawable/chevron_right_24px.xml | 11 + .../main/res/drawable/traffic_jam_48px.xml | 10 + .../data/src/main/res/values-de/strings.xml | 1 + .../data/src/main/res/values-el/strings.xml | 1 + .../data/src/main/res/values-pl/strings.xml | 1 + common/data/src/main/res/values/strings.xml | 1 + .../kouros/navigation/model/RouteModelTest.kt | 15 - gradle/libs.versions.toml | 21 +- 45 files changed, 1121 insertions(+), 935 deletions(-) create mode 100644 common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt create mode 100644 common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt delete mode 100644 common/car/src/main/java/com/kouros/navigation/car/screen/observers/PreviewRouteObserver.kt delete mode 100644 common/car/src/main/java/com/kouros/navigation/car/screen/observers/RecentPlaceObserver.kt create mode 100644 common/car/src/test/java/com/kouros/navigation/car/screen/NavigationScreenTest.kt create mode 100644 common/data/src/main/res/drawable/chevron_right_24px.xml create mode 100644 common/data/src/main/res/drawable/traffic_jam_48px.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b8607f1..90262c2 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 = 71 - versionName = "0.2.0.71" + versionCode = 73 + versionName = "0.2.0.73" 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 a0ecb0d..77bf96a 100644 --- a/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt +++ b/app/src/main/java/com/kouros/navigation/ui/MainActivity.kt @@ -294,7 +294,7 @@ class MainActivity : ComponentActivity() { fun updateLocation(location: Location?) { if (location != null && lastLocation.latitude != location.position.latitude && lastLocation.longitude != location.position.longitude) { val currentLocation = location(location.position.longitude, location.position.latitude) - if (location.bearing != null) { + if (location.bearing != null && location.bearingAccuracy!! <= 20.0) { currentLocation.bearing = location.bearing!!.toFloat() } if (routeModel.isNavigating()) { diff --git a/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt b/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt index b9d7bd5..9992f40 100644 --- a/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt +++ b/app/src/main/java/com/kouros/navigation/ui/settings/DisplayScreen.kt @@ -1,22 +1,17 @@ package com.kouros.navigation.ui.settings -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -24,7 +19,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -36,7 +30,6 @@ import com.kouros.navigation.ui.components.SectionTitle import com.kouros.navigation.ui.components.SettingSwitch - @OptIn(ExperimentalMaterial3Api::class) @Composable fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { @@ -45,6 +38,7 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { val show3D by viewModel.show3D.collectAsState() val showTraffic by viewModel.traffic.collectAsState() val distanceMode by viewModel.distanceMode.collectAsState() + val driveSuggestion by viewModel.tripSuggestion.collectAsState() Scaffold( topBar = { @@ -90,6 +84,12 @@ fun DisplayScreen(viewModel: SettingsViewModel, navigateBack: () -> Unit) { checked = showTraffic, onCheckedChange = viewModel::onTraffic ) + + SettingSwitch( + title = stringResource(R.string.trip_suggestion), + checked = driveSuggestion, + onCheckedChange = viewModel::onTripSuggestion + ) } } diff --git a/common/car/build.gradle.kts b/common/car/build.gradle.kts index abce27b..9c3dc48 100644 --- a/common/car/build.gradle.kts +++ b/common/car/build.gradle.kts @@ -1,3 +1,4 @@ + plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.compose) @@ -31,6 +32,9 @@ android { } } +val mockitoAgent = configurations.create("mockitoAgent") + + dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.car.app) @@ -48,10 +52,19 @@ dependencies { implementation(libs.play.services.location) implementation(libs.androidx.datastore.core) implementation(libs.androidx.monitor) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.runner) + androidTestImplementation(libs.androidx.rules) + testImplementation(libs.junit) testImplementation(libs.mockito.core) testImplementation(libs.mockito.kotlin) - androidTestImplementation(libs.androidx.runner) - androidTestImplementation(libs.androidx.rules) -} \ No newline at end of file + testImplementation(libs.androidx.car.app.testing) + testImplementation(libs.robolectric) + testImplementation(libs.google.truth) + testImplementation(libs.androidx.test.core) + mockitoAgent(libs.mockito.core) { isTransitive = false } +} + + 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 57a2c9b..235c2de 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 @@ -38,8 +38,10 @@ class RouteModelTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext val repository = getSettingsRepository(appContext) runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) } - val routeJson = appContext.resources.openRawResource(R.raw.tomtom_routing) - val routeJsonString = routeJson.bufferedReader().use { it.readText() } + val routeJsonString = TomTomRepository().fetchUrl( + "https://kouros-online.de/tomtom_routing.json", + false + ) assertNotEquals("", routeJsonString) routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal) routeModel.startNavigation(routeJsonString) @@ -49,7 +51,7 @@ class RouteModelTest { fun checkRoute() { assertEquals(true, routeModel.isNavigating()) assertEquals(routeModel.curRoute.summary.distance, 11116.0, 10.0) - assertEquals(routeModel.curRoute.summary.duration, 1483.0, 10.0) + assertEquals(routeModel.curRoute.summary.duration, 1581.0, 10.0) } @Test @@ -60,7 +62,7 @@ class RouteModelTest { val stepData = routeModel.currentStep() assertEquals(stepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(stepData.instruction, "Silcherstraße") - assertEquals(stepData.leftStepDistance, 25.0, 1.0) + assertEquals(stepData.leftStepDistance, 20.0, 5.0) val nextStepData = routeModel.nextStep() assertEquals(nextStepData.currentManeuverType, Maneuver.TYPE_TURN_NORMAL_RIGHT) assertEquals(nextStepData.instruction, "Schmalkaldener Straße") @@ -141,7 +143,7 @@ class RouteModelTest { if (index in 61..61) { routeModel.updateLocation(curLocation, NavigationViewModel(TomTomRepository())) val stepData = routeModel.currentStep() - assertEquals(stepData.lane.size, 2) + assertEquals(stepData.lane.size, 3) assertEquals(stepData.lane.first().valid, true) assertEquals(stepData.lane.first().indications.first(), "STRAIGHT") } @@ -203,6 +205,6 @@ class RouteModelTest { val location: Location = location(11.578911, 48.185565) routeModel.updateLocation(location, NavigationViewModel(TomTomRepository())) val step = routeModel.currentStep() - assertEquals(step.leftStepDistance, 34.0, 1.0) + assertEquals(step.leftStepDistance, 26.0, 1.0) } } \ No newline at end of file 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 b46ef32..4ae1b92 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 @@ -24,9 +24,11 @@ import androidx.lifecycle.coroutineScope import androidx.lifecycle.lifecycleScope import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.navigation.Simulation +import com.kouros.navigation.car.screen.NavigationListener import com.kouros.navigation.car.screen.NavigationScreen import com.kouros.navigation.car.screen.RequestPermissionScreen import com.kouros.navigation.car.screen.SearchScreen +import com.kouros.navigation.car.screen.checkPermission 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 @@ -41,9 +43,12 @@ 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.flow.first import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.time.LocalDateTime +import java.time.ZoneOffset /** @@ -52,7 +57,7 @@ import kotlinx.coroutines.launch * car hardware sensors, routing engine selection, and screen navigation. * Implements NavigationScreen.Listener for handling navigation events. */ -class NavigationSession : Session(), NavigationScreen.Listener { +class NavigationSession : Session(), NavigationListener { // Flag to enable/disable contact access feature val useContacts = false @@ -80,6 +85,8 @@ class NavigationSession : Session(), NavigationScreen.Listener { val simulation = Simulation() + var navigationManagerStarted = false + /** * Lifecycle observer for managing session lifecycle events. * Cleans up resources when the session is destroyed. @@ -148,11 +155,11 @@ class NavigationSession : Session(), NavigationScreen.Listener { when (connectionState) { CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> Unit CarConnection.CONNECTION_TYPE_NATIVE -> { - navigationScreen.checkPermission(AUTOMOTIVE_CAR_SPEED_PERMISSION) + navigationViewModel.permissionGranted.value = checkPermission(carContext,AUTOMOTIVE_CAR_SPEED_PERMISSION) } CarConnection.CONNECTION_TYPE_PROJECTION -> { - navigationScreen.checkPermission(GMS_CAR_SPEED_PERMISSION) + navigationViewModel.permissionGranted.value = checkPermission(carContext, GMS_CAR_SPEED_PERMISSION) } } } @@ -210,6 +217,7 @@ class NavigationSession : Session(), NavigationScreen.Listener { // Called when the app should simulate navigation (e.g., for testing) deviceLocationManager.stopLocationUpdates() autoDriveEnabled = true + surfaceRenderer.viewStyle = ViewStyle.VIEW simulation.startSimulation( routeModel, lifecycle.coroutineScope ) { location -> @@ -241,10 +249,10 @@ class NavigationSession : Session(), NavigationScreen.Listener { shouldUseCarLocationFlow = carSensorManager.shouldUseCarLocation(), onLocationUpdate = ::updateLocation, onInitialLocation = { location -> - navigationViewModel.loadRecentPlace( + navigationViewModel.loadRecentPlaces( + carContext, location, surfaceRenderer.carOrientation, - carContext ) } ) @@ -325,7 +333,7 @@ class NavigationSession : Session(), NavigationScreen.Listener { private fun handleNavigateIntent(screenManager: ScreenManager) { screenManager.popToRoot() screenManager.pushForResult( - SearchScreen(carContext, surfaceRenderer, navigationViewModel) + SearchScreen(carContext, surfaceRenderer, navigationViewModel, emptyList()) ) { result -> // Handle search result if needed } @@ -359,6 +367,7 @@ class NavigationSession : Session(), NavigationScreen.Listener { if (routeModel.isNavigating()) { handleNavigationLocation(location) } else { + navigationScreen.checkTraffic(LocalDateTime.now(ZoneOffset.UTC), location) surfaceRenderer.updateLocation(location) } } @@ -417,7 +426,9 @@ class NavigationSession : Session(), NavigationScreen.Listener { * Called when user starts navigation */ override fun startNavigation() { + surfaceRenderer.viewStyle = ViewStyle.VIEW navigationManager.navigationStarted() + navigationManagerStarted = true if (autoDriveEnabled) { simulation.startSimulation( routeModel, lifecycle.coroutineScope @@ -428,7 +439,9 @@ class NavigationSession : Session(), NavigationScreen.Listener { } override fun updateTrip(trip: Trip) { - navigationManager.updateTrip(trip) + if (navigationManagerStarted) { + navigationManager.updateTrip(trip) + } } /** 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 7102334..e59ebc9 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 @@ -392,6 +392,7 @@ class SurfaceRenderer( * Sets route data for active navigation and switches to VIEW mode. */ fun setRouteData() { + println("SetRouteData") routeData.value = routeModel.curRoute.routeGeoJson viewStyle = ViewStyle.VIEW } @@ -457,6 +458,11 @@ class SurfaceRenderer( } + + fun updateTrafficData(routeModel: RouteCarModel) { + + } + /** * Displays a specific location (e.g., amenity/POI) on the map. */ 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 0d46ff7..370f3a8 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 @@ -31,30 +31,6 @@ import java.util.Locale class NavigationUtils(private var carContext: CarContext) { - private fun createToastAction( - @StringRes titleRes: Int, @StringRes toastStringRes: Int, - flags: Int - ): Action { - return Action.Builder() - .setOnClickListener { showToast(toastStringRes) } - .setTitle(createCarText(titleRes)) - .setFlags(flags) - .build() - } - - - fun showToast(@StringRes toastStringRes: Int) { - CarToast.makeText( - carContext, - carContext.getString(toastStringRes), - CarToast.LENGTH_SHORT - ) - .show() - } - - fun createCarText(@StringRes stringRes: Int): CarText { - return CarText.create(carContext.getString(stringRes)) - } fun createCarIcon(@DrawableRes iconRes: Int): CarIcon { return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() diff --git a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt index dabbbb8..f3ddb0b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/navigation/RouteCarModel.kt @@ -1,9 +1,8 @@ package com.kouros.navigation.car.navigation -import android.speech.tts.TextToSpeech import android.text.SpannableString -import android.util.Log -import androidx.annotation.DrawableRes +import android.text.SpannableStringBuilder +import android.text.Spanned import androidx.annotation.StringRes import androidx.car.app.AppManager import androidx.car.app.CarContext @@ -16,6 +15,8 @@ import androidx.car.app.model.CarIcon import androidx.car.app.model.CarText import androidx.car.app.model.DateTimeWithZone import androidx.car.app.model.Distance +import androidx.car.app.model.DurationSpan +import androidx.car.app.model.ForegroundCarColorSpan import androidx.car.app.navigation.model.Lane import androidx.car.app.navigation.model.LaneDirection import androidx.car.app.navigation.model.Maneuver @@ -25,10 +26,11 @@ import androidx.car.app.navigation.model.Step import androidx.car.app.navigation.model.TravelEstimate import androidx.core.graphics.drawable.IconCompat import com.kouros.data.R +import com.kouros.navigation.car.screen.createCarIcon import com.kouros.navigation.data.StepData import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.formattedDistance -import java.util.Locale +import java.time.Duration import java.util.TimeZone import java.util.concurrent.TimeUnit @@ -115,8 +117,8 @@ class RouteCarModel : RouteModel() { .setRemainingTimeColor(CarColor.GREEN) .setRemainingDistanceColor(CarColor.BLUE) if (traffic > 0) { - travelBuilder.setTripText(CarText.create("$traffic min")) - travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.warning_24px)) + travelBuilder.setTripText(createDelay(traffic)) + travelBuilder.setTripIcon(createCarIcon(carContext, R.drawable.traffic_jam_48px)) } if (navState.travelMessage.isNotEmpty()) { @@ -125,7 +127,22 @@ class RouteCarModel : RouteModel() { } return travelBuilder.build() } - + private fun createDelay(delay: Int): CarText { + val delayBuilder = SpannableStringBuilder() + delayBuilder.append( + " ", + DurationSpan.create(Duration.ofMinutes(delay.toLong())), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + delayBuilder.setSpan( + ForegroundCarColorSpan.create(CarColor.RED), + 0, + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + return CarText.Builder(delayBuilder) + .build() + } fun addLanes(carContext: CarContext, step: Step.Builder, stepData: StepData) : Boolean { var laneImageAdded = false stepData.lane.forEach { @@ -167,9 +184,6 @@ class RouteCarModel : RouteModel() { return CarText.create(carContext.getString(stringRes)) } - fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon { - return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).build() - } fun createCarIcon(iconCompat: IconCompat): CarIcon { return CarIcon.Builder(iconCompat).build() 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 bf2f0b1..86d260e 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 @@ -19,10 +19,6 @@ class Simulation { lifecycleScope: LifecycleCoroutineScope, updateLocation: (Location) -> Unit ) { - // A92 - //updateLocation(location(11.709508, 48.338923 )) - //updateLocation(homeVogelhart) - if (routeModel.navState.route.isRouteValid()) { val points = routeModel.curRoute.waypoints if (points.isEmpty()) return 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 3432a40..ef87d91 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 @@ -30,9 +30,8 @@ class CategoriesScreen( private val carContext: CarContext, private val surfaceRenderer: SurfaceRenderer, private val navigationViewModel: NavigationViewModel, -) : Screen(carContext), CategoryObserverCallback { +) : Screen(carContext) { - private val categoryObserver = CategoryObserver(this) private var category = "" var categories: List = listOf( @@ -41,17 +40,9 @@ class CategoriesScreen( Category(id = CHARGING_STATION, name = carContext.getString(R.string.charging_station)) ) - private val backPressedCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - navigationViewModel.elements.value = emptyList() - invalidate() - } - } init { - navigationViewModel.elements.value = emptyList() - navigationViewModel.elements.observe(this, categoryObserver) - carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback) + } override fun onGetTemplate(): Template { @@ -61,10 +52,23 @@ class CategoriesScreen( itemListBuilder.addItem( Row.Builder() .setTitle(it.name) - .setImage(carIcon(carContext,it.id, -1)) + .setImage(carIcon(carContext, it.id, -1)) .setOnClickListener { category = it.id - navigationViewModel.getAmenities(it.id, surfaceRenderer.lastLocation) + screenManager + .pushForResult( + CategoryScreen( + carContext, + surfaceRenderer, + category, + navigationViewModel, + ) + ) { obj: Any? -> + if (obj != null) { + setResult(obj) + finish() + } + } } .setBrowsable(true) .build() @@ -83,36 +87,6 @@ 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, category: String, index: Int): CarIcon { @@ -125,7 +99,12 @@ fun carIcon(context: CarContext, category: String, index: Int): CarIcon { } return CarIcon.Builder(IconCompat.createWithResource(context, resId)).build() } else { - return CarIcon.Builder(NavigationUtils(context).createNumberIcon(category, index.toString())).build() + return CarIcon.Builder( + NavigationUtils(context).createNumberIcon( + category, + index.toString() + ) + ).build() } } 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 674fc77..8276d28 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 @@ -5,6 +5,8 @@ import androidx.car.app.CarContext import androidx.car.app.Screen import androidx.car.app.constraints.ConstraintManager import androidx.car.app.model.Action +import androidx.car.app.model.Action.FLAG_DEFAULT +import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.ActionStrip import androidx.car.app.model.CarText import androidx.car.app.model.Header @@ -15,6 +17,8 @@ import androidx.car.app.model.Template import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.versioning.CarAppApiLevels +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.NavigationUtils @@ -37,11 +41,25 @@ class CategoryScreen( private val surfaceRenderer: SurfaceRenderer, private val category: String, private val navigationViewModel: NavigationViewModel, - private var elements: List, -) : Screen(carContext) { + + ) : Screen(carContext), CategoryObserverCallback { val maxListItems: Int = 30 + var elements: List = emptyList() + private val categoryObserver = CategoryObserver(this) + + private var loading = true + + init { + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + navigationViewModel.elements.value = emptyList() + } + }) + navigationViewModel.elements.observe(this, categoryObserver) + navigationViewModel.getAmenities(category, surfaceRenderer.lastLocation) + } override fun onGetTemplate(): Template { val listBuilder = ItemList.Builder() @@ -72,17 +90,22 @@ class CategoryScreen( .setTitle(getTitle(carContext, category)) .build() val builder = MapWithContentTemplate.Builder() - .setContentTemplate( - ListTemplate.Builder() - .setHeader(header) - .setSingleList(listBuilder.build()) - .build() - ) .setMapController( MapController.Builder().setMapActionStrip( getMapActionStrip() ).build() ) + + val content = ListTemplate.Builder() + .setHeader(header) + if (loading) { + content.setLoading(true) + } else { + content.setSingleList(listBuilder.build()) + } + .build() + builder.setContentTemplate(content.build()) + return builder.build() } @@ -122,28 +145,24 @@ class CategoryScreen( } else { row.addText(carText("${it.tags.openingHours}")) } - val navigationUtils = NavigationUtils(carContext) row.addAction( - Action.Builder() - .setOnClickListener { - navigationViewModel.loadRoute( - carContext, - currentLocation = surfaceRenderer.lastLocation, - location(it.lon, it.lat), - surfaceRenderer.carOrientation + createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT, { + navigationViewModel.loadRoute( + carContext, + currentLocation = surfaceRenderer.lastLocation, + location(it.lon, it.lat), + surfaceRenderer.carOrientation + ) + setResult( + Place( + name = name, + category = CHARGING_STATION, + latitude = it.lat, + longitude = it.lon ) - setResult( - Place( - name = name, - category = CHARGING_STATION, - latitude = it.lat, - longitude = it.lon - ) - ) - finish() - } - .setIcon(navigationUtils.createCarIcon(R.drawable.navigation_48px)) - .build()) + ) + finish() + })) return row.build() } @@ -179,10 +198,25 @@ class CategoryScreen( @DrawableRes iconRes: Int, scale: Int ): Action { - val navigationUtils = NavigationUtils(carContext) - return Action.Builder() - .setOnClickListener { surfaceRenderer.handleScale(scale) } - .setIcon(navigationUtils.createCarIcon(iconRes)) - .build() + return createAction(carContext, iconRes, FLAG_IS_PERSISTENT, { + surfaceRenderer.handleScale(scale) + }) + } + + 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) + this.elements = elements + loading = false + } + + override fun invalidateScreen() { + invalidate() } } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt new file mode 100644 index 0000000..6784384 --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/NavigationListener.kt @@ -0,0 +1,17 @@ +package com.kouros.navigation.car.screen + +import androidx.car.app.navigation.model.Trip + + + +/** A listener for navigation start and stop signals. */ +interface NavigationListener { + /** Stops navigation. */ + fun stopNavigation() + + /** Starts navigation. */ + fun startNavigation() + + /** Updates trip information. */ + fun updateTrip(trip: Trip) +} 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 95e23be..4080690 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 @@ -1,30 +1,33 @@ package com.kouros.navigation.car.screen -import android.content.pm.PackageManager import android.location.Location import android.location.LocationManager import android.os.CountDownTimer import android.os.Handler +import android.util.Log import androidx.car.app.CarContext import androidx.car.app.Screen -import androidx.car.app.ScreenManager import androidx.car.app.model.Action import androidx.car.app.model.Action.FLAG_DEFAULT +import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.ActionStrip import androidx.car.app.model.CarColor import androidx.car.app.model.CarIcon import androidx.car.app.model.Distance import androidx.car.app.model.Header -import androidx.car.app.model.MessageTemplate +import androidx.car.app.model.ItemList +import androidx.car.app.model.ListTemplate +import androidx.car.app.model.Row import androidx.car.app.model.Template import androidx.car.app.navigation.model.Destination -import androidx.car.app.navigation.model.Maneuver import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.navigation.model.MessageInfo import androidx.car.app.navigation.model.NavigationTemplate import androidx.car.app.navigation.model.RoutingInfo import androidx.car.app.navigation.model.Trip import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope @@ -35,11 +38,14 @@ import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.car.screen.observers.NavigationObserverCallback import com.kouros.navigation.car.screen.observers.NavigationObserverManager import com.kouros.navigation.car.screen.settings.SettingsScreen +import com.kouros.navigation.data.Constants import com.kouros.navigation.data.Constants.DESTINATION_ARRIVAL_DISTANCE +import com.kouros.navigation.data.Constants.ROUTE_UPDATE 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.RouteModel import com.kouros.navigation.utils.GeoUtils import com.kouros.navigation.utils.formattedDistance import com.kouros.navigation.utils.getSettingsRepository @@ -47,7 +53,6 @@ import com.kouros.navigation.utils.getSettingsViewModel import com.kouros.navigation.utils.location import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.time.Duration import java.time.LocalDateTime import java.time.ZoneOffset @@ -57,154 +62,98 @@ import kotlin.math.absoluteValue * Main screen for car navigation. * Handles different navigation states and provides corresponding templates. */ -class NavigationScreen( +open class NavigationScreen( carContext: CarContext, private var surfaceRenderer: SurfaceRenderer, private var routeModel: RouteCarModel, - private var listener: Listener, + private var listener: NavigationListener, private val navigationViewModel: NavigationViewModel ) : Screen(carContext), NavigationObserverCallback { - /** A listener for navigation start and stop signals. */ - interface Listener { - /** Stops navigation. */ - fun stopNavigation() + val backGroundColor = CarColor.GREEN - /** Starts navigation. */ - fun startNavigation() - - /** Updates trip information. */ - fun updateTrip(trip: Trip) - } - - val backGroundColor = CarColor.BLUE var currentNavigationLocation = Location(LocationManager.GPS_PROVIDER) - var recentPlace = Place() + + var recentPlaces = emptyList() + + var recentPlace: Place = Place() var navigationType = NavigationType.VIEW - var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0) + var lastTrafficDate: LocalDateTime = LocalDateTime.MIN + + var lastRouteDate: LocalDateTime = LocalDateTime.MIN var lastCameraSearch = 0 var speedCameras = listOf() val observerManager = NavigationObserverManager(navigationViewModel, this) + val repository = getSettingsRepository(carContext) + val settingsViewModel = getSettingsViewModel(carContext) - var distanceMode = 0 + private var distanceMode = 0 - init { - observerManager.attachAllObservers(this) - lifecycleScope.launch { - settingsViewModel.routingEngine.first() - settingsViewModel.recentPlaces.first() - } - repository.distanceModeFlow.asLiveData().observe(this, Observer { - distanceMode = it - }) - } + private var tripSuggestion = false - /** - * Handles the received route string. - * Starts navigation and invalidates the screen. - */ - override fun onRouteReceived(route: String) { - if (route.isNotEmpty()) { - prepareRoute(route) + private var tripSuggestionCalled = false + + private var routingEngine = 0 + + private var showTraffic = false; + private var arrivalTimer: CountDownTimer? = null + private var reRouteTimer: CountDownTimer? = null + + val observerRecentPlaces = Observer> { newPlaces -> + recentPlaces = newPlaces + if (newPlaces.isNotEmpty() && !tripSuggestionCalled) { + tripSuggestionCalled = true + navigationType = NavigationType.RECENT invalidate() } } - /** - * Prepare route and start navigation - */ - private fun prepareRoute(route: String) { - val routingEngine = runBlocking { repository.routingEngineFlow.first() } - routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) - navigationType = NavigationType.NAVIGATION - routeModel.startNavigation(route) - if (routeModel.hasLegs()) { - settingsViewModel.onLastRouteChanged(route) + init { + observerManager.attachAllObservers(this) + lifecycleScope.launch { + settingsViewModel.tripSuggestion.first() + settingsViewModel.routingEngine.first() } - surfaceRenderer.setRouteData() - listener.startNavigation() - } + repository.distanceModeFlow.asLiveData().observe(this, Observer { + distanceMode = it + }) - /** - * Checks if navigation is currently active. - */ - override fun isNavigating(): Boolean = routeModel.isNavigating() - - /** - * Handles the received recent place. - * Updates the navigation type to RECENT and invalidates the screen. - */ - override fun onRecentPlaceReceived(place: Place) { - recentPlace = place - navigationType = NavigationType.RECENT - invalidate() - } - - /** - * Handles received traffic data and updates the surface renderer. - */ - override fun onTrafficReceived(traffic: Map) { - surfaceRenderer.setTrafficData(traffic) - } - - /** - * Handles the received place search result. - * Navigates to the specified place. - */ - override fun onPlaceSearchResultReceived(place: Place) { - navigateToPlace(place) - } - - /** - * Handles received speed camera data. - * Updates the surface renderer with the camera locations. - */ - override fun onSpeedCamerasReceived(cameras: List) { - speedCameras = cameras - val coordinates = mutableListOf>() - cameras.forEach { - coordinates.add(listOf(it.lon, it.lat)) - } - val speedData = GeoUtils.createPointCollection(coordinates, "radar") - surfaceRenderer.speedCamerasData.value = speedData - } - - /** - * Handles received maximum speed data and updates the surface renderer. - */ - override fun onMaxSpeedReceived(speed: Int) { - surfaceRenderer.maxSpeed.value = speed - } - - /** - * Handles the received previe route string. - * Starts navigation and invalidates the screen. - */ - override fun onPreviewRouteReceived(route: String) { - if (navigationType == NavigationType.NAVIGATION && route.isNotEmpty()) { - startPreviewScreen(route) - } - } - - /** - * Invalidates the screen. - */ - override fun invalidateScreen() { - invalidate() + repository.trafficFlow.asLiveData().observe(this, Observer { + showTraffic = it + }) + repository.tripSuggestionFlow.asLiveData().observe(this, Observer { + navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) + tripSuggestion = it + }) + repository.routingEngineFlow.asLiveData().observe(this, Observer { + routingEngine = it + }) + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + arrivalTimer?.cancel() + reRouteTimer?.cancel() + } + }) } /** * Returns the appropriate template based on the current navigation state. */ override fun onGetTemplate(): Template { - - val actionStripBuilder = createActionStripBuilder() + val actionStripBuilder = createActionStripBuilder({ + createAction( + carContext, + R.drawable.search_48px, + FLAG_IS_PERSISTENT, + onClickAction = { startSearchScreen()} + ) + }, { settingsAction() }) return when (navigationType) { NavigationType.NAVIGATION -> navigationTemplate(actionStripBuilder) - NavigationType.RECENT -> navigationRecentPlaceTemplate() + NavigationType.RECENT -> navigationRecentPlacesTemplate() NavigationType.REROUTE -> navigationRerouteTemplate(actionStripBuilder) NavigationType.ARRIVAL -> navigationEndTemplate(actionStripBuilder) else -> navigationViewTemplate(actionStripBuilder) @@ -214,9 +163,9 @@ class NavigationScreen( /** * Creates and returns a NavigationTemplate for the active navigation state. */ - private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): NavigationTemplate { + private fun navigationTemplate(actionStripBuilder: ActionStrip.Builder): Template { actionStripBuilder.addAction( - stopAction() + createAction(carContext, R.drawable.ic_close_white_24dp, FLAG_IS_PERSISTENT,{ stopNavigation() }) ) updateTrip() return NavigationTemplate.Builder() @@ -225,7 +174,20 @@ class NavigationScreen( ) .setDestinationTravelEstimate(routeModel.travelEstimate(carContext, distanceMode)) .setActionStrip(actionStripBuilder.build()) - .setMapActionStrip(mapActionStripBuilder().build()) + .setMapActionStrip( + mapActionStrip( + surfaceRenderer.viewStyle, + { zoomPlus() }, { zoomMinus() }, { + createAction( + carContext = carContext, R.drawable.ic_zoom_out_24, + FLAG_IS_PERSISTENT, + onClickAction = { + surfaceRenderer.viewStyle = ViewStyle.VIEW + invalidate() + } + ) + }) + ) .setBackgroundColor(backGroundColor) .build() } @@ -235,38 +197,41 @@ class NavigationScreen( */ private fun navigationViewTemplate(actionStripBuilder: ActionStrip.Builder): Template { return NavigationTemplate.Builder() - .setBackgroundColor(CarColor.SECONDARY) + .setBackgroundColor(backGroundColor) .setActionStrip(actionStripBuilder.build()) - .setMapActionStrip(mapActionStripBuilder().build()) + .setMapActionStrip( + mapActionStrip( + surfaceRenderer.viewStyle, + { zoomPlus() }, { zoomMinus() }, { + createAction( + carContext = carContext, R.drawable.ic_zoom_out_24, + onClickAction = { + surfaceRenderer.viewStyle = ViewStyle.VIEW + invalidate() + }) + }) + ) .build() } /** - * Creates and returns a template for the arrival or end state of navigation. + * Creates and returns a template for the arrival. */ private fun navigationEndTemplate(actionStripBuilder: ActionStrip.Builder): Template { - if (routeModel.navState.arrived) { - val timer = object : CountDownTimer(8000, 1000) { - override fun onTick(millisUntilFinished: Long) {} - override fun onFinish() { - routeModel.navState = routeModel.navState.copy(arrived = false) - navigationType = NavigationType.VIEW - invalidate() - } + arrivalTimer?.cancel() + arrivalTimer = object : CountDownTimer(8000, 1000) { + override fun onTick(millisUntilFinished: Long) {} + override fun onFinish() { + routeModel.navState = routeModel.navState.copy(arrived = false) + navigationType = NavigationType.VIEW + invalidate() } - timer.start() - return navigationArrivedTemplate(actionStripBuilder) - } else { - return NavigationTemplate.Builder() - .setBackgroundColor(CarColor.SECONDARY) - .setActionStrip(actionStripBuilder.build()) - .setMapActionStrip(mapActionStripBuilder().build()) - .build() } + arrivalTimer?.start() + return navigationArrivedTemplate(actionStripBuilder) } - /** * Creates and returns a NavigationTemplate specifically for when the destination is reached. */ @@ -292,36 +257,82 @@ class NavigationScreen( ) .build() ) - .setBackgroundColor(CarColor.SECONDARY) + .setBackgroundColor(backGroundColor) .setActionStrip(actionStripBuilder.build()) - .setMapActionStrip(mapActionStripBuilder().build()) + .setMapActionStrip( + mapActionStrip( + surfaceRenderer.viewStyle, + { zoomPlus() }, { zoomMinus() }, { + createAction( + carContext = carContext, R.drawable.ic_zoom_out_24, + onClickAction = { + surfaceRenderer.viewStyle = ViewStyle.VIEW + invalidate() + }) + }) + ) .build() } /** * Creates and returns a template showing recent places or destinations. */ - fun navigationRecentPlaceTemplate(): Template { - val messageTemplate = MessageTemplate.Builder( - recentPlace.name + "\n" - + recentPlace.city - ) - .setHeader( - Header.Builder() - .setTitle(carContext.getString(R.string.drive_now)) - .build() + fun navigationRecentPlacesTemplate(): Template { + if (!tripSuggestion || recentPlaces.isEmpty()) { + navigationType = NavigationType.VIEW + return navigationViewTemplate( + createActionStripBuilder( + { + createAction( + carContext, + R.drawable.search_48px, + FLAG_IS_PERSISTENT, + { startSearchScreen() }) + }, + { settingsAction() }) ) - .addAction(navigateAction()) - .addAction(closeAction()) - .build() + } + val listBuilder = ItemList.Builder() + recentPlaces.filter { it.category == Constants.RECENT }.forEach { + val row = Row.Builder() + .setTitle(it.name!!) + .addAction( + createNavigateAction(it) + ) + .setOnClickListener { + navigateToPlace(it) + } + listBuilder.addItem( + row.build() + ) + } + val contentTemplate = + ListTemplate.Builder() + .setHeader( + Header.Builder() + .setTitle(carContext.getString(R.string.drive_now)) + .addEndHeaderAction(closeAction()) + .build() + ) + .setSingleList(listBuilder.build()) + .build() val builder = MapWithContentTemplate.Builder() - .setContentTemplate(messageTemplate) + .setContentTemplate(contentTemplate) .setActionStrip( - mapActionStripBuilder() - .addAction(settingsAction()) - .addAction(searchAction()) - .build() + mapActionStrip( + ViewStyle.VIEW, + { settingsAction() }, + { createAction(carContext, R.drawable.search_48px, FLAG_IS_PERSISTENT,{ startSearchScreen() }) }, + { + createAction( + carContext = carContext, R.drawable.ic_zoom_out_24, + FLAG_IS_PERSISTENT, + onClickAction = { + surfaceRenderer.viewStyle = ViewStyle.VIEW + invalidate() + }) + }) ) return builder.build() } @@ -355,208 +366,98 @@ class NavigationScreen( return routingInfo.build() } - /** - * Creates an ActionStrip builder with common search and settings actions. - */ - private fun createActionStripBuilder(): ActionStrip.Builder { - val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() - actionStripBuilder.addAction( - searchAction() - ) - actionStripBuilder.addAction( - settingsAction() - ) - return actionStripBuilder - } - - /** - * Creates an ActionStrip builder for map-related actions like zoom and pan. - */ - private fun mapActionStripBuilder(): ActionStrip.Builder { - val actionStripBuilder = ActionStrip.Builder() - .addAction(zoomPlus()) - .addAction(zoomMinus()) - if (surfaceRenderer.viewStyle == ViewStyle.PAN_VIEW) { - actionStripBuilder - .addAction( - panAction() - ) - } - return actionStripBuilder - } - - /** - * Creates a stop navigation action. - */ - private fun stopAction(): Action { - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_close_white_24dp - ) - ) - .build() - ) - .setOnClickListener { - stopNavigation() - } - .build() - } /** * Creates an action to start navigation to a specific place. */ - private fun navigateAction(): Action { - navigationType = NavigationType.NAVIGATION - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.navigation_48px - ) - ) - .build() - ) - .setOnClickListener { - val navigateTo = location(recentPlace.longitude, recentPlace.latitude) - navigationViewModel.loadPreviewRoute( - carContext, - surfaceRenderer.lastLocation, - navigateTo, - surfaceRenderer.carOrientation - ) -// val navigateTo = location(recentPlace.longitude, recentPlace.latitude) -// navigationViewModel.loadRoute( -// carContext, -// surfaceRenderer.lastLocation, -// navigateTo, -// surfaceRenderer.carOrientation -// ) -// routeModel.navState = routeModel.navState.copy(destination = recentPlace) + private fun createNavigateAction(place: Place): Action { + recentPlace = place + return createAction( + carContext, R.drawable.chevron_right_24px, + onClickAction = { + screenManager + .pushForResult( + RoutePreviewScreen( + carContext, + RoutePreviewType.SINGLE_ROUTE, + surfaceRenderer, + place, + navigationViewModel, + ) + ) { obj: Any? -> + if (obj != null) { + navigateToPlace(place) + } + } } - .build() + ) } /** * Creates an action to close the current view or template. */ private fun closeAction(): Action { - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_close_white_24dp - ) - ) - .build() - ) - .setOnClickListener { + return createAction( + carContext, R.drawable.ic_close_white_24dp, + onClickAction = { navigationType = NavigationType.VIEW invalidate() } - .setFlags(FLAG_DEFAULT) - .build() - } - - /** - * Creates an action to start the search screen. - */ - private fun searchAction(): Action { - return Action.Builder() - .setIcon(routeModel.createCarIcon(carContext, R.drawable.search_48px)) - .setOnClickListener { - startSearchScreen() - } - .build() + ) } /** * Creates an action to start the settings screen. */ private fun settingsAction(): Action { - return Action.Builder() - .setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px)) - .setOnClickListener { + return createAction( + carContext, R.drawable.settings_48px, + FLAG_IS_PERSISTENT, + onClickAction = { screenManager.push(SettingsScreen(carContext, navigationViewModel)) } - .build() + ) } /** * Creates an action to zoom in on the map. */ private fun zoomPlus(): Action { - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_zoom_in_24 - ) - ) - .build() - ).setOnClickListener { + return createAction( + carContext, R.drawable.ic_zoom_in_24, + FLAG_IS_PERSISTENT, + onClickAction = { surfaceRenderer.handleScale(1) invalidate() } - .build() + ) } /** * Creates an action to zoom out on the map. */ private fun zoomMinus(): Action { - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_zoom_out_24 - ) - ) - .build() - ).setOnClickListener { + return createAction( + carContext, R.drawable.ic_zoom_out_24, + onClickAction = { surfaceRenderer.handleScale(-1) invalidate() } - .build() + ) } - /** - * Creates an action to enable map panning. - */ - private fun panAction(): Action { - return Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.ic_pan_24 - ) - ) - .build() - ).setOnClickListener { - surfaceRenderer.viewStyle = ViewStyle.VIEW - invalidate() - } - .build() - } /** * Pushes the search screen and handles the search result. */ private fun startSearchScreen() { - screenManager .pushForResult( SearchScreen( carContext, surfaceRenderer, - navigationViewModel + navigationViewModel, + recentPlaces ) ) { obj: Any? -> if (obj != null) { @@ -574,44 +475,12 @@ class NavigationScreen( } } - /** - * Pushes the search screen and handles the search result. - */ - private fun startPreviewScreen(route: String) { - val repository = getSettingsRepository(carContext) - val routingEngine = runBlocking { repository.routingEngineFlow.first() } - routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) - routeModel.startNavigation(route) - surfaceRenderer.setPreviewRouteData(routeModel) - screenManager - .pushForResult( - RoutePreviewScreen( - carContext, - RoutePreviewType.SINGLE_ROUTE, - surfaceRenderer, - recentPlace, - navigationViewModel, - routeModel = routeModel - ) - ) { obj: Any? -> - if (obj != null) { - navigateToPlace(recentPlace) - } else { - routeModel.stopNavigation() - navigationType = NavigationType.VIEW - surfaceRenderer.clearRouteData() - invalidate() - } - } - } - /** * Loads a route to the specified place and sets it as the destination. */ fun navigateToPlace(place: Place) { val preview = navigationViewModel.previewRoute.value navigationViewModel.previewRoute.value = "" - navigationType = NavigationType.NAVIGATION val location = location(place.longitude, place.latitude) navigationViewModel.saveRecent(carContext, place) currentNavigationLocation = location @@ -627,6 +496,7 @@ class NavigationScreen( navigationViewModel.route.value = preview } routeModel.navState = routeModel.navState.copy(destination = place) + surfaceRenderer.viewStyle = ViewStyle.VIEW invalidate() } @@ -650,25 +520,27 @@ class NavigationScreen( invalidate() val mainThreadHandler = Handler(carContext.mainLooper) mainThreadHandler.post { - object : CountDownTimer(2000, 1000) { + reRouteTimer?.cancel() + reRouteTimer = object : CountDownTimer(2000, 1000) { override fun onTick(millisUntilFinished: Long) {} override fun onFinish() { navigationType = NavigationType.NAVIGATION reRoute(destination) } - }.start() + } + reRouteTimer?.start() } } /** - * Re-requests a route for the specified destination. + * Re-requests a route for the specified place. */ - fun reRoute(destination: Place) { - val dest = location(destination.longitude, destination.latitude) + fun reRoute(place: Place) { + val destination = location(place.longitude, place.latitude) navigationViewModel.loadRoute( carContext, surfaceRenderer.lastLocation, - dest, + destination, surfaceRenderer.carOrientation ) } @@ -678,32 +550,58 @@ class NavigationScreen( */ fun updateTrip(location: Location) { val current = LocalDateTime.now(ZoneOffset.UTC) + //checkRoute(current, location) + checkTraffic(current, location) + + updateSpeedCamera(location) + + routeModel.updateLocation(location, navigationViewModel) + checkArrival() + + invalidate() + } + + /** + * Checks if a new route is needed based on the time since the last update. + */ + private fun checkRoute(current: LocalDateTime, location: Location) { + val duration = Duration.between(current, lastRouteDate) + if (duration.abs().seconds > ROUTE_UPDATE) { + lastRouteDate = current + val destination = location( + routeModel.navState.destination.longitude, + routeModel.navState.destination.latitude + ) + navigationViewModel.loadRoute( + carContext, + location, + destination, + surfaceRenderer.carOrientation + ) + } + } + + /** + * Checks if traffic data needs to be updated based on the time since the last update. + */ + fun checkTraffic(current: LocalDateTime, location: Location) { val duration = Duration.between(current, lastTrafficDate) - if (duration.abs().seconds > TRAFFIC_UPDATE) { + if (showTraffic && duration.abs().seconds > TRAFFIC_UPDATE) { lastTrafficDate = current navigationViewModel.loadTraffic(carContext, location, surfaceRenderer.carOrientation) } - updateSpeedCamera(location) - with(routeModel) { - updateLocation(location, navigationViewModel) - checkArrival() - } - invalidate() } /** * Checks for arrival */ - private fun RouteCarModel.checkArrival() { - if ((navState.maneuverType == Maneuver.TYPE_DESTINATION - || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT - || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT - || navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT) - && routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE + fun checkArrival() { + if (routeModel.isArrival() + && routeModel.routeCalculator.leftStepDistance() < DESTINATION_ARRIVAL_DISTANCE ) { listener.stopNavigation() settingsViewModel.onLastRouteChanged("") - navState = navState.copy(arrived = true) + routeModel.navState = routeModel.navState.copy(arrived = true) surfaceRenderer.routeData.value = "" navigationType = NavigationType.ARRIVAL invalidate() @@ -757,7 +655,7 @@ class NavigationScreen( updatedCameras.add(it) } val sortedList = updatedCameras.sortedWith(compareBy { it.distance }) - val camera = sortedList.first() + val camera = sortedList.firstOrNull() ?: return val bearingRoute = surfaceRenderer.lastLocation.bearingTo(location) val bearingSpeedCamera = if (camera.tags.direction != null) { try { @@ -776,29 +674,94 @@ class NavigationScreen( } /** - * Checks for a specific permission and updates the view model upon grant. + * Handles the received route string. + * Starts navigation and invalidates the screen. */ - fun checkPermission(permission: String) { - if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { - val permissions: MutableList = ArrayList() - permissions.add(permission) - val screenManager = - carContext.getCarService(ScreenManager::class.java) - screenManager - .push( - RequestPermissionScreen( - carContext, - permissionCheckCallback = { - screenManager.pop() - navigationViewModel.permissionGranted.value = true - }, - permissions - ) - ) - } else { - navigationViewModel.permissionGranted.value = true + override fun onRouteReceived(route: String) { + if (route.isNotEmpty()) { + if (routeModel.isNavigating()) { + updateRoute(route) + } else { + prepareRoute(route) + } + invalidate() } } + + /** + * Prepare route and start navigation + */ + private fun prepareRoute(route: String) { + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + navigationType = NavigationType.NAVIGATION + routeModel.startNavigation(route) + if (routeModel.hasLegs()) { + settingsViewModel.onLastRouteChanged(route) + } + surfaceRenderer.setRouteData() + listener.startNavigation() + } + + /** + * Update route and traffic data + */ + private fun updateRoute(route: String) { + val routeModel = RouteModel() + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + navigationType = NavigationType.NAVIGATION + routeModel.startNavigation(route) + } + + /** + * Checks if navigation is currently active. + */ + override fun isNavigating(): Boolean = routeModel.isNavigating() + + /** + * Handles received traffic data and updates the surface renderer. + */ + override fun onTrafficReceived(traffic: Map) { + if (traffic.isNotEmpty()) { + surfaceRenderer.setTrafficData(traffic) + } + } + + /** + * Handles the received place search result. + * Navigates to the specified place. + */ + override fun onPlaceSearchResultReceived(place: Place) { + navigateToPlace(place) + } + + /** + * Handles received speed camera data. + * Updates the surface renderer with the camera locations. + */ + override fun onSpeedCamerasReceived(cameras: List) { + speedCameras = cameras + val coordinates = mutableListOf>() + cameras.forEach { + coordinates.add(listOf(it.lon, it.lat)) + } + val speedData = GeoUtils.createPointCollection(coordinates, "radar") + surfaceRenderer.speedCamerasData.value = speedData + } + + /** + * Handles received maximum speed data and updates the surface renderer. + */ + override fun onMaxSpeedReceived(speed: Int) { + surfaceRenderer.maxSpeed.value = speed + } + + /** + * Invalidates the screen. + */ + override fun invalidateScreen() { + invalidate() + } + } /** diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt index 28eafbb..5dbf045 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/PlaceListScreen.kt @@ -3,6 +3,7 @@ package com.kouros.navigation.car.screen import android.net.Uri import android.text.Spannable import android.text.SpannableString +import android.util.Log import androidx.car.app.CarContext import androidx.car.app.CarToast import androidx.car.app.Screen @@ -16,7 +17,10 @@ import androidx.car.app.model.ListTemplate import androidx.car.app.model.Row import androidx.car.app.model.Template import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer +import androidx.lifecycle.asLiveData import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer import com.kouros.navigation.car.navigation.RouteCarModel @@ -26,17 +30,13 @@ import com.kouros.navigation.data.Constants.RECENT import com.kouros.navigation.data.Place import com.kouros.navigation.model.NavigationViewModel import com.kouros.navigation.utils.getSettingsRepository -import com.kouros.navigation.utils.location -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.runBlocking - class PlaceListScreen( private val carContext: CarContext, private val surfaceRenderer: SurfaceRenderer, private val category: String, private val navigationViewModel: NavigationViewModel, - private val places: List + private val recentPlaces: List, ) : Screen(carContext) { val routeModel = RouteCarModel() @@ -45,45 +45,28 @@ class PlaceListScreen( var mPlaces = mutableListOf() - val previewObserver = Observer { route -> - if (route.isNotEmpty()) { - val repository = getSettingsRepository(carContext) - val routingEngine = runBlocking { repository.routingEngineFlow.first() } - routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) - routeModel.startNavigation(route) - surfaceRenderer.setPreviewRouteData(routeModel) - screenManager - .pushForResult( - RoutePreviewScreen( - carContext, - RoutePreviewType.MULTI_ROUTE, - surfaceRenderer, - place, - navigationViewModel, - routeModel = routeModel - ) - ) { obj: Any? -> - if (obj != null) { - setResult(obj) - finish() - } - } - } - } + val repository = getSettingsRepository(carContext) + + private var routingEngine = 0 init { - // loadPlaces() - navigationViewModel.recentPlaces.value = emptyList() - navigationViewModel.previewRoute.value = "" - - mPlaces.addAll(places) - navigationViewModel.previewRoute.observe(this, previewObserver) + repository.routingEngineFlow.asLiveData().observe(this, Observer { + routingEngine = it + }) + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + navigationViewModel.recentPlaces.value = emptyList() + } + }) } + /** + * Returns the appropriate template based on the current navigation state. + */ override fun onGetTemplate(): Template { val itemListBuilder = ItemList.Builder() .setNoItemsMessage(carContext.getString(R.string.no_places)) - mPlaces.forEach { + recentPlaces.filter { it.category == category }.forEach { val street = if (it.street != null) { it.street } else { @@ -104,13 +87,21 @@ class PlaceListScreen( it.street, // avatar = null ) - val location = location(place.longitude, place.latitude) - navigationViewModel.loadPreviewRoute( - carContext, - surfaceRenderer.lastLocation, - location, - surfaceRenderer.carOrientation - ) + screenManager + .pushForResult( + RoutePreviewScreen( + carContext, + RoutePreviewType.MULTI_ROUTE, + surfaceRenderer, + place, + navigationViewModel, + ) + ) { obj: Any? -> + if (obj != null) { + setResult(obj) + finish() + } + } } if (category != CONTACTS) { row.addText(SpannableString(" ").apply { @@ -148,14 +139,11 @@ class PlaceListScreen( .build() } - private fun deleteAction(place: Place): Action = Action.Builder() - .setIcon( - RouteCarModel().createCarIcon( - carContext, - R.drawable.ic_close_white_24dp - ) - ) - .setOnClickListener { + /** + * Creates an Action to delete a place. + */ + private fun deleteAction(place: Place): Action = + createAction(carContext, R.drawable.ic_close_white_24dp) { navigationViewModel.deletePlace(carContext, place) CarToast.makeText( carContext, @@ -164,7 +152,7 @@ class PlaceListScreen( mPlaces.remove(place) invalidate() } - .build() + fun contactIcon(avatar: Uri?, category: String?): CarIcon { if (category == RECENT || avatar == null) { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt index 41ba89b..dfc3e1c 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RequestPermissionScreen.kt @@ -1,9 +1,11 @@ package com.kouros.navigation.car.screen import android.Manifest.permission +import android.content.pm.PackageManager import androidx.car.app.CarContext import androidx.car.app.CarToast import androidx.car.app.Screen +import androidx.car.app.ScreenManager import androidx.car.app.model.Action import androidx.car.app.model.CarColor import androidx.car.app.model.MessageTemplate @@ -46,7 +48,6 @@ class RequestPermissionScreen( ).show() if (!approved!!.isEmpty()) { permissionCheckCallback.onPermissionGranted() - //mContactsPermissionCheckCallback.onPermissionGranted() finish() } } @@ -62,4 +63,31 @@ class RequestPermissionScreen( .addAction(action) .build() } + +} + +/** + * Checks for a specific permission and return the result of the check. + */ +fun checkPermission(carContext: CarContext, permission: String) : Boolean { + if (carContext.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { + val permissions: MutableList = ArrayList() + permissions.add(permission) + val screenManager = + carContext.getCarService(ScreenManager::class.java) + screenManager + .push( + RequestPermissionScreen( + carContext, + permissionCheckCallback = { + screenManager.pop() + return@RequestPermissionScreen + }, + permissions + ) + ) + } else { + return true + } + return false } diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt index 4fdf111..d3a8b3b 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/RoutePreviewScreen.kt @@ -3,6 +3,7 @@ package com.kouros.navigation.car.screen import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned +import android.util.Log import androidx.activity.OnBackPressedCallback import androidx.annotation.DrawableRes import androidx.car.app.CarContext @@ -11,6 +12,7 @@ import androidx.car.app.Screen import androidx.car.app.constraints.ConstraintManager import androidx.car.app.model.Action import androidx.car.app.model.Action.FLAG_DEFAULT +import androidx.car.app.model.Action.FLAG_IS_PERSISTENT import androidx.car.app.model.ActionStrip import androidx.car.app.model.CarColor import androidx.car.app.model.CarIcon @@ -27,26 +29,34 @@ import androidx.car.app.navigation.model.MapController import androidx.car.app.navigation.model.MapWithContentTemplate import androidx.car.app.versioning.CarAppApiLevels import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import androidx.lifecycle.asLiveData +import androidx.lifecycle.lifecycleScope import com.kouros.data.R import com.kouros.navigation.car.SurfaceRenderer +import com.kouros.navigation.car.ViewStyle import com.kouros.navigation.car.navigation.NavigationUtils import com.kouros.navigation.car.navigation.RouteCarModel import com.kouros.navigation.data.Place import com.kouros.navigation.data.route.Routes import com.kouros.navigation.model.NavigationViewModel +import com.kouros.navigation.utils.getSettingsRepository +import com.kouros.navigation.utils.location +import kotlinx.coroutines.launch import java.math.BigDecimal import java.math.RoundingMode import java.time.Duration import kotlin.math.min -/** Creates a screen using the new [androidx.car.app.navigation.model.MapWithContentTemplate] */ +/** Creates a screen using the new [MapWithContentTemplate] */ class RoutePreviewScreen( carContext: CarContext, private var routeType: RoutePreviewType, private var surfaceRenderer: SurfaceRenderer, private var destination: Place, private val navigationViewModel: NavigationViewModel, - private val routeModel: RouteCarModel ) : Screen(carContext) { private var isFavorite = false @@ -54,19 +64,68 @@ class RoutePreviewScreen( val maxListItems: Int = 3 val navigationUtils = NavigationUtils(carContext) + val repository = getSettingsRepository(carContext) + + var routingEngine = -1 + var routeSelected = false + + val routeModel = RouteCarModel() + + var loading = true + private val backPressedCallback = object : OnBackPressedCallback(false) { override fun handleOnBackPressed() { invalidate() } } + val observer = Observer { route -> + if (route.isNotEmpty() && routingEngine != -1) { + routeModel.navState = routeModel.navState.copy(routingEngine = routingEngine) + routeModel.startNavigation(route) + surfaceRenderer.setPreviewRouteData(routeModel) + loading = false + invalidate() + } + } + val trafficObserver = Observer> { traffic -> + if (traffic.isNotEmpty()) { + navigationViewModel.traffic.value = emptyMap() + surfaceRenderer.setTrafficData(traffic) + } + } + init { + navigationViewModel.previewRoute.observe(this, observer) carContext.onBackPressedDispatcher.addCallback(this, backPressedCallback) + navigationViewModel.traffic.observe(this, trafficObserver) + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStop(owner: LifecycleOwner) { + navigationViewModel.previewRoute.value = "" + } + }) + repository.trafficFlow.asLiveData().observe(this, Observer { + if (it) { + getTraffic() + } + }) + repository.routingEngineFlow.asLiveData().observe(this, Observer { + routingEngine = it + + }) + lifecycleScope.launch { + navigationViewModel.loadPreviewRoute( + carContext, + surfaceRenderer.lastLocation, + location(destination.longitude, destination.latitude), + surfaceRenderer.carOrientation + ) + + } } override fun onGetTemplate(): Template { - val itemListBuilder = ItemList.Builder() if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) { val listLimit = min( @@ -78,7 +137,9 @@ class RoutePreviewScreen( ) var index = 0 routeModel.route.routes.forEach { route -> - itemListBuilder.addItem(createRow(route, index++)) + if (index < listLimit) { + itemListBuilder.addItem(createRow(route, index++)) + } } } @@ -106,12 +167,16 @@ class RoutePreviewScreen( CarText.Builder("Wait") .build() } - val content = if (routeType == RoutePreviewType.MULTI_ROUTE) { - ListTemplate.Builder() + val listContent = ListTemplate.Builder() .setHeader(header.build()) - .setSingleList(itemListBuilder.build()) + if (loading) { + listContent.setLoading(true) + } else { + listContent.setSingleList(itemListBuilder.build()) + } .build() + listContent.build() } else { val navigateActionIcon: CarIcon = CarIcon.Builder( IconCompat.createWithResource( @@ -123,75 +188,78 @@ class RoutePreviewScreen( carContext, R.drawable.alt_route_48px ) ).build() - val navigateAction = Action.Builder() - .setFlags(FLAG_DEFAULT) - .setIcon(navigateActionIcon) - .setOnClickListener { onNavigate(routeModel.navState.currentRouteIndex) } - .build() - val selectRouteAction = Action.Builder() - .setIcon(selectRouteIcon) - .setOnClickListener { - routeType = RoutePreviewType.MULTI_ROUTE - invalidate() - } - .build() - MessageTemplate.Builder( - message - ) + val navigateAction = + createAction(carContext, R.drawable.navigation_48px, FLAG_DEFAULT,{ + onNavigate(routeModel.navState.currentRouteIndex) + }) + val selectRouteAction = createAction(carContext, R.drawable.alt_route_48px, FLAG_IS_PERSISTENT, { + routeType = RoutePreviewType.MULTI_ROUTE + invalidate() + }) + val listContent = MessageTemplate.Builder(message) .setHeader(header.build()) .addAction(navigateAction) .addAction(selectRouteAction) - .setLoading(message.toString() == "Wait") - .build() + if (loading) { + listContent.setLoading(true) + } + listContent.build() } - - val template = MapWithContentTemplate.Builder() .setContentTemplate(content) .setMapController( MapController.Builder().setMapActionStrip( - getMapActionStrip() - ).build() + mapActionStrip(ViewStyle.PREVIEW, {zoomPlus()}, { zoomMinus()}, { + zoomMinus() + } )).build() + ) if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) { - template.setActionStrip(createActionStripBuilder().build()) + template.setActionStrip(createActionStrip { + createAction( + carContext, R.drawable.navigation_48px, + onClickAction = { + onNavigate(routeModel.navState.currentRouteIndex) + } + ) + }) } return template.build() } - private fun createActionStripBuilder(): ActionStrip.Builder { - val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() - actionStripBuilder.addAction( - navigateAction() - ) - return actionStripBuilder - } - - private fun navigateAction(): Action { - return Action.Builder() - .setIcon(routeModel.createCarIcon(carContext, R.drawable.navigation_48px)) - .setFlags(FLAG_DEFAULT) - .setOnClickListener { - onNavigate(routeModel.navState.currentRouteIndex) + private fun zoomPlus(): Action { + return createAction( + carContext, R.drawable.ic_zoom_in_24, + FLAG_IS_PERSISTENT, + onClickAction = { + surfaceRenderer.handleScale(1) + invalidate() } - .build() + ) } - private fun favoriteAction(): Action = Action.Builder() - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - if (isFavorite) - R.drawable.ic_favorite_filled_white_24dp - else - R.drawable.ic_favorite_white_24dp - ) - ) - .build() + /** + * Creates an action to zoom out on the map. + */ + private fun zoomMinus(): Action { + return createAction( + carContext, R.drawable.ic_zoom_out_24, + onClickAction = { + surfaceRenderer.handleScale(-1) + invalidate() + } ) - .setOnClickListener { + } + + private fun favoriteAction(): Action = + createAction( + carContext, if (isFavorite) + R.drawable.ic_favorite_filled_white_24dp + else + R.drawable.ic_favorite_white_24dp + , FLAG_IS_PERSISTENT, + ) { isFavorite = !isFavorite CarToast.makeText( carContext, @@ -208,26 +276,17 @@ class RoutePreviewScreen( navigationViewModel.saveFavorite(carContext, destination) invalidate() } - .build() - private fun deleteFavoriteAction(): Action = Action.Builder() - .setOnClickListener { + + private fun deleteFavoriteAction(): Action = + createAction(carContext, R.drawable.heart_minus_48px, FLAG_IS_PERSISTENT,{ if (isFavorite) { navigationViewModel.deleteFavorite(carContext, destination) } isFavorite = !isFavorite finish() - } - .setIcon( - CarIcon.Builder( - IconCompat.createWithResource( - carContext, - R.drawable.heart_minus_48px - ) - ) - .build() - ) - .build() + }) + private fun createRouteText(route: Routes): CarText { val time = route.summary.duration @@ -245,21 +304,13 @@ class RoutePreviewScreen( } private fun createRow(route: Routes, index: Int): Row { - val navigateActionIcon: CarIcon = CarIcon.Builder( - IconCompat.createWithResource( - carContext, R.drawable.navigation_48px - ) - ).build() - val navigateAction = Action.Builder() - .setFlags(FLAG_DEFAULT) - .setIcon(navigateActionIcon) - .setOnClickListener { this.onNavigate(index) } - .build() - + val navigateAction = createAction(carContext, R.drawable.navigation_48px ) { + this.onNavigate(index) + } val routeText = createRouteText(route) var street = "" var maxDistance = 0.0 - routeModel.route.routes[index].legs.first().steps.forEach { + routeModel.steps.forEach { if (it.distance > maxDistance) { maxDistance = it.distance street = it.street @@ -271,9 +322,9 @@ class RoutePreviewScreen( .setOnClickListener { onRouteSelected(index) } .addText(street) .addAction(navigateAction) - if (route.summary.trafficDelay > 60) { row.addText(createDelay(route)) + row.setImage(NavigationUtils(carContext).createCarIcon(R.drawable.traffic_jam_48px)) } return row.build() } @@ -294,6 +345,7 @@ class RoutePreviewScreen( ) return delayBuilder } + private fun onNavigate(index: Int) { destination.routeIndex = index setResult(destination) @@ -303,30 +355,19 @@ class RoutePreviewScreen( private fun onRouteSelected(index: Int) { routeModel.navState = routeModel.navState.copy(currentRouteIndex = index) surfaceRenderer.setPreviewRouteData(routeModel) + surfaceRenderer.updateTrafficData(routeModel) routeSelected = true invalidate() } - fun getMapActionStrip(): ActionStrip { - return ActionStrip.Builder() - .addAction( - createToastAction(R.drawable.ic_zoom_in_24) - ) - .addAction( - createToastAction(R.drawable.ic_zoom_out_24) - ) - .addAction(Action.PAN) - .build() + private fun getTraffic() { + navigationViewModel.loadTraffic( + carContext, + surfaceRenderer.lastLocation, + surfaceRenderer.carOrientation + ) } - private fun createToastAction( - @DrawableRes iconRes: Int - ): Action { - return Action.Builder() - .setOnClickListener { surfaceRenderer.handleScale(-1) } - .setIcon(navigationUtils.createCarIcon(iconRes)) - .build() - } } enum class RoutePreviewType { diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt new file mode 100644 index 0000000..96a2a8c --- /dev/null +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/ScreenUtils.kt @@ -0,0 +1,62 @@ +package com.kouros.navigation.car.screen + +import androidx.annotation.DrawableRes +import androidx.car.app.CarContext +import androidx.car.app.model.Action +import androidx.car.app.model.Action.FLAG_DEFAULT +import androidx.car.app.model.ActionStrip +import androidx.car.app.model.CarIcon +import androidx.core.graphics.drawable.IconCompat +import com.kouros.navigation.car.ViewStyle + +fun createActionStrip(executeAction: () -> Action): ActionStrip { + val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() + actionStripBuilder.addAction( + executeAction() + ) + return actionStripBuilder.build() +} + + fun createActionStripBuilder(action1: () -> Action, action2: () -> Action): ActionStrip.Builder { + val actionStripBuilder: ActionStrip.Builder = ActionStrip.Builder() + actionStripBuilder.addAction( + action1() + ) + actionStripBuilder.addAction( + action2() + ) + return actionStripBuilder +} + +/** + * Creates an ActionStrip builder for map-related actions like zoom and pan. + */ +fun mapActionStrip(viewStyle: ViewStyle, zoomPlus: () -> Action, zoomMinus: () -> Action , panAction: () -> Action): ActionStrip { + val actionStripBuilder = ActionStrip.Builder() + .addAction(zoomPlus()) + .addAction(zoomMinus()) + if (viewStyle == ViewStyle.PAN_VIEW) { + actionStripBuilder + .addAction( + panAction() + ) + } + return actionStripBuilder.build() +} + +/** + * Creates an action to do something. + */ +fun createAction(carContext: CarContext, @DrawableRes iconRes: Int, flag: Int = FLAG_DEFAULT, onClickAction: () -> Unit): Action { + return Action.Builder() + .setIcon(createCarIcon(carContext, iconRes)) + .setFlags(flag) + .setOnClickListener { + onClickAction() + } + .build() +} + +fun createCarIcon(carContext: CarContext, @DrawableRes iconRes: Int): CarIcon { + return CarIcon.Builder(IconCompat.createWithResource(carContext, iconRes)).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 1df9fad..5f498c1 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 @@ -28,6 +28,7 @@ class SearchScreen( carContext: CarContext, private var surfaceRenderer: SurfaceRenderer, private val navigationViewModel: NavigationViewModel, + private val recentPlaces: List, ) : Screen(carContext) { var isSearchComplete: Boolean = false @@ -44,54 +45,16 @@ class SearchScreen( lateinit var searchResult: List val observer = Observer> { newSearch -> - searchResult = newSearch - invalidate() - } - - val observerRecentPlaces = Observer> { newPlaces -> - if (newPlaces.isNotEmpty()) { - screenManager - .pushForResult( - PlaceListScreen( - carContext, - surfaceRenderer, - RECENT, - navigationViewModel, - newPlaces - ) - ) { obj: Any? -> - surfaceRenderer.clearRouteData() - if (obj != null) { - setResult(obj) - finish() - } - } + if (newSearch.isNotEmpty()) { + navigationViewModel.searchPlaces.value = emptyList() + searchResult = newSearch + invalidate() } } - val observerFavorites = Observer> { newPlaces -> - screenManager - .pushForResult( - PlaceListScreen( - carContext, - surfaceRenderer, - FAVORITES, - navigationViewModel, - newPlaces - ) - ) { obj: Any? -> - if (obj != null) { - setResult(obj) - finish() - } - } - } - init { navigationViewModel.searchPlaces.observe(this, observer) - navigationViewModel.recentPlaces.observe(this, observerRecentPlaces) - navigationViewModel.favorites.observe(this, observerFavorites) } override fun onGetTemplate(): Template { @@ -124,20 +87,22 @@ class SearchScreen( } } } else { - if (it.id == RECENT) { - navigationViewModel.loadRecentPlaces( - carContext, - surfaceRenderer.lastLocation, - surfaceRenderer.carOrientation - ) - } - if (it.id == FAVORITES) { - navigationViewModel.loadFavorites( - carContext, - surfaceRenderer.lastLocation, - surfaceRenderer.carOrientation - ) - } + screenManager + .pushForResult( + PlaceListScreen( + carContext, + surfaceRenderer, + it.id, + navigationViewModel, + recentPlaces + ) + ) { obj: Any? -> + surfaceRenderer.viewStyle = ViewStyle.VIEW + if (obj != null) { + setResult(obj) + finish() + } + } } } .setBrowsable(true) diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverCallback.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverCallback.kt index 89d2947..0a6a1e4 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverCallback.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverCallback.kt @@ -10,10 +10,7 @@ import com.kouros.navigation.data.overpass.Elements interface NavigationObserverCallback { /** Called when a route is received and navigation should start */ fun onRouteReceived(route: String) - - /** Called when a recent place is selected but navigation hasn't started */ - fun onRecentPlaceReceived(place: Place) - + /** Check if currently navigating */ fun isNavigating(): Boolean @@ -29,9 +26,6 @@ interface NavigationObserverCallback { /** Called when max speed is updated */ fun onMaxSpeedReceived(speed: Int) - /** Called when a preview route is received and navigation should start */ - fun onPreviewRouteReceived(route: String) - /** Called to request UI invalidation/refresh */ fun invalidateScreen() diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt index f1535bc..fc6e81e 100644 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt +++ b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/NavigationObserverManager.kt @@ -12,12 +12,10 @@ class NavigationObserverManager( ) { val routeObserver = RouteObserver(callback) - val recentPlaceObserver = RecentPlaceObserver(callback) val trafficObserver = TrafficObserver(callback) val placeSearchObserver = PlaceSearchObserver(callback) val speedCameraObserver = SpeedCameraObserver(callback) val maxSpeedObserver = MaxSpeedObserver(callback) - val previewObserver = PreviewRouteObserver(callback) /** * Attaches all observers to the ViewModel. @@ -26,11 +24,9 @@ class NavigationObserverManager( fun attachAllObservers(screen: androidx.car.app.Screen) { viewModel.route.observe(screen, routeObserver) viewModel.traffic.observe(screen, trafficObserver) - viewModel.recentPlace.observe(screen, recentPlaceObserver) viewModel.placeLocation.observe(screen, placeSearchObserver) viewModel.speedCameras.observe(screen, speedCameraObserver) viewModel.maxSpeed.observe(screen, maxSpeedObserver) - viewModel.previewRoute.observe(screen, previewObserver) } /** diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/PreviewRouteObserver.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/PreviewRouteObserver.kt deleted file mode 100644 index 7a35841..0000000 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/PreviewRouteObserver.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.kouros.navigation.car.screen.observers - -import androidx.lifecycle.Observer - -class PreviewRouteObserver( - private val callback: NavigationObserverCallback -) : Observer { - - override fun onChanged(value: String) { - if (value.isNotEmpty()) { - callback.onPreviewRouteReceived(value) - } - } -} \ No newline at end of file diff --git a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/RecentPlaceObserver.kt b/common/car/src/main/java/com/kouros/navigation/car/screen/observers/RecentPlaceObserver.kt deleted file mode 100644 index ef92504..0000000 --- a/common/car/src/main/java/com/kouros/navigation/car/screen/observers/RecentPlaceObserver.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kouros.navigation.car.screen.observers - -import androidx.lifecycle.Observer -import com.kouros.navigation.data.Place - -/** - * Observer for recent place updates. Updates the recent place when navigation is not active. - */ -class RecentPlaceObserver( - private val callback: NavigationObserverCallback -) : Observer { - - override fun onChanged(value: Place) { - if (!callback.isNavigating()) { - callback.onRecentPlaceReceived(value) - } - } -} 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 82911dd..76758af 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 @@ -23,6 +23,8 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { private var showTraffic = false + private var tripSuggestion = false + val settingsViewModel = getSettingsViewModel(carContext) override fun onGetTemplate(): Template { @@ -34,7 +36,12 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { buildingToggleState = it invalidate() } + settingsViewModel.tripSuggestion.asLiveData().observe(this) { + tripSuggestion = it + invalidate() + } val listBuilder = ItemList.Builder() + val buildingToggle: Toggle = Toggle.Builder { checked: Boolean -> settingsViewModel.onShow3DChanged(checked) @@ -49,6 +56,13 @@ class DisplaySettings(private val carContext: CarContext) : Screen(carContext) { }.setChecked(showTraffic).build() listBuilder.addItem(buildRowForTemplate(R.string.traffic, trafficToggle)) + val tripSuggestionToggle: Toggle = + Toggle.Builder { checked: Boolean -> + settingsViewModel.onTripSuggestion(checked) + tripSuggestion = !tripSuggestion + }.setChecked(tripSuggestion).build() + listBuilder.addItem(buildRowForTemplate(R.string.trip_suggestion, tripSuggestionToggle)) + listBuilder.addItem( buildRowForScreenTemplate( DarkModeSettings(carContext), diff --git a/common/car/src/test/java/com/kouros/navigation/car/screen/NavigationScreenTest.kt b/common/car/src/test/java/com/kouros/navigation/car/screen/NavigationScreenTest.kt new file mode 100644 index 0000000..5608f9b --- /dev/null +++ b/common/car/src/test/java/com/kouros/navigation/car/screen/NavigationScreenTest.kt @@ -0,0 +1,133 @@ +package com.kouros.navigation.car.screen + +import androidx.car.app.testing.ScreenController +import androidx.car.app.testing.TestCarContext +import androidx.car.app.navigation.model.NavigationTemplate +import androidx.lifecycle.MutableLiveData +import androidx.test.core.app.ApplicationProvider +import com.kouros.navigation.car.SurfaceRenderer +import com.kouros.navigation.car.navigation.RouteCarModel +import com.kouros.navigation.model.NavigationViewModel +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import org.robolectric.RobolectricTestRunner +import com.google.common.truth.Truth.assertThat +import com.kouros.navigation.data.NavigationState +import com.kouros.navigation.data.Place +import com.kouros.navigation.model.RouteCalculator + +@RunWith(RobolectricTestRunner::class) +class NavigationScreenTest { + + private lateinit var testCarContext: TestCarContext + + @Mock + private lateinit var mockSurfaceRenderer: SurfaceRenderer + + @Mock + private lateinit var mockRouteModel: RouteCarModel + + @Mock + private lateinit var mockListener: NavigationListener + + @Mock + private lateinit var mockViewModel: NavigationViewModel + + private lateinit var navigationScreen: NavigationScreen + private lateinit var screenController: ScreenController + + @Mock + private lateinit var mockRouteCalculator: RouteCalculator + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + + testCarContext = TestCarContext.createCarContext(ApplicationProvider.getApplicationContext()) + + // Setup initial state + `when`(mockRouteModel.isNavigating()).thenReturn(true) + `when`(mockViewModel.route).thenReturn(MutableLiveData()) + `when`(mockViewModel.traffic).thenReturn(MutableLiveData()) + `when`(mockViewModel.recentPlaces).thenReturn(MutableLiveData()) + `when`(mockViewModel.placeLocation).thenReturn(MutableLiveData()) + `when`(mockViewModel.speedCameras).thenReturn(MutableLiveData()) + `when`(mockViewModel.maxSpeed).thenReturn(MutableLiveData()) + `when`(mockViewModel.previewRoute).thenReturn(MutableLiveData()) + `when`(mockSurfaceRenderer.routeData).thenReturn(MutableLiveData()) + + navigationScreen = NavigationScreen( + testCarContext, + mockSurfaceRenderer, + mockRouteModel, + mockListener, + mockViewModel + ) + screenController = ScreenController(navigationScreen) + } + + @Test + fun onGetTemplate_whenNavigating_returnsNavigationTemplate() { + // Arrange + navigationScreen.navigationType = NavigationType.NAVIGATION + + // Act + val template = screenController.screen + + // Assert + assertThat(template).isInstanceOf(NavigationScreen::class.java) + } + + @Test + fun startNavigation_updatesState() { + // Arrange + navigationScreen.navigationType = NavigationType.NAVIGATION + + // Assert + assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.NAVIGATION) + } + + @Test + fun stopNavigation_updatesState() { + // Arrange + navigationScreen.navigationType = NavigationType.NAVIGATION + + // Act + navigationScreen.stopNavigation() + + // Assert + assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.VIEW) + } + + @Test + fun rerouteNavigation_updatesState() { + // Arrange + navigationScreen.navigationType = NavigationType.NAVIGATION + + // Act + navigationScreen.calculateNewRoute(Place()) + + // Assert + assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.REROUTE) + } + + @Test + fun arrivalNavigation_updatesState() { + // Arrange + navigationScreen.navigationType = NavigationType.NAVIGATION + + `when`(mockRouteModel.isArrival()).thenReturn(true) + `when`(mockRouteModel.routeCalculator).thenReturn(mockRouteCalculator) + `when`(mockRouteCalculator.leftStepDistance()).thenReturn(19.0) + `when`(mockRouteModel.navState).thenReturn(NavigationState()) + // Act + navigationScreen.checkArrival() + + // Assert + assertThat(navigationScreen.navigationType).isEqualTo(NavigationType.ARRIVAL) + } +} diff --git a/common/car/src/test/java/com/kouros/navigation/car/screen/observers/ObserversTest.kt b/common/car/src/test/java/com/kouros/navigation/car/screen/observers/ObserversTest.kt index 9e0d7e7..7e6f73c 100644 --- a/common/car/src/test/java/com/kouros/navigation/car/screen/observers/ObserversTest.kt +++ b/common/car/src/test/java/com/kouros/navigation/car/screen/observers/ObserversTest.kt @@ -41,30 +41,6 @@ class ObserversTest { verify(mockCallback, never()).onRouteReceived(any()) } - @Test - fun `RecentPlaceObserver triggers callback when navigating is false`() { - val observer = RecentPlaceObserver(mockCallback) - val testPlace = createTestPlace() - whenever(mockCallback.isNavigating()).thenReturn(false) - - observer.onChanged(testPlace) - - verify(mockCallback).isNavigating() - verify(mockCallback).onRecentPlaceReceived(testPlace) - } - - @Test - fun `RecentPlaceObserver does not trigger callback when navigating is true`() { - val observer = RecentPlaceObserver(mockCallback) - val testPlace = createTestPlace() - whenever(mockCallback.isNavigating()).thenReturn(true) - - observer.onChanged(testPlace) - - verify(mockCallback).isNavigating() - verify(mockCallback, never()).onRecentPlaceReceived(any()) - } - @Test fun `TrafficObserver triggers callback with traffic data and invalidates screen`() { val observer = TrafficObserver(mockCallback) @@ -119,15 +95,15 @@ class ObserversTest { } // Helper methods - private fun createTestPlace(): Place { - return Place( + private fun createTestPlace():List { + return listOf(Place( name = "Test Place", street = "Test Street", city = "Test City", latitude = 52.0, longitude = 10.0, category = Constants.FAVORITES - ) + )) } private fun createTestSearchResult(): SearchResult { 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 2894879..eb2f80e 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 @@ -130,6 +130,8 @@ object Constants { const val TRAFFIC_UPDATE = 300 + const val ROUTE_UPDATE = 60 + const val INSTRUCTION_DISTANCE = 50 const val GMS_CAR_SPEED_PERMISSION = "com.google.android.gms.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 4c60280..56a8a76 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 @@ -1,29 +1,7 @@ -/* - * Copyright 2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.kouros.navigation.data import android.content.Context import android.location.Location -import com.google.gson.GsonBuilder -import com.kouros.data.R -import com.kouros.navigation.data.osrm.OsrmRepository -import com.kouros.navigation.data.osrm.OsrmResponse -import com.kouros.navigation.data.osrm.OsrmRoute -import com.kouros.navigation.model.RouteModel import com.kouros.navigation.utils.GeoUtils.calculateSquareRadius import java.net.Authenticator import java.net.HttpURLConnection @@ -41,7 +19,7 @@ abstract class NavigationRepository { abstract fun getRoute( context: Context, currentLocation: Location, - location: Location, + destination: Location, carOrientation: Float, searchFilter: SearchFilter ): String @@ -59,7 +37,7 @@ abstract class NavigationRepository { } fun searchPlaces(search: String, location: Location): String { - val box = calculateSquareRadius(location.latitude, location.longitude, 100.0) + val box = calculateSquareRadius(location.latitude, location.longitude, 800.0) val viewbox = "&bounded=1&viewbox=${box}" return fetchUrl( "${nominatimUrl}search?q=$search&format=jsonv2&addressdetails=true$viewbox", 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 c26ec63..99840a4 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 @@ -53,6 +53,8 @@ class DataStoreManager(private val context: Context) { val TRAFFIC = booleanPreferencesKey("Traffic") + val TRIP_SUGGESTION = booleanPreferencesKey("TripSuggestion") + } // Read values @@ -129,6 +131,11 @@ class DataStoreManager(private val context: Context) { preferences[PreferencesKeys.TRAFFIC] == true } + val tripSuggestionFlow: Flow = + context.dataStore.data.map { preferences -> + preferences[PreferencesKeys.TRIP_SUGGESTION] == true + } + // Save values suspend fun setShow3D(enabled: Boolean) { context.dataStore.edit { preferences -> @@ -207,4 +214,10 @@ class DataStoreManager(private val context: Context) { preferences[PreferencesKeys.TRAFFIC] = enabled } } + + suspend fun setTripSuggestion(enabled: Boolean) { + context.dataStore.edit { preferences -> + preferences[PreferencesKeys.TRIP_SUGGESTION] = enabled + } + } } diff --git a/common/data/src/main/java/com/kouros/navigation/data/route/Step.kt b/common/data/src/main/java/com/kouros/navigation/data/route/Step.kt index 4e7a9cd..dca8367 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/route/Step.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/route/Step.kt @@ -12,4 +12,5 @@ data class Step( val distance: Double = 0.0, val street : String = "", val intersection: List = mutableListOf(), + val countryCode : String = "" ) 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 014de13..772ca9a 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 @@ -19,9 +19,9 @@ const val tomtomTrafficUrl = "https://api.tomtom.com/traffic/services/5/incident private const val tomtomFields = "{incidents{type,geometry{type,coordinates},properties{iconCategory,events{description}}}}" -const val useAsset = false +const val useLocal = false -const val useAssetTraffic = false +const val useLocalTraffic = false class TomTomRepository : NavigationRepository() { @@ -32,12 +32,11 @@ class TomTomRepository : NavigationRepository() { carOrientation: Float, searchFilter: SearchFilter ): String { - if (useAsset) { - val resourceId: Int = context.resources - .getIdentifier("tomtom_routing", "raw", context.packageName) - val routeJson = context.resources.openRawResource(resourceId) - val routeJsonString = routeJson.bufferedReader().use { it.readText() } - return routeJsonString + if (useLocal) { + return fetchUrl( + "https://kouros-online.de/tomtom_routing.json", + false + ) } var filter = "" if (searchFilter.avoidMotorway) { @@ -76,11 +75,11 @@ class TomTomRepository : NavigationRepository() { return "" } val bbox = calculateSquareRadius(location.latitude, location.longitude, 15.0) - return if (useAssetTraffic) { - val resourceId: Int = context.resources - .getIdentifier("tomtom_traffic", "raw", context.packageName) - val trafficJson = context.resources.openRawResource(resourceId) - trafficJson.bufferedReader().use { it.readText() } + return if (useLocalTraffic) { + fetchUrl( + "https://kouros-online.de/tomtom_traffic.json", + false + ) } else { val trafficResult = fetchUrl( "$tomtomTrafficUrl?key=$tomtomApiKey&bbox=$bbox&fields=$tomtomFields&language=en-GB&timeValidityFilter=present", diff --git a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt index 890195e..04fcfea 100644 --- a/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt +++ b/common/data/src/main/java/com/kouros/navigation/data/tomtom/TomTomRoute.kt @@ -62,7 +62,9 @@ class TomTomRoute { lastPointIndex = instruction.pointIndex val intersections = mutableListOf() route.sections?.forEach { section -> - if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex && section.endPointIndex >= lastPointIndex) { + if (section.sectionType == "LANES" && section.startPointIndex <= lastPointIndex + && section.endPointIndex >= lastPointIndex + ) { val lanes = mutableListOf() var startIndex = 0 var lastLane: Lane? = null @@ -87,25 +89,25 @@ class TomTomRoute { lastLane = lane } intersections.add(Intersection(waypoints[startIndex], lanes)) - } - stepDistance = - route.guidance.instructions[index].routeOffsetInMeters - stepDistance - stepDuration = - route.guidance.instructions[index].travelTimeInSeconds - stepDuration - val step = Step( - index = stepIndex, - street = street, - distance = stepDistance, - duration = stepDuration, - maneuver = maneuver, - intersection = intersections - ) - stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() - stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() - steps.add(step) - stepIndex += 1 } + stepDistance = + route.guidance.instructions[index].routeOffsetInMeters - stepDistance + stepDuration = + route.guidance.instructions[index].travelTimeInSeconds - stepDuration + val step = Step( + index = stepIndex, + street = street, + distance = stepDistance, + duration = stepDuration, + maneuver = maneuver, + intersection = intersections, + countryCode = lastInstruction.countryCode + ) + stepDistance = route.guidance.instructions[index].routeOffsetInMeters.toDouble() + stepDuration = route.guidance.instructions[index].travelTimeInSeconds.toDouble() + steps.add(step) + stepIndex += 1 } legs.add(Leg(steps)) val routeGeoJson = createLineStringCollection(waypoints) diff --git a/common/data/src/main/java/com/kouros/navigation/model/IconMapper.kt b/common/data/src/main/java/com/kouros/navigation/model/IconMapper.kt index 34c06f8..731dc7f 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/IconMapper.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/IconMapper.kt @@ -16,7 +16,7 @@ import com.kouros.navigation.data.StepData import java.util.Collections import java.util.Locale -class IconMapper() { +class IconMapper { fun maneuverIcon(routeManeuverType: Int): Int { var currentTurnIcon = R.drawable.ic_turn_name_change 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 42d67af..f3afdf5 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 @@ -5,6 +5,8 @@ import android.content.Context import android.location.Location import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -60,11 +62,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo MutableLiveData() } - /** LiveData containing list of favorite saved places */ - val favorites: MutableLiveData> by lazy { - MutableLiveData() - } - /** LiveData containing search results from Nominatim geocoding */ val searchPlaces: MutableLiveData> by lazy { MutableLiveData() @@ -147,7 +144,8 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo var id: Long = 0 if (rp.isNotEmpty()) { for (place in places.places) { - if (place.category.equals(Constants.RECENT)) { + if (place.category.equals(Constants.RECENT) + || place.category.equals(Constants.FAVORITES)) { val plLocation = location(place.longitude, place.latitude) if (place.latitude != 0.0) { val distance = @@ -172,43 +170,6 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo } } - /** - * Loads favorite places from Preferences and calculates distances. - * Posts the sorted list to favorites LiveData. - */ - fun loadFavorites(context: Context, location: Location, carOrientation: Float) { - viewModelScope.launch(Dispatchers.IO) { - try { - val settingsRepository = getSettingsRepository(context) - val rp = settingsRepository.recentPlacesFlow.first() - val gson = GsonBuilder().serializeNulls().create() - val recentPlaces = gson.fromJson(rp, Places::class.java) - val pl = mutableListOf() - if (rp.isNotEmpty()) { - for (place in recentPlaces.places) { - if (place.category.equals(Constants.FAVORITES)) { - val plLocation = location(place.longitude, place.latitude) - if (place.latitude != 0.0) { - val distance = - repository.getRouteDistance( - location, - plLocation, - carOrientation, - context - ) - place.distance = distance.toFloat() - } - pl.add(place) - } - } - } - favorites.postValue(pl) - } catch (e: Exception) { - e.printStackTrace() - } - } - } - /** * Calculates a route between current location and destination. * Posts the route JSON to route LiveData. @@ -216,7 +177,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo fun loadRoute( context: Context, currentLocation: Location, - location: Location, + destination: Location, carOrientation: Float ) { viewModelScope.launch(Dispatchers.IO) { @@ -225,7 +186,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo repository.getRoute( context, currentLocation, - location, + destination, carOrientation, getSearchFilter(context) ) @@ -296,7 +257,7 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo currentLocation: Location, location: Location, carOrientation: Float - ) { + ): String? { viewModelScope.launch(Dispatchers.IO) { try { previewRoute.postValue( @@ -312,8 +273,10 @@ class NavigationViewModel(private val repository: NavigationRepository) : ViewMo e.printStackTrace() } } + return previewRoute.value } + /** * Loads device contacts with addresses and converts to Place objects. * Posts results to contactAddress LiveData. diff --git a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt index 64fdcdf..7ad710c 100644 --- a/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt +++ b/common/data/src/main/java/com/kouros/navigation/model/RouteModel.kt @@ -13,7 +13,7 @@ import com.kouros.navigation.data.route.Leg import com.kouros.navigation.data.route.Routes import com.kouros.navigation.data.route.Step import com.kouros.navigation.utils.location -import kotlin.math.absoluteValue + open class RouteModel { @@ -33,6 +33,9 @@ open class RouteModel { val currentStep: Step get() = navState.route.nextStep(0) + val steps: List + get() = curLeg.steps + fun startNavigation(routeString: String) { navState = navState.copy( route = Route.Builder() @@ -145,7 +148,20 @@ open class RouteModel { ) } + /** + * Checks for navigating + */ fun isNavigating(): Boolean { return navState.navigating } + + /** + * Checks for arrival + */ + fun isArrival(): Boolean { + return navState.maneuverType == Maneuver.TYPE_DESTINATION + || navState.maneuverType == Maneuver.TYPE_DESTINATION_LEFT + || navState.maneuverType == Maneuver.TYPE_DESTINATION_RIGHT + || navState.maneuverType == Maneuver.TYPE_DESTINATION_STRAIGHT + } } \ No newline at end of file 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 d9cd742..0828beb 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 @@ -90,6 +90,12 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel( false ) + val tripSuggestion = repository.tripSuggestionFlow.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + false + ) + fun onShow3DChanged(enabled: Boolean) { viewModelScope.launch { repository.setShow3D(enabled) } } @@ -138,4 +144,8 @@ class SettingsViewModel(private val repository: SettingsRepository) : ViewModel( fun onTraffic(enabled: Boolean) { viewModelScope.launch { repository.setTraffic(enabled) } } + + fun onTripSuggestion(enabled: Boolean) { + viewModelScope.launch { repository.setTripSuggestion(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 48cba26..2a8efa5 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 @@ -44,6 +44,9 @@ class SettingsRepository( val trafficFlow: Flow = dataStoreManager.trafficFlow + val tripSuggestionFlow: Flow = + dataStoreManager.tripSuggestionFlow + suspend fun setShow3D(enabled: Boolean) { dataStoreManager.setShow3D(enabled) } @@ -95,4 +98,8 @@ class SettingsRepository( suspend fun setTraffic(enabled: Boolean) { dataStoreManager.setTraffic(enabled) } + + suspend fun setTripSuggestion(enabled: Boolean) { + dataStoreManager.setTripSuggestion(enabled) + } } diff --git a/common/data/src/main/res/drawable/chevron_right_24px.xml b/common/data/src/main/res/drawable/chevron_right_24px.xml new file mode 100644 index 0000000..3d036ec --- /dev/null +++ b/common/data/src/main/res/drawable/chevron_right_24px.xml @@ -0,0 +1,11 @@ + + + diff --git a/common/data/src/main/res/drawable/traffic_jam_48px.xml b/common/data/src/main/res/drawable/traffic_jam_48px.xml new file mode 100644 index 0000000..80b1761 --- /dev/null +++ b/common/data/src/main/res/drawable/traffic_jam_48px.xml @@ -0,0 +1,10 @@ + + + diff --git a/common/data/src/main/res/values-de/strings.xml b/common/data/src/main/res/values-de/strings.xml index 0f2771a..d786a06 100644 --- a/common/data/src/main/res/values-de/strings.xml +++ b/common/data/src/main/res/values-de/strings.xml @@ -65,4 +65,5 @@ Keine Kategorien Allgemein Verkehr anzeigen + Fahrten-Vorschläge diff --git a/common/data/src/main/res/values-el/strings.xml b/common/data/src/main/res/values-el/strings.xml index be63d99..33fc688 100644 --- a/common/data/src/main/res/values-el/strings.xml +++ b/common/data/src/main/res/values-el/strings.xml @@ -49,4 +49,5 @@ Δεν υπάρχουν κατηγορίες Γενικά Εμφάνιση κίνησης + Προτάσεις διαδρομής diff --git a/common/data/src/main/res/values-pl/strings.xml b/common/data/src/main/res/values-pl/strings.xml index c9bb914..8a45bc3 100644 --- a/common/data/src/main/res/values-pl/strings.xml +++ b/common/data/src/main/res/values-pl/strings.xml @@ -49,4 +49,5 @@ Brak kategorii do wyświetlenia Ogólne Pokaż natężenie ruchu + Sugestie dotyczące podróży diff --git a/common/data/src/main/res/values/strings.xml b/common/data/src/main/res/values/strings.xml index e97ca2d..f51af49 100644 --- a/common/data/src/main/res/values/strings.xml +++ b/common/data/src/main/res/values/strings.xml @@ -52,4 +52,5 @@ No categories to show General Show traffic + Trip suggestions \ No newline at end of file diff --git a/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt index 12c5fb9..05e3670 100644 --- a/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt +++ b/common/data/src/test/java/com/kouros/navigation/model/RouteModelTest.kt @@ -86,21 +86,6 @@ class RouteModelTest { assert(routeModel.navState.currentLocation.longitude == 11.57936) } - @Test - fun `currentStep returns StepData `() { - val stepData = routeModel.currentStep() - assert(stepData.leftStepDistance == 0.0) - assert(stepData.instruction == "Milbertshofener Straße") - } - - @Test - fun `nextStep returns StepData `() { - routeModel.currentStep() - val stepData = routeModel.nextStep() - assert(stepData.leftStepDistance == 0.0) - assert(stepData.instruction == "Bad-Soden-Straße") - } - @Test fun `stopNavigation updates route and sets navigating to false `() { routeModel.stopNavigation() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df14c5e..ebdcef5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,25 +4,25 @@ androidGpxParser = "2.3.1" androidSdkTurf = "6.0.1" datastore = "1.2.1" gradle = "9.1.0" -koinAndroid = "4.1.1" -koinAndroidxCompose = "4.1.1" -koinComposeViewmodel = "4.1.1" -koinCore = "4.1.1" -kotlin = "2.3.10" +koinAndroid = "4.2.0" +koinAndroidxCompose = "4.2.0" +koinComposeViewmodel = "4.2.0" +koinCore = "4.2.0" +kotlin = "2.3.20" coreKtx = "1.18.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" kotlinxSerializationJson = "1.10.0" lifecycleRuntimeKtx = "2.10.0" -composeBom = "2026.02.01" +composeBom = "2026.03.00" appcompat = "1.7.1" material = "1.13.0" carApp = "1.7.0" androidx-car = "1.7.0" materialIconsExtended = "1.7.8" mockitoCore = "5.23.0" -mockitoKotlin = "6.2.3" +mockitoKotlin = "6.3.0" rules = "1.7.0" runner = "1.7.0" material3 = "1.4.0" @@ -44,6 +44,9 @@ foundationLayout = "1.10.5" datastorePreferences = "1.2.1" datastoreCore = "1.2.1" monitor = "1.8.0" +robolectric = "4.16.1" +truth = "1.4.5" +testCore = "1.7.0" [libraries] android-gpx-parser = { module = "com.github.ticofab:android-gpx-parser", version.ref = "androidGpxParser" } @@ -69,6 +72,7 @@ koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-car-app = { group = "androidx.car.app", name = "app", version.ref = "carApp" } +androidx-car-app-testing = { group = "androidx.car.app", name = "app-testing", version.ref = "carApp" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoCore" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" } maplibre-compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } @@ -91,6 +95,9 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "datastoreCore" } androidx-monitor = { group = "androidx.test", name = "monitor", version.ref = "monitor" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +google-truth = { module = "com.google.truth:truth", version.ref = "truth" } +androidx-test-core = { module = "androidx.test:core", version.ref = "testCore" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }