Skip to content

Commit 2ce2d19

Browse files
authored
NAVAND-777: add route refresh on demand (#6610)
1 parent 46c2e4f commit 2ce2d19

File tree

49 files changed

+10739
-2155
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+10739
-2155
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
- Added `RouteRefreshController` interface to manage route refreshes. Retrieve it via `MapboxNavigation#routeRefreshController`.
2+
- Added `RouteRefreshController#requestImmediateRouteRefresh` to trigger route refresh request immediately.
3+
- Moved `MapboxNavigation#registerRouteRefreshStateObserver` to `RouteRefreshController#registerRouteRefreshStateObserver`. To migrate, change:
4+
```kotlin
5+
mapboxNavigation.registerRouteRefreshStateObserver(observer)
6+
```
7+
to
8+
```kotlin
9+
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
10+
```
11+
- Moved `MapboxNavigation#unregisterRouteRefreshStateObserver` to `RouteRefreshController#unregisterRouteRefreshStateObserver`. To migrate, change:
12+
```kotlin
13+
mapboxNavigation.unregisterRouteRefreshStateObserver(observer)
14+
```
15+
to
16+
```kotlin
17+
mapboxNavigation.routeRefreshController.unregisterRouteRefreshStateObserver(observer)
18+
```

instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/EVRouteRefreshTest.kt

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.annotation.IdRes
55
import com.mapbox.api.directions.v5.DirectionsCriteria
66
import com.mapbox.api.directions.v5.models.DirectionsWaypoint
77
import com.mapbox.api.directions.v5.models.RouteOptions
8-
import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse
98
import com.mapbox.geojson.Point
109
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
1110
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
@@ -20,6 +19,7 @@ import com.mapbox.navigation.core.directions.session.RoutesExtra
2019
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
2120
import com.mapbox.navigation.instrumentation_tests.R
2221
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
22+
import com.mapbox.navigation.instrumentation_tests.utils.DynamicResponseModifier
2323
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
2424
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.getSuccessfulResultOrThrowException
2525
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.requestRoutes
@@ -715,39 +715,3 @@ class EVRouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.
715715
mockWebServerRule.requestHandlers.add(0, routeHandler)
716716
}
717717
}
718-
719-
private class DynamicResponseModifier : (String) -> String {
720-
721-
var numberOfInvocations = 0
722-
723-
override fun invoke(p1: String): String {
724-
numberOfInvocations++
725-
val originalResponse = DirectionsRefreshResponse.fromJson(p1)
726-
val newRoute = originalResponse.route()!!
727-
.toBuilder()
728-
.legs(
729-
originalResponse.route()!!.legs()!!.map {
730-
it
731-
.toBuilder()
732-
.annotation(
733-
it.annotation()!!
734-
.toBuilder()
735-
.speed(
736-
it.annotation()!!.speed()!!.map {
737-
it + numberOfInvocations * 0.1
738-
}
739-
)
740-
.build()
741-
)
742-
.build()
743-
}
744-
)
745-
.build()
746-
return DirectionsRefreshResponse.builder()
747-
.route(newRoute)
748-
.code(originalResponse.code())
749-
.message(originalResponse.message())
750-
.build()
751-
.toJson()
752-
}
753-
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package com.mapbox.navigation.instrumentation_tests.core
2+
3+
import android.location.Location
4+
import com.mapbox.api.directions.v5.DirectionsCriteria
5+
import com.mapbox.api.directions.v5.models.RouteOptions
6+
import com.mapbox.geojson.Point
7+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
8+
import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions
9+
import com.mapbox.navigation.base.options.NavigationOptions
10+
import com.mapbox.navigation.base.options.RoutingTilesOptions
11+
import com.mapbox.navigation.base.route.NavigationRoute
12+
import com.mapbox.navigation.base.route.RouteRefreshOptions
13+
import com.mapbox.navigation.core.MapboxNavigation
14+
import com.mapbox.navigation.core.MapboxNavigationProvider
15+
import com.mapbox.navigation.core.directions.session.RoutesExtra
16+
import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult
17+
import com.mapbox.navigation.core.routerefresh.RouteRefreshExtra
18+
import com.mapbox.navigation.instrumentation_tests.R
19+
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
20+
import com.mapbox.navigation.instrumentation_tests.utils.DynamicResponseModifier
21+
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
22+
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.getSuccessfulResultOrThrowException
23+
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.requestRoutes
24+
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.routesUpdates
25+
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.sdkTest
26+
import com.mapbox.navigation.instrumentation_tests.utils.coroutines.setNavigationRoutesAndWaitForUpdate
27+
import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRefreshHandler
28+
import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRequestHandler
29+
import com.mapbox.navigation.instrumentation_tests.utils.http.MockRoutingTileEndpointErrorRequestHandler
30+
import com.mapbox.navigation.instrumentation_tests.utils.http.NthAttemptHandler
31+
import com.mapbox.navigation.instrumentation_tests.utils.location.MockLocationReplayerRule
32+
import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText
33+
import com.mapbox.navigation.testing.ui.BaseTest
34+
import com.mapbox.navigation.testing.ui.http.MockRequestHandler
35+
import com.mapbox.navigation.testing.ui.utils.getMapboxAccessTokenFromResources
36+
import kotlinx.coroutines.delay
37+
import kotlinx.coroutines.flow.filter
38+
import kotlinx.coroutines.flow.first
39+
import kotlinx.coroutines.flow.take
40+
import kotlinx.coroutines.flow.toList
41+
import org.junit.Assert.assertEquals
42+
import org.junit.Before
43+
import org.junit.Rule
44+
import org.junit.Test
45+
import java.net.URI
46+
import java.util.concurrent.TimeUnit
47+
48+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
49+
class RouteRefreshOnDemandTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.java) {
50+
51+
@get:Rule
52+
val mapboxNavigationRule = MapboxNavigationRule()
53+
54+
@get:Rule
55+
val mockLocationReplayerRule = MockLocationReplayerRule(mockLocationUpdatesRule)
56+
57+
private lateinit var baseRefreshHandler: MockDirectionsRefreshHandler
58+
private lateinit var mapboxNavigation: MapboxNavigation
59+
private val twoCoordinates = listOf(
60+
Point.fromLngLat(-121.496066, 38.577764),
61+
Point.fromLngLat(-121.480279, 38.57674)
62+
)
63+
64+
@Before
65+
fun setUp() {
66+
baseRefreshHandler = MockDirectionsRefreshHandler(
67+
"route_response_single_route_refresh",
68+
readRawFileText(activity, R.raw.route_response_route_refresh_annotations),
69+
)
70+
}
71+
72+
override fun setupMockLocation(): Location = mockLocationUpdatesRule.generateLocationUpdate {
73+
latitude = twoCoordinates[0].latitude()
74+
longitude = twoCoordinates[0].longitude()
75+
bearing = 190f
76+
}
77+
78+
@Test
79+
fun immediate_route_refresh_before_planned() = sdkTest {
80+
val observer = TestObserver()
81+
val routeRefreshes = mutableListOf<RoutesUpdatedResult>()
82+
setupMockRequestHandlers(baseRefreshHandler)
83+
baseRefreshHandler.jsonResponseModifier = DynamicResponseModifier()
84+
createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5000))
85+
val routeOptions = generateRouteOptions(twoCoordinates)
86+
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
87+
.getSuccessfulResultOrThrowException()
88+
.routes
89+
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
90+
mapboxNavigation.startTripSession()
91+
stayOnInitialPosition()
92+
mapboxNavigation.registerRoutesObserver {
93+
if (it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH) {
94+
routeRefreshes.add(it)
95+
}
96+
}
97+
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)
98+
delay(2500)
99+
100+
mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()
101+
val refreshedRoutes = mapboxNavigation.routesUpdates()
102+
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
103+
.first()
104+
assertEquals(1, routeRefreshes.size)
105+
assertEquals(
106+
224.2239,
107+
requestedRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
108+
0.0001
109+
)
110+
assertEquals(
111+
258.767,
112+
refreshedRoutes.navigationRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
113+
0.0001
114+
)
115+
116+
// no route refresh 4 seconds after refresh on demand
117+
delay(4000)
118+
assertEquals(1, routeRefreshes.size)
119+
120+
delay(1000)
121+
// has new refresh 5 seconds after refresh on demand
122+
mapboxNavigation.routesUpdates()
123+
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
124+
.take(2)
125+
.toList()
126+
127+
assertEquals(
128+
listOf(
129+
RouteRefreshExtra.REFRESH_STATE_STARTED,
130+
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
131+
RouteRefreshExtra.REFRESH_STATE_STARTED,
132+
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
133+
),
134+
observer.getStatesSnapshot()
135+
)
136+
}
137+
138+
@Test
139+
fun route_refresh_on_demand_between_planned_attempts() = sdkTest {
140+
val observer = TestObserver()
141+
baseRefreshHandler.jsonResponseModifier = DynamicResponseModifier()
142+
setupMockRequestHandlers(
143+
NthAttemptHandler(baseRefreshHandler, 1)
144+
)
145+
146+
createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000))
147+
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
148+
mapboxNavigation.startTripSession()
149+
val routeOptions = generateRouteOptions(twoCoordinates)
150+
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
151+
.getSuccessfulResultOrThrowException()
152+
.routes
153+
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)
154+
delay(8000) // refresh interval + accuracy
155+
156+
mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()
157+
158+
// one from immediate and the next planned
159+
mapboxNavigation.routesUpdates()
160+
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
161+
.take(2)
162+
.toList()
163+
164+
assertEquals(
165+
listOf(
166+
RouteRefreshExtra.REFRESH_STATE_STARTED,
167+
RouteRefreshExtra.REFRESH_STATE_CANCELED,
168+
RouteRefreshExtra.REFRESH_STATE_STARTED,
169+
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
170+
RouteRefreshExtra.REFRESH_STATE_STARTED,
171+
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS,
172+
),
173+
observer.getStatesSnapshot()
174+
)
175+
}
176+
177+
private fun createMapboxNavigation(routeRefreshOptions: RouteRefreshOptions) {
178+
mapboxNavigation = MapboxNavigationProvider.create(
179+
NavigationOptions.Builder(activity)
180+
.accessToken(getMapboxAccessTokenFromResources(activity))
181+
.routeRefreshOptions(routeRefreshOptions)
182+
.routingTilesOptions(
183+
RoutingTilesOptions.Builder()
184+
.tilesBaseUri(URI(mockWebServerRule.baseUrl))
185+
.build()
186+
)
187+
.navigatorPredictionMillis(0L)
188+
.build()
189+
)
190+
}
191+
192+
private fun stayOnInitialPosition() {
193+
mockLocationReplayerRule.loopUpdate(
194+
mockLocationUpdatesRule.generateLocationUpdate {
195+
latitude = twoCoordinates[0].latitude()
196+
longitude = twoCoordinates[0].longitude()
197+
bearing = 190f
198+
},
199+
times = 120
200+
)
201+
}
202+
203+
private fun generateRouteOptions(coordinates: List<Point>): RouteOptions {
204+
return RouteOptions.builder().applyDefaultNavigationOptions()
205+
.profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC)
206+
.alternatives(true)
207+
.coordinatesList(coordinates)
208+
.baseUrl(mockWebServerRule.baseUrl) // Comment out to test a real server
209+
.build()
210+
}
211+
212+
private fun setupMockRequestHandlers(
213+
refreshHandler: MockRequestHandler,
214+
) {
215+
mockWebServerRule.requestHandlers.clear()
216+
mockWebServerRule.requestHandlers.add(
217+
MockDirectionsRequestHandler(
218+
"driving-traffic",
219+
readRawFileText(activity, R.raw.route_response_single_route_refresh),
220+
twoCoordinates
221+
)
222+
)
223+
mockWebServerRule.requestHandlers.add(refreshHandler)
224+
mockWebServerRule.requestHandlers.add(MockRoutingTileEndpointErrorRequestHandler())
225+
}
226+
227+
private fun NavigationRoute.getSumOfDurationAnnotationsFromLeg(legIndex: Int): Double =
228+
directionsRoute.legs()?.get(legIndex)
229+
?.annotation()
230+
?.duration()
231+
?.sum()!!
232+
233+
private fun createRouteRefreshOptionsWithInvalidInterval(
234+
intervalMillis: Long
235+
): RouteRefreshOptions {
236+
val routeRefreshOptions = RouteRefreshOptions.Builder()
237+
.intervalMillis(TimeUnit.SECONDS.toMillis(30))
238+
.build()
239+
RouteRefreshOptions::class.java.getDeclaredField("intervalMillis").apply {
240+
isAccessible = true
241+
set(routeRefreshOptions, intervalMillis)
242+
}
243+
return routeRefreshOptions
244+
}
245+
}

instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import com.mapbox.navigation.base.trip.model.RouteProgress
1919
import com.mapbox.navigation.core.MapboxNavigation
2020
import com.mapbox.navigation.core.MapboxNavigationProvider
2121
import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_REFRESH
22+
import com.mapbox.navigation.core.routerefresh.RouteRefreshExtra
23+
import com.mapbox.navigation.core.routerefresh.RouteRefreshStateResult
24+
import com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver
2225
import com.mapbox.navigation.instrumentation_tests.R
2326
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
2427
import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule
@@ -60,7 +63,6 @@ import java.net.URI
6063
import java.util.concurrent.TimeUnit
6164
import kotlin.math.absoluteValue
6265

63-
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
6466
class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.java) {
6567

6668
@get:Rule
@@ -303,6 +305,7 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
303305
waitForRouteToSuccessfullyRefresh()
304306
}
305307

308+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
306309
@Test
307310
fun routeSuccessfullyRefreshesAfterInvalidationOfExpiringData() = sdkTest {
308311
val routeOptions = generateRouteOptions(twoCoordinates)
@@ -313,6 +316,8 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
313316
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(routes)
314317
mapboxNavigation.startTripSession()
315318
stayOnInitialPosition()
319+
val observer = TestObserver()
320+
mapboxNavigation.routeRefreshController.registerRouteRefreshStateObserver(observer)
316321
// act
317322
val refreshedRoutes = mapboxNavigation.routesUpdates()
318323
.filter { it.reason == ROUTES_UPDATE_REASON_REFRESH }
@@ -331,6 +336,16 @@ class RouteRefreshTest : BaseTest<EmptyTestActivity>(EmptyTestActivity::class.ja
331336
)
332337
failByRequestRouteRefreshResponse.failResponse = false
333338
waitForRouteToSuccessfullyRefresh()
339+
assertEquals(
340+
listOf(
341+
RouteRefreshExtra.REFRESH_STATE_STARTED,
342+
RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED,
343+
RouteRefreshExtra.REFRESH_STATE_CLEARED_EXPIRED,
344+
RouteRefreshExtra.REFRESH_STATE_STARTED,
345+
RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS
346+
),
347+
observer.getStatesSnapshot()
348+
)
334349
}
335350

336351
@Test
@@ -906,3 +921,15 @@ private fun NavigationRoute.getIncidentsIdFromTheRoute(legIndex: Int): List<Stri
906921
directionsRoute.legs()?.get(legIndex)
907922
?.incidents()
908923
?.map { it.id() }
924+
925+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
926+
class TestObserver : RouteRefreshStatesObserver {
927+
928+
private val states = mutableListOf<RouteRefreshStateResult>()
929+
930+
override fun onNewState(result: RouteRefreshStateResult) {
931+
states.add(result)
932+
}
933+
934+
fun getStatesSnapshot(): List<String> = states.map { it.state }
935+
}

0 commit comments

Comments
 (0)