Skip to content

Commit

Permalink
Make ReplayRouteSession handle route changes
Browse files Browse the repository at this point in the history
  • Loading branch information
kmadsen committed Jan 28, 2023
1 parent bc34e46 commit 959b5ea
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 71 deletions.
5 changes: 3 additions & 2 deletions android-auto-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ 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 project(':libnavui-dropin')
// implementation("com.mapbox.navigation:ui-dropin:2.10.0")
implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.43")

// 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 @@ -29,12 +26,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 @@ -57,10 +53,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 @@ -143,31 +140,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 @@ -12,7 +12,6 @@ import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.trip.model.RouteProgress
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.history.MapboxHistoryReaderProvider
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.replay.MapboxReplayer
Expand All @@ -24,6 +23,7 @@ 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
Expand Down Expand Up @@ -67,25 +67,33 @@ import java.util.Collections
class ReplayRouteSession : MapboxNavigationObserver {

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
private var coroutineScope: CoroutineScope? = 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 Down Expand Up @@ -113,24 +121,23 @@ class ReplayRouteSession : MapboxNavigationObserver {
}

override fun onAttached(mapboxNavigation: MapboxNavigation) {
val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { this.coroutineScope = it }
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()
observeStateFlow(mapboxNavigation).launchIn(coroutineScope)
}

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

Expand Down Expand Up @@ -158,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 @@ -167,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.onEach
*/
@ExperimentalPreviewMapboxNavigationAPI
class MapboxTripStarter internal constructor(
private val services: MapboxTripStarterServices = MapboxTripStarterServices()
services: MapboxTripStarterServices = MapboxTripStarterServices()
) : MapboxNavigationObserver {

private val tripType = MutableStateFlow<MapboxTripStarterType>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class ReplayRouteSessionTest {
sut.setOptions(firstOptions)

assertNotEquals(firstOptions, initialOptions)
assertEquals(firstOptions, sut.getOptions())
assertEquals(firstOptions, sut.getOptions().value)
}

@Test
Expand All @@ -158,7 +158,7 @@ class ReplayRouteSessionTest {
sut.setOptions(firstOptions)

assertNotEquals(firstOptions, initialOptions)
assertEquals(firstOptions, sut.getOptions())
assertEquals(firstOptions, sut.getOptions().value)
}

@Test
Expand Down Expand Up @@ -355,7 +355,6 @@ class ReplayRouteSessionTest {
progressObserver.captured.onRouteProgressChanged(secondRouteProgress)

verify(exactly = 2) {
replayer.clearEvents()
replayer.pushEvents(any())
}
verifyOrder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class MapboxTripStarterTest {
sut.onDetached(mapboxNavigation)

verifyOrder {
replayRouteSession.onDetached(mapboxNavigation)
replayHistorySession.onDetached(mapboxNavigation)
replayRouteSession.onAttached(mapboxNavigation)
replayRouteSession.onDetached(mapboxNavigation)
Expand Down

0 comments on commit 959b5ea

Please sign in to comment.