Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ReplayRouteSession and TripSessionStarter handle route changes #6913

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions android-auto-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ dependencies {
// This example is used for development so it may depend on unstable versions.
// Examples based on final versions can be found in the examples repository.
// https://github.com/mapbox/mapbox-navigation-android-examples
implementation("com.mapbox.navigation:ui-dropin:2.10.0-rc.1")
implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.42")
implementation("com.mapbox.navigation:ui-dropin:2.10.0")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to use 2.11.0 beta, otherwise it doesn't compile.

implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.43")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android Auto uses 1.0.0-beta.44.


// Dependencies needed for this example.
implementation dependenciesList.androidXCore
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package com.mapbox.navigation.examples.androidauto.car

import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import androidx.car.app.Screen
import androidx.car.app.Session
import androidx.car.app.model.ActionStrip
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.mapbox.android.core.permissions.PermissionsManager
import com.mapbox.androidauto.MapboxCarContext
import com.mapbox.androidauto.action.MapboxScreenActionStripProvider
Expand All @@ -30,12 +27,11 @@ import com.mapbox.maps.applyDefaultParams
import com.mapbox.maps.extension.androidauto.MapboxCarMap
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.requireMapboxNavigation
import com.mapbox.navigation.core.replay.route.ReplayRouteSession
import com.mapbox.navigation.core.trip.session.TripSessionState
import com.mapbox.navigation.core.trip.MapboxTripStarter
import com.mapbox.navigation.examples.androidauto.CarAppSyncComponent
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class MainCarSession : Session() {
Expand All @@ -59,10 +55,11 @@ class MainCarSession : Session() {
}
}
}
private val mapboxNavigation by requireMapboxNavigation()
private val replayRouteSession = ReplayRouteSession()
private val mapboxTripStarter = MapboxTripStarter.getRegisteredInstance()

init {
MapboxNavigationApp.attach(this)

// Decide how you want the car and app to interact. In this example, the car and app
// are kept in sync where they essentially mirror each other.
CarAppSyncComponent.getInstance().setCarSession(this)
Expand Down Expand Up @@ -145,31 +142,9 @@ class MainCarSession : Session() {
// computer terminal.
// adb shell dumpsys activity service com.mapbox.navigation.examples.androidauto.car.MainCarAppService AUTO_DRIVE
private fun observeAutoDrive() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
mapboxCarContext.mapboxNavigationManager.autoDriveEnabledFlow.collect {
refreshTripSession()
}
}
}
}

@SuppressLint("MissingPermission")
private fun refreshTripSession() {
val isAutoDriveEnabled = mapboxCarContext.mapboxNavigationManager
.autoDriveEnabledFlow.value
if (!PermissionsManager.areLocationPermissionsGranted(carContext)) {
mapboxNavigation.stopTripSession()
return
}

if (isAutoDriveEnabled) {
MapboxNavigationApp.registerObserver(replayRouteSession)
} else {
MapboxNavigationApp.unregisterObserver(replayRouteSession)
if (mapboxNavigation.getTripSessionState() != TripSessionState.STARTED) {
mapboxNavigation.startTripSession()
}
}
mapboxCarContext.mapboxNavigationManager.autoDriveEnabledFlow
.filter { it }
.onEach { mapboxTripStarter.enableReplayRoute() }
.launchIn(lifecycleScope)
}
}
3 changes: 3 additions & 0 deletions changelog/unreleased/bugfixes/6913.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Make `MapboxTripStarterType.ReplayRoute` and `ReplayRouteSession` automatically move to the origin of the route after `MapboxNavigation.setNavigationRoutes`.
- Make `ReplayRouteSession` observe the `ReplayRouteSessionOptions` so changes can be made without creating a new instance of the session.
- Change `ReplayRouteSession.getOptions()` to return a `StateFlow` so the options can be observed.
2 changes: 1 addition & 1 deletion libnavigation-core/api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ package com.mapbox.navigation.core.replay.route {

@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class ReplayRouteSession implements com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver {
ctor public ReplayRouteSession();
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getOptions();
method public kotlinx.coroutines.flow.StateFlow<com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions> getOptions();
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
method public com.mapbox.navigation.core.replay.route.ReplayRouteSession setOptions(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ import com.mapbox.navigation.core.replay.history.ReplayEventUpdateLocation
import com.mapbox.navigation.core.replay.history.ReplayEventsObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
import com.mapbox.navigation.utils.internal.logW
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.util.Collections

/**
Expand Down Expand Up @@ -54,26 +66,34 @@ import java.util.Collections
@ExperimentalPreviewMapboxNavigationAPI
class ReplayRouteSession : MapboxNavigationObserver {

private var options = ReplayRouteSessionOptions.Builder().build()

private lateinit var replayRouteMapper: ReplayRouteMapper
private lateinit var coroutineScope: CoroutineScope
private val optionsFlow = MutableStateFlow(ReplayRouteSessionOptions.Builder().build())
private var mapboxNavigation: MapboxNavigation? = null
private var lastLocationEvent: ReplayEventUpdateLocation? = null
private var polylineDecodeStream: ReplayPolylineDecodeStream? = null
private var currentRoute: NavigationRoute? = null
set(value) {
field = value
isNewRouteInitialized = value != null
}
private var isNewRouteInitialized = false

private val routeProgressObserver = RouteProgressObserver { routeProgress ->
if (currentRoute?.id != routeProgress.navigationRoute.id) {
currentRoute = routeProgress.navigationRoute
onRouteChanged(routeProgress)
onRouteProgressRouteChanged(routeProgress)
}
}

private val routesObserver = RoutesObserver { result ->
if (result.navigationRoutes.isEmpty()) {
val route = result.navigationRoutes.firstOrNull()
if (route == null) {
mapboxNavigation?.resetReplayLocation()
currentRoute = null
polylineDecodeStream = null
} else if (!isNewRouteInitialized && currentRoute?.id != route.id) {
onInitializeNewRoute(route)
}
}

Expand All @@ -89,34 +109,46 @@ class ReplayRouteSession : MapboxNavigationObserver {
* setOptions(getOptions().toBuilder().locationResetEnabled(false).build())
* ```
*/
fun getOptions(): ReplayRouteSessionOptions = options
fun getOptions(): StateFlow<ReplayRouteSessionOptions> = optionsFlow.asStateFlow()

/**
* Set new options for the [ReplayRouteSession]. This will not effect previously simulated
* events, the end behavior will depend on the values you have used. If you want to guarantee
* the effect of the options, you need to set options before [MapboxNavigation] is attached.
*/
fun setOptions(options: ReplayRouteSessionOptions) = apply {
this.options = options
if (::replayRouteMapper.isInitialized) {
replayRouteMapper.options = this.options.replayRouteOptions
}
this.optionsFlow.value = options
}

override fun onAttached(mapboxNavigation: MapboxNavigation) {
this.replayRouteMapper = ReplayRouteMapper(options.replayRouteOptions)
this.coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
this.mapboxNavigation = mapboxNavigation
mapboxNavigation.startReplayTripSession()
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
observeStateFlow(mapboxNavigation).launchIn(coroutineScope)
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.mapboxReplayer.registerObserver(replayEventsObserver)
mapboxNavigation.mapboxReplayer.play()
}

private fun observeStateFlow(mapboxNavigation: MapboxNavigation): Flow<*> {
return optionsFlow.mapDistinct { it.replayRouteOptions }.onEach { replayRouteOptions ->
mapboxNavigation.mapboxReplayer.clearEvents()
this.replayRouteMapper = ReplayRouteMapper(replayRouteOptions)
currentRoute = null
mapboxNavigation.resetTripSession {
mapboxNavigation.mapboxReplayer.play()
}
}
}

private inline fun <T, R> Flow<T>.mapDistinct(
crossinline transform: suspend (value: T) -> R
): Flow<R> = map(transform).distinctUntilChanged()

private fun MapboxNavigation.resetReplayLocation() {
mapboxReplayer.clearEvents()
resetTripSession {
if (options.locationResetEnabled) {
if (optionsFlow.value.locationResetEnabled) {
val context = navigationOptions.applicationContext
if (PermissionsManager.areLocationPermissionsGranted(context)) {
pushRealLocation(context)
Expand All @@ -133,6 +165,9 @@ class ReplayRouteSession : MapboxNavigationObserver {
}

override fun onDetached(mapboxNavigation: MapboxNavigation) {
if (this::coroutineScope.isInitialized) {
coroutineScope.cancel()
}
mapboxNavigation.unregisterRoutesObserver(routesObserver)
mapboxNavigation.unregisterRouteProgressObserver(routeProgressObserver)
mapboxNavigation.mapboxReplayer.unregisterObserver(replayEventsObserver)
Expand All @@ -142,28 +177,43 @@ class ReplayRouteSession : MapboxNavigationObserver {
this.currentRoute = null
}

private fun onRouteChanged(routeProgress: RouteProgress) {
private fun onInitializeNewRoute(route: NavigationRoute) {
mapboxNavigation?.mapboxReplayer?.clearEvents()
this.replayRouteMapper = ReplayRouteMapper(optionsFlow.value.replayRouteOptions)
mapboxNavigation?.resetTripSession {
route.routeOptions.coordinatesList().firstOrNull()?.let {
val replayFirstLocation = replayRouteMapper.mapPointList(listOf(it))
mapboxNavigation?.mapboxReplayer?.pushEvents(replayFirstLocation)
}
mapboxNavigation?.mapboxReplayer?.play()
}
isNewRouteInitialized = true
}

private fun onRouteProgressRouteChanged(routeProgress: RouteProgress) {
val navigationRoute = routeProgress.navigationRoute
val mapboxReplayer = mapboxNavigation?.mapboxReplayer ?: return
mapboxReplayer.clearEvents()
mapboxReplayer.play()
val geometries = navigationRoute.directionsRoute.routeOptions()!!.geometries()
val usesPolyline6 = geometries.contains(DirectionsCriteria.GEOMETRY_POLYLINE6)
val geometry = navigationRoute.directionsRoute.geometry()
if (!usesPolyline6 || geometry.isNullOrEmpty()) {
logW(LOG_CATEGORY) {
"The NavigationRouteReplay must have geometry encoded with polyline6 " +
"$geometries $geometry"
mapboxNavigation?.resetTripSession {
mapboxReplayer.play()
val geometries = navigationRoute.directionsRoute.routeOptions()!!.geometries()
val usesPolyline6 = geometries.contains(DirectionsCriteria.GEOMETRY_POLYLINE6)
val geometry = navigationRoute.directionsRoute.geometry()
if (!usesPolyline6 || geometry.isNullOrEmpty()) {
logW(LOG_CATEGORY) {
"The NavigationRouteReplay must have geometry encoded with polyline6 " +
"$geometries $geometry"
}
return@resetTripSession
}
return
}
polylineDecodeStream = ReplayPolylineDecodeStream(geometry, 6)
polylineDecodeStream = ReplayPolylineDecodeStream(geometry, 6)

// Skip up to the current geometry index. There is some imprecision here because the
// distance traveled is not equal to a route index.
polylineDecodeStream?.skip(routeProgress.currentRouteGeometryIndex)
// Skip up to the current geometry index. There is some imprecision here because the
// distance traveled is not equal to a route index.
polylineDecodeStream?.skip(routeProgress.currentRouteGeometryIndex)

pushMorePoints()
pushMorePoints()
}
}

private fun isLastEventPlayed(events: List<ReplayEventBase>): Boolean {
Expand All @@ -174,7 +224,7 @@ class ReplayRouteSession : MapboxNavigationObserver {
}

private fun pushMorePoints() {
val nextPoints = polylineDecodeStream?.decode(options.decodeMinDistance) ?: return
val nextPoints = polylineDecodeStream?.decode(optionsFlow.value.decodeMinDistance) ?: return
val nextReplayLocations = replayRouteMapper.mapPointList(nextPoints)
lastLocationEvent = nextReplayLocations.lastOrNull { it is ReplayEventUpdateLocation }
as? ReplayEventUpdateLocation
Expand Down
Loading