Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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,240 @@
package com.mapbox.navigation.instrumentation_tests.core

import android.location.Location
import androidx.annotation.IntegerRes
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.instrumentation_tests.R
import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity
import com.mapbox.navigation.instrumentation_tests.utils.DelayedResponseModifier
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.FailByRequestMockRequestHandler
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.location.MockLocationReplayerRule
import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText
import com.mapbox.navigation.testing.ui.BaseTest
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.After
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 failedRefreshHandlerWrapper: FailByRequestMockRequestHandler
private lateinit var refreshHandler: MockDirectionsRefreshHandler
private lateinit var mapboxNavigation: MapboxNavigation
private val twoCoordinates = listOf(
Point.fromLngLat(-121.496066, 38.577764),
Point.fromLngLat(-121.480279, 38.57674)
)

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

@Before
fun setup() {
setupMockRequestHandlers(
twoCoordinates,
R.raw.route_response_route_refresh,
R.raw.route_response_route_refresh_annotations,
"route_response_route_refresh"
)
}

@After
fun tearDown() {
failedRefreshHandlerWrapper.failResponse = false
}

@Test(timeout = 10_000)
Comment thread
vadzim-vys marked this conversation as resolved.
Outdated
fun route_refresh_on_demand_executes_before_refresh_interval() = sdkTest {
val routeRefreshOptions = RouteRefreshOptions.Builder()
.intervalMillis(TimeUnit.MINUTES.toMillis(1))
.build()
createMapboxNavigation(routeRefreshOptions)
val routeOptions = generateRouteOptions(twoCoordinates)
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
.getSuccessfulResultOrThrowException()
.routes
mapboxNavigation.startTripSession()
stayOnInitialPosition()
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)

mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()
val refreshedRoutes = mapboxNavigation.routesUpdates()
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
.first()

assertEquals(
224.2239,
requestedRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
0.0001
)
assertEquals(
258.767,
refreshedRoutes.navigationRoutes[0].getSumOfDurationAnnotationsFromLeg(0),
0.0001
)
}

@Test
fun route_refresh_on_demand_invalidates_planned_timer() = sdkTest {
val routeRefreshes = mutableListOf<RoutesUpdatedResult>()
val routeRefreshOptions = RouteRefreshOptions.Builder()
.intervalMillis(TimeUnit.SECONDS.toMillis(30))
.build()
RouteRefreshOptions::class.java.getDeclaredField("intervalMillis").apply {
isAccessible = true
set(routeRefreshOptions, 5_000L)
}
refreshHandler.jsonResponseModifier = DynamicResponseModifier()
createMapboxNavigation(routeRefreshOptions)
val routeOptions = generateRouteOptions(twoCoordinates)
val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions)
.getSuccessfulResultOrThrowException()
.routes
mapboxNavigation.startTripSession()
stayOnInitialPosition()
mapboxNavigation.registerRoutesObserver {
if (it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH) {
routeRefreshes.add(it)
}
}
mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes)
delay(2500)

mapboxNavigation.routeRefreshController.requestImmediateRouteRefresh()
mapboxNavigation.routesUpdates()
.filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH }
.first()
assertEquals(1, routeRefreshes.size)

// 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()
}

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(
coordinates: List<Point>,
@IntegerRes routesResponse: Int,
@IntegerRes refreshResponse: Int,
responseTestUuid: String,
acceptedGeometryIndex: Int? = null,
) {
mockWebServerRule.requestHandlers.clear()
mockWebServerRule.requestHandlers.add(
MockDirectionsRequestHandler(
"driving-traffic",
readRawFileText(activity, routesResponse),
coordinates
)
)
refreshHandler = MockDirectionsRefreshHandler(
responseTestUuid,
readRawFileText(activity, refreshResponse),
acceptedGeometryIndex
)
failedRefreshHandlerWrapper = FailByRequestMockRequestHandler(refreshHandler)
mockWebServerRule.requestHandlers.add(failedRefreshHandlerWrapper)
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
}
}
Loading