Preview
This commit is contained in:
@@ -7,17 +7,12 @@ import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.data.Constants.homeHohenwaldeck
|
||||
import com.kouros.navigation.data.Constants.homeVogelhart
|
||||
import com.kouros.navigation.data.RouteEngine
|
||||
import com.kouros.navigation.data.tomtom.TomTomRepository
|
||||
import com.kouros.navigation.model.NavigationViewModel
|
||||
import com.kouros.navigation.model.RouteModel
|
||||
import com.kouros.navigation.utils.getSettingsRepository
|
||||
import com.kouros.navigation.utils.location
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
import org.junit.Test
|
||||
@@ -25,7 +20,6 @@ import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.maplibre.compose.expressions.dsl.step
|
||||
import kotlin.collections.forEach
|
||||
|
||||
/**
|
||||
@@ -44,7 +38,7 @@ class RouteModelTest {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val repository = getSettingsRepository(appContext)
|
||||
runBlocking { repository.setRoutingEngine(RouteEngine.TOMTOM.ordinal) }
|
||||
val routeJson = appContext.resources.openRawResource(R.raw.tomom_routing)
|
||||
val routeJson = appContext.resources.openRawResource(R.raw.tomtom_routing)
|
||||
val routeJsonString = routeJson.bufferedReader().use { it.readText() }
|
||||
assertNotEquals("", routeJsonString)
|
||||
routeModel.navState = routeModel.navState.copy(routingEngine = RouteEngine.TOMTOM.ordinal)
|
||||
|
||||
@@ -418,6 +418,7 @@ class SurfaceRenderer(
|
||||
* Sets route data for active navigation and switches to VIEW mode.
|
||||
*/
|
||||
fun clearRouteData() {
|
||||
updateLocation(lastLocation)
|
||||
routeData.value = ""
|
||||
viewStyle = ViewStyle.VIEW
|
||||
cameraPosition.postValue(
|
||||
|
||||
@@ -85,14 +85,10 @@ class NavigationScreen(
|
||||
var lastTrafficDate: LocalDateTime? = LocalDateTime.of(1960, 6, 21, 0, 0)
|
||||
var lastCameraSearch = 0
|
||||
var speedCameras = listOf<Elements>()
|
||||
|
||||
val observerManager = NavigationObserverManager(navigationViewModel, this)
|
||||
|
||||
val repository = getSettingsRepository(carContext)
|
||||
|
||||
val settingsViewModel = getSettingsViewModel(carContext)
|
||||
|
||||
|
||||
var distanceMode = 0
|
||||
|
||||
init {
|
||||
@@ -112,19 +108,26 @@ class NavigationScreen(
|
||||
*/
|
||||
override fun onRouteReceived(route: String) {
|
||||
if (route.isNotEmpty()) {
|
||||
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)
|
||||
}
|
||||
surfaceRenderer.setRouteData()
|
||||
listener.startNavigation()
|
||||
prepareRoute(route)
|
||||
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)
|
||||
}
|
||||
surfaceRenderer.setRouteData()
|
||||
listener.startNavigation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if navigation is currently active.
|
||||
*/
|
||||
@@ -176,6 +179,16 @@ class NavigationScreen(
|
||||
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.
|
||||
*/
|
||||
@@ -409,13 +422,20 @@ class NavigationScreen(
|
||||
)
|
||||
.setOnClickListener {
|
||||
val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||
navigationViewModel.loadRoute(
|
||||
navigationViewModel.loadPreviewRoute(
|
||||
carContext,
|
||||
surfaceRenderer.lastLocation,
|
||||
navigateTo,
|
||||
surfaceRenderer.carOrientation
|
||||
)
|
||||
routeModel.navState = routeModel.navState.copy(destination = recentPlace)
|
||||
// val navigateTo = location(recentPlace.longitude, recentPlace.latitude)
|
||||
// navigationViewModel.loadRoute(
|
||||
// carContext,
|
||||
// surfaceRenderer.lastLocation,
|
||||
// navigateTo,
|
||||
// surfaceRenderer.carOrientation
|
||||
// )
|
||||
// routeModel.navState = routeModel.navState.copy(destination = recentPlace)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
@@ -458,7 +478,7 @@ class NavigationScreen(
|
||||
* Creates an action to start the settings screen.
|
||||
*/
|
||||
private fun settingsAction(): Action {
|
||||
return Action.Builder()
|
||||
return Action.Builder()
|
||||
.setIcon(routeModel.createCarIcon(carContext, R.drawable.settings_48px))
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext, navigationViewModel))
|
||||
@@ -554,12 +574,44 @@ 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
|
||||
navigationType = NavigationType.VIEW
|
||||
navigationViewModel.previewRoute.value = ""
|
||||
navigationType = NavigationType.NAVIGATION
|
||||
val location = location(place.longitude, place.latitude)
|
||||
navigationViewModel.saveRecent(carContext, place)
|
||||
currentNavigationLocation = location
|
||||
@@ -663,7 +715,7 @@ class NavigationScreen(
|
||||
* This includes destination name, address, travel estimate, and loading status.
|
||||
*/
|
||||
private fun updateTrip() {
|
||||
if (routeModel.isNavigating()) {
|
||||
if (routeModel.isNavigating() && !routeModel.navState.destination.name.isNullOrEmpty()) {
|
||||
val tripBuilder = Trip.Builder()
|
||||
val destination = Destination.Builder()
|
||||
.setName(routeModel.navState.destination.name ?: "")
|
||||
|
||||
@@ -56,6 +56,7 @@ class PlaceListScreen(
|
||||
.pushForResult(
|
||||
RoutePreviewScreen(
|
||||
carContext,
|
||||
RoutePreviewType.MULTI_ROUTE,
|
||||
surfaceRenderer,
|
||||
place,
|
||||
navigationViewModel,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.kouros.navigation.car.screen
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.car.app.CarContext
|
||||
@@ -11,9 +12,11 @@ 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.ActionStrip
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.CarText
|
||||
import androidx.car.app.model.DurationSpan
|
||||
import androidx.car.app.model.ForegroundCarColorSpan
|
||||
import androidx.car.app.model.Header
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
@@ -24,7 +27,6 @@ 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.Lifecycle
|
||||
import com.kouros.data.R
|
||||
import com.kouros.navigation.car.SurfaceRenderer
|
||||
import com.kouros.navigation.car.navigation.NavigationUtils
|
||||
@@ -32,14 +34,15 @@ 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.model.RouteModel
|
||||
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] */
|
||||
class RoutePreviewScreen(
|
||||
carContext: CarContext,
|
||||
private var routeType: RoutePreviewType,
|
||||
private var surfaceRenderer: SurfaceRenderer,
|
||||
private var destination: Place,
|
||||
private val navigationViewModel: NavigationViewModel,
|
||||
@@ -51,6 +54,7 @@ class RoutePreviewScreen(
|
||||
val maxListItems: Int = 3
|
||||
val navigationUtils = NavigationUtils(carContext)
|
||||
|
||||
var routeSelected = false
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
invalidate()
|
||||
@@ -63,7 +67,6 @@ class RoutePreviewScreen(
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
|
||||
|
||||
val itemListBuilder = ItemList.Builder()
|
||||
if (carContext.getCarAppApiLevel() > CarAppApiLevels.LEVEL_1) {
|
||||
val listLimit = min(
|
||||
@@ -79,12 +82,16 @@ class RoutePreviewScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val street = if (destination.street.isNullOrEmpty()) {
|
||||
carContext.getString((R.string.route_preview))
|
||||
} else {
|
||||
destination.street.toString()
|
||||
}
|
||||
val header = Header.Builder()
|
||||
.setStartHeaderAction(Action.BACK)
|
||||
.setTitle(carContext.getString(R.string.route_preview))
|
||||
.setTitle(street)
|
||||
|
||||
if (routeModel.route.routes.size == 1) {
|
||||
if (routeType == RoutePreviewType.SINGLE_ROUTE) {
|
||||
header.addEndHeaderAction(
|
||||
favoriteAction()
|
||||
)
|
||||
@@ -99,17 +106,8 @@ class RoutePreviewScreen(
|
||||
CarText.Builder("Wait")
|
||||
.build()
|
||||
}
|
||||
if (routeModel.route.routes.size == 1) {
|
||||
val timer = object : CountDownTimer(5000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {}
|
||||
override fun onFinish() {
|
||||
onNavigate(0)
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
}
|
||||
|
||||
val content = if (routeModel.route.routes.size > 1) {
|
||||
val content = if (routeType == RoutePreviewType.MULTI_ROUTE) {
|
||||
ListTemplate.Builder()
|
||||
.setHeader(header.build())
|
||||
.setSingleList(itemListBuilder.build())
|
||||
@@ -120,26 +118,63 @@ class RoutePreviewScreen(
|
||||
carContext, R.drawable.navigation_48px
|
||||
)
|
||||
).build()
|
||||
val selectRouteIcon: CarIcon = CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext, R.drawable.alt_route_48px
|
||||
)
|
||||
).build()
|
||||
val navigateAction = Action.Builder()
|
||||
.setFlags(FLAG_DEFAULT)
|
||||
.setIcon(navigateActionIcon)
|
||||
.setOnClickListener { this.onNavigate(0) }
|
||||
.setOnClickListener { onNavigate(routeModel.navState.currentRouteIndex) }
|
||||
.build()
|
||||
val selectRouteAction = Action.Builder()
|
||||
.setIcon(selectRouteIcon)
|
||||
.setOnClickListener {
|
||||
routeType = RoutePreviewType.MULTI_ROUTE
|
||||
invalidate()
|
||||
}
|
||||
.build()
|
||||
MessageTemplate.Builder(
|
||||
message
|
||||
)
|
||||
.setHeader(header.build())
|
||||
.addAction(navigateAction)
|
||||
.addAction(selectRouteAction)
|
||||
.setLoading(message.toString() == "Wait")
|
||||
.build()
|
||||
}
|
||||
return MapWithContentTemplate.Builder()
|
||||
|
||||
|
||||
|
||||
val template = MapWithContentTemplate.Builder()
|
||||
.setContentTemplate(content)
|
||||
.setMapController(
|
||||
MapController.Builder().setMapActionStrip(
|
||||
getMapActionStrip()
|
||||
).build()
|
||||
)
|
||||
if (routeType == RoutePreviewType.MULTI_ROUTE && !routeSelected) {
|
||||
template.setActionStrip(createActionStripBuilder().build())
|
||||
}
|
||||
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)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -187,7 +222,7 @@ class RoutePreviewScreen(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_pan_24
|
||||
R.drawable.heart_minus_48px
|
||||
)
|
||||
)
|
||||
.build()
|
||||
@@ -230,7 +265,6 @@ class RoutePreviewScreen(
|
||||
street = it.street
|
||||
}
|
||||
}
|
||||
val delay = (route.summary.trafficDelay / 60).toInt().toString()
|
||||
|
||||
val row = Row.Builder()
|
||||
.setTitle(routeText)
|
||||
@@ -239,11 +273,27 @@ class RoutePreviewScreen(
|
||||
.addAction(navigateAction)
|
||||
|
||||
if (route.summary.trafficDelay > 60) {
|
||||
row.addText("$delay min")
|
||||
row.addText(createDelay(route))
|
||||
}
|
||||
return row.build()
|
||||
}
|
||||
|
||||
private fun createDelay(route: Routes): SpannableStringBuilder {
|
||||
val delay = (route.summary.trafficDelay / 60)
|
||||
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 delayBuilder
|
||||
}
|
||||
private fun onNavigate(index: Int) {
|
||||
destination.routeIndex = index
|
||||
setResult(destination)
|
||||
@@ -253,6 +303,8 @@ class RoutePreviewScreen(
|
||||
private fun onRouteSelected(index: Int) {
|
||||
routeModel.navState = routeModel.navState.copy(currentRouteIndex = index)
|
||||
surfaceRenderer.setPreviewRouteData(routeModel)
|
||||
routeSelected = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun getMapActionStrip(): ActionStrip {
|
||||
@@ -276,3 +328,7 @@ class RoutePreviewScreen(
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
enum class RoutePreviewType {
|
||||
SINGLE_ROUTE, MULTI_ROUTE
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ 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()
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ class NavigationObserverManager(
|
||||
val placeSearchObserver = PlaceSearchObserver(callback)
|
||||
val speedCameraObserver = SpeedCameraObserver(callback)
|
||||
val maxSpeedObserver = MaxSpeedObserver(callback)
|
||||
|
||||
val previewObserver = PreviewRouteObserver(callback)
|
||||
|
||||
/**
|
||||
* Attaches all observers to the ViewModel.
|
||||
* Call this from NavigationScreen's init block or lifecycle method.
|
||||
@@ -29,6 +30,7 @@ class NavigationObserverManager(
|
||||
viewModel.placeLocation.observe(screen, placeSearchObserver)
|
||||
viewModel.speedCameras.observe(screen, speedCameraObserver)
|
||||
viewModel.maxSpeed.observe(screen, maxSpeedObserver)
|
||||
viewModel.previewRoute.observe(screen, previewObserver)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user