From e6834f3be324f6e28d254a116a59e4c228e2ca8d Mon Sep 17 00:00:00 2001 From: Dzina Dybouskaya Date: Fri, 11 Nov 2022 22:25:34 +0300 Subject: [PATCH 01/19] NAVAND-777: add route refresh on demand --- CHANGELOG.md | 2 + .../core/EVRouteRefreshTest.kt | 38 +- .../core/RouteRefreshOnDemandTest.kt | 216 ++ .../core/RouteRefreshStateTest.kt | 547 +++++ .../utils/DynamicResponseModifier.kt | 39 + .../utils/http/NthAttemptHandler.kt | 24 + ...e_refresh_annotations_without_traffic.json | 397 ++++ .../route_response_single_route_refresh.json | 1899 +++++++++++++++++ ..._single_route_refresh_without_traffic.json | 1848 ++++++++++++++++ libnavigation-core/api/current.txt | 1 + .../navigation/core/MapboxNavigation.kt | 36 +- .../core/routerefresh/CancellableHandler.kt | 29 + .../ImmediateRouteRefreshController.kt | 38 + .../navigation/core/routerefresh/Pausable.kt | 8 + .../PlannedRouteRefreshController.kt | 138 ++ .../routerefresh/RefreshObserversManager.kt | 34 + .../routerefresh/RetryRouteRefreshStrategy.kt | 20 + .../core/routerefresh/RouteRefreshLog.kt | 6 + .../RouteRefreshProgressObserver.kt | 12 + .../routerefresh/RouteRefreshStateChanger.kt | 39 + .../routerefresh/RouteRefreshStateHolder.kt | 61 + .../routerefresh/RouteRefreshValidator.kt | 33 + .../core/routerefresh/RouteRefresher.kt | 218 ++ .../routerefresh/RouteRefresherExecutor.kt | 41 + .../routerefresh/RouteRefresherListener.kt | 5 + .../navigation/core/MapboxNavigationTest.kt | 95 +- .../routerefresh/CancellableHandlerTest.kt | 100 + .../ImmediateRouteRefreshControllerTest.kt | 98 + .../PlannedRouteRefreshControllerTest.kt | 645 ++++++ .../RefreshObserversManagerTest.kt | 138 ++ .../RetryRouteRefreshStrategyTest.kt | 75 + .../RouteRefreshStateChangerTest.kt | 128 ++ .../RouteRefreshStateHolderTest.kt | 406 ++++ .../routerefresh/RouteRefreshValidatorTest.kt | 130 ++ .../RouteRefresherExecutorTest.kt | 126 ++ .../core/routerefresh/RouteRefresherTest.kt | 620 ++++++ .../factories/DirectionsResponseFactories.kt | 2 +- 37 files changed, 8176 insertions(+), 116 deletions(-) create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/DynamicResponseModifier.kt create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/NthAttemptHandler.kt create mode 100644 instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations_without_traffic.json create mode 100644 instrumentation-tests/src/main/res/raw/route_response_single_route_refresh.json create mode 100644 instrumentation-tests/src/main/res/raw/route_response_single_route_refresh_without_traffic.json create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/CancellableHandler.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshController.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/Pausable.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategy.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshLog.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshProgressObserver.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolder.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidator.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherListener.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/CancellableHandlerTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshControllerTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManagerTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategyTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolderTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidatorTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index becc6fc7d41..3a5182b81b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone. ## Unreleased #### Features +- Added `MapboxNavigation#refreshRoutesImmediately` to trigger route refresh request immediately. [#6610](https://github.com/mapbox/mapbox-navigation-android/pull/6610) #### Bug fixes and improvements ## Mapbox Navigation SDK 2.11.0-alpha.1 - 13 January, 2023 @@ -591,6 +592,7 @@ This release depends on, and has been tested with, the following Mapbox dependen [Changes between v2.10.0-alpha.1 and v2.10.0-alpha.2](https://github.com/mapbox/mapbox-navigation-android/compare/v2.10.0-alpha.1...v2.10.0-alpha.2) #### Features +- Added `MapboxNavigation#refreshRoutesImmediately` to request an immediate refresh of current routes. #### Bug fixes and improvements - Fixed an issue where "silent waypoints" (not regular waypoints that define legs) had markers added on the map when route line was drawn with `MapboxRouteLineApi` and `MapboxRouteLineView`. [#6526](https://github.com/mapbox/mapbox-navigation-android/pull/6526) - Fixed an issue where `DirectionsResponse#waypoints` list was cleared after a successful non-EV route refresh. [#6539](https://github.com/mapbox/mapbox-navigation-android/pull/6539) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/EVRouteRefreshTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/EVRouteRefreshTest.kt index 0a7fdf69f54..a15e1031b29 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/EVRouteRefreshTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/EVRouteRefreshTest.kt @@ -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 @@ -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 @@ -715,39 +715,3 @@ class EVRouteRefreshTest : BaseTest(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() - } -} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt new file mode 100644 index 00000000000..c879a992cad --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt @@ -0,0 +1,216 @@ +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.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.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.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.net.URI +import java.util.concurrent.TimeUnit + +class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity::class.java) { + + @get:Rule + val mapboxNavigationRule = MapboxNavigationRule() + + @get:Rule + val mockLocationReplayerRule = MockLocationReplayerRule(mockLocationUpdatesRule) + + 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" + ) + } + + @Test + 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.refreshRoutesImmediately() + 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() + val routeRefreshOptions = RouteRefreshOptions.Builder() + .intervalMillis(TimeUnit.SECONDS.toMillis(30)) + .build() + RouteRefreshOptions::class.java.getDeclaredField("intervalMillis").apply { + isAccessible = true + set(routeRefreshOptions, 10_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(5000) + + mapboxNavigation.refreshRoutesImmediately() + mapboxNavigation.routesUpdates() + .filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH } + .first() + assertEquals(1, routeRefreshes.size) + + // no route refresh 6 seconds after refresh on demand + delay(6000) + assertEquals(1, routeRefreshes.size) + + delay(4000) + // has new refresh 10 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): 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, + @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 + ) + mockWebServerRule.requestHandlers.add(FailByRequestMockRequestHandler(refreshHandler)) + mockWebServerRule.requestHandlers.add(MockRoutingTileEndpointErrorRequestHandler()) + } + + private fun NavigationRoute.getSumOfDurationAnnotationsFromLeg(legIndex: Int): Double = + directionsRoute.legs()?.get(legIndex) + ?.annotation() + ?.duration() + ?.sum()!! +} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt new file mode 100644 index 00000000000..74b58e41945 --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt @@ -0,0 +1,547 @@ +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.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.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.clearNavigationRoutesAndWaitForUpdate +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.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.Rule +import org.junit.Test +import java.net.URI +import java.util.concurrent.TimeUnit + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class RouteRefreshStateTest : BaseTest(EmptyTestActivity::class.java) { + + @get:Rule + val mapboxNavigationRule = MapboxNavigationRule() + + private lateinit var mapboxNavigation: MapboxNavigation + private val observer = TestObserver() + + private val coordinates = listOf( + Point.fromLngLat(-121.496066, 38.577764), + Point.fromLngLat(-121.480279, 38.57674) + ) + + override fun setupMockLocation(): Location = mockLocationUpdatesRule.generateLocationUpdate { + latitude = coordinates[0].latitude() + longitude = coordinates[0].longitude() + } + + @Test + fun emptyRoutesOnDemandRefreshTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(1_000)) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + mapboxNavigation.clearNavigationRoutesAndWaitForUpdate() + delay(2000) // refresh interval + accuracy + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.refreshRoutesImmediately() + + assertEquals(emptyList(), observer.getStatesSnapshot()) + } + + @Test + fun routeRefreshOnDemandForInvalidRoutes() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes(enableRefresh = false) + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + delay(7000) // refresh interval + accuracy + + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.refreshRoutesImmediately() + delay(1000) // accuracy + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun invalidRoutesRefreshTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(1_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes(enableRefresh = false) + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + delay(2000) // refresh interval + accuracy + + assertEquals( + listOf(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED), + observer.getStatesSnapshot() + ) + } + + @Test + fun successfulRefreshTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(10_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun successfulFromSecondAttemptRefreshTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler( + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ), + 1 + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun threeFailedAttemptsThenSuccessfulRefreshTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh_without_traffic, + NthAttemptHandler( + createRefreshHandler( + R.raw.route_response_route_refresh_annotations_without_traffic, + "route_response_single_route_refresh_without_traffic" + ), + 3 + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(4_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun routeRefreshIsNotCancelledOnDestroyTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler( + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ), + 2 + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + delay(8000) // refresh interval + accuracy + + mapboxNavigation.onDestroy() + delay(2000) // accuracy: to wait for potential cancelled + + assertEquals( + listOf(RouteRefreshExtra.REFRESH_STATE_STARTED), + observer.getStatesSnapshot() + ) + } + + @Test + fun successfulRouteRefreshOnDemandTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(10_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + mapboxNavigation.refreshRoutesImmediately() + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun failedRouteRefreshOnDemandTest() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler( + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ), + 1 + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(10_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + mapboxNavigation.refreshRoutesImmediately() + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun multipleRouteRefreshesOnDemandTest() = sdkTest { + val refreshHandler = createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + refreshHandler.jsonResponseModifier = DelayedResponseModifier( + 4000, + DynamicResponseModifier() + ) + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + refreshHandler + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(20_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + + mapboxNavigation.refreshRoutesImmediately() + delay(2000) // start refresh request + mapboxNavigation.refreshRoutesImmediately() + mapboxNavigation.refreshRoutesImmediately() + mapboxNavigation.refreshRoutesImmediately() + + waitForRefreshes(2) + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + ), + observer.getStatesSnapshot() + ) + assertEquals(2, refreshHandler.handledRequests.size) + } + + @Test + fun routeRefreshOnDemandThenPlannedTest() = sdkTest { + val refreshHandler = createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + refreshHandler.jsonResponseModifier = DynamicResponseModifier() + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + refreshHandler + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + + mapboxNavigation.refreshRoutesImmediately() + delay(5000) + + waitForRefreshes(2) // immediate + planned + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + ), + observer.getStatesSnapshot() + ) + } + + @Test + fun routeRefreshOnDemandBetweenPlannedAttemptsTest() = sdkTest { + val refreshHandler = createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + refreshHandler.jsonResponseModifier = DynamicResponseModifier() + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler(refreshHandler, 1) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + delay(8000) // refresh interval + accuracy + + mapboxNavigation.refreshRoutesImmediately() + + waitForRefreshes(2) // one from immediate and the next planned + + 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 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 + } + + private fun setupMockRequestHandlers( + coordinates: List, + @IntegerRes routesResponse: Int, + refreshHandler: MockRequestHandler + ) { + mockWebServerRule.requestHandlers.clear() + mockWebServerRule.requestHandlers.add(MockRoutingTileEndpointErrorRequestHandler()) + mockWebServerRule.requestHandlers.add( + MockDirectionsRequestHandler( + "driving-traffic", + readRawFileText(activity, routesResponse), + coordinates + ) + ) + mockWebServerRule.requestHandlers.add(refreshHandler) + } + + private fun createRefreshHandler( + @IntegerRes refreshResponse: Int, + testUuid: String, + ): MockDirectionsRefreshHandler { + return MockDirectionsRefreshHandler( + testUuid, + readRawFileText(activity, refreshResponse), + null + ) + } + + private suspend fun requestRoutes( + coordinates: List = this.coordinates, + enableRefresh: Boolean = true, + ): List = + mapboxNavigation.requestRoutes( + generateRouteOptions(coordinates, enableRefresh) + ) + .getSuccessfulResultOrThrowException() + .routes + + private fun generateRouteOptions( + coordinates: List, + enableRefresh: Boolean, + ): RouteOptions { + return RouteOptions.builder().applyDefaultNavigationOptions() + .profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC) + .enableRefresh(enableRefresh) + .alternatives(true) + .coordinatesList(coordinates) + .baseUrl(mockWebServerRule.baseUrl) // Comment out to test a real server + .build() + } + + private suspend fun waitForRefresh() { + mapboxNavigation.routesUpdates().filter { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH + }.first() + } + + private suspend fun waitForRefreshes(n: Int) { + mapboxNavigation.routesUpdates().filter { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH + }.take(n).toList() + } +} + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +private class TestObserver : RouteRefreshStatesObserver { + + private val states = mutableListOf() + + override fun onNewState(result: RouteRefreshStateResult) { + states.add(result) + } + + fun getStatesSnapshot(): List = states.map { it.state } +} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/DynamicResponseModifier.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/DynamicResponseModifier.kt new file mode 100644 index 00000000000..8e9c62f6d7e --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/DynamicResponseModifier.kt @@ -0,0 +1,39 @@ +package com.mapbox.navigation.instrumentation_tests.utils + +import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse + +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() + } +} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/NthAttemptHandler.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/NthAttemptHandler.kt new file mode 100644 index 00000000000..13879b08538 --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/NthAttemptHandler.kt @@ -0,0 +1,24 @@ +package com.mapbox.navigation.instrumentation_tests.utils.http + +import com.mapbox.navigation.testing.ui.http.MockRequestHandler +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest + +class NthAttemptHandler( + private val originalHandler: MockRequestHandler, + private val successfulAttemptNumber: Int, +) : MockRequestHandler { + + private var attemptsCount = 0 + + override fun handle(request: RecordedRequest): MockResponse? { + val response = originalHandler.handle(request) + val result = if (attemptsCount < successfulAttemptNumber) { + null + } else { + response + } + if (response != null) attemptsCount++ + return result + } +} diff --git a/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations_without_traffic.json b/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations_without_traffic.json new file mode 100644 index 00000000000..da644afc3f4 --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations_without_traffic.json @@ -0,0 +1,397 @@ +{ + "code":"Ok", + "route": { + "legs": [ + { + "closures": [ + { + "geometry_index_start": 1, + "geometry_index_end": 3 + } + ], + "incidents": [ + { + "length": 20, + "affected_road_names": [ + "9th Street" + ], + "id": "14158569638505033", + "type": "lane_restriction", + "congestion": { + "value": 101 + }, + "description": "Möllendorffstrasse: Bauarbeiten zwischen Storkower Strasse und Scheffelstrasse", + "long_description": "Fahrbahnverengung von auf eine Fahrspur wegen Bauarbeiten auf der Möllendorffstrasse in Richtung Süden zwischen Storkower Strasse und Scheffelstrasse.", + "impact": "low", + "alertc_codes": [ + 743 + ], + "geometry_index_start": 10, + "geometry_index_end": 15, + "creation_time": "2022-05-11T14:10:36Z", + "start_time": "2022-04-19T05:00:00Z", + "end_time": "2022-06-30T21:59:00Z", + "iso_3166_1_alpha2": "DE", + "iso_3166_1_alpha3": "DEU", + "lanes_blocked": [] + }, + { + "length": 30, + "affected_road_names": [ + "9th Street" + ], + "id": "11589180127444257", + "type": "lane_restriction", + "congestion": { + "value": 101 + }, + "description": "Rummelsburger Landstrasse: Bauarbeiten um Fritz-König-Weg", + "long_description": "Fahrbahnverengung von auf eine Fahrspur wegen Bauarbeiten auf der Rummelsburger Landstrasse in Richtung Süden zwischen Rummelsburger Landstrasse und Fritz-König-Weg.", + "impact": "low", + "alertc_codes": [ + 743 + ], + "geometry_index_start": 30, + "geometry_index_end": 38, + "creation_time": "2022-05-11T14:10:36Z", + "start_time": "2022-04-29T13:08:25Z", + "end_time": "2022-05-30T21:59:00Z", + "iso_3166_1_alpha2": "DE", + "iso_3166_1_alpha3": "DEU", + "lanes_blocked": [] + } + ], + "annotation": { + "maxspeed": [ + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + } + ], + "speed": [ + 5, + 5, + 12.2, + 12.2, + 6.1, + 6.1, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 1.7, + 13.3, + 9.2, + 9.4, + 11.1, + 11.1, + 5.6, + 5.6, + 8.3, + 8.3, + 2.5, + 12.2, + 12.2, + 9.7, + 9.7, + 9.7, + 9.7, + 9.7, + 11.7, + 8.6, + 9.4, + 11.9, + 8.3, + 12.2, + 12.2, + 12.2, + 12.2 + ], + "distance": [ + 26.6, + 8.9, + 13.3, + 11.4, + 11.7, + 24.5, + 4, + 4.6, + 4.9, + 4.9, + 4.8, + 4.8, + 4.7, + 4, + 5.7, + 4.7, + 4.7, + 4.8, + 4.7, + 4.9, + 4.7, + 4.8, + 34.7, + 11.4, + 126.6, + 30, + 10.4, + 23.4, + 62, + 2.1, + 2.1, + 62, + 65.2, + 4.9, + 116.3, + 35.9, + 83.8, + 3.4, + 3.7, + 41.6, + 101, + 122.1, + 123.4, + 74.4, + 46, + 48.6, + 31.5, + 9.7, + 30.5 + ], + "duration": [ + 5.323, + 10.787, + 1.087, + 0.93, + 1.918, + 4.003, + 0.687, + 0.787, + 0.837, + 0.832, + 0.83, + 0.816, + 0.813, + 0.689, + 0.972, + 0.808, + 0.811, + 0.823, + 0.808, + 0.835, + 0.806, + 0.828, + 20.809, + 0.855, + 13.816, + 3.175, + 0.94, + 2.107, + 11.166, + 0.382, + 0.255, + 7.436, + 26.078, + 0.398, + 9.519, + 3.688, + 8.62, + 0.351, + 0.376, + 4.278, + 8.661, + 14.175, + 13.062, + 6.225, + 5.52, + 3.976, + 2.58, + 0.79, + 52.499 + ] + } + } + ] + } +} \ No newline at end of file diff --git a/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh.json b/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh.json new file mode 100644 index 00000000000..6c273810e6c --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh.json @@ -0,0 +1,1899 @@ +{ + "routes": [ + { + "weight_typical": 366.114, + "duration_typical": 260.004, + "weight_name": "auto", + "weight": 358.967, + "duration": 254.169, + "distance": 2045.35, + "legs": [ + { + "admins": [ + { + "iso_3166_1_alpha3": "USA", + "iso_3166_1": "US" + } + ], + "incidents": [ + { + "length": 50, + "affected_road_names": [ + "9th Street" + ], + "id": "11589180127444257", + "type": "lane_restriction", + "congestion": { + "value": 20 + }, + "description": "Rummelsburger Landstrasse: Bauarbeiten um Fritz-K\u00f6nig-Weg", + "long_description": "Fahrbahnverengung von auf eine Fahrspur wegen Bauarbeiten auf der Rummelsburger Landstrasse in Richtung S\u00fcden zwischen Rummelsburger Landstrasse und Fritz-K\u00f6nig-Weg.", + "impact": "low", + "alertc_codes": [ + 743 + ], + "geometry_index_start": 3, + "geometry_index_end": 8, + "creation_time": "2022-05-11T14:10:36Z", + "start_time": "2022-04-29T13:08:25Z", + "end_time": "3022-05-30T21:59:00Z", + "iso_3166_1_alpha2": "US", + "iso_3166_1_alpha3": "USA", + "lanes_blocked": [] + } + ], + "annotation": { + "maxspeed": [ + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + } + ], + "congestion": [ + "unknown", + "unknown", + "unknown", + "unknown", + "moderate", + "moderate", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "unknown", + "unknown", + "low", + "low", + "low", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "low", + "low", + "low", + "unknown", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "low", + "unknown", + "unknown", + "unknown", + "unknown", + "low" + ], + "speed": [ + 4.2, + 4.2, + 10.6, + 10.6, + 9.4, + 9.4, + 6.1, + 9.7, + 9.7, + 9.7, + 9.7, + 7.5, + 7.5, + 7.5, + 11.4, + 11.4, + 11.4, + 12.8, + 12.8, + 12.8, + 6.9, + 6.9, + 10.3, + 5.8, + 10, + 10.8, + 11.7, + 11.7, + 11.7, + 8.9, + 10.3, + 10.3, + 9.7, + 11.1, + 11.1, + 5.6, + 11.9, + 11.9, + 13.9, + 13.9, + 13.6, + 13.6, + 16.1, + 11.7, + 9.7, + 9.4, + 9.7, + 9.7, + 9.4 + ], + "distance": [ + 27.2, + 8.9, + 13.3, + 11.4, + 10.7, + 109.9, + 121.8, + 6.9, + 4, + 100.3, + 10.6, + 11.2, + 101.6, + 10.2, + 10.1, + 126.8, + 11, + 9.3, + 102, + 11.1, + 11.5, + 110.5, + 63.9, + 58.7, + 64.3, + 48.2, + 18.8, + 3.7, + 14.6, + 113.9, + 19.6, + 45.6, + 64.3, + 26.4, + 38.4, + 64.4, + 48.6, + 31.5, + 9.7, + 30.5, + 50.8, + 13.8, + 61.4, + 122.1, + 34.1, + 29.9, + 7.8, + 8, + 9.2 + ], + "duration": [ + 6.518, + 2.144, + 1.258, + 1.077, + 1.134, + 11.638, + 19.931, + 0.709, + 0.409, + 10.317, + 1.093, + 1.5, + 13.541, + 1.362, + 0.89, + 11.132, + 0.969, + 0.727, + 7.985, + 0.87, + 1.654, + 15.913, + 6.217, + 10.058, + 6.432, + 4.449, + 1.614, + 0.32, + 1.252, + 12.814, + 1.903, + 4.442, + 6.616, + 2.379, + 3.459, + 11.6, + 4.068, + 2.64, + 0.695, + 2.199, + 3.735, + 1.013, + 3.81, + 10.466, + 3.505, + 3.165, + 0.801, + 0.826, + 0.975 + ] + }, + "weight_typical": 366.114, + "duration_typical": 260.004, + "weight": 358.967, + "duration": 254.169, + "steps": [ + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "Drive south on 9th Street. Then, in 600 feet, Turn left onto N Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Drive south on 9th Street. Then, in 600 feet, Turn left onto N Street.", + "distanceAlongGeometry": 182.142 + }, + { + "ssmlAnnouncement": "Turn left onto N Street, U.S. 40 Historic.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn left onto N Street, U.S. 40 Historic.", + "distanceAlongGeometry": 63.333 + } + ], + "intersections": [ + { + "entry": [ + true + ], + "bearings": [ + 199 + ], + "duration": 8.674, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.626, + "geometry_index": 0, + "location": [ + -121.496066, + 38.577764 + ] + }, + { + "entry": [ + false, + true + ], + "in": 0, + "bearings": [ + 19, + 199 + ], + "duration": 4.376, + "turn_weight": 2, + "turn_duration": 2.007, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "weight": 4.901, + "geometry_index": 2, + "location": [ + -121.496199, + 38.577457 + ] + }, + { + "bearings": [ + 18, + 199 + ], + "entry": [ + false, + true + ], + "in": 0, + "turn_weight": 2, + "turn_duration": 2.007, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "geometry_index": 4, + "location": [ + -121.496289, + 38.577247 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "N Street" + }, + { + "type": "delimiter", + "text": "\/" + }, + { + "type": "icon", + "text": "US 40 Historic" + } + ], + "type": "turn", + "modifier": "left", + "text": "N Street \/ US 40 Historic" + }, + "distanceAlongGeometry": 182.142 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "depart", + "instruction": "Drive south on 9th Street.", + "bearing_after": 199, + "bearing_before": 0, + "location": [ + -121.496066, + 38.577764 + ] + }, + "speedLimitSign": "mutcd", + "name": "9th Street", + "weight_typical": 33.221, + "duration_typical": 27.869, + "duration": 27.869, + "distance": 182.142, + "driving_side": "right", + "weight": 33.221, + "mode": "driving", + "geometry": "gerqhAb_pvfFlMfEvC`A`F`B`EpAtDnAny@bX" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "Continue for a half mile.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Continue for a half mile.", + "distanceAlongGeometry": 868.667 + }, + { + "ssmlAnnouncement": "In a quarter mile, Turn left onto 16th Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Turn left onto 16th Street.", + "distanceAlongGeometry": 402.336 + }, + { + "ssmlAnnouncement": "Turn left onto 16th Street, California 1 60.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn left onto 16th Street, California 1 60.", + "distanceAlongGeometry": 80 + } + ], + "intersections": [ + { + "entry": [ + false, + true + ], + "in": 0, + "bearings": [ + 19, + 109 + ], + "duration": 25.358, + "turn_weight": 20, + "turn_duration": 5.395, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "weight": 44.455, + "geometry_index": 6, + "location": [ + -121.496731, + 38.57622 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.739, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.882, + "geometry_index": 7, + "location": [ + -121.495405, + 38.57587 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 110, + 289 + ], + "duration": 0.419, + "turn_weight": 2, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.504, + "geometry_index": 8, + "location": [ + -121.49533, + 38.57585 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 290 + ], + "duration": 10.306, + "turn_weight": 0.5, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 13.1, + "geometry_index": 9, + "location": [ + -121.495287, + 38.575838 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 110, + 289 + ], + "duration": 1.139, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.886, + "geometry_index": 10, + "location": [ + -121.494198, + 38.575543 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 290 + ], + "duration": 1.487, + "turn_weight": 0.5, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.297, + "geometry_index": 11, + "location": [ + -121.494083, + 38.575511 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 288 + ], + "duration": 13.619, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 17.16, + "geometry_index": 12, + "location": [ + -121.49396, + 38.57548 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 112, + 289 + ], + "duration": 1.342, + "turn_weight": 0.5, + "turn_duration": 0.009, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.133, + "geometry_index": 13, + "location": [ + -121.492854, + 38.575189 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 106, + 292 + ], + "duration": 0.904, + "turn_weight": 2, + "turn_duration": 0.026, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.076, + "geometry_index": 14, + "location": [ + -121.492745, + 38.575155 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 286 + ], + "duration": 11.159, + "turn_weight": 0.5, + "turn_duration": 0.008, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 14.16, + "geometry_index": 15, + "location": [ + -121.492633, + 38.57513 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.985, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.683, + "geometry_index": 16, + "location": [ + -121.491254, + 38.574763 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.723, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.863, + "geometry_index": 17, + "location": [ + -121.491134, + 38.574731 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 8.002, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.279, + "geometry_index": 18, + "location": [ + -121.491033, + 38.574704 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.88, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.055, + "geometry_index": 19, + "location": [ + -121.489923, + 38.574409 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.603, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.44, + "geometry_index": 20, + "location": [ + -121.489802, + 38.574377 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 16.003, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 21.58, + "geometry_index": 21, + "location": [ + -121.489677, + 38.574344 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 289 + ], + "duration": 6.246, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 9.628, + "geometry_index": 22, + "location": [ + -121.488475, + 38.574024 + ] + }, + { + "bearings": [ + 110, + 288 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 23, + "location": [ + -121.487777, + 38.573846 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "16th Street" + }, + { + "type": "delimiter", + "text": "\/" + }, + { + "imageBaseURL": "https:\/\/mapbox-navigation-shields.s3.amazonaws.com\/public\/shields\/v4\/US\/ca-160", + "type": "icon", + "text": "CA 160" + } + ], + "type": "turn", + "modifier": "left", + "text": "16th Street \/ CA 160" + }, + "distanceAlongGeometry": 882 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn left onto N Street\/US 40 Historic.", + "modifier": "left", + "bearing_after": 109, + "bearing_before": 199, + "location": [ + -121.496731, + 38.57622 + ] + }, + "speedLimitSign": "mutcd", + "name": "N Street", + "weight_typical": 168.071, + "duration_typical": 111.038, + "duration": 111.038, + "distance": 882, + "driving_side": "right", + "weight": 168.071, + "mode": "driving", + "ref": "US 40 Historic", + "geometry": "wdoqhAthqvfFzT{qAf@uCVuAlQacA~@eF|@uFdQcdAbAyEp@_F|UeuA~@oFt@iElQkdA~@qF`AyF~RcjAbJsj@`Juf@" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "In a quarter mile, Turn right onto J Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Turn right onto J Street.", + "distanceAlongGeometry": 508.666 + }, + { + "ssmlAnnouncement": "Turn right onto J Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn right onto J Street.", + "distanceAlongGeometry": 66.667 + } + ], + "intersections": [ + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 290 + ], + "duration": 12.022, + "turn_weight": 7.5, + "turn_duration": 5.622, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 15.34, + "geometry_index": 24, + "location": [ + -121.487142, + 38.573669 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 4.45, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 7.428, + "geometry_index": 25, + "location": [ + -121.486904, + 38.574216 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 1.991, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.415, + "geometry_index": 26, + "location": [ + -121.486726, + 38.574626 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 1.305, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.075, + "geometry_index": 28, + "location": [ + -121.486643, + 38.574818 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 199 + ], + "duration": 12.844, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 17.711, + "geometry_index": 29, + "location": [ + -121.486588, + 38.574942 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 198 + ], + "duration": 3.965, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.384, + "geometry_index": 30, + "location": [ + -121.486187, + 38.575916 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 198 + ], + "duration": 4.495, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.983, + "geometry_index": 31, + "location": [ + -121.486117, + 38.576083 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 198 + ], + "duration": 6.602, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.064, + "geometry_index": 32, + "location": [ + -121.485951, + 38.576472 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 4.359, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.866, + "geometry_index": 33, + "location": [ + -121.485713, + 38.577019 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 20, + 199 + ], + "duration": 3.427, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.69, + "geometry_index": 34, + "location": [ + -121.485616, + 38.577244 + ] + }, + { + "bearings": [ + 19, + 200 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 2, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 35, + "location": [ + -121.485467, + 38.577569 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "J Street" + } + ], + "type": "turn", + "modifier": "right", + "text": "J Street" + }, + "distanceAlongGeometry": 522 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn left onto 16th Street\/CA 160\/US 40 Historic.", + "modifier": "left", + "bearing_after": 19, + "bearing_before": 110, + "location": [ + -121.487142, + 38.573669 + ] + }, + "speedLimitSign": "mutcd", + "name": "16th Street", + "weight_typical": 93.067, + "duration_typical": 67, + "duration": 67, + "distance": 522, + "driving_side": "right", + "weight": 93.067, + "mode": "driving", + "ref": "CA 160; US 40 Historic", + "geometry": "iejqhAjq~ufFea@{MsXcJ_IkC_AYwFmB{{@aXmIkCiWkIea@{MaMaEiSiHia@uM" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "In a quarter mile, Your destination will be on the right.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Your destination will be on the right.", + "distanceAlongGeometry": 443.209 + }, + { + "ssmlAnnouncement": "Your destination is on the right.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Your destination is on the right.", + "distanceAlongGeometry": 68.056 + } + ], + "intersections": [ + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 199 + ], + "duration": 8.107, + "turn_weight": 8, + "turn_duration": 4.005, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 13.025, + "geometry_index": 36, + "location": [ + -121.485232, + 38.578118 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 289 + ], + "duration": 2.698, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.782, + "geometry_index": 37, + "location": [ + -121.484704, + 38.577976 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 288 + ], + "duration": 0.739, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.382, + "geometry_index": 38, + "location": [ + -121.48436, + 38.577887 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 2.251, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.234, + "geometry_index": 39, + "location": [ + -121.484255, + 38.577859 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 5.766, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.59, + "geometry_index": 40, + "location": [ + -121.483923, + 38.57777 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.048, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.76, + "geometry_index": 41, + "location": [ + -121.48337, + 38.577623 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 3.805, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.138, + "geometry_index": 42, + "location": [ + -121.48322, + 38.577583 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 12.476, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 14.81, + "geometry_index": 43, + "location": [ + -121.482552, + 38.577406 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 5.516, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.284, + "geometry_index": 44, + "location": [ + -121.481224, + 38.577052 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 3.196, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.391, + "geometry_index": 45, + "location": [ + -121.480853, + 38.576954 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.665, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.516, + "geometry_index": 46, + "location": [ + -121.480528, + 38.576867 + ] + }, + { + "bearings": [ + 108, + 289 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 48, + "location": [ + -121.480356, + 38.576821 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "Your destination will be on the right" + } + ], + "type": "arrive", + "modifier": "right", + "text": "Your destination will be on the right" + }, + "distanceAlongGeometry": 459.209 + }, + { + "primary": { + "components": [ + { + "type": "text", + "text": "Your destination is on the right" + } + ], + "type": "arrive", + "modifier": "right", + "text": "Your destination is on the right" + }, + "distanceAlongGeometry": 68.056 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn right onto J Street.", + "modifier": "right", + "bearing_after": 109, + "bearing_before": 19, + "location": [ + -121.485232, + 38.578118 + ] + }, + "speedLimitSign": "mutcd", + "name": "J Street", + "weight_typical": 71.754, + "duration_typical": 54.096, + "duration": 48.262, + "distance": 459.209, + "driving_side": "right", + "weight": 64.607, + "mode": "driving", + "geometry": "k{rqhA~yzufFzG_`@pDoTv@qEpDwSdHqa@nAkH`Jwh@bU_rAbEeVlDiSj@iDn@mDr@gE" + }, + { + "voiceInstructions": [], + "intersections": [ + { + "bearings": [ + 288 + ], + "entry": [ + true + ], + "in": 0, + "admin_index": 0, + "geometry_index": 49, + "location": [ + -121.480256, + 38.576795 + ] + } + ], + "bannerInstructions": [], + "speedLimitUnit": "mph", + "maneuver": { + "type": "arrive", + "instruction": "Your destination is on the right.", + "modifier": "right", + "bearing_after": 0, + "bearing_before": 108, + "location": [ + -121.480256, + 38.576795 + ] + }, + "speedLimitSign": "mutcd", + "name": "J Street", + "weight_typical": 0, + "duration_typical": 0, + "duration": 0, + "distance": 0, + "driving_side": "right", + "weight": 0, + "mode": "driving", + "geometry": "uhpqhA~bqufF??" + } + ], + "distance": 2045.35, + "summary": "N Street, 16th Street" + } + ], + "geometry": "gerqhAb_pvfFlMfEvC`A`F`B`EpAtDnAny@bXzT{qAf@uCVuAlQacA~@eF|@uFdQcdAbAyEp@_F|UeuA~@oFt@iElQkdA~@qF`AyF~RcjAbJsj@`Juf@ea@{MsXcJ_IkC_AYwFmB{{@aXmIkCiWkIea@{MaMaEiSiHia@uMzG_`@pDoTv@qEpDwSdHqa@nAkH`Jwh@bU_rAbEeVlDiSj@iDn@mDr@gE", + "voiceLocale": "en-US" + } + ], + "waypoints": [ + { + "distance": 8.347, + "name": "9th Street", + "location": [ + -121.496066, + 38.577764 + ] + }, + { + "distance": 6.435, + "name": "J Street", + "location": [ + -121.480256, + 38.576795 + ] + } + ], + "code": "Ok", + "uuid": "route_response_single_route_refresh" +} \ No newline at end of file diff --git a/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh_without_traffic.json b/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh_without_traffic.json new file mode 100644 index 00000000000..dd3b7ae5a2d --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/route_response_single_route_refresh_without_traffic.json @@ -0,0 +1,1848 @@ +{ + "routes": [ + { + "weight_typical": 366.114, + "duration_typical": 260.004, + "weight_name": "auto", + "weight": 358.967, + "duration": 254.169, + "distance": 2045.35, + "legs": [ + { + "admins": [ + { + "iso_3166_1_alpha3": "USA", + "iso_3166_1": "US" + } + ], + "incidents": [ + { + "length": 50, + "affected_road_names": [ + "9th Street" + ], + "id": "11589180127444257", + "type": "lane_restriction", + "congestion": { + "value": 20 + }, + "description": "Rummelsburger Landstrasse: Bauarbeiten um Fritz-K\u00f6nig-Weg", + "long_description": "Fahrbahnverengung von auf eine Fahrspur wegen Bauarbeiten auf der Rummelsburger Landstrasse in Richtung S\u00fcden zwischen Rummelsburger Landstrasse und Fritz-K\u00f6nig-Weg.", + "impact": "low", + "alertc_codes": [ + 743 + ], + "geometry_index_start": 3, + "geometry_index_end": 8, + "creation_time": "2022-05-11T14:10:36Z", + "start_time": "2022-04-29T13:08:25Z", + "end_time": "3022-05-30T21:59:00Z", + "iso_3166_1_alpha2": "US", + "iso_3166_1_alpha3": "USA", + "lanes_blocked": [] + } + ], + "annotation": { + "maxspeed": [ + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 40, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "speed": 48, + "unit": "km\/h" + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + } + ], + "speed": [ + 4.2, + 4.2, + 10.6, + 10.6, + 9.4, + 9.4, + 6.1, + 9.7, + 9.7, + 9.7, + 9.7, + 7.5, + 7.5, + 7.5, + 11.4, + 11.4, + 11.4, + 12.8, + 12.8, + 12.8, + 6.9, + 6.9, + 10.3, + 5.8, + 10, + 10.8, + 11.7, + 11.7, + 11.7, + 8.9, + 10.3, + 10.3, + 9.7, + 11.1, + 11.1, + 5.6, + 11.9, + 11.9, + 13.9, + 13.9, + 13.6, + 13.6, + 16.1, + 11.7, + 9.7, + 9.4, + 9.7, + 9.7, + 9.4 + ], + "distance": [ + 27.2, + 8.9, + 13.3, + 11.4, + 10.7, + 109.9, + 121.8, + 6.9, + 4, + 100.3, + 10.6, + 11.2, + 101.6, + 10.2, + 10.1, + 126.8, + 11, + 9.3, + 102, + 11.1, + 11.5, + 110.5, + 63.9, + 58.7, + 64.3, + 48.2, + 18.8, + 3.7, + 14.6, + 113.9, + 19.6, + 45.6, + 64.3, + 26.4, + 38.4, + 64.4, + 48.6, + 31.5, + 9.7, + 30.5, + 50.8, + 13.8, + 61.4, + 122.1, + 34.1, + 29.9, + 7.8, + 8, + 9.2 + ], + "duration": [ + 6.518, + 2.144, + 1.258, + 1.077, + 1.134, + 11.638, + 19.931, + 0.709, + 0.409, + 10.317, + 1.093, + 1.5, + 13.541, + 1.362, + 0.89, + 11.132, + 0.969, + 0.727, + 7.985, + 0.87, + 1.654, + 15.913, + 6.217, + 10.058, + 6.432, + 4.449, + 1.614, + 0.32, + 1.252, + 12.814, + 1.903, + 4.442, + 6.616, + 2.379, + 3.459, + 11.6, + 4.068, + 2.64, + 0.695, + 2.199, + 3.735, + 1.013, + 3.81, + 10.466, + 3.505, + 3.165, + 0.801, + 0.826, + 0.975 + ] + }, + "weight_typical": 366.114, + "duration_typical": 260.004, + "weight": 358.967, + "duration": 254.169, + "steps": [ + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "Drive south on 9th Street. Then, in 600 feet, Turn left onto N Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Drive south on 9th Street. Then, in 600 feet, Turn left onto N Street.", + "distanceAlongGeometry": 182.142 + }, + { + "ssmlAnnouncement": "Turn left onto N Street, U.S. 40 Historic.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn left onto N Street, U.S. 40 Historic.", + "distanceAlongGeometry": 63.333 + } + ], + "intersections": [ + { + "entry": [ + true + ], + "bearings": [ + 199 + ], + "duration": 8.674, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.626, + "geometry_index": 0, + "location": [ + -121.496066, + 38.577764 + ] + }, + { + "entry": [ + false, + true + ], + "in": 0, + "bearings": [ + 19, + 199 + ], + "duration": 4.376, + "turn_weight": 2, + "turn_duration": 2.007, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "weight": 4.901, + "geometry_index": 2, + "location": [ + -121.496199, + 38.577457 + ] + }, + { + "bearings": [ + 18, + 199 + ], + "entry": [ + false, + true + ], + "in": 0, + "turn_weight": 2, + "turn_duration": 2.007, + "mapbox_streets_v8": { + "class": "street" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "geometry_index": 4, + "location": [ + -121.496289, + 38.577247 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "N Street" + }, + { + "type": "delimiter", + "text": "\/" + }, + { + "type": "icon", + "text": "US 40 Historic" + } + ], + "type": "turn", + "modifier": "left", + "text": "N Street \/ US 40 Historic" + }, + "distanceAlongGeometry": 182.142 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "depart", + "instruction": "Drive south on 9th Street.", + "bearing_after": 199, + "bearing_before": 0, + "location": [ + -121.496066, + 38.577764 + ] + }, + "speedLimitSign": "mutcd", + "name": "9th Street", + "weight_typical": 33.221, + "duration_typical": 27.869, + "duration": 27.869, + "distance": 182.142, + "driving_side": "right", + "weight": 33.221, + "mode": "driving", + "geometry": "gerqhAb_pvfFlMfEvC`A`F`B`EpAtDnAny@bX" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "Continue for a half mile.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Continue for a half mile.", + "distanceAlongGeometry": 868.667 + }, + { + "ssmlAnnouncement": "In a quarter mile, Turn left onto 16th Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Turn left onto 16th Street.", + "distanceAlongGeometry": 402.336 + }, + { + "ssmlAnnouncement": "Turn left onto 16th Street, California 1 60.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn left onto 16th Street, California 1 60.", + "distanceAlongGeometry": 80 + } + ], + "intersections": [ + { + "entry": [ + false, + true + ], + "in": 0, + "bearings": [ + 19, + 109 + ], + "duration": 25.358, + "turn_weight": 20, + "turn_duration": 5.395, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 1, + "weight": 44.455, + "geometry_index": 6, + "location": [ + -121.496731, + 38.57622 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.739, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.882, + "geometry_index": 7, + "location": [ + -121.495405, + 38.57587 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 110, + 289 + ], + "duration": 0.419, + "turn_weight": 2, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.504, + "geometry_index": 8, + "location": [ + -121.49533, + 38.57585 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 290 + ], + "duration": 10.306, + "turn_weight": 0.5, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 13.1, + "geometry_index": 9, + "location": [ + -121.495287, + 38.575838 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 110, + 289 + ], + "duration": 1.139, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.886, + "geometry_index": 10, + "location": [ + -121.494198, + 38.575543 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 290 + ], + "duration": 1.487, + "turn_weight": 0.5, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.297, + "geometry_index": 11, + "location": [ + -121.494083, + 38.575511 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 288 + ], + "duration": 13.619, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 17.16, + "geometry_index": 12, + "location": [ + -121.49396, + 38.57548 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 112, + 289 + ], + "duration": 1.342, + "turn_weight": 0.5, + "turn_duration": 0.009, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.133, + "geometry_index": 13, + "location": [ + -121.492854, + 38.575189 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 106, + 292 + ], + "duration": 0.904, + "turn_weight": 2, + "turn_duration": 0.026, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.076, + "geometry_index": 14, + "location": [ + -121.492745, + 38.575155 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 286 + ], + "duration": 11.159, + "turn_weight": 0.5, + "turn_duration": 0.008, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 14.16, + "geometry_index": 15, + "location": [ + -121.492633, + 38.57513 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.985, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.683, + "geometry_index": 16, + "location": [ + -121.491254, + 38.574763 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.723, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.863, + "geometry_index": 17, + "location": [ + -121.491134, + 38.574731 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 8.002, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.279, + "geometry_index": 18, + "location": [ + -121.491033, + 38.574704 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 0.88, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.055, + "geometry_index": 19, + "location": [ + -121.489923, + 38.574409 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.603, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.44, + "geometry_index": 20, + "location": [ + -121.489802, + 38.574377 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 16.003, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 21.58, + "geometry_index": 21, + "location": [ + -121.489677, + 38.574344 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 289 + ], + "duration": 6.246, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 9.628, + "geometry_index": 22, + "location": [ + -121.488475, + 38.574024 + ] + }, + { + "bearings": [ + 110, + 288 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 23, + "location": [ + -121.487777, + 38.573846 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "16th Street" + }, + { + "type": "delimiter", + "text": "\/" + }, + { + "imageBaseURL": "https:\/\/mapbox-navigation-shields.s3.amazonaws.com\/public\/shields\/v4\/US\/ca-160", + "type": "icon", + "text": "CA 160" + } + ], + "type": "turn", + "modifier": "left", + "text": "16th Street \/ CA 160" + }, + "distanceAlongGeometry": 882 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn left onto N Street\/US 40 Historic.", + "modifier": "left", + "bearing_after": 109, + "bearing_before": 199, + "location": [ + -121.496731, + 38.57622 + ] + }, + "speedLimitSign": "mutcd", + "name": "N Street", + "weight_typical": 168.071, + "duration_typical": 111.038, + "duration": 111.038, + "distance": 882, + "driving_side": "right", + "weight": 168.071, + "mode": "driving", + "ref": "US 40 Historic", + "geometry": "wdoqhAthqvfFzT{qAf@uCVuAlQacA~@eF|@uFdQcdAbAyEp@_F|UeuA~@oFt@iElQkdA~@qF`AyF~RcjAbJsj@`Juf@" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "In a quarter mile, Turn right onto J Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Turn right onto J Street.", + "distanceAlongGeometry": 508.666 + }, + { + "ssmlAnnouncement": "Turn right onto J Street.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Turn right onto J Street.", + "distanceAlongGeometry": 66.667 + } + ], + "intersections": [ + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 290 + ], + "duration": 12.022, + "turn_weight": 7.5, + "turn_duration": 5.622, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 15.34, + "geometry_index": 24, + "location": [ + -121.487142, + 38.573669 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 4.45, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 7.428, + "geometry_index": 25, + "location": [ + -121.486904, + 38.574216 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 1.991, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.415, + "geometry_index": 26, + "location": [ + -121.486726, + 38.574626 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 1.305, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.075, + "geometry_index": 28, + "location": [ + -121.486643, + 38.574818 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 199 + ], + "duration": 12.844, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 17.711, + "geometry_index": 29, + "location": [ + -121.486588, + 38.574942 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 198 + ], + "duration": 3.965, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.384, + "geometry_index": 30, + "location": [ + -121.486187, + 38.575916 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 18, + 198 + ], + "duration": 4.495, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.983, + "geometry_index": 31, + "location": [ + -121.486117, + 38.576083 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 198 + ], + "duration": 6.602, + "turn_weight": 2, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 10.064, + "geometry_index": 32, + "location": [ + -121.485951, + 38.576472 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 19, + 199 + ], + "duration": 4.359, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.866, + "geometry_index": 33, + "location": [ + -121.485713, + 38.577019 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 20, + 199 + ], + "duration": 3.427, + "turn_weight": 0.5, + "turn_duration": 0.007, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.69, + "geometry_index": 34, + "location": [ + -121.485616, + 38.577244 + ] + }, + { + "bearings": [ + 19, + 200 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 2, + "turn_duration": 0.021, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 35, + "location": [ + -121.485467, + 38.577569 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "J Street" + } + ], + "type": "turn", + "modifier": "right", + "text": "J Street" + }, + "distanceAlongGeometry": 522 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn left onto 16th Street\/CA 160\/US 40 Historic.", + "modifier": "left", + "bearing_after": 19, + "bearing_before": 110, + "location": [ + -121.487142, + 38.573669 + ] + }, + "speedLimitSign": "mutcd", + "name": "16th Street", + "weight_typical": 93.067, + "duration_typical": 67, + "duration": 67, + "distance": 522, + "driving_side": "right", + "weight": 93.067, + "mode": "driving", + "ref": "CA 160; US 40 Historic", + "geometry": "iejqhAjq~ufFea@{MsXcJ_IkC_AYwFmB{{@aXmIkCiWkIea@{MaMaEiSiHia@uM" + }, + { + "voiceInstructions": [ + { + "ssmlAnnouncement": "In a quarter mile, Your destination will be on the right.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "In a quarter mile, Your destination will be on the right.", + "distanceAlongGeometry": 443.209 + }, + { + "ssmlAnnouncement": "Your destination is on the right.<\/prosody><\/amazon:effect><\/speak>", + "announcement": "Your destination is on the right.", + "distanceAlongGeometry": 68.056 + } + ], + "intersections": [ + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 199 + ], + "duration": 8.107, + "turn_weight": 8, + "turn_duration": 4.005, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 13.025, + "geometry_index": 36, + "location": [ + -121.485232, + 38.578118 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 108, + 289 + ], + "duration": 2.698, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.782, + "geometry_index": 37, + "location": [ + -121.484704, + 38.577976 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 288 + ], + "duration": 0.739, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.382, + "geometry_index": 38, + "location": [ + -121.48436, + 38.577887 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 2.251, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 3.234, + "geometry_index": 39, + "location": [ + -121.484255, + 38.577859 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 5.766, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.59, + "geometry_index": 40, + "location": [ + -121.483923, + 38.57777 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.048, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 1.76, + "geometry_index": 41, + "location": [ + -121.48337, + 38.577623 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 3.805, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 5.138, + "geometry_index": 42, + "location": [ + -121.48322, + 38.577583 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 12.476, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 14.81, + "geometry_index": 43, + "location": [ + -121.482552, + 38.577406 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 5.516, + "turn_weight": 2, + "turn_duration": 2.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 6.284, + "geometry_index": 44, + "location": [ + -121.481224, + 38.577052 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 3.196, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 4.391, + "geometry_index": 45, + "location": [ + -121.480853, + 38.576954 + ] + }, + { + "entry": [ + true, + false + ], + "in": 1, + "bearings": [ + 109, + 289 + ], + "duration": 1.665, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "weight": 2.516, + "geometry_index": 46, + "location": [ + -121.480528, + 38.576867 + ] + }, + { + "bearings": [ + 108, + 289 + ], + "entry": [ + true, + false + ], + "in": 1, + "turn_weight": 0.5, + "turn_duration": 0.019, + "mapbox_streets_v8": { + "class": "secondary" + }, + "is_urban": true, + "admin_index": 0, + "out": 0, + "geometry_index": 48, + "location": [ + -121.480356, + 38.576821 + ] + } + ], + "bannerInstructions": [ + { + "primary": { + "components": [ + { + "type": "text", + "text": "Your destination will be on the right" + } + ], + "type": "arrive", + "modifier": "right", + "text": "Your destination will be on the right" + }, + "distanceAlongGeometry": 459.209 + }, + { + "primary": { + "components": [ + { + "type": "text", + "text": "Your destination is on the right" + } + ], + "type": "arrive", + "modifier": "right", + "text": "Your destination is on the right" + }, + "distanceAlongGeometry": 68.056 + } + ], + "speedLimitUnit": "mph", + "maneuver": { + "type": "turn", + "instruction": "Turn right onto J Street.", + "modifier": "right", + "bearing_after": 109, + "bearing_before": 19, + "location": [ + -121.485232, + 38.578118 + ] + }, + "speedLimitSign": "mutcd", + "name": "J Street", + "weight_typical": 71.754, + "duration_typical": 54.096, + "duration": 48.262, + "distance": 459.209, + "driving_side": "right", + "weight": 64.607, + "mode": "driving", + "geometry": "k{rqhA~yzufFzG_`@pDoTv@qEpDwSdHqa@nAkH`Jwh@bU_rAbEeVlDiSj@iDn@mDr@gE" + }, + { + "voiceInstructions": [], + "intersections": [ + { + "bearings": [ + 288 + ], + "entry": [ + true + ], + "in": 0, + "admin_index": 0, + "geometry_index": 49, + "location": [ + -121.480256, + 38.576795 + ] + } + ], + "bannerInstructions": [], + "speedLimitUnit": "mph", + "maneuver": { + "type": "arrive", + "instruction": "Your destination is on the right.", + "modifier": "right", + "bearing_after": 0, + "bearing_before": 108, + "location": [ + -121.480256, + 38.576795 + ] + }, + "speedLimitSign": "mutcd", + "name": "J Street", + "weight_typical": 0, + "duration_typical": 0, + "duration": 0, + "distance": 0, + "driving_side": "right", + "weight": 0, + "mode": "driving", + "geometry": "uhpqhA~bqufF??" + } + ], + "distance": 2045.35, + "summary": "N Street, 16th Street" + } + ], + "geometry": "gerqhAb_pvfFlMfEvC`A`F`B`EpAtDnAny@bXzT{qAf@uCVuAlQacA~@eF|@uFdQcdAbAyEp@_F|UeuA~@oFt@iElQkdA~@qF`AyF~RcjAbJsj@`Juf@ea@{MsXcJ_IkC_AYwFmB{{@aXmIkCiWkIea@{MaMaEiSiHia@uMzG_`@pDoTv@qEpDwSdHqa@nAkH`Jwh@bU_rAbEeVlDiSj@iDn@mDr@gE", + "voiceLocale": "en-US" + } + ], + "waypoints": [ + { + "distance": 8.347, + "name": "9th Street", + "location": [ + -121.496066, + 38.577764 + ] + }, + { + "distance": 6.435, + "name": "J Street", + "location": [ + -121.480256, + 38.576795 + ] + } + ], + "code": "Ok", + "uuid": "route_response_single_route_refresh_without_traffic" +} \ No newline at end of file diff --git a/libnavigation-core/api/current.txt b/libnavigation-core/api/current.txt index a44335082fc..5c2dc134327 100644 --- a/libnavigation-core/api/current.txt +++ b/libnavigation-core/api/current.txt @@ -43,6 +43,7 @@ package com.mapbox.navigation.core { method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void postUserFeedback(String feedbackType, String description, @com.mapbox.navigation.core.telemetry.events.FeedbackEvent.Source String feedbackSource, String screenshot, String![]? feedbackSubType = emptyArray(), com.mapbox.navigation.core.telemetry.events.FeedbackMetadata feedbackMetadata); method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void postUserFeedback(String feedbackType, String description, @com.mapbox.navigation.core.telemetry.events.FeedbackEvent.Source String feedbackSource, String screenshot, com.mapbox.navigation.core.telemetry.events.FeedbackMetadata feedbackMetadata); method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public com.mapbox.navigation.core.telemetry.events.FeedbackMetadataWrapper provideFeedbackMetadataWrapper(); + method public void refreshRoutesImmediately(); method public void registerArrivalObserver(com.mapbox.navigation.core.arrival.ArrivalObserver arrivalObserver); method public void registerBannerInstructionsObserver(com.mapbox.navigation.core.trip.session.BannerInstructionsObserver bannerInstructionsObserver); method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void registerDeveloperMetadataObserver(com.mapbox.navigation.core.DeveloperMetadataObserver developerMetadataObserver); diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index 58655e55bde..870e749a4f0 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -141,7 +141,6 @@ import com.mapbox.navigator.SetRoutesReason import com.mapbox.navigator.TileEndpointConfiguration import com.mapbox.navigator.TilesConfig import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.launch @@ -268,7 +267,6 @@ class MapboxNavigation @VisibleForTesting internal constructor( private val internalFallbackVersionsObserver: FallbackVersionsObserver private val routeAlternativesController: RouteAlternativesController private val routeRefreshController: RouteRefreshController - private var routeRefreshScope = createChildScope() private val arrivalProgressObserver: ArrivalProgressObserver private val electronicHorizonOptions: ElectronicHorizonOptions = ElectronicHorizonOptions( navigationOptions.eHorizonOptions.length, @@ -553,12 +551,16 @@ class MapboxNavigation @VisibleForTesting internal constructor( threadController, ) routeRefreshController = RouteRefreshControllerProvider.createRouteRefreshController( + threadController, navigationOptions.routeRefreshOptions, directionsSession, routeRefreshRequestDataProvider, routeAlternativesController, evDynamicDataHolder ) + routeRefreshController.registerRouteRefreshObserver { + internalSetNavigationRoutes(it.routes, SetRoutes.RefreshRoutes(it.routeProgressData)) + } defaultRerouteController = MapboxRerouteController( directionsSession, @@ -1022,6 +1024,14 @@ class MapboxNavigation @VisibleForTesting internal constructor( CacheHandleWrapper.requestRoadGraphDataUpdate(navigator.cache, callback) } + /** + * Immediately refresh current navigation routes. + * Listen for refreshed routes using [RoutesObserver]. + */ + fun refreshRoutesImmediately() { + routeRefreshController.requestImmediateRouteRefresh(getNavigationRoutes()) + } + private fun internalSetNavigationRoutes( routes: List, setRoutesInfo: SetRoutes, @@ -1041,7 +1051,6 @@ class MapboxNavigation @VisibleForTesting internal constructor( // do not interrupt reroute when primary route has not changed } } - restartRouteRefreshScope() threadController.getMainScopeAndRootJob().scope.launch(Dispatchers.Main.immediate) { routeUpdateMutex.withLock { historyRecordingStateHandler.setRoutes(routes) @@ -1217,7 +1226,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( ReachabilityService.removeReachabilityObserver(it) reachabilityObserverId = null } - routeRefreshController.unregisterAllRouteRefreshStateObservers() + routeRefreshController.destroy() routesPreviewController.unregisterAllRoutesPreviewObservers() isDestroyed = true @@ -1892,17 +1901,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( private fun createInternalRoutesObserver() = RoutesObserver { result -> latestLegIndex = null routeRefreshRequestDataProvider.onNewRoutes() - if (result.navigationRoutes.isNotEmpty()) { - routeRefreshScope.launch { - val refreshed = routeRefreshController.refresh( - result.navigationRoutes - ) - internalSetNavigationRoutes( - refreshed.routes, - SetRoutes.RefreshRoutes(refreshed.routeProgressData), - ) - } - } + routeRefreshController.requestPlannedRouteRefresh(result.navigationRoutes) } private fun createInternalOffRouteObserver() = OffRouteObserver { offRoute -> @@ -2066,13 +2065,6 @@ class MapboxNavigation @VisibleForTesting internal constructor( } } - private fun createChildScope() = threadController.getMainScopeAndRootJob().scope - - private fun restartRouteRefreshScope() { - routeRefreshScope.cancel() - routeRefreshScope = createChildScope() - } - @OptIn(ExperimentalPreviewMapboxNavigationAPI::class) private fun setUpRouteCacheClearer() { registerRoutesObserver(routesCacheClearer) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/CancellableHandler.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/CancellableHandler.kt new file mode 100644 index 00000000000..0054dba8dfd --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/CancellableHandler.kt @@ -0,0 +1,29 @@ +package com.mapbox.navigation.core.routerefresh + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +internal class CancellableHandler( + private val scope: CoroutineScope +) { + + private val jobs = linkedMapOf Unit>() + + fun postDelayed(timeout: Long, block: Runnable, cancellationCallback: () -> Unit) { + val job = scope.launch { + delay(timeout) + block.run() + } + jobs[job] = cancellationCallback + job.invokeOnCompletion { jobs.remove(job) } + } + + fun cancelAll() { + HashMap(jobs).forEach { + it.value.invoke() + it.key.cancel() + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshController.kt new file mode 100644 index 00000000000..9c9cd1bf7d3 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshController.kt @@ -0,0 +1,38 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.route.NavigationRoute + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +internal class ImmediateRouteRefreshController( + private val routeRefresherExecutor: RouteRefresherExecutor, + private val plannedRefreshController: Pausable, + private val stateHolder: RouteRefreshStateHolder, + private val listener: RouteRefresherListener +) { + + private val callback = object : RouteRefresherProgressCallback { + + override fun onStarted() { + stateHolder.onStarted() + } + + override fun onResult(routeRefresherResult: RouteRefresherResult) { + if (routeRefresherResult.success) { + stateHolder.onSuccess() + } else { + stateHolder.onFailure(null) + plannedRefreshController.resume() + } + listener.onRoutesRefreshed(routeRefresherResult) + } + } + + fun requestRoutesRefresh(routes: List) { + if (routes.isEmpty()) { + return + } + plannedRefreshController.pause() + routeRefresherExecutor.postRoutesToRefresh(routes, callback) + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/Pausable.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/Pausable.kt new file mode 100644 index 00000000000..0c4aa8a6e09 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/Pausable.kt @@ -0,0 +1,8 @@ +package com.mapbox.navigation.core.routerefresh + +internal interface Pausable { + + fun pause() + + fun resume() +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt new file mode 100644 index 00000000000..d3d4eec4ad0 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt @@ -0,0 +1,138 @@ +package com.mapbox.navigation.core.routerefresh + +import androidx.annotation.VisibleForTesting +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.RouteRefreshOptions +import com.mapbox.navigation.utils.internal.logI +import kotlinx.coroutines.CoroutineScope + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +internal class PlannedRouteRefreshController @VisibleForTesting constructor( + private val routeRefresherExecutor: RouteRefresherExecutor, + private val routeRefreshOptions: RouteRefreshOptions, + private val stateHolder: RouteRefreshStateHolder, + private val listener: RouteRefresherListener, + private val cancellableHandler: CancellableHandler, + private val retryStrategy: RetryRouteRefreshStrategy, +) : Pausable { + + constructor( + routeRefresherExecutor: RouteRefresherExecutor, + routeRefreshOptions: RouteRefreshOptions, + stateHolder: RouteRefreshStateHolder, + scope: CoroutineScope, + listener: RouteRefresherListener, + ) : this( + routeRefresherExecutor, + routeRefreshOptions, + stateHolder, + listener, + CancellableHandler(scope), + RetryRouteRefreshStrategy(maxRetryCount = 2) + ) + + private var routesToRefresh: List? = null + + fun startRoutesRefreshing(routes: List) { + cancellableHandler.cancelAll() + routesToRefresh = null + if (routes.isEmpty()) { + logI("Routes are empty", RouteRefreshLog.LOG_CATEGORY) + stateHolder.reset() + return + } + val routesValidationResults = routes.map { RouteRefreshValidator.validateRoute(it) } + if ( + routesValidationResults.any { it is RouteRefreshValidator.RouteValidationResult.Valid } + ) { + routesToRefresh = routes + scheduleNewUpdate(routes) + } else { + val message = + RouteRefreshValidator.joinValidationErrorMessages( + routesValidationResults.mapIndexed { index, routeValidationResult -> + routeValidationResult to routes[index] + } + ) + val logMessage = "No routes which could be refreshed. $message" + logI(logMessage, RouteRefreshLog.LOG_CATEGORY) + stateHolder.onFailure(logMessage) + stateHolder.reset() + } + } + + override fun pause() { + cancellableHandler.cancelAll() + } + + override fun resume() { + routesToRefresh?.let { + if (retryStrategy.shouldRetry()) { + scheduleUpdateRetry(it) + } + } + } + + private fun scheduleNewUpdate(routes: List) { + retryStrategy.reset() + postAttempt { + executePlannedRefresh(routes, shouldNotifyOnStart = true) + } + } + + private fun scheduleUpdateRetry(routes: List) { + postAttempt { executePlannedRefresh(routes, shouldNotifyOnStart = false) } + } + + private fun postAttempt(attemptBlock: () -> Unit) { + cancellableHandler.postDelayed( + timeout = routeRefreshOptions.intervalMillis, + block = attemptBlock, + cancellationCallback = { stateHolder.onCancel() } + ) + } + + private fun executePlannedRefresh( + routes: List, + shouldNotifyOnStart: Boolean + ) { + routeRefresherExecutor.postRoutesToRefresh( + routes, + createCallback(routes, shouldNotifyOnStart) + ) + } + + private fun createCallback( + routes: List, + shouldNotifyOnStart: Boolean + ): RouteRefresherProgressCallback { + return object : RouteRefresherProgressCallback { + + override fun onStarted() { + if (shouldNotifyOnStart) { + stateHolder.onStarted() + } + } + + override fun onResult(routeRefresherResult: RouteRefresherResult) { + retryStrategy.onNextAttempt() + if (routeRefresherResult.success) { + stateHolder.onSuccess() + listener.onRoutesRefreshed(routeRefresherResult) + } else { + if (retryStrategy.shouldRetry()) { + scheduleUpdateRetry(routes) + } else { + stateHolder.onFailure(null) + if (routeRefresherResult.refreshedRoutes != routes) { + listener.onRoutesRefreshed(routeRefresherResult) + } else { + scheduleNewUpdate(routes) + } + } + } + } + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt new file mode 100644 index 00000000000..77382426b3a --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt @@ -0,0 +1,34 @@ +package com.mapbox.navigation.core.routerefresh + +import java.util.concurrent.CopyOnWriteArraySet + +internal fun interface RouteRefreshObserver { + + fun onRoutesRefreshed(routeInfo: RefreshedRouteInfo) +} + +internal class RefreshObserversManager : RouteRefresherListener { + + private val refreshObservers = CopyOnWriteArraySet() + + fun registerObserver(observer: RouteRefreshObserver) { + refreshObservers.add(observer) + } + + fun unregisterObserver(observer: RouteRefreshObserver) { + refreshObservers.remove(observer) + } + + fun unregisterAllObservers() { + refreshObservers.clear() + } + + override fun onRoutesRefreshed(result: RouteRefresherResult) { + refreshObservers.forEach { observer -> + observer.onRoutesRefreshed(result.toRefreshedRoutesInfo()) + } + } + + private fun RouteRefresherResult.toRefreshedRoutesInfo() = + RefreshedRouteInfo(refreshedRoutes, routeProgressData) +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategy.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategy.kt new file mode 100644 index 00000000000..24292fc6fc1 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategy.kt @@ -0,0 +1,20 @@ +package com.mapbox.navigation.core.routerefresh + +internal class RetryRouteRefreshStrategy( + private val maxRetryCount: Int +) { + + private var attemptNumber = 0 + + fun reset() { + attemptNumber = 0 + } + + fun shouldRetry(): Boolean { + return attemptNumber <= maxRetryCount + } + + fun onNextAttempt() { + attemptNumber++ + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshLog.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshLog.kt new file mode 100644 index 00000000000..ed99ef19607 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshLog.kt @@ -0,0 +1,6 @@ +package com.mapbox.navigation.core.routerefresh + +internal object RouteRefreshLog { + + const val LOG_CATEGORY = "RouteRefreshController" +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshProgressObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshProgressObserver.kt new file mode 100644 index 00000000000..b4372bd564e --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshProgressObserver.kt @@ -0,0 +1,12 @@ +package com.mapbox.navigation.core.routerefresh + +internal interface RouteRefreshProgressObserver { + + fun onStarted() + + fun onSuccess() + + fun onFailure(message: String?) + + fun onCancel() +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt new file mode 100644 index 00000000000..20963b1def8 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt @@ -0,0 +1,39 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +internal object RouteRefreshStateChanger { + + private val allowedTransitions = mapOf( + null to listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ), + RouteRefreshExtra.REFRESH_STATE_STARTED to listOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + null, + ), + RouteRefreshExtra.REFRESH_STATE_CANCELED to listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + null, + ), + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED to listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + null, + ), + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS to listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + null + ), + ) + + fun canChange( + @RouteRefreshExtra.RouteRefreshState from: String?, + @RouteRefreshExtra.RouteRefreshState to: String? + ): Boolean { + return to in allowedTransitions[from]!! + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolder.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolder.kt new file mode 100644 index 00000000000..8a7e6d5bf44 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolder.kt @@ -0,0 +1,61 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import java.util.concurrent.CopyOnWriteArraySet + +@ExperimentalPreviewMapboxNavigationAPI +internal class RouteRefreshStateHolder : RouteRefreshProgressObserver { + + private val observers = CopyOnWriteArraySet() + + private var state: RouteRefreshStateResult? = null + + override fun onStarted() { + onNewState(RouteRefreshExtra.REFRESH_STATE_STARTED) + } + + override fun onSuccess() { + onNewState(RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS) + } + + override fun onFailure(message: String?) { + onNewState(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, message) + } + + override fun onCancel() { + onNewState(RouteRefreshExtra.REFRESH_STATE_CANCELED) + } + + fun reset() { + onNewState(null) + } + + fun registerRouteRefreshStateObserver(observer: RouteRefreshStatesObserver) { + observers.add(observer) + state?.let { observer.onNewState(it) } + } + + fun unregisterRouteRefreshStateObserver( + observer: RouteRefreshStatesObserver + ) { + observers.remove(observer) + } + + fun unregisterAllRouteRefreshStateObservers() { + observers.clear() + } + + private fun onNewState( + @RouteRefreshExtra.RouteRefreshState state: String?, + message: String? = null + ) { + val oldState = this.state?.state + if (oldState != state && RouteRefreshStateChanger.canChange(from = oldState, to = state)) { + val newState = state?.let { RouteRefreshStateResult(it, message) } + this.state = newState + if (newState != null) { + observers.forEach { it.onNewState(newState) } + } + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidator.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidator.kt new file mode 100644 index 00000000000..2e1d6e07415 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidator.kt @@ -0,0 +1,33 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.route.NavigationRoute + +internal object RouteRefreshValidator { + + fun joinValidationErrorMessages( + validations: List> + ): String = validations.mapNotNull { pair -> + (pair.first as? RouteValidationResult.Invalid)?.let { invalidResult -> + "${pair.second.id} ${invalidResult.reason}" + } + }.joinToString(separator = ". ") + + fun validateRoute(route: NavigationRoute): RouteValidationResult = when { + route.routeOptions.enableRefresh() != true -> + RouteValidationResult.Invalid("RouteOptions#enableRefresh is false") + route.directionsRoute.requestUuid()?.isNotBlank() != true -> + RouteValidationResult.Invalid( + "DirectionsRoute#requestUuid is blank. " + + "This can be caused by a route being generated by " + + "an Onboard router (in offline mode). " + + "Make sure to switch to an Offboard route when possible, " + + "only Offboard routes support the refresh feature." + ) + else -> RouteValidationResult.Valid + } + + sealed class RouteValidationResult { + object Valid : RouteValidationResult() + data class Invalid(val reason: String) : RouteValidationResult() + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt new file mode 100644 index 00000000000..f480eea5d41 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt @@ -0,0 +1,218 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.RouteLeg +import com.mapbox.navigation.base.internal.RouteRefreshRequestData +import com.mapbox.navigation.base.internal.route.update +import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.NavigationRouterRefreshCallback +import com.mapbox.navigation.base.route.NavigationRouterRefreshError +import com.mapbox.navigation.core.RouteProgressData +import com.mapbox.navigation.core.RouteProgressDataProvider +import com.mapbox.navigation.core.directions.session.RouteRefresh +import com.mapbox.navigation.core.ev.EVDynamicDataHolder +import com.mapbox.navigation.utils.internal.logE +import com.mapbox.navigation.utils.internal.logI +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull +import java.util.Date +import kotlin.coroutines.resume + +internal data class RouteRefresherResult( + val success: Boolean, + val refreshedRoutes: List, + val routeProgressData: RouteProgressData +) + +internal class RouteRefresher( + private val routeProgressDataProvider: RouteProgressDataProvider, + private val evDataHolder: EVDynamicDataHolder, + private val routeDiffProvider: DirectionsRouteDiffProvider, + private val routeRefresh: RouteRefresh, + private val localDateProvider: () -> Date, +) { + + suspend fun refresh( + routes: List, + routeRefreshTimeout: Long + ): RouteRefresherResult { + val routeProgressData = routeProgressDataProvider.getRouteRefreshRequestDataOrWait() + val refreshedRoutes = refreshRoutesOrNull(routes, routeProgressData, routeRefreshTimeout) + return if (refreshedRoutes.any { it != null }) { + RouteRefresherResult( + true, + refreshedRoutes.mapIndexed { index, navigationRoute -> + navigationRoute ?: routes[index] + }, + routeProgressData + ) + } else { + RouteRefresherResult( + false, + routes.map { removeExpiringDataFromRoute(it, routeProgressData.legIndex) }, + routeProgressData + ) + } + } + + private suspend fun refreshRoutesOrNull( + routes: List, + routeProgressData: RouteProgressData, + timeout: Long + ): List { + return coroutineScope { + routes.map { route -> + async { + withTimeoutOrNull(timeout) { + refreshRouteOrNull(route, routeProgressData) + } + } + } + }.awaitAll() + } + + private suspend fun refreshRouteOrNull( + route: NavigationRoute, + routeProgressData: RouteProgressData, + ): NavigationRoute? { + val validationResult = RouteRefreshValidator.validateRoute(route) + if (validationResult is RouteRefreshValidator.RouteValidationResult.Invalid) { + logI( + "route ${route.id} can't be refreshed because ${validationResult.reason}", + RouteRefreshLog.LOG_CATEGORY + ) + return null + } + val routeRefreshRequestData = RouteRefreshRequestData( + routeProgressData.legIndex, + routeProgressData.routeGeometryIndex, + routeProgressData.legGeometryIndex, + evDataHolder.currentData(route.routeOptions.unrecognizedJsonProperties ?: emptyMap()) + ) + return when (val result = requestRouteRefresh(route, routeRefreshRequestData)) { + is RouteRefreshResult.Fail -> { + logE( + "Route refresh error: ${result.error.message} " + + "throwable=${result.error.throwable}", + RouteRefreshLog.LOG_CATEGORY + ) + null + } + is RouteRefreshResult.Success -> { + logI( + "Received refreshed route ${result.route.id}", + RouteRefreshLog.LOG_CATEGORY + ) + logRoutesDiff( + newRoute = result.route, + oldRoute = route, + currentLegIndex = routeRefreshRequestData.legIndex + ) + result.route + } + } + } + + private suspend fun requestRouteRefresh( + route: NavigationRoute, + routeRefreshRequestData: RouteRefreshRequestData + ): RouteRefreshResult = + suspendCancellableCoroutine { continuation -> + val requestId = routeRefresh.requestRouteRefresh( + route, + routeRefreshRequestData, + object : NavigationRouterRefreshCallback { + override fun onRefreshReady(route: NavigationRoute) { + continuation.resume(RouteRefreshResult.Success(route)) + } + + override fun onFailure(error: NavigationRouterRefreshError) { + continuation.resume(RouteRefreshResult.Fail(error)) + } + } + ) + continuation.invokeOnCancellation { + logI( + "Route refresh for route ${route.id} was cancelled after timeout", + RouteRefreshLog.LOG_CATEGORY + ) + routeRefresh.cancelRouteRefreshRequest(requestId) + } + } + + private fun logRoutesDiff( + newRoute: NavigationRoute, + oldRoute: NavigationRoute, + currentLegIndex: Int, + ) { + val routeDiffs = routeDiffProvider.buildRouteDiffs( + oldRoute, + newRoute, + currentLegIndex, + ) + if (routeDiffs.isEmpty()) { + logI( + "No changes in annotations for route ${newRoute.id}", + RouteRefreshLog.LOG_CATEGORY + ) + } else { + for (diff in routeDiffs) { + logI(diff, RouteRefreshLog.LOG_CATEGORY) + } + } + } + + private fun removeExpiringDataFromRoute( + route: NavigationRoute, + currentLegIndex: Int, + ): NavigationRoute { + val routeLegs = route.directionsRoute.legs() + val directionsRouteBlock: DirectionsRoute.() -> DirectionsRoute = { + toBuilder().legs( + routeLegs?.mapIndexed { legIndex, leg -> + val legHasAlreadyBeenPassed = legIndex < currentLegIndex + if (legHasAlreadyBeenPassed) { + leg + } else { + removeExpiredDataFromLeg(leg) + } + } + ).build() + } + return route.update( + directionsRouteBlock = directionsRouteBlock, + directionsResponseBlock = { this } + ) + } + + private fun removeExpiredDataFromLeg(leg: RouteLeg): RouteLeg { + val oldAnnotation = leg.annotation() + return leg.toBuilder() + .annotation( + oldAnnotation?.let { nonNullOldAnnotation -> + nonNullOldAnnotation.toBuilder() + .congestion(nonNullOldAnnotation.congestion()?.map { "unknown" }) + .congestionNumeric(nonNullOldAnnotation.congestionNumeric()?.map { null }) + .build() + } + ) + .incidents( + leg.incidents()?.filter { + val parsed = parseISO8601DateToLocalTimeOrNull(it.endTime()) + ?: return@filter true + val currentDate = localDateProvider() + parsed > currentDate + } + ) + .build() + } + + private sealed class RouteRefreshResult { + data class Success(val route: NavigationRoute) : RouteRefreshResult() + data class Fail(val error: NavigationRouterRefreshError) : RouteRefreshResult() + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt new file mode 100644 index 00000000000..fb7aabf4205 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt @@ -0,0 +1,41 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.route.NavigationRoute +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal interface RouteRefresherProgressCallback { + + fun onStarted() + + fun onResult(routeRefresherResult: RouteRefresherResult) +} + +internal class RouteRefresherExecutor( + private val routeRefresher: RouteRefresher, + private val scope: CoroutineScope, + private val timeout: Long, +) { + + private val mutex = Mutex() + private val queue = ArrayDeque, RouteRefresherProgressCallback>>() + + fun postRoutesToRefresh( + routes: List, + callback: RouteRefresherProgressCallback + ) { + queue.clear() + queue.add(routes to callback) + scope.launch { + mutex.withLock { + queue.removeFirstOrNull()?.let { + it.second.onStarted() + val result = routeRefresher.refresh(it.first, timeout) + it.second.onResult(result) + } + } + } + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherListener.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherListener.kt new file mode 100644 index 00000000000..4b967c2f126 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherListener.kt @@ -0,0 +1,5 @@ +package com.mapbox.navigation.core.routerefresh + +internal fun interface RouteRefresherListener { + fun onRoutesRefreshed(result: RouteRefresherResult) +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt index f61710d105e..4099c725193 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt @@ -30,6 +30,7 @@ import com.mapbox.navigation.core.reroute.NavigationRerouteController import com.mapbox.navigation.core.reroute.RerouteController import com.mapbox.navigation.core.reroute.RerouteState import com.mapbox.navigation.core.routerefresh.RefreshedRouteInfo +import com.mapbox.navigation.core.routerefresh.RouteRefreshObserver import com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry import com.mapbox.navigation.core.testutil.createRoutesUpdatedResult @@ -65,7 +66,6 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.delay @@ -723,7 +723,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { coVerifyOrder { routeProgressDataProvider.onNewRoutes() - routeRefreshController.refresh(routes) + routeRefreshController.requestPlannedRouteRefresh(routes) } } @@ -744,7 +744,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { coVerify(exactly = 1) { routeProgressDataProvider.onNewRoutes() } - coVerify(exactly = 0) { routeRefreshController.refresh(any()) } + coVerify(exactly = 1) { routeRefreshController.requestPlannedRouteRefresh(emptyList()) } } @Test @@ -1442,49 +1442,6 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { } } - @Test - fun `route refresh of previous route completes after new route is set`() = - coroutineRule.runBlockingTest { - every { NavigationComponentProvider.createDirectionsSession(any()) } answers { - MapboxDirectionsSession(mockk(relaxed = true)) - } - createMapboxNavigation() - val first = listOf(createNavigationRoute(createDirectionsRoute(requestUuid = "test1"))) - val second = listOf(createNavigationRoute(createDirectionsRoute(requestUuid = "test2"))) - val refreshOrFirstRoute = CompletableDeferred() - coEvery { routeRefreshController.refresh(any()) } coAnswers { - CompletableDeferred().await() // never completes - firstArg() - } - coEvery { routeRefreshController.refresh(first) } coAnswers { - refreshOrFirstRoute.await() - RefreshedRouteInfo( - listOf(createNavigationRoute(createDirectionsRoute(requestUuid = "test1.1"))), - RouteProgressData(1, 2, 3) - ) - } - coEvery { tripSession.setRoutes(second, any()) } coAnswers { - NativeSetRouteValue(second, emptyList()) - } - - val routesUpdates = mutableListOf() - mapboxNavigation.registerRoutesObserver { - routesUpdates.add(it) - } - mapboxNavigation.setNavigationRoutes(first) - mapboxNavigation.setNavigationRoutes(second) - refreshOrFirstRoute.complete(Unit) - - assertEquals( - listOf(first, second), - routesUpdates.map { it.navigationRoutes } - ) - assertEquals( - listOf(RoutesExtra.ROUTES_UPDATE_REASON_NEW, RoutesExtra.ROUTES_UPDATE_REASON_NEW), - routesUpdates.map { it.reason } - ) - } - @Test fun `set route - new routes immediately interrupts reroute`() = coroutineRule.runBlockingTest { createMapboxNavigation() @@ -1657,8 +1614,8 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { ) } returns NativeSetRouteError("some error") coEvery { - routeRefreshController.refresh(routes) - } returns RefreshedRouteInfo(refreshedRoutes, routeProgressData) + routeRefreshController.requestPlannedRouteRefresh(routes) + } coAnswers { RefreshedRouteInfo(refreshedRoutes, routeProgressData) } verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } routeObserversSlot.forEach { @@ -1764,15 +1721,15 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { listOf(routeWithId("id#0"), routeWithId("id#1"), routeWithId("id#2")) val acceptedRefreshRoutes = listOf(refreshedRoutes[0], refreshedRoutes[2]) val ignoredRefreshRoutes = listOf(IgnoredRoute(refreshedRoutes[1], invalidRouteReason)) - coEvery { - routeRefreshController.refresh(routes) - } returns RefreshedRouteInfo(refreshedRoutes, routeProgressData) coEvery { tripSession.setRoutes(refreshedRoutes, ofType(SetRoutes.RefreshRoutes::class)) } returns NativeSetRouteValue(refreshedRoutes, listOf(alternativeWithId("id#2"))) routeObserversSlot.forEach { it.onRoutesChanged(RoutesUpdatedResult(routes, emptyList(), reason)) } + interceptRefreshObserver().onRoutesRefreshed( + RefreshedRouteInfo(refreshedRoutes, routeProgressData) + ) coVerify(exactly = 1) { tripSession.setRoutes( @@ -1807,15 +1764,15 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } val refreshedRoutes = listOf(mockk(relaxed = true)) - coEvery { - routeRefreshController.refresh(routes) - } returns RefreshedRouteInfo(refreshedRoutes, routeProgressData) coEvery { tripSession.setRoutes(any(), any()) } returns NativeSetRouteError("some error") routeObserversSlot.forEach { it.onRoutesChanged(RoutesUpdatedResult(routes, ignoredRoutes, reason)) } + interceptRefreshObserver().onRoutesRefreshed( + RefreshedRouteInfo(refreshedRoutes, routeProgressData) + ) coVerify(exactly = 1) { tripSession.setRoutes( @@ -1973,13 +1930,13 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { } @Test - fun onDestroyUnregisterAllRouteRefreshStateObserver() { + fun onDestroyDestroysRouteRefreshController() { createMapboxNavigation() mapboxNavigation.onDestroy() verify(exactly = 1) { - routeRefreshController.unregisterAllRouteRefreshStateObservers() + routeRefreshController.destroy() } } @@ -2019,6 +1976,32 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { } } + @Test + fun refreshRoutesImmediatelyNoRoutes() { + createMapboxNavigation() + + mapboxNavigation.refreshRoutesImmediately() + + verify(exactly = 1) { routeRefreshController.requestImmediateRouteRefresh(emptyList()) } + } + + @Test + fun refreshRoutesImmediatelyHasRoutes() = coroutineRule.runBlockingTest { + val routes = listOf(createNavigationRoute(), createNavigationRoute()) + createMapboxNavigation() + every { directionsSession.routes } returns routes + + mapboxNavigation.refreshRoutesImmediately() + + verify(exactly = 1) { routeRefreshController.requestImmediateRouteRefresh(routes) } + } + + private fun interceptRefreshObserver(): RouteRefreshObserver { + val observers = mutableListOf() + verify { routeRefreshController.registerRouteRefreshObserver(capture(observers)) } + return observers.last() + } + private fun alternativeWithId(mockId: String): RouteAlternative { val mockedRoute = mockk { every { routeId } returns mockId diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/CancellableHandlerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/CancellableHandlerTest.kt new file mode 100644 index 00000000000..f3c5b6bb6b7 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/CancellableHandlerTest.kt @@ -0,0 +1,100 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.testing.MainCoroutineRule +import io.mockk.clearAllMocks +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class CancellableHandlerTest { + + @get:Rule + val coroutineRule = MainCoroutineRule() + + private val runnable = mockk(relaxed = true) + private val cancellation = mockk<() -> Unit>(relaxed = true) + private val handler = CancellableHandler(coroutineRule.coroutineScope) + + @Test + fun postExecutesAfterTimeout() = coroutineRule.runBlockingTest { + val timeout = 7000L + + handler.postDelayed(timeout, runnable, cancellation) + + verify(exactly = 0) { + runnable.run() + cancellation.invoke() + } + coroutineRule.testDispatcher.advanceTimeBy(timeout) + verify(exactly = 1) { + runnable.run() + } + verify(exactly = 0) { + cancellation.invoke() + } + } + + @Test + fun postRemovesBlockFromMap() = coroutineRule.runBlockingTest { + val timeout = 7000L + handler.postDelayed(timeout, runnable, cancellation) + coroutineRule.testDispatcher.advanceTimeBy(timeout) + + handler.cancelAll() + + verify(exactly = 0) { + cancellation.invoke() + } + } + + @Test + fun cancelAll_noJobs() = coroutineRule.runBlockingTest { + handler.cancelAll() + } + + @Test + fun cancelAll_hasIncompleteJob() = coroutineRule.runBlockingTest { + handler.postDelayed(1000, runnable, cancellation) + + handler.cancelAll() + + verify(exactly = 1) { + cancellation.invoke() + } + coroutineRule.testDispatcher.advanceTimeBy(1000) + verify(exactly = 0) { + runnable.run() + } + } + + @Test + fun cancelAll_multipleJobs() = coroutineRule.runBlockingTest { + val runnable2 = mockk(relaxed = true) + val cancellation2 = mockk<() -> Unit>(relaxed = true) + handler.postDelayed(1000, runnable, cancellation) + handler.postDelayed(500, runnable2, cancellation2) + + handler.cancelAll() + + verify(exactly = 1) { + cancellation.invoke() + cancellation2.invoke() + } + } + + @Test + fun postJobAfterItHasBeenCancel() = coroutineRule.runBlockingTest { + handler.postDelayed(1000, runnable, cancellation) + handler.cancelAll() + clearAllMocks(answers = false) + + handler.postDelayed(1000, runnable, cancellation) + coroutineRule.testDispatcher.advanceTimeBy(1000) + + verify(exactly = 1) { runnable.run() } + verify(exactly = 0) { cancellation.invoke() } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshControllerTest.kt new file mode 100644 index 00000000000..a8102093276 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ImmediateRouteRefreshControllerTest.kt @@ -0,0 +1,98 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.core.RouteProgressData +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class ImmediateRouteRefreshControllerTest { + + private val routeRefresherExecutor = mockk(relaxed = true) + private val plannedRefreshController = mockk(relaxed = true) + private val stateHolder = mockk(relaxed = true) + private val listener = mockk(relaxed = true) + private val routes = listOf(mockk()) + + private val sut = ImmediateRouteRefreshController( + routeRefresherExecutor, + plannedRefreshController, + stateHolder, + listener + ) + + @Test + fun requestRoutesRefreshWithEmptyRoutes() { + sut.requestRoutesRefresh(emptyList()) + + verify(exactly = 0) { + plannedRefreshController.pause() + routeRefresherExecutor.postRoutesToRefresh(any(), any()) + } + } + + @Test + fun requestRoutesRefreshPausesPlannedController() { + sut.requestRoutesRefresh(routes) + + verify(exactly = 1) { plannedRefreshController.pause() } + } + + @Test + fun requestRoutesRefreshPostsRefreshRequest() { + sut.requestRoutesRefresh(routes) + + verify(exactly = 1) { routeRefresherExecutor.postRoutesToRefresh(routes, any()) } + } + + @Test + fun routesRefreshStarted() { + sut.requestRoutesRefresh(routes) + val callback = interceptCallback() + + callback.onStarted() + + verify(exactly = 1) { stateHolder.onStarted() } + } + + @Test + fun routesRefreshFinishedSuccessfully() { + sut.requestRoutesRefresh(routes) + val callback = interceptCallback() + val result = RouteRefresherResult( + true, + listOf(mockk(), mockk()), + RouteProgressData(1, 2, 3) + ) + + callback.onResult(result) + + verify(exactly = 1) { stateHolder.onSuccess() } + verify(exactly = 1) { listener.onRoutesRefreshed(result) } + } + + @Test + fun routesRefreshFinishedWithFailure() { + sut.requestRoutesRefresh(routes) + val callback = interceptCallback() + val result = RouteRefresherResult( + false, + listOf(mockk(), mockk()), + RouteProgressData(1, 2, 3) + ) + + callback.onResult(result) + + verify(exactly = 1) { stateHolder.onFailure(null) } + verify(exactly = 1) { plannedRefreshController.resume() } + verify(exactly = 1) { listener.onRoutesRefreshed(result) } + } + + private fun interceptCallback(): RouteRefresherProgressCallback { + val callbacks = mutableListOf() + verify { routeRefresherExecutor.postRoutesToRefresh(any(), capture(callbacks)) } + return callbacks.last() + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt new file mode 100644 index 00000000000..3759d5bad11 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt @@ -0,0 +1,645 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.RouteRefreshOptions +import com.mapbox.navigation.core.RouteProgressData +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import com.mapbox.navigation.utils.internal.LoggerFrontend +import io.mockk.Called +import io.mockk.clearAllMocks +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class PlannedRouteRefreshControllerTest { + + private val logger = mockk(relaxed = true) + + @get:Rule + val loggerRule = LoggingFrontendTestRule(logger) + + private val executor = mockk(relaxed = true) + private val stateHolder = mockk(relaxed = true) + private val listener = mockk(relaxed = true) + private val cancellableHandler = mockk(relaxed = true) + private val retryStrategy = mockk(relaxed = true) + private val interval = 40000L + private val routeRefreshOptions = RouteRefreshOptions.Builder().intervalMillis(interval).build() + private val sut = PlannedRouteRefreshController( + executor, + routeRefreshOptions, + stateHolder, + listener, + cancellableHandler, + retryStrategy + ) + + @Before + fun setUp() { + mockkObject(RouteRefreshValidator) + } + + @After + fun tearDown() { + unmockkObject(RouteRefreshValidator) + } + + @Test + fun startRoutesRefreshing_emptyRoutes() { + sut.startRoutesRefreshing(emptyList()) + + verify(exactly = 1) { + cancellableHandler.cancelAll() + stateHolder.reset() + logger.logI("Routes are empty", "RouteRefreshController") + } + verify(exactly = 0) { + stateHolder.onFailure(any()) + RouteRefreshValidator.validateRoute(any()) + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun startRoutesRefreshing_allRoutesAreInvalid() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val validation1 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 1") + val validation2 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 2") + val message = "some message" + val expectedLogMessage = "No routes which could be refreshed. $message" + every { + RouteRefreshValidator.validateRoute(route1) + } returns validation1 + every { + RouteRefreshValidator.validateRoute(route2) + } returns validation2 + every { + RouteRefreshValidator.joinValidationErrorMessages( + listOf(validation1 to route1, validation2 to route2) + ) + } returns message + + sut.startRoutesRefreshing(listOf(route1, route2)) + + verify(exactly = 1) { + logger.logI(expectedLogMessage, "RouteRefreshController") + } + verifyOrder { + stateHolder.onFailure(expectedLogMessage) + stateHolder.reset() + } + verify(exactly = 0) { cancellableHandler.postDelayed(any(), any(), any()) } + } + + @Test + fun startRoutesRefreshing_someRoutesAreInvalid() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + val validation1 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 1") + every { + RouteRefreshValidator.validateRoute(route1) + } returns validation1 + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + + verify(exactly = 1) { + retryStrategy.reset() + cancellableHandler.postDelayed(interval, any(), any()) + } + } + + @Test + fun startRoutesRefreshing_allRoutesAreValid() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + + verify(exactly = 1) { + retryStrategy.reset() + cancellableHandler.postDelayed(interval, any(), any()) + } + } + + @Test + fun startRoutesRefreshing_resetsRetryStrategy() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + + verify(exactly = 1) { retryStrategy.reset() } + } + + @Test + fun startRoutesRefreshing_postsCancellableTask() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + + val attemptBlocks = mutableListOf() + val cancellableBlocks = mutableListOf<() -> Unit>() + verify(exactly = 1) { + cancellableHandler.postDelayed( + interval, + capture(attemptBlocks), + capture(cancellableBlocks) + ) + } + cancellableBlocks.last().invoke() + verify { stateHolder.onCancel() } + attemptBlocks.last().run() + verify(exactly = 1) { executor.postRoutesToRefresh(routes, any()) } + } + + @Test + fun startRoutesRefreshing_notifiesOnStart() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + + startRequest() + verify(exactly = 1) { + stateHolder.onStarted() + } + } + + @Test + fun finishRequestIncrementsAttempt() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + finishRequest( + RouteRefresherResult( + false, + listOf(route1, route2), + RouteProgressData(1, 2, 3) + ) + ) + + verify(exactly = 1) { retryStrategy.onNextAttempt() } + } + + @Test + fun finishRequestSuccessfully() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + + sut.startRoutesRefreshing(routes) + val result = RouteRefresherResult(true, listOf(route1, route2), RouteProgressData(1, 2, 3)) + finishRequest(result) + + verify(exactly = 1) { + stateHolder.onSuccess() + listener.onRoutesRefreshed(result) + } + verify { cancellableHandler wasNot Called } + } + + @Test + fun finishRequestUnsuccessfullyShouldRetry() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { retryStrategy.shouldRetry() } returns true + + sut.startRoutesRefreshing(routes) + clearMocks(retryStrategy, answers = false) + val result = RouteRefresherResult(false, listOf(route1, route2), RouteProgressData(1, 2, 3)) + finishRequest(result) + + verify(exactly = 1) { + cancellableHandler.postDelayed(interval, any(), any()) + } + verify(exactly = 0) { + stateHolder.onFailure(any()) + listener.onRoutesRefreshed(any()) + retryStrategy.reset() + } + } + + @Test + fun finishRequestUnsuccessfullyShouldRetryDoesNotNotifyOnStart() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { retryStrategy.shouldRetry() } returns true + + sut.startRoutesRefreshing(routes) + val result = RouteRefresherResult(false, listOf(route1, route2), RouteProgressData(1, 2, 3)) + finishRequest(result) + startRequest() + + verify(exactly = 0) { stateHolder.onStarted() } + } + + @Test + fun finishRequestUnsuccessfullyShouldNotRetryRoutesDidNotChange() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { retryStrategy.shouldRetry() } returns false + + sut.startRoutesRefreshing(routes) + clearMocks(retryStrategy, answers = false) + val result = RouteRefresherResult(false, listOf(route1, route2), RouteProgressData(1, 2, 3)) + finishRequest(result) + + verifyOrder { + stateHolder.onFailure(null) + retryStrategy.reset() + cancellableHandler.postDelayed(interval, any(), any()) + } + verify(exactly = 0) { + listener.onRoutesRefreshed(any()) + } + } + + @Test + fun finishRequestUnsuccessfullyShouldNotRetryRoutesDidNotChangeShouldNotifyOnStart() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { retryStrategy.shouldRetry() } returns false + + sut.startRoutesRefreshing(routes) + val result = RouteRefresherResult(false, listOf(route1, route2), RouteProgressData(1, 2, 3)) + finishRequest(result) + startRequest() + + verify(exactly = 1) { stateHolder.onStarted() } + } + + @Test + fun finishRequestUnsuccessfullyShouldNotRetryRoutesChanged() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val newRoute1 = mockk(relaxed = true) + val newRoute2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { retryStrategy.shouldRetry() } returns false + + sut.startRoutesRefreshing(routes) + val result = RouteRefresherResult( + false, + listOf(newRoute1, newRoute2), + RouteProgressData(1, 2, 3) + ) + finishRequest(result) + + verify(exactly = 1) { + stateHolder.onFailure(null) + listener.onRoutesRefreshed(result) + } + verify(exactly = 0) { + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + private fun startRequest() { + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed( + any(), + capture(attemptBlocks), + any() + ) + } + attemptBlocks.last().run() + val progressCallbacks = mutableListOf() + verify(exactly = 1) { executor.postRoutesToRefresh(any(), capture(progressCallbacks)) } + progressCallbacks.last().onStarted() + } + + private fun finishRequest(result: RouteRefresherResult) { + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed( + any(), + capture(attemptBlocks), + any() + ) + } + clearMocks(cancellableHandler, answers = false) + attemptBlocks.last().run() + val progressCallbacks = mutableListOf() + verify(exactly = 1) { executor.postRoutesToRefresh(any(), capture(progressCallbacks)) } + clearMocks(executor, answers = false) + progressCallbacks.last().onResult(result) + } + + @Test + fun pause() { + sut.pause() + + verify(exactly = 1) { cancellableHandler.cancelAll() } + } + + @Test + fun resumeNoRoutes() { + sut.resume() + + verify(exactly = 0) { + retryStrategy.shouldRetry() + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun resumeHasRoutesShouldNotRetry() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns false + + sut.resume() + + verify(exactly = 0) { cancellableHandler.postDelayed(any(), any(), any()) } + } + + @Test + fun resumeHasRoutesShouldRetry() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + verify(exactly = 1) { cancellableHandler.postDelayed(interval, any(), any()) } + } + + @Test + fun resumeHasRoutesShouldRetryDoesNotNotifyOnStart() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + startRequest() + + verify(exactly = 0) { stateHolder.onStarted() } + } + + @Test + fun emptyRoutesAreNotRemembered() { + sut.startRoutesRefreshing(emptyList()) + clearAllMocks(answers = false) + + sut.resume() + + verify(exactly = 0) { + retryStrategy.shouldRetry() + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun invalidRoutesAreNotRemembered() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val validation1 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 1") + val validation2 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 2") + val message = "some message" + every { + RouteRefreshValidator.validateRoute(route1) + } returns validation1 + every { + RouteRefreshValidator.validateRoute(route2) + } returns validation2 + every { + RouteRefreshValidator.joinValidationErrorMessages( + listOf(validation1 to route1, validation2 to route2) + ) + } returns message + sut.startRoutesRefreshing(listOf(route1, route2)) + clearAllMocks(answers = false) + + sut.resume() + + verify(exactly = 0) { + retryStrategy.shouldRetry() + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun partiallyInvalidRoutesAreRemembered() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + val validation1 = RouteRefreshValidator.RouteValidationResult.Invalid("some reason 1") + every { + RouteRefreshValidator.validateRoute(route1) + } returns validation1 + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed(interval, capture(attemptBlocks), any()) + } + attemptBlocks.last().run() + verify(exactly = 1) { executor.postRoutesToRefresh(routes, any()) } + } + + @Test + fun validRoutesAreRemembered() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed(interval, capture(attemptBlocks), any()) + } + attemptBlocks.last().run() + verify(exactly = 1) { executor.postRoutesToRefresh(routes, any()) } + } + + @Test + fun emptyRoutesResetOldValidRoutes() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + sut.startRoutesRefreshing(emptyList()) + clearAllMocks(answers = false) + + sut.resume() + + verify(exactly = 0) { + retryStrategy.shouldRetry() + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun invalidRoutesResetOldValidRoutes() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + every { + RouteRefreshValidator.validateRoute(route1) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Invalid("") + sut.startRoutesRefreshing(listOf(route1)) + sut.startRoutesRefreshing(listOf(route2)) + clearAllMocks(answers = false) + + sut.resume() + + verify(exactly = 0) { + retryStrategy.shouldRetry() + cancellableHandler.postDelayed(any(), any(), any()) + } + } + + @Test + fun partiallyValidRoutesResetOldValidRoutes() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val route3 = mockk(relaxed = true) + val route4 = mockk(relaxed = true) + every { + RouteRefreshValidator.validateRoute(route1) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { + RouteRefreshValidator.validateRoute(route3) + } returns RouteRefreshValidator.RouteValidationResult.Invalid("") + every { + RouteRefreshValidator.validateRoute(route4) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(listOf(route1, route2)) + sut.startRoutesRefreshing(listOf(route3, route4)) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed(interval, capture(attemptBlocks), any()) + } + attemptBlocks.last().run() + verify(exactly = 1) { executor.postRoutesToRefresh(listOf(route3, route4), any()) } + } + + @Test + fun validRoutesResetOldValidRoutes() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val route3 = mockk(relaxed = true) + val route4 = mockk(relaxed = true) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(listOf(route1, route2)) + sut.startRoutesRefreshing(listOf(route3, route4)) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + val attemptBlocks = mutableListOf() + verify(exactly = 1) { + cancellableHandler.postDelayed(interval, capture(attemptBlocks), any()) + } + attemptBlocks.last().run() + verify(exactly = 1) { executor.postRoutesToRefresh(listOf(route3, route4), any()) } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManagerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManagerTest.kt new file mode 100644 index 00000000000..467bd269501 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManagerTest.kt @@ -0,0 +1,138 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.core.RouteProgressData +import io.mockk.clearAllMocks +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class RefreshObserversManagerTest { + + private val sut = RefreshObserversManager() + private val observer = mockk(relaxed = true) + private val inputResult = RouteRefresherResult( + true, + listOf(mockk()), + RouteProgressData(1, 2, 3) + ) + private val outputResult = RefreshedRouteInfo( + inputResult.refreshedRoutes, + inputResult.routeProgressData + ) + + @Test + fun registerObserverThenReceiveUpdate() { + sut.registerObserver(observer) + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 1) { observer.onRoutesRefreshed(outputResult) } + } + + @Test + fun registerUnregisteredObserver() { + sut.registerObserver(observer) + sut.unregisterObserver(observer) + sut.registerObserver(observer) + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 1) { observer.onRoutesRefreshed(outputResult) } + } + + @Test + fun receiveUpdateThenRegisterObserver() { + sut.onRoutesRefreshed(inputResult) + + sut.registerObserver(observer) + + verify(exactly = 0) { observer.onRoutesRefreshed(any()) } + } + + @Test + fun receiveUpdateAfterUnregisterObserver() { + sut.registerObserver(observer) + sut.unregisterObserver(observer) + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 0) { observer.onRoutesRefreshed(any()) } + } + + @Test + fun receiveUpdateAfterUnregisterAllObservers() { + val observer2 = mockk(relaxed = true) + sut.registerObserver(observer) + sut.registerObserver(observer2) + sut.unregisterAllObservers() + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 0) { + observer.onRoutesRefreshed(any()) + observer2.onRoutesRefreshed(any()) + } + } + + @Test + fun receiveUpdateToNotifyMultipleObservers() { + val observer2 = mockk(relaxed = true) + sut.registerObserver(observer) + sut.registerObserver(observer2) + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 1) { + observer.onRoutesRefreshed(outputResult) + observer2.onRoutesRefreshed(outputResult) + } + } + + @Test + fun receiveUpdateWithOneRegisteredAndOneUnregisteredObserver() { + val observer2 = mockk(relaxed = true) + sut.registerObserver(observer) + sut.registerObserver(observer2) + sut.unregisterObserver(observer2) + + sut.onRoutesRefreshed(inputResult) + + verify(exactly = 1) { + observer.onRoutesRefreshed(outputResult) + } + verify(exactly = 0) { + observer2.onRoutesRefreshed(any()) + } + } + + @Test + fun receiveMultipleUpdates() { + val inputResult2 = RouteRefresherResult( + true, + listOf(mockk(), mockk()), + RouteProgressData(4, 5, 6) + ) + val outputResult2 = RefreshedRouteInfo( + inputResult2.refreshedRoutes, + inputResult2.routeProgressData + ) + + sut.registerObserver(observer) + sut.onRoutesRefreshed(inputResult) + clearAllMocks(answers = false) + + sut.onRoutesRefreshed(inputResult2) + + verify { observer.onRoutesRefreshed(outputResult2) } + } + + @Test + fun unregisterUnknownObserver() { + sut.unregisterObserver(observer) + } + + @Test + fun unregisterAllObserversWhenNoneRegistered() { + sut.unregisterAllObservers() + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategyTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategyTest.kt new file mode 100644 index 00000000000..2e4fae053ee --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RetryRouteRefreshStrategyTest.kt @@ -0,0 +1,75 @@ +package com.mapbox.navigation.core.routerefresh + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class RetryRouteRefreshStrategyTest { + + @Test + fun maxRetryCountIsZero() { + val sut = RetryRouteRefreshStrategy(0) + + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertFalse(sut.shouldRetry()) + + sut.reset() + + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertFalse(sut.shouldRetry()) + } + + @Test + fun maxRetryCountIsThree() { + val sut = RetryRouteRefreshStrategy(3) + + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertFalse(sut.shouldRetry()) + + sut.reset() + + assertTrue(sut.shouldRetry()) + } + + @Test + fun shouldRetryDoesNotChangeState() { + val sut = RetryRouteRefreshStrategy(0) + + assertTrue(sut.shouldRetry()) + assertTrue(sut.shouldRetry()) + } + + @Test + fun resetWhenMaxAttemptsCountIsNotReached() { + val sut = RetryRouteRefreshStrategy(2) + sut.onNextAttempt() + + sut.reset() + + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertTrue(sut.shouldRetry()) + + sut.onNextAttempt() + assertFalse(sut.shouldRetry()) + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt new file mode 100644 index 00000000000..e518a537f58 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt @@ -0,0 +1,128 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import kotlin.reflect.full.memberProperties + +@RunWith(Parameterized::class) +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class RouteRefreshStateChangerTest( + private val from: String?, + private val to: String?, + private val expected: Boolean, +) { + + companion object { + + @Parameterized.Parameters(name = "from {0} to {1} should be {2}") + @JvmStatic + fun data(): Collection> { + val result = listOf>( + arrayOf(null, null, false), + arrayOf(null, RouteRefreshExtra.REFRESH_STATE_STARTED, true), + arrayOf(null, RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, true), + arrayOf(null, RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, false), + arrayOf(null, RouteRefreshExtra.REFRESH_STATE_CANCELED, false), + arrayOf(RouteRefreshExtra.REFRESH_STATE_STARTED, null, true), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + true + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + true + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + true + ), + arrayOf(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, null, true), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + true + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + false + ), + arrayOf(RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, null, true), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_STARTED, + true + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + false + ), + arrayOf(RouteRefreshExtra.REFRESH_STATE_CANCELED, null, true), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_CANCELED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + true + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_CANCELED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_CANCELED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + false + ), + arrayOf( + RouteRefreshExtra.REFRESH_STATE_CANCELED, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + false + ), + ) + val expectedNumberOfStates = getExpectedNumberOfStates() + assertEquals(expectedNumberOfStates * expectedNumberOfStates, result.size) + return result + } + + private fun getExpectedNumberOfStates(): Int { + return RouteRefreshExtra::class.memberProperties.size + 1 // 1 for null + } + } + + @Test + fun canChange() { + assertEquals(expected, RouteRefreshStateChanger.canChange(from, to)) + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolderTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolderTest.kt new file mode 100644 index 00000000000..86e6ccf216c --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateHolderTest.kt @@ -0,0 +1,406 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalPreviewMapboxNavigationAPI::class) +class RouteRefreshStateHolderTest { + + private val observer = mockk(relaxed = true) + private val sut = RouteRefreshStateHolder() + + @Before + fun setUp() { + sut.registerRouteRefreshStateObserver(observer) + mockkObject(RouteRefreshStateChanger) + every { RouteRefreshStateChanger.canChange(any(), any()) } returns true + } + + @After + fun tearDown() { + unmockkObject(RouteRefreshStateChanger) + } + + @Test + fun `null to started`() { + sut.onStarted() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange(null, RouteRefreshExtra.REFRESH_STATE_STARTED) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_STARTED, null) + ) + } + } + + @Test + fun `failed to started can change`() { + sut.onFailure(null) + clearAllMocks(answers = false) + + sut.onStarted() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult( + RouteRefreshExtra.REFRESH_STATE_STARTED, + null + ) + ) + } + } + + @Test + fun `failed to started cannot change`() { + every { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED + ) + } returns false + sut.onFailure(null) + clearAllMocks(answers = false) + + sut.onStarted() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED + ) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `started to started`() { + sut.onStarted() + clearAllMocks(answers = false) + + sut.onStarted() + + verify(exactly = 0) { RouteRefreshStateChanger.canChange(any(), any()) } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `null to success`() { + sut.onSuccess() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + null, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + null + ) + ) + } + } + + @Test + fun `failed to success can change`() { + sut.onFailure(null) + clearAllMocks(answers = false) + + sut.onSuccess() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult( + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + null + ) + ) + } + } + + @Test + fun `failed to success cannot change`() { + every { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ) + } returns false + sut.onFailure(null) + clearAllMocks(answers = false) + + sut.onSuccess() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `success to success`() { + sut.onSuccess() + clearAllMocks(answers = false) + + sut.onSuccess() + + verify(exactly = 0) { RouteRefreshStateChanger.canChange(any(), any()) } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `null to failed`() { + val message = "some message" + sut.onFailure(message) + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + null, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult( + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + message + ) + ) + } + } + + @Test + fun `started to failed can change`() { + val message = "some message" + sut.onStarted() + clearAllMocks(answers = false) + + sut.onFailure(message) + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, message) + ) + } + } + + @Test + fun `started to failed cannot change`() { + val message = "some message" + every { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ) + } returns false + sut.onStarted() + clearAllMocks(answers = false) + + sut.onFailure(message) + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED + ) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `failed to failed`() { + val message = "come message" + sut.onFailure(message) + clearAllMocks(answers = false) + + sut.onFailure(message) + + verify(exactly = 0) { RouteRefreshStateChanger.canChange(any(), any()) } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `null to cancelled`() { + sut.onCancel() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange(null, RouteRefreshExtra.REFRESH_STATE_CANCELED) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_CANCELED, null) + ) + } + } + + @Test + fun `started to cancelled can change`() { + sut.onStarted() + clearAllMocks(answers = false) + + sut.onCancel() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_CANCELED + ) + } + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_CANCELED, null) + ) + } + } + + @Test + fun `started to cancelled cannot change`() { + every { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_CANCELED + ) + } returns false + sut.onStarted() + clearAllMocks(answers = false) + + sut.onCancel() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_CANCELED + ) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `cancelled to cancelled`() { + sut.onCancel() + clearAllMocks(answers = false) + + sut.onCancel() + + verify(exactly = 0) { RouteRefreshStateChanger.canChange(any(), any()) } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `null to null`() { + sut.reset() + + verify(exactly = 0) { RouteRefreshStateChanger.canChange(any(), any()) } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `started to null can change`() { + sut.onStarted() + clearAllMocks(answers = false) + + sut.reset() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange(RouteRefreshExtra.REFRESH_STATE_STARTED, null) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun `started to null cannot change`() { + every { + RouteRefreshStateChanger.canChange(RouteRefreshExtra.REFRESH_STATE_STARTED, null) + } returns false + sut.onStarted() + clearAllMocks(answers = false) + + sut.reset() + + verify(exactly = 1) { + RouteRefreshStateChanger.canChange(RouteRefreshExtra.REFRESH_STATE_STARTED, null) + } + verify(exactly = 0) { observer.onNewState(any()) } + } + + @Test + fun observersNotification() { + val secondObserver = mockk(relaxed = true) + sut.onStarted() + + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_STARTED, null) + ) + } + clearAllMocks(answers = false) + + sut.registerRouteRefreshStateObserver(secondObserver) + + sut.onCancel() + + verify(exactly = 1) { + observer.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_CANCELED, null) + ) + secondObserver.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_CANCELED, null) + ) + } + clearAllMocks(answers = false) + + sut.unregisterRouteRefreshStateObserver(observer) + + sut.onFailure(null) + + verify(exactly = 1) { + secondObserver.onNewState( + RouteRefreshStateResult(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, null) + ) + } + verify(exactly = 0) { observer.onNewState(any()) } + clearAllMocks(answers = false) + + sut.unregisterAllRouteRefreshStateObservers() + + sut.onStarted() + + verify(exactly = 0) { + observer.onNewState(any()) + secondObserver.onNewState(any()) + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidatorTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidatorTest.kt new file mode 100644 index 00000000000..1f98d0e1ecc --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshValidatorTest.kt @@ -0,0 +1,130 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.testing.factories.createDirectionsRoute +import com.mapbox.navigation.testing.factories.createRouteOptions +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test + +class RouteRefreshValidatorTest { + + private val noUuidMessage = "DirectionsRoute#requestUuid is blank. " + + "This can be caused by a route being generated by " + + "an Onboard router (in offline mode). " + + "Make sure to switch to an Offboard route when possible, " + + "only Offboard routes support the refresh feature." + + private val route = mockk(relaxed = true) + + @Test + fun `validateRoute valid`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = true) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = "uuid") + + assertEquals( + RouteRefreshValidator.RouteValidationResult.Valid, + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `validateRoute enableRefresh is null`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = null) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = "uuid") + + assertEquals( + RouteRefreshValidator.RouteValidationResult + .Invalid("RouteOptions#enableRefresh is false"), + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `validateRoute enableRefresh is false`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = false) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = "uuid") + + assertEquals( + RouteRefreshValidator.RouteValidationResult + .Invalid("RouteOptions#enableRefresh is false"), + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `validateRoute requestUuid is null`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = true) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = null) + + assertEquals( + RouteRefreshValidator.RouteValidationResult.Invalid(noUuidMessage), + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `validateRoute requestUuid is empty`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = true) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = "") + + assertEquals( + RouteRefreshValidator.RouteValidationResult.Invalid(noUuidMessage), + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `validateRoute requestUuid is blank`() { + every { route.routeOptions } returns createRouteOptions(enableRefresh = true) + every { route.directionsRoute } returns createDirectionsRoute(requestUuid = " ") + + assertEquals( + RouteRefreshValidator.RouteValidationResult.Invalid(noUuidMessage), + RouteRefreshValidator.validateRoute(route) + ) + } + + @Test + fun `joinValidationErrorMessages empty list`() { + assertEquals("", RouteRefreshValidator.joinValidationErrorMessages(emptyList())) + } + + @Test + fun `joinValidationErrorMessages all valid`() { + val list = listOf>( + RouteRefreshValidator.RouteValidationResult.Valid to mockk(relaxed = true), + RouteRefreshValidator.RouteValidationResult.Valid to mockk(relaxed = true), + RouteRefreshValidator.RouteValidationResult.Valid to mockk(relaxed = true), + ) + assertEquals("", RouteRefreshValidator.joinValidationErrorMessages(list)) + } + + @Test + fun `joinValidationErrorMessages one invalid`() { + val list = listOf>( + RouteRefreshValidator.RouteValidationResult.Valid to mockk(relaxed = true), + RouteRefreshValidator.RouteValidationResult.Valid to mockk(relaxed = true), + RouteRefreshValidator.RouteValidationResult.Invalid("some reason") to + mockk(relaxed = true) { every { id } returns "id#0" } + ) + assertEquals("id#0 some reason", RouteRefreshValidator.joinValidationErrorMessages(list)) + } + + @Test + fun `joinValidationErrorMessages all invalid`() { + val list = listOf>( + RouteRefreshValidator.RouteValidationResult.Invalid("reason 1") to + mockk(relaxed = true) { every { id } returns "id#0" }, + RouteRefreshValidator.RouteValidationResult.Invalid("reason 2") to + mockk(relaxed = true) { every { id } returns "id#1" }, + RouteRefreshValidator.RouteValidationResult.Invalid("reason 3") to + mockk(relaxed = true) { every { id } returns "id#2" } + ) + assertEquals( + "id#0 reason 1. id#1 reason 2. id#2 reason 3", + RouteRefreshValidator.joinValidationErrorMessages(list) + ) + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt new file mode 100644 index 00000000000..64fc84dc0d3 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt @@ -0,0 +1,126 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.core.RouteProgressData +import com.mapbox.navigation.testing.MainCoroutineRule +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class RouteRefresherExecutorTest { + + @get:Rule + val coroutineRule = MainCoroutineRule() + private val routeRefresherResult = RouteRefresherResult( + true, + emptyList(), + RouteProgressData(1, 2, 3) + ) + private val routeRefresher = mockk(relaxed = true) { + coEvery { refresh(any(), any()) } returns routeRefresherResult + } + private val timeout = 100L + private val sut = RouteRefresherExecutor(routeRefresher, coroutineRule.coroutineScope, timeout) + private val routes = listOf(mockk(), mockk()) + private val callback = mockk(relaxed = true) + + @Test + fun postRoutesToRefresh() = coroutineRule.runBlockingTest { + sut.postRoutesToRefresh(routes, callback) + + coVerifyOrder { + callback.onStarted() + callback.onResult(routeRefresherResult) + } + coVerify(exactly = 1) { + routeRefresher.refresh(routes, timeout) + } + } + + @Test + fun twoRequestsAreNotExecutedSimultaneously() = coroutineRule.runBlockingTest { + val routes2 = listOf(mockk(), mockk(), mockk()) + val callback2 = mockk(relaxed = true) + val routeRefresherResult2 = RouteRefresherResult( + false, + emptyList(), + RouteProgressData(4, 5, 6) + ) + + coEvery { routeRefresher.refresh(routes, any()) } coAnswers { + delay(10000) + routeRefresherResult + } + coEvery { routeRefresher.refresh(routes2, any()) } returns routeRefresherResult2 + + sut.postRoutesToRefresh(routes, callback) + + coVerify(exactly = 1) { callback.onStarted() } + coVerify(exactly = 0) { callback.onResult(any()) } + clearAllMocks(answers = false) + + sut.postRoutesToRefresh(routes2, callback2) + + coVerify(exactly = 0) { callback.onResult(any()) } + coVerify(exactly = 0) { callback2.onStarted() } + coVerify(exactly = 0) { callback2.onResult(any()) } + + coroutineRule.testDispatcher.advanceTimeBy(10000) + + coVerifyOrder { + callback.onResult(routeRefresherResult) + callback2.onStarted() + callback2.onResult(routeRefresherResult2) + } + } + + @Test + fun onlyTwoRequestsCanBeInQueue() = coroutineRule.runBlockingTest { + val routes2 = listOf(mockk(), mockk(), mockk()) + val routes3 = listOf(mockk()) + val routes4 = listOf(mockk(), mockk()) + val callback2 = mockk(relaxed = true) + val callback3 = mockk(relaxed = true) + val callback4 = mockk(relaxed = true) + + coEvery { routeRefresher.refresh(routes, any()) } coAnswers { + delay(10000) + routeRefresherResult + } + + sut.postRoutesToRefresh(routes, callback) + sut.postRoutesToRefresh(routes2, callback2) + sut.postRoutesToRefresh(routes3, callback3) + sut.postRoutesToRefresh(routes4, callback4) + + coroutineRule.testDispatcher.advanceTimeBy(10000) + + coVerify(exactly = 1) { + routeRefresher.refresh(routes, timeout) + routeRefresher.refresh(routes4, timeout) + } + coVerify(exactly = 0) { + routeRefresher.refresh(routes2, timeout) + routeRefresher.refresh(routes3, timeout) + } + coVerify(exactly = 1) { + callback.onStarted() + callback.onResult(any()) + callback4.onStarted() + callback4.onResult(any()) + } + coVerify(exactly = 0) { + callback2.onStarted() + callback2.onResult(any()) + callback3.onStarted() + callback3.onResult(any()) + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherTest.kt new file mode 100644 index 00000000000..c8a10f8bde6 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherTest.kt @@ -0,0 +1,620 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.api.directions.v5.models.LegAnnotation +import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.NavigationRouterRefreshCallback +import com.mapbox.navigation.core.RouteProgressData +import com.mapbox.navigation.core.RouteProgressDataProvider +import com.mapbox.navigation.core.directions.session.RouteRefresh +import com.mapbox.navigation.core.ev.EVDynamicDataHolder +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.testing.factories.createDirectionsRoute +import com.mapbox.navigation.testing.factories.createIncident +import com.mapbox.navigation.testing.factories.createNavigationRoute +import com.mapbox.navigation.testing.factories.createRouteLeg +import com.mapbox.navigation.utils.internal.LoggerFrontend +import io.mockk.Called +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.spyk +import io.mockk.unmockkObject +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.Date + +@OptIn(ExperimentalCoroutinesApi::class) +class RouteRefresherTest { + + private val logger = mockk(relaxed = true) + + @get:Rule + val coroutineRule = MainCoroutineRule() + + @get:Rule + val loggerRule = LoggingFrontendTestRule(logger) + private val routeProgressData = RouteProgressData(1, 2, 3) + private val routeProgressDataProvider = mockk(relaxed = true) { + coEvery { getRouteRefreshRequestDataOrWait() } returns routeProgressData + } + private val evDataHolder = mockk(relaxed = true) + private val routeDiffProvider = mockk(relaxed = true) + private val routeRefresh = mockk(relaxed = true) + private val localDateProvider = mockk<() -> Date>(relaxed = true) + private val sut = RouteRefresher( + routeProgressDataProvider, + evDataHolder, + routeDiffProvider, + routeRefresh, + localDateProvider + ) + + @Before + fun setUp() { + mockkObject(RouteRefreshValidator) + } + + @After + fun tearDown() { + unmockkObject(RouteRefreshValidator) + } + + @Test + fun refresh_emptyList() = coroutineRule.runBlockingTest { + val expected = RouteRefresherResult(false, emptyList(), routeProgressData) + val actual = sut.refresh(emptyList(), 10) + assertEquals(expected, actual) + verify { logger wasNot Called } + } + + @Test + fun refresh_allRoutesAreRefreshed() = coroutineRule.runBlockingTest { + val route1Id = "id#1" + val route2Id = "id#2" + val route1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val route2 = mockk(relaxed = true) { + every { id } returns route2Id + } + val newRoute1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val newRoute2 = mockk(relaxed = true) { + every { id } returns route2Id + } + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route1, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute1) + 0 + } + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute2) + 0 + } + every { routeDiffProvider.buildRouteDiffs(route1, newRoute1, 1) } returns listOf( + "diff#1", + "diff#2" + ) + every { routeDiffProvider.buildRouteDiffs(route2, newRoute2, 1) } returns listOf( + "diff#3", + "diff#4" + ) + val expected = RouteRefresherResult(true, listOf(newRoute1, newRoute2), routeProgressData) + + val actual = sut.refresh(listOf(route1, route2), 10) + assertEquals(expected, actual) + verify(exactly = 1) { + logger.logI("Received refreshed route $route1Id", "RouteRefreshController") + logger.logI("Received refreshed route $route2Id", "RouteRefreshController") + logger.logI("diff#1", "RouteRefreshController") + logger.logI("diff#2", "RouteRefreshController") + logger.logI("diff#3", "RouteRefreshController") + logger.logI("diff#4", "RouteRefreshController") + } + } + + @Test + fun refresh_allRoutesAreRefreshed_noDiff() = coroutineRule.runBlockingTest { + val route1Id = "id#1" + val route2Id = "id#2" + val route1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val route2 = mockk(relaxed = true) { + every { id } returns route2Id + } + val newRoute1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val newRoute2 = mockk(relaxed = true) { + every { id } returns route2Id + } + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route1, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute1) + 0 + } + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute2) + 0 + } + every { routeDiffProvider.buildRouteDiffs(route1, newRoute1, 1) } returns emptyList() + every { routeDiffProvider.buildRouteDiffs(route2, newRoute2, 1) } returns emptyList() + + sut.refresh(listOf(route1, route2), 10) + + verify(exactly = 1) { + logger.logI("Received refreshed route $route1Id", "RouteRefreshController") + logger.logI("Received refreshed route $route2Id", "RouteRefreshController") + logger.logI("No changes in annotations for route $route1Id", "RouteRefreshController") + logger.logI("No changes in annotations for route $route2Id", "RouteRefreshController") + } + } + + @Test + fun refresh_onlyOneRouteIsRefreshed() = coroutineRule.runBlockingTest { + val route1Id = "id#1" + val route2Id = "id#2" + val route1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val route2 = mockk(relaxed = true) { + every { id } returns route2Id + } + val newRoute2 = mockk(relaxed = true) { + every { id } returns route2Id + } + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route1, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onFailure( + mockk(relaxed = true) { + every { message } returns "error message" + every { throwable } returns ConcurrentModificationException() + } + ) + 0 + } + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute2) + 0 + } + every { routeDiffProvider.buildRouteDiffs(any(), any(), 1) } returns listOf( + "diff#1", + "diff#2" + ) + val expected = RouteRefresherResult(true, listOf(route1, newRoute2), routeProgressData) + + val actual = sut.refresh(listOf(route1, route2), 10) + + assertEquals(expected, actual) + verify(exactly = 1) { + logger.logE( + "Route refresh error: error message " + + "throwable=java.util.ConcurrentModificationException", + "RouteRefreshController" + ) + logger.logI("Received refreshed route $route2Id", "RouteRefreshController") + logger.logI("diff#1", "RouteRefreshController") + logger.logI("diff#2", "RouteRefreshController") + } + } + + @Test + fun refresh_noRoutesAreRefreshed() = coroutineRule.runBlockingTest { + every { + localDateProvider() + } returns parseISO8601DateToLocalTimeOrNull("2022-06-30T20:00:00Z")!! + val route1 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "heavy")) + .congestionNumeric(listOf(90, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + createIncident(endTime = "2022-06-30T19:59:00Z"), + ) + ), + ) + ) + ) + val expectedNewRoute1 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + ) + ), + ) + ) + ) + val route2 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "moderate")) + .congestionNumeric(listOf(90, 80)) + .build(), + incidents = null + ), + createRouteLeg( + annotation = null, + incidents = listOf( + createIncident(endTime = "2022-06-31T22:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder().build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T22:50:00Z"), + ) + ), + ) + ) + ) + val expectedNewRoute2 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = null + ), + createRouteLeg( + annotation = null, + incidents = listOf( + createIncident(endTime = "2022-06-31T22:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder().build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T22:50:00Z"), + ) + ), + ) + ) + ) + val route3 = createNavigationRoute(directionsRoute = createDirectionsRoute(legs = null)) + val expectedNewRoute3 = createNavigationRoute( + directionsRoute = createDirectionsRoute(legs = null) + ) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route1, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onFailure( + mockk(relaxed = true) { + every { message } returns "error message 1" + every { throwable } returns ConcurrentModificationException() + } + ) + 0 + } + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onFailure( + mockk(relaxed = true) { + every { message } returns "error message 2" + every { throwable } returns IllegalStateException() + } + ) + 0 + } + every { routeRefresh.requestRouteRefresh(route3, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onFailure( + mockk(relaxed = true) { + every { message } returns "error message 3" + every { throwable } returns IndexOutOfBoundsException() + } + ) + 0 + } + val expected = RouteRefresherResult( + false, + listOf(expectedNewRoute1, expectedNewRoute2, expectedNewRoute3), + routeProgressData + ) + + val actual = sut.refresh(listOf(route1, route2, route3), 10) + + assertEquals(expected, actual) + verify(exactly = 1) { + logger.logE( + "Route refresh error: error message 1 " + + "throwable=java.util.ConcurrentModificationException", + "RouteRefreshController" + ) + logger.logE( + "Route refresh error: error message 2 " + + "throwable=java.lang.IllegalStateException", + "RouteRefreshController" + ) + logger.logE( + "Route refresh error: error message 3 " + + "throwable=java.lang.IndexOutOfBoundsException", + "RouteRefreshController" + ) + } + } + + @Test + fun refresh_oneRouteRefreshFailsByTimeout() = coroutineRule.runBlockingTest { + val route1Id = "id#1" + val route2Id = "id#2" + val route1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val route2 = mockk(relaxed = true) { + every { id } returns route2Id + } + val newRoute2 = mockk(relaxed = true) { + every { id } returns route2Id + } + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route1, any(), any()) } returns 0 + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute2) + 0 + } + val expected = RouteRefresherResult(true, listOf(route1, newRoute2), routeProgressData) + + val actual = sut.refresh(listOf(route1, route2), 10) + + assertEquals(expected, actual) + verify(exactly = 1) { + logger.logI( + "Route refresh for route $route1Id was cancelled after timeout", + "RouteRefreshController" + ) + logger.logI("Received refreshed route $route2Id", "RouteRefreshController") + } + } + + @Test + fun refresh_oneRouteIsInvalid() = coroutineRule.runBlockingTest { + val route1Id = "route1" + val route2Id = "route2" + val reason = "some reason" + val route1 = mockk(relaxed = true) { + every { id } returns route1Id + } + val route2 = mockk(relaxed = true) { + every { id } returns route2Id + } + val newRoute2 = mockk(relaxed = true) { + every { id } returns route2Id + } + every { + RouteRefreshValidator.validateRoute(route1) + } returns RouteRefreshValidator.RouteValidationResult.Invalid(reason) + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Valid + every { routeRefresh.requestRouteRefresh(route2, any(), any()) } answers { + (args[2] as NavigationRouterRefreshCallback).onRefreshReady(newRoute2) + 0 + } + val expected = RouteRefresherResult(true, listOf(route1, newRoute2), routeProgressData) + + val actual = sut.refresh(listOf(route1, route2), 10) + assertEquals(expected, actual) + + verify { + logger.logI( + "route $route1Id can't be refreshed because $reason", + "RouteRefreshController" + ) + logger.logI("Received refreshed route $route2Id", "RouteRefreshController") + } + } + + @Test + fun refresh_allRoutesAreInvalid() = coroutineRule.runBlockingTest { + val route1Id = "route1" + val reason1 = "some reason 1" + val route2Id = "route2" + val reason2 = "some reason 2" + every { + localDateProvider() + } returns parseISO8601DateToLocalTimeOrNull("2022-06-30T20:00:00Z")!! + val route1 = spyk( + createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "heavy")) + .congestionNumeric(listOf(90, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + createIncident(endTime = "2022-06-30T19:59:00Z"), + ) + ), + ) + ) + ) + ) { + every { id } returns route1Id + } + val expectedNewRoute1 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + ) + ), + ) + ) + ) + val route2 = spyk( + createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "moderate")) + .congestionNumeric(listOf(90, 80)) + .build(), + incidents = null + ), + ) + ) + ) + ) { every { id } returns route2Id } + val expectedNewRoute2 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = null + ), + ) + ) + ) + every { + RouteRefreshValidator.validateRoute(route1) + } returns RouteRefreshValidator.RouteValidationResult.Invalid(reason1) + every { + RouteRefreshValidator.validateRoute(route2) + } returns RouteRefreshValidator.RouteValidationResult.Invalid(reason2) + val expected = RouteRefresherResult( + false, + listOf(expectedNewRoute1, expectedNewRoute2), + routeProgressData + ) + + val actual = sut.refresh(listOf(route1, route2), 10) + assertEquals(expected, actual) + + verify { + logger.logI( + "route $route1Id can't be refreshed because $reason1", + "RouteRefreshController" + ) + logger.logI( + "route $route2Id can't be refreshed because $reason2", + "RouteRefreshController" + ) + } + } +} diff --git a/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/DirectionsResponseFactories.kt b/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/DirectionsResponseFactories.kt index 8d325a443c7..aa35468ff7d 100644 --- a/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/DirectionsResponseFactories.kt +++ b/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/DirectionsResponseFactories.kt @@ -230,7 +230,7 @@ fun createRouteOptions( coordinatesList: List = createCoordinatesList(2), profile: String = DirectionsCriteria.PROFILE_DRIVING, unrecognizedProperties: Map? = null, - enableRefresh: Boolean = false, + enableRefresh: Boolean? = false, waypointsPerRoute: Boolean? = null, ): RouteOptions { return RouteOptions From 1171b60e4d84b4dac82e8d6d61943c9e4d7e8fd6 Mon Sep 17 00:00:00 2001 From: Dzina Dybouskaya Date: Thu, 24 Nov 2022 15:24:14 +0300 Subject: [PATCH 02/19] NAVSDK-777: meet code review --- CHANGELOG.md | 1 - .../core/RouteRefreshOnDemandTest.kt | 14 +-- .../core/RouteRefreshStateTest.kt | 113 +++++++++++++++++- .../navigation/core/MapboxNavigation.kt | 5 + .../PlannedRouteRefreshController.kt | 21 ++-- .../routerefresh/RouteRefreshStateChanger.kt | 1 + .../core/routerefresh/RouteRefresher.kt | 4 +- .../PlannedRouteRefreshControllerTest.kt | 83 ++++++++++++- .../RouteRefreshStateChangerTest.kt | 2 +- 9 files changed, 217 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5182b81b1..287ec6d303c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -592,7 +592,6 @@ This release depends on, and has been tested with, the following Mapbox dependen [Changes between v2.10.0-alpha.1 and v2.10.0-alpha.2](https://github.com/mapbox/mapbox-navigation-android/compare/v2.10.0-alpha.1...v2.10.0-alpha.2) #### Features -- Added `MapboxNavigation#refreshRoutesImmediately` to request an immediate refresh of current routes. #### Bug fixes and improvements - Fixed an issue where "silent waypoints" (not regular waypoints that define legs) had markers added on the map when route line was drawn with `MapboxRouteLineApi` and `MapboxRouteLineView`. [#6526](https://github.com/mapbox/mapbox-navigation-android/pull/6526) - Fixed an issue where `DirectionsResponse#waypoints` list was cleared after a successful non-EV route refresh. [#6539](https://github.com/mapbox/mapbox-navigation-android/pull/6539) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt index c879a992cad..563f187c15f 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt @@ -74,7 +74,7 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: ) } - @Test + @Test(timeout = 10_000) fun route_refresh_on_demand_executes_before_refresh_interval() = sdkTest { val routeRefreshOptions = RouteRefreshOptions.Builder() .intervalMillis(TimeUnit.MINUTES.toMillis(1)) @@ -113,7 +113,7 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: .build() RouteRefreshOptions::class.java.getDeclaredField("intervalMillis").apply { isAccessible = true - set(routeRefreshOptions, 10_000L) + set(routeRefreshOptions, 5_000L) } refreshHandler.jsonResponseModifier = DynamicResponseModifier() createMapboxNavigation(routeRefreshOptions) @@ -129,7 +129,7 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: } } mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) - delay(5000) + delay(2500) mapboxNavigation.refreshRoutesImmediately() mapboxNavigation.routesUpdates() @@ -137,12 +137,12 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: .first() assertEquals(1, routeRefreshes.size) - // no route refresh 6 seconds after refresh on demand - delay(6000) + // no route refresh 4 seconds after refresh on demand + delay(4000) assertEquals(1, routeRefreshes.size) - delay(4000) - // has new refresh 10 seconds after refresh on demand + delay(1000) + // has new refresh 5 seconds after refresh on demand mapboxNavigation.routesUpdates() .filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH } .take(2) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt index 74b58e41945..148ac352a63 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt @@ -173,6 +173,41 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla ) } + @Test + fun notStartedUntilTimeElapses() = sdkTest { + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutes(requestedRoutes) + + delay(4000) + + assertEquals( + emptyList(), + observer.getStatesSnapshot() + ) + + waitForRefresh() + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS + ), + observer.getStatesSnapshot() + ) + } + @Test fun successfulFromSecondAttemptRefreshTest() = sdkTest { setupMockRequestHandlers( @@ -238,7 +273,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla } @Test - fun routeRefreshIsNotCancelledOnDestroyTest() = sdkTest { + fun routeRefreshDoesNotDispatchCancelledStateOnDestroyTest() = sdkTest { setupMockRequestHandlers( coordinates, R.raw.route_response_single_route_refresh, @@ -267,7 +302,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla ) } - @Test + @Test(timeout = 5_000) fun successfulRouteRefreshOnDemandTest() = sdkTest { setupMockRequestHandlers( coordinates, @@ -296,7 +331,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla ) } - @Test + @Test(timeout = 5_000) fun failedRouteRefreshOnDemandTest() = sdkTest { setupMockRequestHandlers( coordinates, @@ -405,6 +440,41 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla ) } + @Test + fun routeRefreshOnDemandFailsThenPlannedTest() = sdkTest { + val refreshHandler = createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + refreshHandler.jsonResponseModifier = DynamicResponseModifier() + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler(refreshHandler, 1) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + + mapboxNavigation.refreshRoutesImmediately() + delay(5000) + + waitForRefreshes(2) // immediate + planned + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + ), + observer.getStatesSnapshot() + ) + } + @Test fun routeRefreshOnDemandBetweenPlannedAttemptsTest() = sdkTest { val refreshHandler = createRefreshHandler( @@ -442,6 +512,43 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla ) } + @Test + fun routeRefreshOnDemandFailsBetweenPlannedAttemptsTest() = sdkTest { + val refreshHandler = createRefreshHandler( + R.raw.route_response_route_refresh_annotations, + "route_response_single_route_refresh" + ) + refreshHandler.jsonResponseModifier = DynamicResponseModifier() + setupMockRequestHandlers( + coordinates, + R.raw.route_response_single_route_refresh, + NthAttemptHandler(refreshHandler, 2) + ) + + createMapboxNavigation(createRouteRefreshOptionsWithInvalidInterval(5_000)) + mapboxNavigation.registerRouteRefreshStateObserver(observer) + mapboxNavigation.startTripSession() + val requestedRoutes = requestRoutes() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + delay(8000) // refresh interval + accuracy + + mapboxNavigation.refreshRoutesImmediately() + + waitForRefreshes(2) // one from immediate and the next planned + + assertEquals( + listOf( + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_CANCELED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, + RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, + ), + observer.getStatesSnapshot() + ) + } + private fun createMapboxNavigation(routeRefreshOptions: RouteRefreshOptions) { mapboxNavigation = MapboxNavigationProvider.create( NavigationOptions.Builder(activity) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index 870e749a4f0..8305d913653 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -1027,6 +1027,11 @@ class MapboxNavigation @VisibleForTesting internal constructor( /** * Immediately refresh current navigation routes. * Listen for refreshed routes using [RoutesObserver]. + * + * The on-demand refresh request is not guaranteed to succeed and call the [RoutesObserver], + * [refreshRoutesImmediately] invocations cannot be coupled with + * [RoutesObserver.onRoutesChanged] callbacks for state management. + * You can use [registerRouteRefreshStateObserver] to monitor refresh statuses independently. */ fun refreshRoutesImmediately() { routeRefreshController.requestImmediateRouteRefresh(getNavigationRoutes()) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt index d3d4eec4ad0..d4ab4d10f7e 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt @@ -32,6 +32,7 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( RetryRouteRefreshStrategy(maxRetryCount = 2) ) + private var paused = false private var routesToRefresh: List? = null fun startRoutesRefreshing(routes: List) { @@ -63,13 +64,19 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( } override fun pause() { - cancellableHandler.cancelAll() + if (!paused) { + paused = true + cancellableHandler.cancelAll() + } } override fun resume() { - routesToRefresh?.let { - if (retryStrategy.shouldRetry()) { - scheduleUpdateRetry(it) + if (paused) { + paused = false + routesToRefresh?.let { + if (retryStrategy.shouldRetry()) { + scheduleUpdateRetry(it, shouldNotifyOnStart = true) + } } } } @@ -81,8 +88,8 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( } } - private fun scheduleUpdateRetry(routes: List) { - postAttempt { executePlannedRefresh(routes, shouldNotifyOnStart = false) } + private fun scheduleUpdateRetry(routes: List, shouldNotifyOnStart: Boolean) { + postAttempt { executePlannedRefresh(routes, shouldNotifyOnStart = shouldNotifyOnStart) } } private fun postAttempt(attemptBlock: () -> Unit) { @@ -122,7 +129,7 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( listener.onRoutesRefreshed(routeRefresherResult) } else { if (retryStrategy.shouldRetry()) { - scheduleUpdateRetry(routes) + scheduleUpdateRetry(routes, shouldNotifyOnStart = false) } else { stateHolder.onFailure(null) if (routeRefresherResult.refreshedRoutes != routes) { diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt index 20963b1def8..de3e1442f1c 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChanger.kt @@ -18,6 +18,7 @@ internal object RouteRefreshStateChanger { ), RouteRefreshExtra.REFRESH_STATE_CANCELED to listOf( RouteRefreshExtra.REFRESH_STATE_STARTED, + RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, null, ), RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED to listOf( diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt index f480eea5d41..28bd593d2d3 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt @@ -44,7 +44,7 @@ internal class RouteRefresher( val refreshedRoutes = refreshRoutesOrNull(routes, routeProgressData, routeRefreshTimeout) return if (refreshedRoutes.any { it != null }) { RouteRefresherResult( - true, + success = true, refreshedRoutes.mapIndexed { index, navigationRoute -> navigationRoute ?: routes[index] }, @@ -52,7 +52,7 @@ internal class RouteRefresher( ) } else { RouteRefresherResult( - false, + success = false, routes.map { removeExpiringDataFromRoute(it, routeProgressData.legIndex) }, routeProgressData ) diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt index 3759d5bad11..05696d7e55d 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt @@ -386,14 +386,38 @@ class PlannedRouteRefreshControllerTest { } @Test - fun pause() { + fun pauseNotPaused() { sut.pause() verify(exactly = 1) { cancellableHandler.cancelAll() } } @Test - fun resumeNoRoutes() { + fun pausePaused() { + sut.pause() + clearAllMocks(answers = false) + + sut.pause() + + verify(exactly = 0) { cancellableHandler.cancelAll() } + } + + @Test + fun pauseResumed() { + sut.pause() + sut.resume() + clearAllMocks(answers = false) + + sut.pause() + + verify(exactly = 1) { cancellableHandler.cancelAll() } + } + + @Test + fun resumePausedNoRoutes() { + sut.pause() + clearAllMocks(answers = false) + sut.resume() verify(exactly = 0) { @@ -403,7 +427,7 @@ class PlannedRouteRefreshControllerTest { } @Test - fun resumeHasRoutesShouldNotRetry() { + fun resumePausedHasRoutesShouldNotRetry() { val route1 = mockk(relaxed = true) val route2 = mockk(relaxed = true) val routes = listOf(route1, route2) @@ -411,6 +435,7 @@ class PlannedRouteRefreshControllerTest { RouteRefreshValidator.validateRoute(any()) } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns false @@ -420,7 +445,7 @@ class PlannedRouteRefreshControllerTest { } @Test - fun resumeHasRoutesShouldRetry() { + fun resumePausedHasRoutesShouldRetry() { val route1 = mockk(relaxed = true) val route2 = mockk(relaxed = true) val routes = listOf(route1, route2) @@ -428,6 +453,7 @@ class PlannedRouteRefreshControllerTest { RouteRefreshValidator.validateRoute(any()) } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true @@ -437,7 +463,43 @@ class PlannedRouteRefreshControllerTest { } @Test - fun resumeHasRoutesShouldRetryDoesNotNotifyOnStart() { + fun resumeNotPausedHasRoutesShouldRetry() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + verify(exactly = 0) { cancellableHandler.postDelayed(any(), any(), any()) } + } + + @Test + fun resumeResumedHasRoutesShouldRetry() { + val route1 = mockk(relaxed = true) + val route2 = mockk(relaxed = true) + val routes = listOf(route1, route2) + every { + RouteRefreshValidator.validateRoute(any()) + } returns RouteRefreshValidator.RouteValidationResult.Valid + sut.startRoutesRefreshing(routes) + sut.pause() + sut.resume() + clearAllMocks(answers = false) + every { retryStrategy.shouldRetry() } returns true + + sut.resume() + + verify(exactly = 0) { cancellableHandler.postDelayed(any(), any(), any()) } + } + + @Test + fun resumePausedHasRoutesShouldRetryNotifiesOnStart() { val route1 = mockk(relaxed = true) val route2 = mockk(relaxed = true) val routes = listOf(route1, route2) @@ -445,18 +507,20 @@ class PlannedRouteRefreshControllerTest { RouteRefreshValidator.validateRoute(any()) } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true sut.resume() startRequest() - verify(exactly = 0) { stateHolder.onStarted() } + verify(exactly = 1) { stateHolder.onStarted() } } @Test fun emptyRoutesAreNotRemembered() { sut.startRoutesRefreshing(emptyList()) + sut.pause() clearAllMocks(answers = false) sut.resume() @@ -486,6 +550,7 @@ class PlannedRouteRefreshControllerTest { ) } returns message sut.startRoutesRefreshing(listOf(route1, route2)) + sut.pause() clearAllMocks(answers = false) sut.resume() @@ -509,6 +574,7 @@ class PlannedRouteRefreshControllerTest { RouteRefreshValidator.validateRoute(route2) } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true @@ -531,6 +597,7 @@ class PlannedRouteRefreshControllerTest { RouteRefreshValidator.validateRoute(any()) } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true @@ -554,6 +621,7 @@ class PlannedRouteRefreshControllerTest { } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(routes) sut.startRoutesRefreshing(emptyList()) + sut.pause() clearAllMocks(answers = false) sut.resume() @@ -576,6 +644,7 @@ class PlannedRouteRefreshControllerTest { } returns RouteRefreshValidator.RouteValidationResult.Invalid("") sut.startRoutesRefreshing(listOf(route1)) sut.startRoutesRefreshing(listOf(route2)) + sut.pause() clearAllMocks(answers = false) sut.resume() @@ -606,6 +675,7 @@ class PlannedRouteRefreshControllerTest { } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(listOf(route1, route2)) sut.startRoutesRefreshing(listOf(route3, route4)) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true @@ -630,6 +700,7 @@ class PlannedRouteRefreshControllerTest { } returns RouteRefreshValidator.RouteValidationResult.Valid sut.startRoutesRefreshing(listOf(route1, route2)) sut.startRoutesRefreshing(listOf(route3, route4)) + sut.pause() clearAllMocks(answers = false) every { retryStrategy.shouldRetry() } returns true diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt index e518a537f58..b6dbd8b1fe3 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefreshStateChangerTest.kt @@ -98,7 +98,7 @@ class RouteRefreshStateChangerTest( arrayOf( RouteRefreshExtra.REFRESH_STATE_CANCELED, RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED, - false + true ), arrayOf( RouteRefreshExtra.REFRESH_STATE_CANCELED, From 8a410ac5666e3bd2a67ad3e03a761fb352d908a0 Mon Sep 17 00:00:00 2001 From: Dzina Dybouskaya Date: Fri, 25 Nov 2022 15:08:23 +0300 Subject: [PATCH 03/19] NAVAND-777: remove coalescing refresh requests queue --- .../core/RouteRefreshStateTest.kt | 7 ++- .../routerefresh/RouteRefresherExecutor.kt | 19 +++----- .../RouteRefresherExecutorTest.kt | 43 +------------------ 3 files changed, 11 insertions(+), 58 deletions(-) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt index 148ac352a63..0f7afe84e3e 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt @@ -390,19 +390,18 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla mapboxNavigation.refreshRoutesImmediately() mapboxNavigation.refreshRoutesImmediately() mapboxNavigation.refreshRoutesImmediately() + delay(4000) - waitForRefreshes(2) + waitForRefresh() assertEquals( listOf( RouteRefreshExtra.REFRESH_STATE_STARTED, RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, - RouteRefreshExtra.REFRESH_STATE_STARTED, - RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, ), observer.getStatesSnapshot() ) - assertEquals(2, refreshHandler.handledRequests.size) + assertEquals(1, refreshHandler.handledRequests.size) } @Test diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt index fb7aabf4205..d61c20e76e2 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutor.kt @@ -3,8 +3,6 @@ package com.mapbox.navigation.core.routerefresh import com.mapbox.navigation.base.route.NavigationRoute import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock internal interface RouteRefresherProgressCallback { @@ -19,22 +17,19 @@ internal class RouteRefresherExecutor( private val timeout: Long, ) { - private val mutex = Mutex() - private val queue = ArrayDeque, RouteRefresherProgressCallback>>() + private var hasCurrentRequest = false fun postRoutesToRefresh( routes: List, callback: RouteRefresherProgressCallback ) { - queue.clear() - queue.add(routes to callback) scope.launch { - mutex.withLock { - queue.removeFirstOrNull()?.let { - it.second.onStarted() - val result = routeRefresher.refresh(it.first, timeout) - it.second.onResult(result) - } + if (!hasCurrentRequest) { + hasCurrentRequest = true + callback.onStarted() + val result = routeRefresher.refresh(routes, timeout) + callback.onResult(result) + hasCurrentRequest = false } } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt index 64fc84dc0d3..0c3b49c35eb 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherExecutorTest.kt @@ -74,53 +74,12 @@ class RouteRefresherExecutorTest { coroutineRule.testDispatcher.advanceTimeBy(10000) - coVerifyOrder { + coVerify { callback.onResult(routeRefresherResult) - callback2.onStarted() - callback2.onResult(routeRefresherResult2) - } - } - - @Test - fun onlyTwoRequestsCanBeInQueue() = coroutineRule.runBlockingTest { - val routes2 = listOf(mockk(), mockk(), mockk()) - val routes3 = listOf(mockk()) - val routes4 = listOf(mockk(), mockk()) - val callback2 = mockk(relaxed = true) - val callback3 = mockk(relaxed = true) - val callback4 = mockk(relaxed = true) - - coEvery { routeRefresher.refresh(routes, any()) } coAnswers { - delay(10000) - routeRefresherResult - } - - sut.postRoutesToRefresh(routes, callback) - sut.postRoutesToRefresh(routes2, callback2) - sut.postRoutesToRefresh(routes3, callback3) - sut.postRoutesToRefresh(routes4, callback4) - - coroutineRule.testDispatcher.advanceTimeBy(10000) - - coVerify(exactly = 1) { - routeRefresher.refresh(routes, timeout) - routeRefresher.refresh(routes4, timeout) - } - coVerify(exactly = 0) { - routeRefresher.refresh(routes2, timeout) - routeRefresher.refresh(routes3, timeout) - } - coVerify(exactly = 1) { - callback.onStarted() - callback.onResult(any()) - callback4.onStarted() - callback4.onResult(any()) } coVerify(exactly = 0) { callback2.onStarted() callback2.onResult(any()) - callback3.onStarted() - callback3.onResult(any()) } } } From 257caa15d2fde97635f926bccb84a18a6ba25ba4 Mon Sep 17 00:00:00 2001 From: Dzina Dybouskaya Date: Fri, 25 Nov 2022 17:00:06 +0300 Subject: [PATCH 04/19] NAVAND-777: remove expiring data only after timeout --- .../core/RouteRefreshOnDemandTest.kt | 72 +++++++- .../core/RouteRefreshStateTest.kt | 6 +- .../core/routerefresh/ExpiringDataRemover.kt | 67 +++++++ .../PlannedRouteRefreshController.kt | 14 +- .../routerefresh/RefreshObserversManager.kt | 4 +- .../core/routerefresh/RouteRefresher.kt | 53 +----- .../RouteRefresherResultProcessor.kt | 41 +++++ .../navigation/core/MapboxNavigationTest.kt | 6 +- .../routerefresh/ExpiringDataRemoverTest.kt | 157 +++++++++++++++++ .../PlannedRouteRefreshControllerTest.kt | 35 +--- .../RouteRefresherResultProcessorTest.kt | 163 ++++++++++++++++++ .../core/routerefresh/RouteRefresherTest.kt | 127 +------------- 12 files changed, 521 insertions(+), 224 deletions(-) create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessor.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessorTest.kt diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt index 563f187c15f..c5cc41b526c 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshOnDemandTest.kt @@ -36,6 +36,7 @@ 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 @@ -51,6 +52,7 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: @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( @@ -74,6 +76,11 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: ) } + @After + fun tearDown() { + failedRefreshHandlerWrapper.failResponse = false + } + @Test(timeout = 10_000) fun route_refresh_on_demand_executes_before_refresh_interval() = sdkTest { val routeRefreshOptions = RouteRefreshOptions.Builder() @@ -149,6 +156,55 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: .toList() } + @Test + fun failed_route_refresh_on_demand_does_not_notify_refresh_observer_before_timeout() = sdkTest { + val refreshes = mutableListOf() + val routeRefreshOptions = RouteRefreshOptions.Builder() + .intervalMillis(TimeUnit.MINUTES.toMillis(1)) + .build() + failedRefreshHandlerWrapper.failResponse = true + createMapboxNavigation(routeRefreshOptions) + val routeOptions = generateRouteOptions(twoCoordinates) + val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions) + .getSuccessfulResultOrThrowException() + .routes + mapboxNavigation.startTripSession() + mapboxNavigation.registerRoutesObserver { + if (it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH) { + refreshes.add(it) + } + } + stayOnInitialPosition() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + + mapboxNavigation.refreshRoutesImmediately() + delay(2000) + assertEquals(0, refreshes.size) + } + + @Test + fun failed_route_refresh_on_demand_notifies_refresh_observer_after_timeout() = sdkTest { + val routeRefreshOptions = createRouteRefreshOptionsWithInvalidInterval(3000) + failedRefreshHandlerWrapper.failResponse = true + createMapboxNavigation(routeRefreshOptions) + val routeOptions = generateRouteOptions(twoCoordinates) + val requestedRoutes = mapboxNavigation.requestRoutes(routeOptions) + .getSuccessfulResultOrThrowException() + .routes + mapboxNavigation.startTripSession() + stayOnInitialPosition() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) + + delay(7000) // 2 failed planned attempts + accuracy + mapboxNavigation.refreshRoutesImmediately() // fail and postpone next planned attempt + delay(2000) + mapboxNavigation.refreshRoutesImmediately() // dispatch new routes with REFRESH reason + + mapboxNavigation.routesUpdates() + .filter { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_REFRESH } + .first() + } + private fun createMapboxNavigation(routeRefreshOptions: RouteRefreshOptions) { mapboxNavigation = MapboxNavigationProvider.create( NavigationOptions.Builder(activity) @@ -204,7 +260,8 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: readRawFileText(activity, refreshResponse), acceptedGeometryIndex ) - mockWebServerRule.requestHandlers.add(FailByRequestMockRequestHandler(refreshHandler)) + failedRefreshHandlerWrapper = FailByRequestMockRequestHandler(refreshHandler) + mockWebServerRule.requestHandlers.add(failedRefreshHandlerWrapper) mockWebServerRule.requestHandlers.add(MockRoutingTileEndpointErrorRequestHandler()) } @@ -213,4 +270,17 @@ class RouteRefreshOnDemandTest : BaseTest(EmptyTestActivity:: ?.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 + } } diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt index 0f7afe84e3e..fcd46ecd29f 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshStateTest.kt @@ -352,7 +352,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla mapboxNavigation.setNavigationRoutesAndWaitForUpdate(requestedRoutes) mapboxNavigation.refreshRoutesImmediately() - waitForRefresh() + delay(2500) // execute request assertEquals( listOf( @@ -461,7 +461,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla mapboxNavigation.refreshRoutesImmediately() delay(5000) - waitForRefreshes(2) // immediate + planned + waitForRefresh() assertEquals( listOf( @@ -533,7 +533,7 @@ class RouteRefreshStateTest : BaseTest(EmptyTestActivity::cla mapboxNavigation.refreshRoutesImmediately() - waitForRefreshes(2) // one from immediate and the next planned + waitForRefresh() assertEquals( listOf( diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt new file mode 100644 index 00000000000..6e7dba59c1e --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemover.kt @@ -0,0 +1,67 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.RouteLeg +import com.mapbox.navigation.base.internal.route.update +import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull +import com.mapbox.navigation.base.route.NavigationRoute +import java.util.Date + +internal class ExpiringDataRemover( + private val localDateProvider: () -> Date, +) { + + fun removeExpiringDataFromRoutes( + routes: List, + currentLegIndex: Int, + ): List { + return routes.map { + removeExpiringDataFromRoute(it, currentLegIndex) + } + } + + private fun removeExpiringDataFromRoute( + route: NavigationRoute, + currentLegIndex: Int, + ): NavigationRoute { + val routeLegs = route.directionsRoute.legs() + val directionsRouteBlock: DirectionsRoute.() -> DirectionsRoute = { + toBuilder().legs( + routeLegs?.mapIndexed { legIndex, leg -> + val legHasAlreadyBeenPassed = legIndex < currentLegIndex + if (legHasAlreadyBeenPassed) { + leg + } else { + removeExpiredDataFromLeg(leg) + } + } + ).build() + } + return route.update( + directionsRouteBlock = directionsRouteBlock, + directionsResponseBlock = { this } + ) + } + + private fun removeExpiredDataFromLeg(leg: RouteLeg): RouteLeg { + val oldAnnotation = leg.annotation() + return leg.toBuilder() + .annotation( + oldAnnotation?.let { nonNullOldAnnotation -> + nonNullOldAnnotation.toBuilder() + .congestion(nonNullOldAnnotation.congestion()?.map { "unknown" }) + .congestionNumeric(nonNullOldAnnotation.congestionNumeric()?.map { null }) + .build() + } + ) + .incidents( + leg.incidents()?.filter { + val parsed = parseISO8601DateToLocalTimeOrNull(it.endTime()) + ?: return@filter true + val currentDate = localDateProvider() + parsed > currentDate + } + ) + .build() + } +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt index d4ab4d10f7e..0aace2c422d 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt @@ -29,7 +29,7 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( stateHolder, listener, CancellableHandler(scope), - RetryRouteRefreshStrategy(maxRetryCount = 2) + RetryRouteRefreshStrategy(maxRetryCount = MAX_RETRY_COUNT) ) private var paused = false @@ -132,14 +132,16 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor( scheduleUpdateRetry(routes, shouldNotifyOnStart = false) } else { stateHolder.onFailure(null) - if (routeRefresherResult.refreshedRoutes != routes) { - listener.onRoutesRefreshed(routeRefresherResult) - } else { - scheduleNewUpdate(routes) - } + listener.onRoutesRefreshed(routeRefresherResult) + scheduleNewUpdate(routes) } } } } } + + companion object { + + const val MAX_RETRY_COUNT = 2 + } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt index 77382426b3a..3366cbd9a03 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RefreshObserversManager.kt @@ -7,7 +7,7 @@ internal fun interface RouteRefreshObserver { fun onRoutesRefreshed(routeInfo: RefreshedRouteInfo) } -internal class RefreshObserversManager : RouteRefresherListener { +internal class RefreshObserversManager { private val refreshObservers = CopyOnWriteArraySet() @@ -23,7 +23,7 @@ internal class RefreshObserversManager : RouteRefresherListener { refreshObservers.clear() } - override fun onRoutesRefreshed(result: RouteRefresherResult) { + fun onRoutesRefreshed(result: RouteRefresherResult) { refreshObservers.forEach { observer -> observer.onRoutesRefreshed(result.toRefreshedRoutesInfo()) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt index 28bd593d2d3..5da792767b1 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresher.kt @@ -1,10 +1,6 @@ package com.mapbox.navigation.core.routerefresh -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.navigation.base.internal.RouteRefreshRequestData -import com.mapbox.navigation.base.internal.route.update -import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.base.route.NavigationRouterRefreshCallback import com.mapbox.navigation.base.route.NavigationRouterRefreshError @@ -19,7 +15,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull -import java.util.Date import kotlin.coroutines.resume internal data class RouteRefresherResult( @@ -33,7 +28,6 @@ internal class RouteRefresher( private val evDataHolder: EVDynamicDataHolder, private val routeDiffProvider: DirectionsRouteDiffProvider, private val routeRefresh: RouteRefresh, - private val localDateProvider: () -> Date, ) { suspend fun refresh( @@ -53,7 +47,7 @@ internal class RouteRefresher( } else { RouteRefresherResult( success = false, - routes.map { removeExpiringDataFromRoute(it, routeProgressData.legIndex) }, + routes, routeProgressData ) } @@ -166,51 +160,6 @@ internal class RouteRefresher( } } - private fun removeExpiringDataFromRoute( - route: NavigationRoute, - currentLegIndex: Int, - ): NavigationRoute { - val routeLegs = route.directionsRoute.legs() - val directionsRouteBlock: DirectionsRoute.() -> DirectionsRoute = { - toBuilder().legs( - routeLegs?.mapIndexed { legIndex, leg -> - val legHasAlreadyBeenPassed = legIndex < currentLegIndex - if (legHasAlreadyBeenPassed) { - leg - } else { - removeExpiredDataFromLeg(leg) - } - } - ).build() - } - return route.update( - directionsRouteBlock = directionsRouteBlock, - directionsResponseBlock = { this } - ) - } - - private fun removeExpiredDataFromLeg(leg: RouteLeg): RouteLeg { - val oldAnnotation = leg.annotation() - return leg.toBuilder() - .annotation( - oldAnnotation?.let { nonNullOldAnnotation -> - nonNullOldAnnotation.toBuilder() - .congestion(nonNullOldAnnotation.congestion()?.map { "unknown" }) - .congestionNumeric(nonNullOldAnnotation.congestionNumeric()?.map { null }) - .build() - } - ) - .incidents( - leg.incidents()?.filter { - val parsed = parseISO8601DateToLocalTimeOrNull(it.endTime()) - ?: return@filter true - val currentDate = localDateProvider() - parsed > currentDate - } - ) - .build() - } - private sealed class RouteRefreshResult { data class Success(val route: NavigationRoute) : RouteRefreshResult() data class Fail(val error: NavigationRouterRefreshError) : RouteRefreshResult() diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessor.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessor.kt new file mode 100644 index 00000000000..c55e8fa585d --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessor.kt @@ -0,0 +1,41 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.utils.internal.Time + +internal class RouteRefresherResultProcessor( + private val observersManager: RefreshObserversManager, + private val expiringDataRemover: ExpiringDataRemover, + private val timeProvider: Time, + private val staleDataTimeoutMillis: Long, +) : RouteRefresherListener { + + private var lastRefreshTimeMillis: Long = 0 + + fun reset() { + lastRefreshTimeMillis = timeProvider.millis() + } + + override fun onRoutesRefreshed(result: RouteRefresherResult) { + val currentTime = timeProvider.millis() + if (result.success) { + lastRefreshTimeMillis = currentTime + observersManager.onRoutesRefreshed(result) + } else { + if (currentTime >= lastRefreshTimeMillis + staleDataTimeoutMillis) { + lastRefreshTimeMillis = currentTime + val newRoutes = expiringDataRemover.removeExpiringDataFromRoutes( + result.refreshedRoutes, + result.routeProgressData.legIndex + ) + if (result.refreshedRoutes != newRoutes) { + val processedResult = RouteRefresherResult( + result.success, + newRoutes, + result.routeProgressData + ) + observersManager.onRoutesRefreshed(processedResult) + } + } + } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt index 4099c725193..0f846e1d704 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt @@ -1536,15 +1536,15 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val initialRoutes = listOf(mockk(relaxed = true)) val routeProgressData = RouteProgressData(5, 12, 43) val refreshedRoutes = listOf(mockk(relaxed = true)) - coEvery { - routeRefreshController.refresh(initialRoutes) - } returns RefreshedRouteInfo(refreshedRoutes, routeProgressData) routeObserversSlot.forEach { it.onRoutesChanged( createRoutesUpdatedResult(initialRoutes, RoutesExtra.ROUTES_UPDATE_REASON_NEW) ) } + interceptRefreshObserver().onRoutesRefreshed( + RefreshedRouteInfo(refreshedRoutes, routeProgressData) + ) coVerify(exactly = 1) { tripSession.setRoutes( diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt new file mode 100644 index 00000000000..cc19c0cacc0 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/ExpiringDataRemoverTest.kt @@ -0,0 +1,157 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.api.directions.v5.models.LegAnnotation +import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull +import com.mapbox.navigation.testing.factories.createDirectionsRoute +import com.mapbox.navigation.testing.factories.createIncident +import com.mapbox.navigation.testing.factories.createNavigationRoute +import com.mapbox.navigation.testing.factories.createRouteLeg +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.Date + +class ExpiringDataRemoverTest { + + private val localDateProvider = mockk<() -> Date>(relaxed = true) + private val sut = ExpiringDataRemover(localDateProvider) + + @Test + fun removeExpiringDataFromRoutes() { + every { + localDateProvider() + } returns parseISO8601DateToLocalTimeOrNull("2022-06-30T20:00:00Z")!! + val route1 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "heavy")) + .congestionNumeric(listOf(90, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + createIncident(endTime = "2022-06-30T19:59:00Z"), + ) + ), + ) + ) + ) + val expectedNewRoute1 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "moderate")) + .congestionNumeric(listOf(80, 80)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T21:59:00Z"), + createIncident(endTime = "2022-06-31T21:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-30T20:59:00Z"), + createIncident(endTime = "bad time"), + ) + ), + ) + ) + ) + val route2 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("heavy", "moderate")) + .congestionNumeric(listOf(90, 80)) + .build(), + incidents = null + ), + createRouteLeg( + annotation = null, + incidents = listOf( + createIncident(endTime = "2022-06-31T22:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder().build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T22:50:00Z"), + ) + ), + ) + ) + ) + val expectedNewRoute2 = createNavigationRoute( + directionsRoute = createDirectionsRoute( + legs = listOf( + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("moderate", "heavy")) + .congestionNumeric(listOf(80, 90)) + .build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T10:59:00Z"), + createIncident(endTime = "2022-06-21T10:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder() + .congestion(listOf("unknown", "unknown")) + .congestionNumeric(listOf(null, null)) + .build(), + incidents = null + ), + createRouteLeg( + annotation = null, + incidents = listOf( + createIncident(endTime = "2022-06-31T22:59:00Z"), + ) + ), + createRouteLeg( + annotation = LegAnnotation.builder().build(), + incidents = listOf( + createIncident(endTime = "2022-06-31T22:50:00Z"), + ) + ), + ) + ) + ) + val route3 = createNavigationRoute(directionsRoute = createDirectionsRoute(legs = null)) + val expectedNewRoute3 = createNavigationRoute( + directionsRoute = createDirectionsRoute(legs = null) + ) + + val actual = sut.removeExpiringDataFromRoutes(listOf(route1, route2, route3), 1) + + assertEquals(listOf(expectedNewRoute1, expectedNewRoute2, expectedNewRoute3), actual) + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt index 05696d7e55d..fb89367ecef 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshControllerTest.kt @@ -282,7 +282,7 @@ class PlannedRouteRefreshControllerTest { } @Test - fun finishRequestUnsuccessfullyShouldNotRetryRoutesDidNotChange() { + fun finishRequestUnsuccessfullyShouldNotRetry() { val route1 = mockk(relaxed = true) val route2 = mockk(relaxed = true) val routes = listOf(route1, route2) @@ -301,13 +301,13 @@ class PlannedRouteRefreshControllerTest { retryStrategy.reset() cancellableHandler.postDelayed(interval, any(), any()) } - verify(exactly = 0) { + verify(exactly = 1) { listener.onRoutesRefreshed(any()) } } @Test - fun finishRequestUnsuccessfullyShouldNotRetryRoutesDidNotChangeShouldNotifyOnStart() { + fun finishRequestUnsuccessfullyShouldNotRetryShouldNotifyOnStart() { val route1 = mockk(relaxed = true) val route2 = mockk(relaxed = true) val routes = listOf(route1, route2) @@ -324,35 +324,6 @@ class PlannedRouteRefreshControllerTest { verify(exactly = 1) { stateHolder.onStarted() } } - @Test - fun finishRequestUnsuccessfullyShouldNotRetryRoutesChanged() { - val route1 = mockk(relaxed = true) - val route2 = mockk(relaxed = true) - val newRoute1 = mockk(relaxed = true) - val newRoute2 = mockk(relaxed = true) - val routes = listOf(route1, route2) - every { - RouteRefreshValidator.validateRoute(any()) - } returns RouteRefreshValidator.RouteValidationResult.Valid - every { retryStrategy.shouldRetry() } returns false - - sut.startRoutesRefreshing(routes) - val result = RouteRefresherResult( - false, - listOf(newRoute1, newRoute2), - RouteProgressData(1, 2, 3) - ) - finishRequest(result) - - verify(exactly = 1) { - stateHolder.onFailure(null) - listener.onRoutesRefreshed(result) - } - verify(exactly = 0) { - cancellableHandler.postDelayed(any(), any(), any()) - } - } - private fun startRequest() { val attemptBlocks = mutableListOf() verify(exactly = 1) { diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessorTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessorTest.kt new file mode 100644 index 00000000000..8b41d12a883 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/RouteRefresherResultProcessorTest.kt @@ -0,0 +1,163 @@ +package com.mapbox.navigation.core.routerefresh + +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.core.RouteProgressData +import com.mapbox.navigation.utils.internal.Time +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class RouteRefresherResultProcessorTest { + + private val observersManager = mockk(relaxed = true) + private val expiringDataRemover = mockk(relaxed = true) + private val timeProvider = mockk