This commit is contained in:
Dimitris
2026-03-21 12:24:53 +01:00
parent ada878b23c
commit d1968cfa68
45 changed files with 1121 additions and 935 deletions

View File

@@ -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)
}
}

View File

@@ -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)
}
}
/**

View File

@@ -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.
*/

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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<Category> = 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<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
) {
val loc = location(centerLon, centerLat)
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
screenManager
.pushForResult(
CategoryScreen(
carContext,
surfaceRenderer,
category,
navigationViewModel,
elements
)
) { obj: Any? ->
if (obj != null) {
setResult(obj)
finish()
}
}
}
override fun invalidateScreen() {
}
}
fun carIcon(context: CarContext, 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()
}
}

View File

@@ -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<Elements>,
) : Screen(carContext) {
) : Screen(carContext), CategoryObserverCallback {
val maxListItems: Int = 30
var elements: List<Elements> = 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<Elements>,
centerLat: Double,
centerLon: Double,
coordinates: List<List<Double>>
) {
val loc = location(centerLon, centerLat)
val route = createPointCollection(coordinates, category)
surfaceRenderer.setCategories(loc, route)
this.elements = elements
loading = false
}
override fun invalidateScreen() {
invalidate()
}
}

View File

@@ -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)
}

View File

@@ -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<Place>()
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<Elements>()
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<List<Place>> { 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<String, String>) {
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<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
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<String?> = 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<String, String>) {
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<Elements>) {
speedCameras = cameras
val coordinates = mutableListOf<List<Double>>()
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()
}
}
/**

View File

@@ -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<Place>
private val recentPlaces: List<Place>,
) : Screen(carContext) {
val routeModel = RouteCarModel()
@@ -45,45 +45,28 @@ class PlaceListScreen(
var mPlaces = mutableListOf<Place>()
val previewObserver = Observer<String> { 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) {

View File

@@ -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<String?> = 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
}

View File

@@ -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<String> { 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<Map<String, String>> { 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 {

View File

@@ -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()
}

View File

@@ -28,6 +28,7 @@ class SearchScreen(
carContext: CarContext,
private var surfaceRenderer: SurfaceRenderer,
private val navigationViewModel: NavigationViewModel,
private val recentPlaces: List<Place>,
) : Screen(carContext) {
var isSearchComplete: Boolean = false
@@ -44,54 +45,16 @@ class SearchScreen(
lateinit var searchResult: List<SearchResult>
val observer = Observer<List<SearchResult>> { newSearch ->
searchResult = newSearch
invalidate()
}
val observerRecentPlaces = Observer<List<Place>> { 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<List<Place>> { 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)

View File

@@ -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()

View File

@@ -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)
}
/**

View File

@@ -1,14 +0,0 @@
package com.kouros.navigation.car.screen.observers
import androidx.lifecycle.Observer
class PreviewRouteObserver(
private val callback: NavigationObserverCallback
) : Observer<String> {
override fun onChanged(value: String) {
if (value.isNotEmpty()) {
callback.onPreviewRouteReceived(value)
}
}
}

View File

@@ -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<Place> {
override fun onChanged(value: Place) {
if (!callback.isNavigating()) {
callback.onRecentPlaceReceived(value)
}
}
}

View File

@@ -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),

View File

@@ -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)
}
}

View File

@@ -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<Place> {
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 {