diff --git a/changelog/unreleased/features/7597.md b/changelog/unreleased/features/7597.md new file mode 100644 index 00000000000..7278b65a365 --- /dev/null +++ b/changelog/unreleased/features/7597.md @@ -0,0 +1,3 @@ +- Introduced `RouterFailure#isRetryable` which indicates if that makes sense to retry with this type of failure. +For convenience, you can use an extension property `isRetryable` for the list of `RouterFailure` in `NavigationRouterCallback.onFailure`. +In case of reroute use `RerouteState.Failed.isRetryable`. \ No newline at end of file diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt index 762e4476f72..cbcca80e0f3 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/CoreRerouteTest.kt @@ -22,6 +22,7 @@ import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.MapboxNavigationProvider import com.mapbox.navigation.core.directions.session.RoutesExtra +import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult import com.mapbox.navigation.core.reroute.NavigationRerouteController import com.mapbox.navigation.core.reroute.RerouteController import com.mapbox.navigation.core.reroute.RerouteState @@ -35,17 +36,20 @@ import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRefr import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRequestHandler import com.mapbox.navigation.instrumentation_tests.utils.idling.RouteProgressStateIdlingResource import com.mapbox.navigation.instrumentation_tests.utils.location.MockLocationReplayerRule +import com.mapbox.navigation.instrumentation_tests.utils.location.stayOnPosition import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText import com.mapbox.navigation.instrumentation_tests.utils.routes.MockRoute import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider import com.mapbox.navigation.instrumentation_tests.utils.routes.RoutesProvider.toNavigationRoutes import com.mapbox.navigation.instrumentation_tests.utils.withMapboxNavigation +import com.mapbox.navigation.instrumentation_tests.utils.withoutInternet import com.mapbox.navigation.testing.ui.BaseTest import com.mapbox.navigation.testing.ui.utils.MapboxNavigationRule import com.mapbox.navigation.testing.ui.utils.coroutines.getSuccessfulResultOrThrowException import com.mapbox.navigation.testing.ui.utils.coroutines.navigateNextRouteLeg import com.mapbox.navigation.testing.ui.utils.coroutines.offRouteUpdates import com.mapbox.navigation.testing.ui.utils.coroutines.requestRoutes +import com.mapbox.navigation.testing.ui.utils.coroutines.rerouteStates import com.mapbox.navigation.testing.ui.utils.coroutines.routeProgressUpdates import com.mapbox.navigation.testing.ui.utils.coroutines.routesUpdates import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest @@ -58,11 +62,14 @@ import com.mapbox.navigation.utils.internal.logE import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -105,7 +112,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav mapboxNavigation, RouteProgressState.OFF_ROUTE ) - val mockRoute = RoutesProvider.dc_very_short(activity) + val mockRoute = RoutesProvider.dc_very_short(context) val originLocation = mockRoute.routeWaypoints.first() val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate { latitude = originLocation.latitude() + 0.002 @@ -116,7 +123,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav mockWebServerRule.requestHandlers.add( MockDirectionsRequestHandler( profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, - jsonResponse = readRawFileText(activity, R.raw.reroute_response_dc_very_short), + jsonResponse = readRawFileText(context, R.raw.reroute_response_dc_very_short), expectedCoordinates = listOf( Point.fromLngLat( offRouteLocationUpdate.longitude, @@ -143,7 +150,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build(), object : RouterCallback { @@ -217,7 +224,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_is_cancelled_when_the_user_returns_to_route() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_very_short(activity) + val mockRoute = RoutesProvider.dc_very_short(context) val originLocation = mockRoute.routeWaypoints.first() val initialLocation = mockLocationUpdatesRule.generateLocationUpdate { latitude = originLocation.latitude() @@ -237,7 +244,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav ) val rerouteRequestHandler = MockDirectionsRequestHandler( profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, - jsonResponse = readRawFileText(activity, R.raw.empty_directions_response), + jsonResponse = readRawFileText(context, R.raw.empty_directions_response), expectedCoordinates = listOf( Point.fromLngLat( offRouteLocationUpdate.longitude, @@ -264,7 +271,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val originalRoutes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints) .build() @@ -293,8 +300,8 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val alternativesGenerationDelay = 1_000L val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_with_alternative(activity) - val mockReroute = RoutesProvider.dc_short_with_alternative_reroute(activity) + val mockRoute = RoutesProvider.dc_short_with_alternative(context) + val mockReroute = RoutesProvider.dc_short_with_alternative_reroute(context) val testData = prepareRouteAndRerouteWithAlternatives( mockRoute = mockRoute, mockReroute = mockReroute, @@ -365,8 +372,8 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val mapboxNavigation = createMapboxNavigation( customRefreshInterval = refreshInterval ) - val mockRoute = RoutesProvider.dc_short_with_alternative(activity) - val mockReroute = RoutesProvider.dc_short_with_alternative_reroute(activity) + val mockRoute = RoutesProvider.dc_short_with_alternative(context) + val mockReroute = RoutesProvider.dc_short_with_alternative_reroute(context) val testData = prepareRouteAndRerouteWithAlternatives( mockRoute = mockRoute, mockReroute = mockReroute, @@ -378,7 +385,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val refreshHandler = MockDirectionsRefreshHandler( testUuid = "jpCHHUC26qFOwISCNLjar2xmTfI6Dxd0qCHOoqwt_1VAlESNvsr7Zg==", readRawFileText( - activity, + context, R.raw.route_response_dc_short_with_alternative_refresh_route_0 ), acceptedGeometryIndex = 0 @@ -436,7 +443,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav withMapboxNavigation( historyRecorderRule = mapboxHistoryTestRule ) { mapboxNavigation -> - val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes( + val routes = RoutesProvider.dc_short_with_alternative(context).toNavigationRoutes( routerOrigin = RouterOrigin.Offboard ) val origin = routes.first().routeOptions.coordinatesList().first() @@ -471,7 +478,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav fun events_order_are_guaranteed_during_reroute_to_alternative_with_custom_reroute_controller() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val routes = RoutesProvider.dc_short_with_alternative(activity).toNavigationRoutes() + val routes = RoutesProvider.dc_short_with_alternative(context).toNavigationRoutes() var latestRouteProgress: RouteProgress? = null mapboxNavigation.registerRouteProgressObserver { latestRouteProgress = it @@ -532,7 +539,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_on_single_leg_route_without_alternatives() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_very_short(activity) + val mockRoute = RoutesProvider.dc_very_short(context) val originLocation = mockRoute.routeWaypoints.first() val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate { latitude = originLocation.latitude() + 0.002 @@ -543,7 +550,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav mockWebServerRule.requestHandlers.add( MockDirectionsRequestHandler( profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, - jsonResponse = readRawFileText(activity, R.raw.reroute_response_dc_very_short), + jsonResponse = readRawFileText(context, R.raw.reroute_response_dc_very_short), expectedCoordinates = listOf( Point.fromLngLat( offRouteLocationUpdate.longitude, @@ -559,7 +566,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build() ).getSuccessfulResultOrThrowException().routes @@ -583,7 +590,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_on_multieg_route_without_alternatives() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_very_short_two_legs(activity) + val mockRoute = RoutesProvider.dc_very_short_two_legs(context) val secondLegLocation = mockLocationUpdatesRule.generateLocationUpdate { latitude = mockRoute.routeWaypoints[1].latitude() longitude = mockRoute.routeWaypoints[1].longitude() @@ -598,7 +605,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav MockDirectionsRequestHandler( profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, jsonResponse = readRawFileText( - activity, + context, R.raw.reroute_response_dc_very_short_two_legs ), expectedCoordinates = listOf( @@ -616,7 +623,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build() ).getSuccessfulResultOrThrowException().routes @@ -647,7 +654,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_on_single_leg_route_with_alternatives() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_with_alternative(activity) + val mockRoute = RoutesProvider.dc_short_with_alternative(context) // on alternative val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate { latitude = 38.893403 @@ -660,7 +667,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build() ).getSuccessfulResultOrThrowException().routes @@ -685,7 +692,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_on_multileg_route_first_leg_with_alternatives() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_two_legs_with_alternative(activity) + val mockRoute = RoutesProvider.dc_short_two_legs_with_alternative(context) val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate { latitude = 38.888565 longitude = -77.039343 @@ -697,7 +704,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build() ).getSuccessfulResultOrThrowException().routes @@ -723,7 +730,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_on_multileg_route_second_leg_with_alternatives() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_two_legs_with_alternative_for_second_leg(activity) + val mockRoute = RoutesProvider.dc_short_two_legs_with_alternative_for_second_leg(context) val secondLegLocation = mockLocationUpdatesRule.generateLocationUpdate { latitude = 38.895469 longitude = -77.030394 @@ -740,7 +747,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints).build() ).getSuccessfulResultOrThrowException().routes @@ -773,7 +780,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_from_single_leg_primary_to_multileg_alternative() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_alternative_has_more_legs(activity) + val mockRoute = RoutesProvider.dc_short_alternative_has_more_legs(context) val onRouteLocation = mockLocationUpdatesRule.generateLocationUpdate { latitude = 38.895469 longitude = -77.030394 @@ -790,7 +797,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .waypointsPerRoute(true) .coordinatesList(mockRoute.routeWaypoints).build() @@ -822,7 +829,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav @Test fun reroute_from_multileg_primary_to_single_leg_alternative() = sdkTest { val mapboxNavigation = createMapboxNavigation() - val mockRoute = RoutesProvider.dc_short_alternative_has_more_legs(activity) + val mockRoute = RoutesProvider.dc_short_alternative_has_more_legs(context) val secondLegLocation = mockLocationUpdatesRule.generateLocationUpdate { latitude = 38.895469 longitude = -77.030394 @@ -839,7 +846,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val routes = mapboxNavigation.requestRoutes( RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .waypointsPerRoute(true) .coordinatesList(mockRoute.routeWaypoints).build() @@ -870,13 +877,79 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav assertEquals(routes[1], rerouteResult.navigationRoutes.first()) } + @Test + fun reroute_with_retryable_error() = sdkTest { + val mockRoute = RoutesProvider.dc_very_short(context) + val originLocation = mockRoute.routeWaypoints.first() + val initialLocation = mockLocationUpdatesRule.generateLocationUpdate { + latitude = originLocation.latitude() + longitude = originLocation.longitude() + } + val offRouteLocationUpdate = mockLocationUpdatesRule.generateLocationUpdate { + latitude = originLocation.latitude() + 0.002 + longitude = originLocation.longitude() + } + mockWebServerRule.requestHandlers.addAll(mockRoute.mockRequestHandlers) + mockWebServerRule.requestHandlers.add( + MockDirectionsRequestHandler( + profile = DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, + jsonResponse = readRawFileText(context, R.raw.reroute_response_dc_very_short), + expectedCoordinates = listOf( + Point.fromLngLat( + offRouteLocationUpdate.longitude, + offRouteLocationUpdate.latitude + ), + mockRoute.routeWaypoints.last() + ), + relaxedExpectedCoordinates = true + ) + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { mapboxNavigation -> + val originalRoutes = mapboxNavigation.requestRoutes( + RouteOptions.builder() + .applyDefaultNavigationOptions() + .applyLanguageAndVoiceUnitOptions(context) + .baseUrl(mockWebServerRule.baseUrl) + .coordinatesList(mockRoute.routeWaypoints) + .build() + ).getSuccessfulResultOrThrowException().routes + stayOnPosition(initialLocation) { + mapboxNavigation.startTripSession() + mapboxNavigation.flowLocationMatcherResult().first() + mapboxNavigation.setNavigationRoutesAndWaitForUpdate(originalRoutes) + } + withoutInternet { + stayOnPosition(offRouteLocationUpdate) { + mapboxNavigation.offRouteUpdates().filter { it }.first() + val failedState = mapboxNavigation.getRerouteController()!! + .rerouteStates() + .filterIsInstance() + .first() + assertTrue(failedState.isRetryable) + } + } + stayOnPosition(offRouteLocationUpdate) { + mapboxNavigation.getRerouteController()!! + .reroute { routes, _ -> + mapboxNavigation.setNavigationRoutes(routes) + } + + mapboxNavigation.routesUpdates() + .drop(1) // skipping initial route + .first { it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW } + } + } + } + private fun createMapboxNavigation(customRefreshInterval: Long? = null): MapboxNavigation { var mapboxNavigation: MapboxNavigation? = null fun create(): MapboxNavigation { MapboxNavigationProvider.destroy() - val navigationOptions = NavigationOptions.Builder(activity) - .accessToken(getMapboxAccessTokenFromResources(activity)) + val navigationOptions = NavigationOptions.Builder(context) + .accessToken(getMapboxAccessTokenFromResources(context)) .historyRecorderOptions( HistoryRecorderOptions.Builder() .build() @@ -951,7 +1024,7 @@ class CoreRerouteTest : BaseTest(EmptyTestActivity::class.jav val originalRouteOptions = RouteOptions.builder() .applyDefaultNavigationOptions() - .applyLanguageAndVoiceUnitOptions(activity) + .applyLanguageAndVoiceUnitOptions(context) .baseUrl(mockWebServerRule.baseUrl) .coordinatesList(mockRoute.routeWaypoints) .alternatives(true) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRequestTests.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRequestTests.kt new file mode 100644 index 00000000000..209a81353df --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRequestTests.kt @@ -0,0 +1,224 @@ +package com.mapbox.navigation.instrumentation_tests.core + +import android.location.Location +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.route.isRetryable +import com.mapbox.navigation.instrumentation_tests.R +import com.mapbox.navigation.instrumentation_tests.utils.history.MapboxHistoryTestRule +import com.mapbox.navigation.instrumentation_tests.utils.readRawFileText +import com.mapbox.navigation.instrumentation_tests.utils.withMapboxNavigation +import com.mapbox.navigation.instrumentation_tests.utils.withoutInternet +import com.mapbox.navigation.testing.ui.BaseCoreNoCleanUpTest +import com.mapbox.navigation.testing.ui.http.MockRequestHandler +import com.mapbox.navigation.testing.ui.utils.coroutines.RouteRequestResult +import com.mapbox.navigation.testing.ui.utils.coroutines.requestRoutes +import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest +import okhttp3.mockwebserver.MockResponse +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +/** + * See https://docs.mapbox.com/api/navigation/directions/#directions-api-errors for info + * about different Directions API failures + */ +class RouteRequestTests : BaseCoreNoCleanUpTest() { + + @get:Rule + val mapboxHistoryTestRule = MapboxHistoryTestRule() + + private val origin = Point.fromLngLat( + 13.361378213031003, + 52.49813341962201 + ) + + override fun setupMockLocation(): Location { + return mockLocationUpdatesRule.generateLocationUpdate { + longitude = origin.longitude() + latitude = origin.latitude() + } + } + + @Test + fun requestRouteWithoutInternetAndTiles() = sdkTest { + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + withoutInternet { + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertTrue(isRetryable) + } + } + } + + @Test + fun error500Unknown() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse().setBody("unexpected server error").setResponseCode(500) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun invalidInput() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody(readRawFileText(context, R.raw.invalid_alternative_response_body)) + .setResponseCode(422) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun wrongSegment() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody(readRawFileText(context, R.raw.wrong_segment_response_body)) + .setResponseCode(200) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun noRouteFound() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody("{\"code\":\"NoRoute\",\"message\":\"No route found\",\"routes\":[]}") + .setResponseCode(200) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun invalidAccessToken() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody("{\"message\":\"Not Authorized - Invalid Token\"}") + .setResponseCode(401) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun noAccessToken() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody(readRawFileText(context, R.raw.no_access_token_response_body)) + .setResponseCode(401) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun forbidden() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody("{\"message\":\"Forbidden\"}") + .setResponseCode(403) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + @Test + fun profileNotFound() = sdkTest { + mockWebServerRule.requestHandlers.add( + MockRequestHandler { + MockResponse() + .setBody("{\"message\":\"Profile not found\"}") + .setResponseCode(401) + } + ) + withMapboxNavigation( + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + val routes = navigation.requestRoutes(createTestRouteOptions()) + assertTrue(routes is RouteRequestResult.Failure) + val isRetryable = (routes as RouteRequestResult.Failure).reasons.isRetryable + assertFalse(isRetryable) + } + } + + private fun createTestRouteOptions(): RouteOptions { + return RouteOptions.builder() + .baseUrl(mockWebServerRule.baseUrl) + .applyDefaultNavigationOptions() + .coordinatesList( + listOf( + origin, + Point.fromLngLat( + 13.361478213031003, + 52.49823341962201 + ) + ) + ) + .build() + } +} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/location/LocationUpdate.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/location/LocationUpdate.kt index 41587dcb10d..ac46737539e 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/location/LocationUpdate.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/location/LocationUpdate.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.instrumentation_tests.utils.location +import android.location.Location import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult import com.mapbox.navigation.testing.ui.BaseCoreNoCleanUpTest @@ -37,6 +38,20 @@ suspend fun BaseCoreNoCleanUpTest.stayOnPosition( } } +suspend fun BaseCoreNoCleanUpTest.stayOnPosition( + location: Location, + frequencyHz: Int = 1, + block: suspend () -> Unit +) { + stayOnPosition( + latitude = location.latitude, + longitude = location.longitude, + bearing = location.bearing, + frequencyHz = frequencyHz, + block = block, + ) +} + suspend fun BaseCoreNoCleanUpTest.stayOnPositionAndWaitForUpdate( mapboxNavigation: MapboxNavigation, latitude: Double, diff --git a/instrumentation-tests/src/main/res/raw/invalid_alternative_response_body.json b/instrumentation-tests/src/main/res/raw/invalid_alternative_response_body.json new file mode 100644 index 00000000000..86e96df579e --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/invalid_alternative_response_body.json @@ -0,0 +1,4 @@ +{ + "message": "annotations value must be one of duration, distance, speed, congestion, congestion_numeric, closure, state_of_charge, maxspeed", + "code": "InvalidInput" +} \ No newline at end of file diff --git a/instrumentation-tests/src/main/res/raw/no_access_token_response_body.json b/instrumentation-tests/src/main/res/raw/no_access_token_response_body.json new file mode 100644 index 00000000000..381e78f3562 --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/no_access_token_response_body.json @@ -0,0 +1,4 @@ +{ + "message": "Not Authorized - Invalid Token", + "error_detail": "No valid token prefix found in access_token parameter" +} \ No newline at end of file diff --git a/instrumentation-tests/src/main/res/raw/wrong_segment_response_body b/instrumentation-tests/src/main/res/raw/wrong_segment_response_body new file mode 100644 index 00000000000..fa7fd3b3d3a --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/wrong_segment_response_body @@ -0,0 +1,5 @@ +{ + "code": "NoSegment", + "message": "Could not find a matching segment for input coordinates", + "routes": [] +} \ No newline at end of file diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RetryableThrowable.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RetryableThrowable.kt new file mode 100644 index 00000000000..18c80af4595 --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/RetryableThrowable.kt @@ -0,0 +1,10 @@ +package com.mapbox.navigation.base.internal.route + +import com.mapbox.navigation.base.route.RouterFailure + +/** + * It exists in order not to break API of data class [RouterFailure] by adding boolean field, + * instead existing fields are used to transfer more data. + */ +@Deprecated("replace by a boolean filed in RouterFailure") +class RetryableThrowable : Throwable(message = "It makes sense to retry in case of that error") diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/Router.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/Router.kt index 102b10c8fae..1da910a1a51 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/Router.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/Router.kt @@ -2,6 +2,7 @@ package com.mapbox.navigation.base.route import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.internal.route.RetryableThrowable import java.net.URL /** @@ -78,4 +79,10 @@ data class RouterFailure @JvmOverloads constructor( val message: String, val code: Int? = null, val throwable: Throwable? = null -) +) { + /** + * Indicates if it makes sense to retry for this type of failure. + * If false, it doesn't make sense to retry route request + */ + val isRetryable get() = throwable is RetryableThrowable +} diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouterEx.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouterEx.kt new file mode 100644 index 00000000000..cd87a113847 --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouterEx.kt @@ -0,0 +1,8 @@ +package com.mapbox.navigation.base.route + +/** + * Indicates if it makes sense to retry for this type of failures. + * If false, it doesn't make sense to retry route request + */ +val List?.isRetryable: Boolean + get() = this?.any { it.isRetryable } ?: false diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/RerouteController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/RerouteController.kt index 3b5e59155e8..49f6c32a39a 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/RerouteController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/RerouteController.kt @@ -4,6 +4,7 @@ import androidx.annotation.UiThread import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.navigation.base.route.RouterFailure import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.base.route.isRetryable import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater import com.mapbox.navigation.core.trip.session.OffRouteObserver @@ -110,7 +111,14 @@ sealed class RerouteState { val message: String, val throwable: Throwable? = null, val reasons: List? = null - ) : RerouteState() + ) : RerouteState() { + + /** + * Indicates if it makes sense to retry for this type of failures. + * If false, it doesn't make sense to retry route request + */ + val isRetryable get() = reasons.isRetryable + } /** * Route fetching is in progress. diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt index 34e4e6cb64c..25dbbcdf992 100644 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt +++ b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt @@ -12,6 +12,7 @@ import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI import com.mapbox.navigation.base.internal.NavigationRouterV2 import com.mapbox.navigation.base.internal.RouteRefreshRequestData import com.mapbox.navigation.base.internal.route.InternalRouter +import com.mapbox.navigation.base.internal.route.RetryableThrowable import com.mapbox.navigation.base.internal.route.refreshRoute import com.mapbox.navigation.base.internal.route.updateExpirationTime import com.mapbox.navigation.base.internal.utils.Constants @@ -86,12 +87,20 @@ class RouterWrapper( ) callback.onCanceled(routeOptions, origin.mapToSdkRouteOrigin()) } else { + val isErrorRetryable = it.type in listOf( + RouterErrorType.NETWORK_ERROR + ) val failureReasons = listOf( RouterFailure( url = urlWithoutToken, routerOrigin = origin.mapToSdkRouteOrigin(), message = it.message, - code = it.code + code = it.code, + throwable = if (isErrorRetryable) { + RetryableThrowable() + } else { + null + } ) ) diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt index 0461d8d5656..4be140b5795 100644 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt +++ b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt @@ -35,6 +35,7 @@ import com.mapbox.navigation.testing.MainCoroutineRule import com.mapbox.navigation.testing.NativeRouteParserRule import com.mapbox.navigation.testing.factories.createDirectionsRoute import com.mapbox.navigation.testing.factories.createNavigationRoute +import com.mapbox.navigation.testing.factories.createRouterError import com.mapbox.navigation.testing.factories.toDataRef import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigation.utils.internal.Time @@ -204,17 +205,6 @@ class RouterWrapperTests { routerWrapper.getRoute(routerOptions, navigationRouterCallback) getRouteSlot.captured.run(routerResultFailureDataRef, nativeOriginOnline) - val expected = listOf( - RouterFailure( - url = routeUrl.toHttpUrlOrNull()!!.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM) - .toUrl(), - routerOrigin = Offboard, - message = FAILURE_MESSAGE, - code = FAILURE_CODE, - throwable = null - ) - ) - verify { router.getRoute( routeUrl, @@ -222,7 +212,20 @@ class RouterWrapperTests { any() ) } - verify { navigationRouterCallback.onFailure(expected, routerOptions) } + verify { + navigationRouterCallback.onFailure( + match { + val failure = it.singleOrNull() ?: return@match false + failure.url == routeUrl.toHttpUrlOrNull()!! + .redactQueryParam(ACCESS_TOKEN_QUERY_PARAM) + .toUrl() && + failure.message == FAILURE_MESSAGE && + failure.code == FAILURE_CODE && + failure.routerOrigin == Offboard + }, + routerOptions + ) + } } @Test @@ -360,6 +363,7 @@ class RouterWrapperTests { assertEquals(expected.routerOrigin, failure.routerOrigin) assertEquals(expected.url, failure.url) assertEquals(expected.throwable!!.message, failure.throwable!!.message) + assertFalse(failure.isRetryable) } @Test @@ -396,6 +400,112 @@ class RouterWrapperTests { assertEquals(expected.routerOrigin, failure.routerOrigin) assertEquals(expected.url, failure.url) assertEquals(expected.throwable!!.message, failure.throwable!!.message) + assertFalse(failure.isRetryable) + } + + @Test + fun `route request network failure`() = + coroutineRule.runBlockingTest { + routerWrapper.getRoute(routerOptions, navigationRouterCallback) + getRouteSlot.captured.run( + ExpectedFactory.createError( + createRouterError( + type = RouterErrorType.NETWORK_ERROR + ) + ), + nativeOriginOnboard + ) + + val failures = slot>() + verify(exactly = 1) { + navigationRouterCallback.onFailure(capture(failures), routerOptions) + } + val failure: RouterFailure = failures.captured[0] + assertTrue(failure.isRetryable) + } + + @Test + fun `route request directions api not critical error`() = + coroutineRule.runBlockingTest { + routerWrapper.getRoute(routerOptions, navigationRouterCallback) + getRouteSlot.captured.run( + ExpectedFactory.createError( + createRouterError( + type = RouterErrorType.DIRECTIONS_ERROR + ) + ), + nativeOriginOnboard + ) + + val failures = slot>() + verify(exactly = 1) { + navigationRouterCallback.onFailure(capture(failures), routerOptions) + } + val failure: RouterFailure = failures.captured[0] + assertFalse(failure.isRetryable) + } + + @Test + fun `route request directions api critical error`() = + coroutineRule.runBlockingTest { + routerWrapper.getRoute(routerOptions, navigationRouterCallback) + getRouteSlot.captured.run( + ExpectedFactory.createError( + createRouterError( + type = RouterErrorType.DIRECTIONS_CRITICAL_ERROR + ) + ), + nativeOriginOnboard + ) + + val failures = slot>() + verify(exactly = 1) { + navigationRouterCallback.onFailure(capture(failures), routerOptions) + } + val failure: RouterFailure = failures.captured[0] + assertFalse(failure.isRetryable) + } + + @Test + fun `route request unknown error`() = + coroutineRule.runBlockingTest { + routerWrapper.getRoute(routerOptions, navigationRouterCallback) + getRouteSlot.captured.run( + ExpectedFactory.createError( + createRouterError( + type = RouterErrorType.UNKNOWN + ) + ), + nativeOriginOnboard + ) + + val failures = slot>() + verify(exactly = 1) { + navigationRouterCallback.onFailure(capture(failures), routerOptions) + } + val failure: RouterFailure = failures.captured[0] + assertFalse(failure.isRetryable) + } + + @Test + fun `route request throttling error`() = + coroutineRule.runBlockingTest { + routerWrapper.getRoute(routerOptions, navigationRouterCallback) + getRouteSlot.captured.run( + ExpectedFactory.createError( + createRouterError( + type = RouterErrorType.THROTTLING_ERROR + ) + ), + nativeOriginOnboard + ) + + val failures = slot>() + verify(exactly = 1) { + navigationRouterCallback.onFailure(capture(failures), routerOptions) + } + val failure: RouterFailure = failures.captured[0] + assertFalse(failure.isRetryable) } @Test @@ -712,78 +822,80 @@ class RouterWrapperTests { } @Test // TODO - fun `route refresh successful with refresh ttl updates route expiration data`() = runBlockingTest { - val options = RouteOptions.builder() - .applyDefaultNavigationOptions() - .coordinatesList( - listOf( - Point.fromLngLat(17.035958238636283, 51.123073179658476), - Point.fromLngLat(17.033342297413395, 51.11608871549779), - Point.fromLngLat(17.030364743939824, 51.11309150868635), - Point.fromLngLat(17.032132688234814, 51.10720758039439) + fun `route refresh successful with refresh ttl updates route expiration data`() = + runBlockingTest { + val options = RouteOptions.builder() + .applyDefaultNavigationOptions() + .coordinatesList( + listOf( + Point.fromLngLat(17.035958238636283, 51.123073179658476), + Point.fromLngLat(17.033342297413395, 51.11608871549779), + Point.fromLngLat(17.030364743939824, 51.11309150868635), + Point.fromLngLat(17.032132688234814, 51.10720758039439) + ) ) - ) - .build() - val route = NavigationRoute.create( - DirectionsResponse.fromJson( - testRouteFixtures.loadMultiLegRouteForRefresh(), - options - ), - options, - com.mapbox.navigation.base.route.RouterOrigin.Custom() - ).first() + .build() + val route = NavigationRoute.create( + DirectionsResponse.fromJson( + testRouteFixtures.loadMultiLegRouteForRefresh(), + options + ), + options, + com.mapbox.navigation.base.route.RouterOrigin.Custom() + ).first() - routerWrapper.getRouteRefresh(route, routeRefreshRequestData, routerRefreshCallback) - refreshRouteSlot.captured.run( - ExpectedFactory.createValue( - testRouteFixtures.loadRefreshForMultiLegRouteWithRefreshTtl() - ), - nativeOriginOnboard, - hashMapOf() - ) + routerWrapper.getRouteRefresh(route, routeRefreshRequestData, routerRefreshCallback) + refreshRouteSlot.captured.run( + ExpectedFactory.createValue( + testRouteFixtures.loadRefreshForMultiLegRouteWithRefreshTtl() + ), + nativeOriginOnboard, + hashMapOf() + ) - val routeCaptor = slot() - verify { routerRefreshCallback.onRefreshReady(capture(routeCaptor)) } + val routeCaptor = slot() + verify { routerRefreshCallback.onRefreshReady(capture(routeCaptor)) } - every { Time.SystemClockImpl.seconds() } returns responseTime + 49 - assertFalse(routeCaptor.captured.isExpired()) - every { Time.SystemClockImpl.seconds() } returns responseTime + 51 - assertTrue(routeCaptor.captured.isExpired()) - } + every { Time.SystemClockImpl.seconds() } returns responseTime + 49 + assertFalse(routeCaptor.captured.isExpired()) + every { Time.SystemClockImpl.seconds() } returns responseTime + 51 + assertTrue(routeCaptor.captured.isExpired()) + } @Test - fun `route refresh successful without refresh ttl does not update route expiration data`() = runBlockingTest { - val options = RouteOptions.builder() - .applyDefaultNavigationOptions() - .coordinatesList( - listOf( - Point.fromLngLat(17.035958238636283, 51.123073179658476), - Point.fromLngLat(17.033342297413395, 51.11608871549779), - Point.fromLngLat(17.030364743939824, 51.11309150868635), - Point.fromLngLat(17.032132688234814, 51.10720758039439) + fun `route refresh successful without refresh ttl does not update route expiration data`() = + runBlockingTest { + val options = RouteOptions.builder() + .applyDefaultNavigationOptions() + .coordinatesList( + listOf( + Point.fromLngLat(17.035958238636283, 51.123073179658476), + Point.fromLngLat(17.033342297413395, 51.11608871549779), + Point.fromLngLat(17.030364743939824, 51.11309150868635), + Point.fromLngLat(17.032132688234814, 51.10720758039439) + ) ) - ) - .build() - val route = createNavigationRoutes( - DirectionsResponse.fromJson( - testRouteFixtures.loadMultiLegRouteForRefresh(), - options - ), - options, - SDKRouteParser.default, - com.mapbox.navigation.base.route.RouterOrigin.Custom(), - 100L - ).first() + .build() + val route = createNavigationRoutes( + DirectionsResponse.fromJson( + testRouteFixtures.loadMultiLegRouteForRefresh(), + options + ), + options, + SDKRouteParser.default, + com.mapbox.navigation.base.route.RouterOrigin.Custom(), + 100L + ).first() - routerWrapper.getRouteRefresh(route, routeRefreshRequestData, routerRefreshCallback) - refreshRouteSlot.captured.run(routerRefreshSuccess, nativeOriginOnboard, hashMapOf()) + routerWrapper.getRouteRefresh(route, routeRefreshRequestData, routerRefreshCallback) + refreshRouteSlot.captured.run(routerRefreshSuccess, nativeOriginOnboard, hashMapOf()) - val routeCaptor = slot() - verify { routerRefreshCallback.onRefreshReady(capture(routeCaptor)) } + val routeCaptor = slot() + verify { routerRefreshCallback.onRefreshReady(capture(routeCaptor)) } - every { Time.SystemClockImpl.seconds() } returns responseTime + 10000 - assertFalse(routeCaptor.captured.isExpired()) - } + every { Time.SystemClockImpl.seconds() } returns responseTime + 10000 + assertFalse(routeCaptor.captured.isExpired()) + } private fun provideDefaultRouteOptions(): RouteOptions { return RouteOptions.builder() diff --git a/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/NativeFactories.kt b/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/NativeFactories.kt index fdca267060b..9312c4c3160 100644 --- a/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/NativeFactories.kt +++ b/libtesting-thirdparty/src/main/java/com/mapbox/navigation/testing/factories/NativeFactories.kt @@ -16,6 +16,8 @@ import com.mapbox.navigator.RouteIndices import com.mapbox.navigator.RouteInfo import com.mapbox.navigator.RouteInterface import com.mapbox.navigator.RouteState +import com.mapbox.navigator.RouterError +import com.mapbox.navigator.RouterErrorType import com.mapbox.navigator.RouterOrigin import com.mapbox.navigator.SpeedLimit import com.mapbox.navigator.SpeedLimitSign @@ -217,3 +219,17 @@ fun String.toDataRef(): DataRef { buffer.put(responseBytes) return DataRef(buffer) } + +fun createRouterError( + message: String = "test error", + code: Int = 0, + type: RouterErrorType = RouterErrorType.UNKNOWN, + requestId: Long = 0L, + refreshTtl: Int? = null +) = RouterError( + message, + code, + type, + requestId, + refreshTtl +) diff --git a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt index 1ddea3f8f2e..a0c9536af44 100644 --- a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt +++ b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt @@ -1,7 +1,10 @@ package com.mapbox.navigation.testing.ui.http +import android.util.Log import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -9,6 +12,7 @@ import okhttp3.mockwebserver.RecordedRequest import org.junit.rules.TestWatcher import org.junit.runner.Description import java.lang.StringBuilder +import kotlin.time.Duration /** * Creates and initializes a [MockWebServer] for each test. @@ -81,10 +85,24 @@ class MockWebServerRule : TestWatcher() { block() } finally { withContext(Dispatchers.IO) { - webServer = MockWebServer() + retryStarting(previousPort) initDispatcher() - webServer.start(previousPort) } } } + + private suspend fun retryStarting(port: Int) { + withTimeoutOrNull(30_000) { + while (true) { + try { + webServer = MockWebServer() + webServer.start(port) + break + } catch (t: Throwable) { + Log.e("MockWebServerRule", "error starting mock web server", t) + } + delay(500) + } + } ?: error("can't start mock server on port $port") + } } diff --git a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/utils/coroutines/Adapters.kt b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/utils/coroutines/Adapters.kt index b479f78f0b1..795f1f730e0 100644 --- a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/utils/coroutines/Adapters.kt +++ b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/utils/coroutines/Adapters.kt @@ -28,9 +28,10 @@ import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult import com.mapbox.navigation.core.history.MapboxHistoryRecorder import com.mapbox.navigation.core.preview.RoutesPreviewObserver import com.mapbox.navigation.core.preview.RoutesPreviewUpdate +import com.mapbox.navigation.core.reroute.RerouteController +import com.mapbox.navigation.core.reroute.RerouteState import com.mapbox.navigation.core.routealternatives.NavigationRouteAlternativesObserver import com.mapbox.navigation.core.routealternatives.RouteAlternativesError -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.core.trip.session.BannerInstructionsObserver @@ -110,6 +111,15 @@ fun MapboxNavigation.offRouteUpdates(): Flow { ) } +fun RerouteController.rerouteStates(): Flow { + return loggedCallbackFlow( + { RerouteController.RerouteStateObserver(it) }, + { registerRerouteStateObserver(it) }, + { unregisterRerouteStateObserver(it) }, + "RerouteState" + ) +} + fun MapboxNavigation.roadObjectsOnRoute(): Flow> { return loggedCallbackFlow( { RoadObjectsOnRouteObserver(it) },