Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone.

## Unreleased
#### Features
- Added experimental routes preview state, see `MapboxNavigaton#setRoutesPreview`, `MapboxNavigaton#changeRoutesPreviewPrimaryRoute`, `MapboxNavigaton#registerRoutesPreviewObserver`, `MapboxNavigaton#getRoutesPreview`. [#6495](https://github.com/mapbox/mapbox-navigation-android/pull/6495)
#### Bug fixes and improvements

## Mapbox Navigation SDK 2.10.0-alpha.2 - 04 November, 2022
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.mapbox.navigation.instrumentation_tests.core

import android.location.Location
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.trip.model.RouteProgressState
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.MapboxNavigationProvider
import com.mapbox.navigation.core.directions.session.RoutesExtra
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
import com.mapbox.navigation.core.preview.RoutesPreviewExtra
import com.mapbox.navigation.core.preview.RoutesPreviewUpdate
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routeProgressUpdates
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routesPreviewUpdates
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routesUpdates
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.sdkTest
import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider
import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider.toNavigationRoutes
import com.mapbox.navigation.testing.ui.BaseTest
import com.mapbox.navigation.testing.ui.utils.getMapboxAccessTokenFromResources
import com.mapbox.navigation.testing.ui.utils.runOnMainSync
import kotlinx.coroutines.flow.first
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class RoutesPreviewTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.java) {

override fun setupMockLocation(): Location = mockLocationUpdatesRule.generateLocationUpdate {
latitude = 38.894721
longitude = -77.031991
}

@get:Rule
val mapboxNavigationRule = MapboxNavigationRule()
private lateinit var mapboxNavigation: MapboxNavigation

@Before
fun setUp() {
runOnMainSync {
mapboxNavigation = MapboxNavigationProvider.create(
NavigationOptions.Builder(activity)
.accessToken(getMapboxAccessTokenFromResources(activity))
.build()
)
}
}

@Test
fun transitions_free_drive_to_preview_to_active_guidance_to_free_drive() = sdkTest {
var currentRoutesPreview: RoutesPreviewUpdate? = null
Copy link
Contributor

Choose a reason for hiding this comment

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

That's an interesting approach for verifying callback invocations. I've used the other 2 so far:

  1. Like yours but with mutableList to keep track of all the values, not only the latest one (useful if you do the checks at the end of the test) - see CopilotIntegrationTest;
  2. Channels (see HistoryRecordingStateChangeObserverTest);

I'd suggest trying channels here. This way you won't have to wait until data updates with flows and then separately check the current value. Because now it's a bit more difficult to keep in mind when to wait for what and there's a risk of introducing inconsistencies and flakiness.
I mean that code like this:

mapboxNavigation.routesPreviewUpdates()
            .first { it.reason == RoutesPreviewUpdateReasons.PREVIEW_NEW }
        assertEquals(RoutesPreviewUpdateReasons.PREVIEW_NEW, currentRoutesPreview?.reason)
        assertEquals(routes, currentRoutesPreview!!.routesPreview!!.routesList)

can be reduced to sth like:

val routePreviewUpdate = channel.receive()
assertEquals(RoutesPreviewUpdateReasons.PREVIEW_NEW, croutePreviewUpdate.reason)
assertEquals(routes, routePreviewUpdate.routesPreview!!.routesList)

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

main point of those listeners is to verify the case when callbacks weren't called https://github.com/mapbox/mapbox-navigation-android/pull/6495/files#diff-dd6f2bb7ffbeeccc5232ca194ae7b4ed2aecb887a9b87e497ab332e5d2b831c8R67

If I didn't need it at the beginning of the test I could have verify current state like this:

val currentRoutesPreivew = mapboxNavigation.routesPreviewUpdates()
            .first { it.reason == RoutesPreviewUpdateReasons.PREVIEW_NEW }
assertEquals(routes, currentRoutesPreview.routesPreview!!.routesList)

but as I already have currentRoutesPreview I just use it

mapboxNavigation.registerRoutesPreviewObserver { update ->
currentRoutesPreview = update
}
var currentRoutes: RoutesUpdatedResult? = null
mapboxNavigation.registerRoutesObserver { update ->
currentRoutes = update
}
// initial free drive
mapboxNavigation.startTripSession()
assertNull(currentRoutesPreview)
assertNull(currentRoutes)
// set routes preview
val routes = RoutesProvider.dc_very_short(activity).toNavigationRoutes()
mapboxNavigation.setRoutesPreview(routes)
mapboxNavigation.routesPreviewUpdates()
.first { it.reason == RoutesPreviewExtra.PREVIEW_NEW }
assertEquals(RoutesPreviewExtra.PREVIEW_NEW, currentRoutesPreview?.reason)
assertEquals(routes, currentRoutesPreview!!.routesPreview!!.routesList)
assertNull(currentRoutes)
// start active guidance
mapboxNavigation.setNavigationRoutes(currentRoutesPreview!!.routesPreview!!.routesList)
mapboxNavigation.setRoutesPreview(emptyList())
mapboxNavigation.routesUpdates()
.first { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW }
mapboxNavigation.routeProgressUpdates()
.first { it.currentState == RouteProgressState.TRACKING }
mapboxNavigation.routesPreviewUpdates()
.first { it.reason == RoutesPreviewExtra.PREVIEW_CLEAN_UP }
assertEquals(RoutesPreviewExtra.PREVIEW_CLEAN_UP, currentRoutesPreview?.reason)
assertNull(currentRoutesPreview!!.routesPreview)
assertEquals(RoutesExtra.ROUTES_UPDATE_REASON_NEW, currentRoutes!!.reason)
assertEquals(routes, currentRoutes!!.navigationRoutes)
// back to free drive
mapboxNavigation.setNavigationRoutes(emptyList())
mapboxNavigation.routesUpdates()
.first { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_CLEAN_UP }
assertEquals(RoutesPreviewExtra.PREVIEW_CLEAN_UP, currentRoutesPreview?.reason)
assertNull(currentRoutesPreview!!.routesPreview)
assertEquals(RoutesExtra.ROUTES_UPDATE_REASON_CLEAN_UP, currentRoutes!!.reason)
assertEquals(emptyList<NavigationRoute>(), currentRoutes!!.navigationRoutes)
}

@Test
fun route_preview_in_parallel_to_active_guidance() = sdkTest {
var currentRoutesPreview: RoutesPreviewUpdate? = null
mapboxNavigation.registerRoutesPreviewObserver { update ->
currentRoutesPreview = update
}
var currentRoutes: RoutesUpdatedResult? = null
mapboxNavigation.registerRoutesObserver { update ->
currentRoutes = update
}
// initial free drive
mapboxNavigation.startTripSession()
assertNull(currentRoutesPreview)
assertNull(currentRoutes)
// set routes preview
val initialRoutes = RoutesProvider.dc_very_short(activity).toNavigationRoutes()
mapboxNavigation.setRoutesPreview(initialRoutes)
mapboxNavigation.routesPreviewUpdates()
.first { it.reason == RoutesPreviewExtra.PREVIEW_NEW }
// start active guidance
mapboxNavigation.setNavigationRoutes(currentRoutesPreview!!.routesPreview!!.routesList)
mapboxNavigation.setRoutesPreview(emptyList())
mapboxNavigation.routeProgressUpdates()
.first { it.currentState == RouteProgressState.TRACKING }
// preview a different route not leaving action guidance
val updatedRoutes = RoutesProvider.dc_very_short_two_legs(activity).toNavigationRoutes()
mapboxNavigation.setRoutesPreview(updatedRoutes)
mapboxNavigation.routesPreviewUpdates()
.first { it.routesPreview?.routesList == updatedRoutes }
assertEquals(
"active guidance should track initial routes",
initialRoutes,
currentRoutes?.navigationRoutes
)
// user decided to switch to previewed routes
mapboxNavigation.setNavigationRoutes(currentRoutesPreview!!.routesPreview!!.routesList)
mapboxNavigation.setRoutesPreview(emptyList())
mapboxNavigation.routeProgressUpdates().first {
it.navigationRoute == updatedRoutes[0]
}
}

@Test
fun start_active_guidance_from_previewed_alternative_route() = sdkTest {
// set routes preview
val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes()
mapboxNavigation.setRoutesPreview(routes)
val preview = mapboxNavigation.routesPreviewUpdates()
.first { it.reason == RoutesPreviewExtra.PREVIEW_NEW }
// switch to alternative route
mapboxNavigation.changeRoutesPreviewPrimaryRoute(
preview.routesPreview!!.originalRoutesList[1]
)
val updatedPreview = mapboxNavigation.routesPreviewUpdates()
.first { it != preview }
// start active guidance
mapboxNavigation.setNavigationRoutes(updatedPreview.routesPreview!!.routesList)
mapboxNavigation.setRoutesPreview(emptyList())
val routesUpdate = mapboxNavigation.routesUpdates()
.first { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW }
assertEquals(
listOf(
routes[1],
routes[0]
),
routesUpdate.navigationRoutes
)
val previewAlternativeMetadata = updatedPreview.routesPreview!!.alternativesMetadata.first()
val activeGuidanceAlternativeMetadata = mapboxNavigation
.getAlternativeMetadataFor(routes[0])!!
assertEquals(
0,
previewAlternativeMetadata.alternativeId
)
assertNotEquals(
previewAlternativeMetadata.alternativeId,
activeGuidanceAlternativeMetadata.alternativeId
)
assertEquals(
previewAlternativeMetadata.infoFromStartOfPrimary,
activeGuidanceAlternativeMetadata.infoFromStartOfPrimary
)
assertEquals(
previewAlternativeMetadata.forkIntersectionOfAlternativeRoute,
activeGuidanceAlternativeMetadata.forkIntersectionOfAlternativeRoute
)
assertEquals(
previewAlternativeMetadata.infoFromFork,
activeGuidanceAlternativeMetadata.infoFromFork
)
assertEquals(
previewAlternativeMetadata.forkIntersectionOfPrimaryRoute,
activeGuidanceAlternativeMetadata.forkIntersectionOfPrimaryRoute
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.mapbox.api.directions.v5.models.BannerInstructions
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.api.directions.v5.models.VoiceInstructions
import com.mapbox.bindgen.Expected
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.NavigationRouterCallback
import com.mapbox.navigation.base.route.RouterFailure
Expand All @@ -18,6 +19,8 @@ import com.mapbox.navigation.core.RoutesSetError
import com.mapbox.navigation.core.RoutesSetSuccess
import com.mapbox.navigation.core.directions.session.RoutesObserver
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
import com.mapbox.navigation.core.preview.RoutesPreviewObserver
import com.mapbox.navigation.core.preview.RoutesPreviewUpdate
import com.mapbox.navigation.core.trip.session.BannerInstructionsObserver
import com.mapbox.navigation.core.trip.session.RoadObjectsOnRouteObserver
import com.mapbox.navigation.core.trip.session.RouteProgressObserver
Expand All @@ -43,6 +46,20 @@ fun MapboxNavigation.routesUpdates(): Flow<RoutesUpdatedResult> {
}
}

@ExperimentalPreviewMapboxNavigationAPI
fun MapboxNavigation.routesPreviewUpdates(): Flow<RoutesPreviewUpdate> {
val navigation = this
return callbackFlow {
val observer = RoutesPreviewObserver {
trySend(it)
}
navigation.registerRoutesPreviewObserver(observer)
awaitClose {
navigation.unregisterRoutesPreviewObserver(observer)
}
}
}

fun MapboxNavigation.routeProgressUpdates(): Flow<RouteProgress> {
val navigation = this
return callbackFlow {
Expand Down
43 changes: 43 additions & 0 deletions libnavigation-core/api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package com.mapbox.navigation.core {
@UiThread public final class MapboxNavigation {
ctor public MapboxNavigation(com.mapbox.navigation.base.options.NavigationOptions navigationOptions);
method public void cancelRouteRequest(long requestId);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI @kotlin.jvm.Throws(exceptionClasses=IllegalArgumentException::class) public void changeRoutesPreviewPrimaryRoute(com.mapbox.navigation.base.route.NavigationRoute newPrimaryRoute) throws java.lang.IllegalArgumentException;
method public com.mapbox.navigation.core.routealternatives.AlternativeRouteMetadata? getAlternativeMetadataFor(com.mapbox.navigation.base.route.NavigationRoute navigationRoute);
method public java.util.List<com.mapbox.navigation.core.routealternatives.AlternativeRouteMetadata> getAlternativeMetadataFor(java.util.List<com.mapbox.navigation.base.route.NavigationRoute> navigationRoutes);
method public com.mapbox.navigator.Experimental getExperimental();
Expand All @@ -26,6 +27,7 @@ package com.mapbox.navigation.core {
method public com.mapbox.navigation.core.trip.session.eh.RoadObjectMatcher getRoadObjectMatcher();
method public com.mapbox.navigation.core.trip.session.eh.RoadObjectsStore getRoadObjectsStore();
method @Deprecated public java.util.List<com.mapbox.api.directions.v5.models.DirectionsRoute> getRoutes();
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public com.mapbox.navigation.core.preview.RoutesPreview? getRoutesPreview();
method public com.mapbox.navigation.core.navigator.TilesetDescriptorFactory getTilesetDescriptorFactory();
method public com.mapbox.navigation.core.trip.session.TripSessionState getTripSessionState();
method public Integer? getZLevel();
Expand Down Expand Up @@ -53,6 +55,7 @@ package com.mapbox.navigation.core {
method public void registerRouteProgressObserver(com.mapbox.navigation.core.trip.session.RouteProgressObserver routeProgressObserver);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void registerRouteRefreshStateObserver(com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver routeRefreshStatesObserver);
method public void registerRoutesObserver(com.mapbox.navigation.core.directions.session.RoutesObserver routesObserver);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void registerRoutesPreviewObserver(com.mapbox.navigation.core.preview.RoutesPreviewObserver observer);
method public void registerTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
method public void registerVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
method public void requestAlternativeRoutes();
Expand All @@ -73,6 +76,8 @@ package com.mapbox.navigation.core {
method public void setRerouteOptionsAdapter(com.mapbox.navigation.core.reroute.RerouteOptionsAdapter? rerouteOptionsAdapter);
method @Deprecated public void setRoutes(java.util.List<? extends com.mapbox.api.directions.v5.models.DirectionsRoute> routes, int initialLegIndex = 0);
method @Deprecated public void setRoutes(java.util.List<? extends com.mapbox.api.directions.v5.models.DirectionsRoute> routes);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void setRoutesPreview(java.util.List<com.mapbox.navigation.base.route.NavigationRoute> routes, int primaryRouteIndex = 0);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void setRoutesPreview(java.util.List<com.mapbox.navigation.base.route.NavigationRoute> routes);
method public void setTripNotificationInterceptor(com.mapbox.navigation.base.trip.notification.TripNotificationInterceptor? interceptor);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void startReplayTripSession(boolean withForegroundService = true);
method @RequiresPermission(anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}) public void startTripSession(boolean withForegroundService = true);
Expand All @@ -92,6 +97,7 @@ package com.mapbox.navigation.core {
method public void unregisterRouteProgressObserver(com.mapbox.navigation.core.trip.session.RouteProgressObserver routeProgressObserver);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void unregisterRouteRefreshStateObserver(com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver routeRefreshStatesObserver);
method public void unregisterRoutesObserver(com.mapbox.navigation.core.directions.session.RoutesObserver routesObserver);
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void unregisterRoutesPreviewObserver(com.mapbox.navigation.core.preview.RoutesPreviewObserver observer);
method public void unregisterTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
method public void unregisterVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
property public final com.mapbox.navigator.Experimental experimental;
Expand Down Expand Up @@ -384,6 +390,43 @@ package com.mapbox.navigation.core.navigator {

}

package com.mapbox.navigation.core.preview {

@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class RoutesPreview {
method public java.util.List<com.mapbox.navigation.core.routealternatives.AlternativeRouteMetadata> getAlternativesMetadata();
method public java.util.List<com.mapbox.navigation.base.route.NavigationRoute> getOriginalRoutesList();
method public com.mapbox.navigation.base.route.NavigationRoute getPrimaryRoute();
method public int getPrimaryRouteIndex();
method public java.util.List<com.mapbox.navigation.base.route.NavigationRoute> getRoutesList();
property public final java.util.List<com.mapbox.navigation.core.routealternatives.AlternativeRouteMetadata> alternativesMetadata;
property public final java.util.List<com.mapbox.navigation.base.route.NavigationRoute> originalRoutesList;
property public final com.mapbox.navigation.base.route.NavigationRoute primaryRoute;
property public final int primaryRouteIndex;
property public final java.util.List<com.mapbox.navigation.base.route.NavigationRoute> routesList;
}

public final class RoutesPreviewExtra {
field public static final com.mapbox.navigation.core.preview.RoutesPreviewExtra INSTANCE;
field public static final String PREVIEW_CLEAN_UP = "PREVIEW_CLEAN_UP";
field public static final String PREVIEW_NEW = "PREVIEW_NEW";
}

@StringDef({com.mapbox.navigation.core.preview.RoutesPreviewExtra.PREVIEW_NEW, com.mapbox.navigation.core.preview.RoutesPreviewExtra.PREVIEW_CLEAN_UP}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public static @interface RoutesPreviewExtra.RoutePreviewUpdateReason {
}

@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public fun interface RoutesPreviewObserver {
method public void routesPreviewUpdated(com.mapbox.navigation.core.preview.RoutesPreviewUpdate update);
}

@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class RoutesPreviewUpdate {
method public String getReason();
method public com.mapbox.navigation.core.preview.RoutesPreview? getRoutesPreview();
property public final String reason;
property public final com.mapbox.navigation.core.preview.RoutesPreview? routesPreview;
}

}

package com.mapbox.navigation.core.replay {

@UiThread public final class MapboxReplayer {
Expand Down
Loading