Skip to content
Merged
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
18 changes: 18 additions & 0 deletions changelog/unreleased/features/6610.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
- Added `RouteRefreshController` interface to manage route refreshes. Retrieve it via `MapboxNavigation#routeRefreshController`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does exposing RouteRefreshController makes our SDK easier to harder to change?
I found myself in a complex situations with reroute logic a few times. It was very hard to change its structure, because we exposed it to the users in our API. Maybe exposing MapboxNavigation#refreshRoute() leaves us more flexibility for future changes? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's just an interface that accumulates refresh-related API.

- Added `RouteRefreshController#requestImmediateRouteRefresh` to trigger route refresh request immediately.
- Moved `MapboxNavigation#registerRouteRefreshStateObserver` to `RouteRefreshController#registerRouteRefreshStateObserver`. To migrate, change:
```kotlin
mapboxNavigation.registerRouteRefreshStateObserver(observer)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe it makes sense to leave mapboxNavigation.registerRouteRefreshStateObserver for the time being and mark as deprecated to avoid breaking the API? I know, it's experimental, but if the price of keeping it is low, why not to leave it for a few more releases to leave customers a time window to migrate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Idk, I'd like to avoid duplicating API. What's more, we will probably forget to delete it. And when it's not experimental anymore, it won't be possible.
I know we've already removed experimental API (for Drop-In UI) with migration guide in the CHANGELOG.

```
to
```kotlin
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
```
- Moved `MapboxNavigation#unregisterRouteRefreshStateObserver` to `RouteRefreshController#unregisterRouteRefreshStateObserver`. To migrate, change:
```kotlin
mapboxNavigation.unregisterRouteRefreshStateObserver(observer)
```
to
```kotlin
mapboxNavigation.routeRefreshController.unregisterRouteRefreshStateObserver(observer)
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.annotation.IdRes
import com.mapbox.api.directions.v5.DirectionsCriteria
import com.mapbox.api.directions.v5.models.DirectionsWaypoint
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse
import com.mapbox.geojson.Point
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
Expand All @@ -20,6 +19,7 @@ import com.mapbox.navigation.core.directions.session.RoutesExtra
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
import com.mapbox.navigation.instrumentation_tests.R
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.DynamicResponseModifier
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.getSuccessfulResultOrThrowException
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.requestRoutes
Expand Down Expand Up @@ -715,39 +715,3 @@ class EVRouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.
mockWebServerRule.requestHandlers.add(0, routeHandler)
}
}

private class DynamicResponseModifier : (String) -> String {

var numberOfInvocations = 0

override fun invoke(p1: String): String {
numberOfInvocations++
val originalResponse = DirectionsRefreshResponse.fromJson(p1)
val newRoute = originalResponse.route()!!
.toBuilder()
.legs(
originalResponse.route()!!.legs()!!.map {
it
.toBuilder()
.annotation(
it.annotation()!!
.toBuilder()
.speed(
it.annotation()!!.speed()!!.map {
it + numberOfInvocations * 0.1
}
)
.build()
)
.build()
}
)
.build()
return DirectionsRefreshResponse.builder()
.route(newRoute)
.code(originalResponse.code())
.message(originalResponse.message())
.build()
.toJson()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package com.mapbox.navigation.instrumentation_tests.core

import android.location.Location
import com.mapbox.api.directions.v5.DirectionsCriteria
import com.mapbox.api.directions.v5.models.RouteOptions
import com.mapbox.geojson.Point
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
import com.mapbox.navigation.base.options.NavigationOptions
import com.mapbox.navigation.base.options.RoutingTilesOptions
import com.mapbox.navigation.base.route.NavigationRoute
import com.mapbox.navigation.base.route.RouteRefreshOptions
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.routerefresh.RouteRefreshExtra
import com.mapbox.navigation.instrumentation_tests.R
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.DynamicResponseModifier
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.getSuccessfulResultOrThrowException
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.requestRoutes
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.coroutines.setNavigationRoutesAndWaitForUpdate
import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRefreshHandler
import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRequestHandler
import com.mapbox.navigation.instrumentation_tests.utils.http.MockRoutingTileEndpointErrorRequestHandler
import com.mapbox.navigation.instrumentation_tests.utils.http.NthAttemptHandler
import com.mapbox.navigation.instrumentation_tests.utils.location.MockLocationReplayerRule
import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText
import com.mapbox.navigation.testing.ui.BaseTest
import com.mapbox.navigation.testing.ui.http.MockRequestHandler
import com.mapbox.navigation.testing.ui.utils.getMapboxAccessTokenFromResources
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URI
import java.util.concurrent.TimeUnit

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

@get:Rule
val mapboxNavigationRule = MapboxNavigationRule()

@get:Rule
val mockLocationReplayerRule = MockLocationReplayerRule(mockLocationUpdatesRule)

private lateinit var baseRefreshHandler: MockDirectionsRefreshHandler
private lateinit var mapboxNavigation: MapboxNavigation
private val twoCoordinates = listOf(
Point.fromLngLat(-121.496066, 38.577764),
Point.fromLngLat(-121.480279, 38.57674)
)

@Before
fun setUp() {
baseRefreshHandler = MockDirectionsRefreshHandler(
"route_response_single_route_refresh",
readRawFileText(activity, R.raw.route_response_route_refresh_annotations),
)
}

override fun setupMockLocation(): Location = mockLocationUpdatesRule.generateLocationUpdate {
latitude = twoCoordinates[0].latitude()
longitude = twoCoordinates[0].longitude()
bearing = 190f
}

@Test
fun immediate_route_refresh_before_planned() = sdkTest {
val observer = TestObserver()
val routeRefreshes = mutableListOf<RoutesUpdatedResult>()
setupMockRequestHandlers(baseRefreshHandler)
baseRefreshHandler.jsonResponseModifier = DynamicResponseModifier()
createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5000))
val routeOptions = generateRouteOptions(twoCoordinates)
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
.getSuccessfulResultOrThrowException()
.routes
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
mapboxNavigation.startTripSession()
stayOnInitialPosition()
mapboxNavigation.registerRoutesObserver {
if (it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH) {
routeRefreshes.add(it)
}
}
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)
delay(2500)

mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()
val refreshedRoutes = mapboxNavigation.routesUpdates()
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
.first()
assertEquals(1, routeRefreshes.size)
assertEquals(
224.2239,
requestedRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
0.0001
)
assertEquals(
258.767,
refreshedRoutes.navigationRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
0.0001
)

// no route refresh 4 seconds after refresh on demand
delay(4000)
assertEquals(1, routeRefreshes.size)

delay(1000)
// has new refresh 5 seconds after refresh on demand
mapboxNavigation.routesUpdates()
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
.take(2)
.toList()

assertEquals(
listOf(
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
),
observer.getStatesSnapshot()
)
}

@Test
fun route_refresh_on_demand_between_planned_attempts() = sdkTest {
val observer = TestObserver()
baseRefreshHandler.jsonResponseModifier = DynamicResponseModifier()
setupMockRequestHandlers(
NthAttemptHandler(baseRefreshHandler, 1)
)

createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000))
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
mapboxNavigation.startTripSession()
val routeOptions = generateRouteOptions(twoCoordinates)
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
.getSuccessfulResultOrThrowException()
.routes
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)
delay(8000) // refresh interval + accuracy

mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()

// one from immediate and the next planned
mapboxNavigation.routesUpdates()
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
.take(2)
.toList()

assertEquals(
listOf(
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_CANCELED,
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
),
observer.getStatesSnapshot()
)
}

private fun createMapboxNavigation(routeRefreshOptions: RouteRefreshOptions) {
mapboxNavigation = MapboxNavigationProvider.create(
NavigationOptions.Builder(activity)
.accessToken(getMapboxAccessTokenFromResources(activity))
.routeRefreshOptions(routeRefreshOptions)
.routingTilesOptions(
RoutingTilesOptions.Builder()
.tilesBaseUri(URI(mockWebServerRule.baseUrl))
.build()
)
.navigatorPredictionMillis(0L)
.build()
)
}

private fun stayOnInitialPosition() {
mockLocationReplayerRule.loopUpdate(
mockLocationUpdatesRule.generateLocationUpdate {
latitude = twoCoordinates[0].latitude()
longitude = twoCoordinates[0].longitude()
bearing = 190f
},
times = 120
)
}

private fun generateRouteOptions(coordinates: List<Point>): RouteOptions {
return RouteOptions.builder().applyDefaultNavigationOptions()
.profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC)
.alternatives(true)
.coordinatesList(coordinates)
.baseUrl(mockWebServerRule.baseUrl) // Comment out to test a real server
.build()
}

private fun setupMockRequestHandlers(
refreshHandler: MockRequestHandler,
) {
mockWebServerRule.requestHandlers.clear()
mockWebServerRule.requestHandlers.add(
MockDirectionsRequestHandler(
"driving-traffic",
readRawFileText(activity, R.raw.route_response_single_route_refresh),
twoCoordinates
)
)
mockWebServerRule.requestHandlers.add(refreshHandler)
mockWebServerRule.requestHandlers.add(MockRoutingTileEndpointErrorRequestHandler())
}

private fun NavigationRoute.getSumOfDurationAnnotationsFromLeg(legIndex: Int): Double =
directionsRoute.legs()?.get(legIndex)
?.annotation()
?.duration()
?.sum()!!

private fun createRouteRefreshOptionsWithInvalidInterval(
intervalMillis: Long
): RouteRefreshOptions {
val routeRefreshOptions = RouteRefreshOptions.Builder()
.intervalMillis(TimeUnit.SECONDS.toMillis(30))
.build()
RouteRefreshOptions::class.java.getDeclaredField("intervalMillis").apply {
isAccessible = true
set(routeRefreshOptions, intervalMillis)
}
return routeRefreshOptions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import com.mapbox.navigation.base.trip.model.RouteProgress
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.MapboxNavigationProvider
import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_REFRESH
import com.mapbox.navigation.core.routerefresh.RouteRefreshExtra
import com.mapbox.navigation.core.routerefresh.RouteRefreshStateResult
import com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver
import com.mapbox.navigation.instrumentation_tests.R
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
Expand Down Expand Up @@ -60,7 +63,6 @@ import java.net.URI
import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue

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

@get:Rule
Expand Down Expand Up @@ -303,6 +305,7 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
waitForRouteToSuccessfullyRefresh()
}

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
@Test
fun routeSuccessfullyRefreshesAfterInvalidationOfExpiringData() = sdkTest {
val routeOptions = generateRouteOptions(twoCoordinates)
Expand All @@ -313,6 +316,8 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(routes)
mapboxNavigation.startTripSession()
stayOnInitialPosition()
val observer = TestObserver()
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
// act
val refreshedRoutes = mapboxNavigation.routesUpdates()
.filter { it.reason == ROUTES_UPDATE_REASON_REFRESH }
Expand All @@ -331,6 +336,16 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
)
failByRequestRouteRefreshResponse.failResponse = false
waitForRouteToSuccessfullyRefresh()
assertEquals(
listOf(
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED,
RouteRefreshExtra.REFRESH_STATE_CLEARED_EXPIRED,
RouteRefreshExtra.REFRESH_STATE_STARTED,
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS
),
observer.getStatesSnapshot()
)
}

@Test
Expand Down Expand Up @@ -906,3 +921,15 @@ private fun NavigationRoute.getIncidentsIdFromTheRoute(legIndex: Int): List<Stri
directionsRoute.legs()?.get(legIndex)
?.incidents()
?.map { it.id() }

@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
class TestObserver : RouteRefreshStatesObserver {

private val states = mutableListOf<RouteRefreshStateResult>()

override fun onNewState(result: RouteRefreshStateResult) {
states.add(result)
}

fun getStatesSnapshot(): List<String> = states.map { it.state }
}
Loading