diff --git a/android-auto-app/build.gradle b/android-auto-app/build.gradle index d4761344b65..b121a01897b 100644 --- a/android-auto-app/build.gradle +++ b/android-auto-app/build.gradle @@ -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") + implementation("com.mapbox.search:mapbox-search-android:1.0.0-beta.43") // Dependencies needed for this example. implementation dependenciesList.androidXCore diff --git a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/car/MainCarSession.kt b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/car/MainCarSession.kt index 015178d955c..4f71c48902d 100644 --- a/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/car/MainCarSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/navigation/examples/androidauto/car/MainCarSession.kt @@ -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 @@ -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() { @@ -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) @@ -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) } } diff --git a/changelog/unreleased/bugfixes/6913.md b/changelog/unreleased/bugfixes/6913.md new file mode 100644 index 00000000000..a9fcc948420 --- /dev/null +++ b/changelog/unreleased/bugfixes/6913.md @@ -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. \ No newline at end of file diff --git a/libnavigation-core/api/current.txt b/libnavigation-core/api/current.txt index c5b38fde81d..d31e1df023b 100644 --- a/libnavigation-core/api/current.txt +++ b/libnavigation-core/api/current.txt @@ -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 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); diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/route/ReplayRouteSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/route/ReplayRouteSession.kt index 17b738d0f75..46d564717fd 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/route/ReplayRouteSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/replay/route/ReplayRouteSession.kt @@ -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 @@ -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 @@ -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) } } @@ -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() + } } } @@ -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) @@ -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): Boolean { diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/MapboxTripStarter.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/MapboxTripStarter.kt index 58751286f13..d62bf08d149 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/MapboxTripStarter.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/MapboxTripStarter.kt @@ -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( @@ -152,14 +152,16 @@ class MapboxTripStarter internal constructor( isLocationPermissionGranted.onEach { granted -> onMapMatchingEnabled(mapboxNavigation, granted) } - MapboxTripStarterType.ReplayRoute -> - replayRouteSession.getOptions().onEach { options -> - onReplayRouteEnabled(mapboxNavigation, options) - } - MapboxTripStarterType.ReplayHistory -> - replayHistorySession.getOptions().onEach { options -> - onReplayHistoryEnabled(mapboxNavigation, options) - } + MapboxTripStarterType.ReplayRoute -> { + replayHistorySession.onDetached(mapboxNavigation) + replayRouteSession.onAttached(mapboxNavigation) + replayRouteSession.getOptions() + } + MapboxTripStarterType.ReplayHistory -> { + replayRouteSession.onDetached(mapboxNavigation) + replayHistorySession.onAttached(mapboxNavigation) + replayHistorySession.getOptions() + } } } } @@ -186,36 +188,6 @@ class MapboxTripStarter internal constructor( } } - /** - * Internally called when the trip type has been set to replay route. - * - * @param mapboxNavigation - * @param options parameters for the [ReplayRouteSession] - */ - private fun onReplayRouteEnabled( - mapboxNavigation: MapboxNavigation, - options: ReplayRouteSessionOptions - ) { - replayHistorySession.onDetached(mapboxNavigation) - replayRouteSession.setOptions(options) - replayRouteSession.onAttached(mapboxNavigation) - } - - /** - * Internally called when the trip type has been set to replay history. - * - * @param mapboxNavigation - * @param options parameters for the [ReplayHistorySession] - */ - private fun onReplayHistoryEnabled( - mapboxNavigation: MapboxNavigation, - options: ReplayHistorySessionOptions - ) { - replayRouteSession.onDetached(mapboxNavigation) - replayHistorySession.setOptions(options) - replayHistorySession.onAttached(mapboxNavigation) - } - /** * Internally called when the trip session needs to be stopped. * diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayRouteSessionTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayRouteSessionTest.kt index 976eaedac72..91058426da4 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayRouteSessionTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/replay/route/ReplayRouteSessionTest.kt @@ -145,7 +145,7 @@ class ReplayRouteSessionTest { sut.setOptions(firstOptions) assertNotEquals(firstOptions, initialOptions) - assertEquals(firstOptions, sut.getOptions()) + assertEquals(firstOptions, sut.getOptions().value) } @Test @@ -158,7 +158,7 @@ class ReplayRouteSessionTest { sut.setOptions(firstOptions) assertNotEquals(firstOptions, initialOptions) - assertEquals(firstOptions, sut.getOptions()) + assertEquals(firstOptions, sut.getOptions().value) } @Test @@ -355,7 +355,6 @@ class ReplayRouteSessionTest { progressObserver.captured.onRouteProgressChanged(secondRouteProgress) verify(exactly = 2) { - replayer.clearEvents() replayer.pushEvents(any()) } verifyOrder { diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/trip/MapboxTripStarterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/trip/MapboxTripStarterTest.kt index e49ac6b8a87..f08d43516a8 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/trip/MapboxTripStarterTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/trip/MapboxTripStarterTest.kt @@ -8,6 +8,7 @@ import com.mapbox.navigation.core.directions.session.RoutesObserver import com.mapbox.navigation.core.replay.history.ReplayHistorySession import com.mapbox.navigation.core.replay.history.ReplayHistorySessionOptions import com.mapbox.navigation.core.replay.route.ReplayRouteSession +import com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions import com.mapbox.navigation.core.trip.session.TripSessionState import com.mapbox.navigation.testing.LoggingFrontendTestRule import com.mapbox.navigation.testing.MainCoroutineRule @@ -44,12 +45,21 @@ class MapboxTripStarterTest { @get:Rule val coroutineRule = MainCoroutineRule() - private val replayRouteSession = mockk(relaxed = true) - private var historyOptions = MutableStateFlow(ReplayHistorySessionOptions.Builder().build()) + private var replayRouteOptions = MutableStateFlow(ReplayRouteSessionOptions.Builder().build()) + private val replayRouteSession = mockk(relaxed = true) { + every { getOptions() } returns replayRouteOptions + every { setOptions(any()) } answers { + replayRouteOptions.value = firstArg() + this@mockk + } + } + private var replayHistoryOptions = MutableStateFlow( + ReplayHistorySessionOptions.Builder().build() + ) private val replayHistorySession = mockk(relaxed = true) { - every { getOptions() } returns historyOptions + every { getOptions() } returns replayHistoryOptions every { setOptions(any()) } answers { - historyOptions.value = firstArg() + replayHistoryOptions.value = firstArg() } } @@ -192,20 +202,20 @@ class MapboxTripStarterTest { val mapboxNavigation = mockMapboxNavigation() sut.enableReplayRoute() - sut.onAttached(mapboxNavigation) - val nextOptions = sut.getReplayRouteSessionOptions().toBuilder() + val customOptions = sut.getReplayRouteSessionOptions().toBuilder() .decodeMinDistance(Double.MAX_VALUE) .build() - sut.enableReplayRoute(nextOptions) + sut.enableReplayRoute(customOptions) + sut.onAttached(mapboxNavigation) verifyOrder { - replayRouteSession.setOptions(any()) + replayRouteSession.setOptions(customOptions) replayRouteSession.onAttached(mapboxNavigation) + } + verify(exactly = 0) { + mapboxNavigation.stopTripSession() replayRouteSession.onDetached(mapboxNavigation) - replayRouteSession.setOptions(nextOptions) - replayRouteSession.onAttached(mapboxNavigation) } - verify(exactly = 0) { mapboxNavigation.stopTripSession() } } @Test @@ -282,17 +292,15 @@ class MapboxTripStarterTest { every { PermissionsManager.areLocationPermissionsGranted(any()) } returns false val mapboxNavigation = mockMapboxNavigation() - sut.enableReplayHistory() - sut.onAttached(mapboxNavigation) val nextOptions = sut.getReplayHistorySessionOptions().toBuilder() .enableSetRoute(false) .build() sut.enableReplayHistory(nextOptions) + sut.onAttached(mapboxNavigation) verifyOrder { - replayHistorySession.setOptions(any()) - replayHistorySession.onAttached(mapboxNavigation) replayHistorySession.setOptions(nextOptions) + replayHistorySession.onAttached(mapboxNavigation) } verify(exactly = 0) { mapboxNavigation.stopTripSession()