From dcb0f32cd3bc2a00eb988b12888ebe5e3ac0ffb9 Mon Sep 17 00:00:00 2001 From: vadzim_v Date: Wed, 14 Jun 2023 13:10:06 +0200 Subject: [PATCH] NAVAND-1380 platform workaround. CP to 2.13 (#7264) --- .circleci/config.yml | 2 +- LICENSE.md | 27 + .../src/main/assets/open_source_licenses.html | 386 +++++++++ changelog/unreleased/bugfixes/7245.md | 1 + .../core/PlatformOfflineOnlineSwitchTest.kt | 273 ++++++ .../http/FailByRequestMockRequestHandler.kt | 3 +- .../base/internal/route/Constants.kt | 3 + .../base/route/RouteAlternativesOptions.kt | 3 +- .../src/main/assets/open_source_licenses.html | 361 ++++++++ libnavigation-core/api/current.txt | 12 + libnavigation-core/build.gradle | 3 + .../navigation/core/MapboxNavigation.kt | 23 +- .../directions/session/DirectionsSession.kt | 13 +- .../session/MapboxDirectionsSession.kt | 38 +- .../SetNavigationRoutesStartedObserver.kt | 11 + .../extensions/MapboxNavigationExtensions.kt | 12 + .../core/reroute/MapboxRerouteController.kt | 50 +- .../OnlineRouteAlternativesSwitch.kt | 339 ++++++++ .../navigation/core/MapboxNavigationTest.kt | 130 ++- .../session/MapboxDirectionsSessionTest.kt | 92 +- .../OnlineRouteAlternativesSwitchTest.kt | 799 ++++++++++++++++++ .../DirectionsRouteDiffProviderTest.kt | 22 +- .../testing/NativeRouteParserRule.kt | 5 +- .../factories/NavigationRouteFactory.kt | 17 +- .../factories/DirectionsResponseFactories.kt | 10 +- qa-test-app/src/main/AndroidManifest.xml | 1 + .../qa_test_app/domain/TestActivitySuite.kt | 9 + ...ionViewOfflineOnlineRouteSwitchActivity.kt | 185 ++++ ...ctivity_navigation_offline_online_view.xml | 19 + qa-test-app/src/main/res/values/strings.xml | 1 + 30 files changed, 2728 insertions(+), 122 deletions(-) create mode 100644 billing-tests/src/main/assets/open_source_licenses.html create mode 100644 changelog/unreleased/bugfixes/7245.md create mode 100644 instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/PlatformOfflineOnlineSwitchTest.kt create mode 100644 libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/Constants.kt create mode 100644 libnavigation-copilot/src/main/assets/open_source_licenses.html create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/SetNavigationRoutesStartedObserver.kt create mode 100644 libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitch.kt create mode 100644 libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitchTest.kt create mode 100644 qa-test-app/src/main/java/com/mapbox/navigation/qa_test_app/view/MapboxNavigationViewOfflineOnlineRouteSwitchActivity.kt create mode 100644 qa-test-app/src/main/res/layout/layout_activity_navigation_offline_online_view.xml diff --git a/.circleci/config.yml b/.circleci/config.yml index 84eedff5eab..3036bd6d34c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -448,7 +448,7 @@ commands: --device model=panther,version=33,locale=it,orientation=landscape \ --use-orchestrator \ --directories-to-pull=/sdcard/Download/mapbox_test \ - --timeout 25m + --timeout 35m run-firebase-robo: parameters: diff --git a/LICENSE.md b/LICENSE.md index 179730d9325..909e0b9a9bc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -498,12 +498,28 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the auto-value-gson-runtime. +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the autotransient (A transient annotation for AutoValue extensions.). +URL: [https://github.com/ZacSweers/AutoTransient/](https://github.com/ZacSweers/AutoTransient/) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Collections Kotlin Extensions (Kotlin extensions for 'collection' artifact). URL: [http://developer.android.com/tools/extras/support-library.html](http://developer.android.com/tools/extras/support-library.html) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) =========================================================================== +Mapbox Navigation uses portions of the Converter: Gson. +License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Core Kotlin Extensions (Kotlin extensions for 'core' artifact). URL: [https://developer.android.com/jetpack/androidx](https://developer.android.com/jetpack/androidx) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) @@ -623,12 +639,23 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens =========================================================================== +Mapbox Navigation uses portions of the okhttp-logging-interceptor (Square’s meticulous HTTP client for Java and Kotlin.). +URL: [https://square.github.io/okhttp/](https://square.github.io/okhttp/) +License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Okio (A modern I/O API for Java). URL: [https://github.com/square/okio/](https://github.com/square/okio/) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) =========================================================================== +Mapbox Navigation uses portions of the Retrofit. +License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the SavedState Kotlin Extensions (Kotlin extensions for 'savedstate' artifact). URL: [https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0](https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0) License: [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) diff --git a/billing-tests/src/main/assets/open_source_licenses.html b/billing-tests/src/main/assets/open_source_licenses.html new file mode 100644 index 00000000000..249f0e92be4 --- /dev/null +++ b/billing-tests/src/main/assets/open_source_licenses.html @@ -0,0 +1,386 @@ + + + + Open source licenses + + +

Notice for packages:

+ + + diff --git a/changelog/unreleased/bugfixes/7245.md b/changelog/unreleased/bugfixes/7245.md new file mode 100644 index 00000000000..96e9f5071c5 --- /dev/null +++ b/changelog/unreleased/bugfixes/7245.md @@ -0,0 +1 @@ +- Added experimental and temporary (i.e. it will be removed in one of the next releases) `OnlineRouteAlternativesSwitch` which requests online route when the current route is offline and automatically switches if such a route is found. It's designed for the case when platform's reachability API doesn't work reliably. \ No newline at end of file diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/PlatformOfflineOnlineSwitchTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/PlatformOfflineOnlineSwitchTest.kt new file mode 100644 index 00000000000..bb3f6bd0d37 --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/PlatformOfflineOnlineSwitchTest.kt @@ -0,0 +1,273 @@ +package com.mapbox.navigation.instrumentation_tests.core + +import android.location.Location +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.core.directions.session.RoutesExtra +import com.mapbox.navigation.core.routealternatives.OnlineRouteAlternativesSwitch +import com.mapbox.navigation.instrumentation_tests.utils.history.MapboxHistoryTestRule +import com.mapbox.navigation.instrumentation_tests.utils.http.FailByRequestMockRequestHandler +import com.mapbox.navigation.instrumentation_tests.utils.location.stayOnPosition +import com.mapbox.navigation.instrumentation_tests.utils.routes.EvRoutesProvider +import com.mapbox.navigation.instrumentation_tests.utils.routes.MockedEvRoutes +import com.mapbox.navigation.instrumentation_tests.utils.tiles.OfflineRegions +import com.mapbox.navigation.instrumentation_tests.utils.tiles.withMapboxNavigationAndOfflineTilesForRegion +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.getSuccessfulResultOrThrowException +import com.mapbox.navigation.testing.ui.utils.coroutines.requestRoutes +import com.mapbox.navigation.testing.ui.utils.coroutines.routesUpdates +import com.mapbox.navigation.testing.ui.utils.coroutines.sdkTest +import com.mapbox.navigation.testing.ui.utils.coroutines.setNavigationRoutesAsync +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Rule +import org.junit.Test + +private const val EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR = 60_000L +private const val ACCURACY_TIMEOUT = 5_000L +private const val TIME_FOR_ONE_ROUTE_REQUEST_TRY = 3_000L + +@OptIn(ExperimentalMapboxNavigationAPI::class) +class PlatformOfflineOnlineSwitchTest : BaseCoreNoCleanUpTest() { + + @get:Rule + val mapboxHistoryTestRule = MapboxHistoryTestRule() + + override fun setupMockLocation(): Location { + return mockLocationUpdatesRule.generateLocationUpdate { + longitude = 13.361378213031003 + latitude = 52.49813341962201 + } + } + + @Test + fun startNavigationOfflineThenSwitchToOnlineRouteWhenInternetAppears() = sdkTest( + timeout = INCREASED_TIMEOUT_BECAUSE_OF_REAL_ROUTING_TILES_USAGE + ) { + val originalTestRoute = setupBerlinEvRoute() + withMapboxNavigationAndOfflineTilesForRegion( + OfflineRegions.Berlin, + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + OnlineRouteAlternativesSwitch().onAttached(navigation) + navigation.startTripSession() + stayOnPosition( + originalTestRoute.origin.latitude(), + originalTestRoute.origin.longitude(), + 0.0f, + ) { + withoutInternet { + val requestResult = navigation.requestRoutes(originalTestRoute.routeOptions) + .getSuccessfulResultOrThrowException() + assertEquals(RouterOrigin.Onboard, requestResult.routerOrigin) + navigation.setNavigationRoutesAsync(requestResult.routes) + } + val onlineRoutes = navigation.routesUpdates().first { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW && + it.navigationRoutes.first().origin == RouterOrigin.Offboard + } + assertEquals( + listOf( + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#0", + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#1", + ), + onlineRoutes.navigationRoutes.map { it.id } + ) + } + } + } + + @Test + fun startNavigationOfflineStayOfflineForAWhileThenTurnOnInternet() = sdkTest( + timeout = INCREASED_TIMEOUT_BECAUSE_OF_REAL_ROUTING_TILES_USAGE + ) { + val originalTestRoute = setupBerlinEvRoute() + withMapboxNavigationAndOfflineTilesForRegion( + OfflineRegions.Berlin, + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + OnlineRouteAlternativesSwitch( + connectTimeoutMilliseconds = 400, + readTimeoutMilliseconds = 1000, + minimumRetryInterval = 500 + ).onAttached(navigation) + navigation.startTripSession() + stayOnPosition( + originalTestRoute.origin.latitude(), + originalTestRoute.origin.longitude(), + 0.0f, + ) { + withoutInternet { + val requestResult = navigation.requestRoutes(originalTestRoute.routeOptions) + .getSuccessfulResultOrThrowException() + assertEquals(RouterOrigin.Onboard, requestResult.routerOrigin) + navigation.setNavigationRoutesAsync(requestResult.routes) + delay(4_000) // cause a few retry intervals to happen + } + val onlineRoutes = withTimeoutOrNull(2_000) { + navigation.routesUpdates().first { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW && + it.navigationRoutes.first().origin == RouterOrigin.Offboard + } + } + assertNotNull( + "online routes weren't calculated in a reasonable time", + onlineRoutes + ) + assertEquals( + listOf( + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#0", + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#1", + ), + onlineRoutes?.navigationRoutes?.map { it.id } + ) + } + } + } + + @Test + fun requestingOnlineRoutesAfterServerError() = sdkTest( + timeout = INCREASED_TIMEOUT_BECAUSE_OF_REAL_ROUTING_TILES_USAGE + + EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR + + ACCURACY_TIMEOUT + ) { + var failRequestHandler: FailByRequestMockRequestHandler? = null + val originalTestRoute = setupBerlinEvRoute { + failRequestHandler = FailByRequestMockRequestHandler(it) + failRequestHandler!! + } + + withMapboxNavigationAndOfflineTilesForRegion( + OfflineRegions.Berlin, + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + OnlineRouteAlternativesSwitch( + connectTimeoutMilliseconds = 400, + readTimeoutMilliseconds = 1000, + minimumRetryInterval = 500 + ).onAttached(navigation) + navigation.startTripSession() + stayOnPosition( + originalTestRoute.origin.latitude(), + originalTestRoute.origin.longitude(), + 0.0f, + ) { + withoutInternet { + val requestResult = navigation.requestRoutes(originalTestRoute.routeOptions) + .getSuccessfulResultOrThrowException() + assertEquals(RouterOrigin.Onboard, requestResult.routerOrigin) + navigation.setNavigationRoutesAsync(requestResult.routes) + failRequestHandler!!.failResponse = true + } + delay(TIME_FOR_ONE_ROUTE_REQUEST_TRY) + failRequestHandler!!.failResponse = false + val onlineRoutesDuringDelay = withTimeoutOrNull( + EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR / 2L + ) { + navigation.routesUpdates().first { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW && + it.navigationRoutes.first().origin == RouterOrigin.Offboard + } + } + assertNull( + "routes shouldn't be requested so fast after server error", + onlineRoutesDuringDelay + ) + val onlineRoutesAfterDelay = withTimeoutOrNull( + EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR / 2L + ) { + navigation.routesUpdates().first { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW && + it.navigationRoutes.first().origin == RouterOrigin.Offboard + } + } + assertNotNull( + "online routes weren't calculated after delay", + onlineRoutesAfterDelay + ) + assertEquals( + listOf( + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#0", + "-aG_uwfS5gl5iXicU7UqdPUTpXY6MFXiiBOXy3_MZkpa4ySvR2WMUw==#1", + ), + onlineRoutesAfterDelay?.navigationRoutes?.map { it.id } + ) + } + } + } + + @Test + fun requestingOnlineRoutesForInvalidRouteRequest() = sdkTest( + timeout = INCREASED_TIMEOUT_BECAUSE_OF_REAL_ROUTING_TILES_USAGE + + EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR + + ACCURACY_TIMEOUT + ) { + var failRequestHandler: FailByRequestMockRequestHandler? = null + val originalTestRoute = setupBerlinEvRoute { + failRequestHandler = FailByRequestMockRequestHandler(it) + failRequestHandler!! + } + + withMapboxNavigationAndOfflineTilesForRegion( + OfflineRegions.Berlin, + historyRecorderRule = mapboxHistoryTestRule + ) { navigation -> + OnlineRouteAlternativesSwitch( + connectTimeoutMilliseconds = 400, + readTimeoutMilliseconds = 1000, + minimumRetryInterval = 500 + ).onAttached(navigation) + navigation.startTripSession() + stayOnPosition( + originalTestRoute.origin.latitude(), + originalTestRoute.origin.longitude(), + 0.0f, + ) { + withoutInternet { + val requestResult = navigation.requestRoutes(originalTestRoute.routeOptions) + .getSuccessfulResultOrThrowException() + assertEquals(RouterOrigin.Onboard, requestResult.routerOrigin) + navigation.setNavigationRoutesAsync(requestResult.routes) + failRequestHandler!!.apply { + failResponse = true + failResponseCode = 400 + } + } + delay(TIME_FOR_ONE_ROUTE_REQUEST_TRY) + failRequestHandler!!.failResponse = false + val onlineRoutesDuringDelay = withTimeoutOrNull( + EXPECTED_RETRY_TIME_AFTER_SERVER_ERROR + + ACCURACY_TIMEOUT + ) { + navigation.routesUpdates().first { + it.reason == RoutesExtra.ROUTES_UPDATE_REASON_NEW && + it.navigationRoutes.first().origin == RouterOrigin.Offboard + } + } + assertNull( + "routes shouldn't be requested after error 4xx", + onlineRoutesDuringDelay + ) + } + } + } + + private fun setupBerlinEvRoute( + requestHandlerInterceptor: (MockRequestHandler) -> MockRequestHandler = { it } + ): MockedEvRoutes { + val originalTestRoute = EvRoutesProvider.getBerlinEvRoute( + context, + mockWebServerRule.baseUrl + ) + mockWebServerRule.requestHandlers.add( + requestHandlerInterceptor(originalTestRoute.mockWebServerHandler) + ) + return originalTestRoute + } +} diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/FailByRequestMockRequestHandler.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/FailByRequestMockRequestHandler.kt index 858bccb7f63..3a1197a083c 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/FailByRequestMockRequestHandler.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/FailByRequestMockRequestHandler.kt @@ -9,12 +9,13 @@ class FailByRequestMockRequestHandler( ) : MockRequestHandler { var failResponse: Boolean = false + var failResponseCode: Int = 500 override fun handle(request: RecordedRequest): MockResponse? { val result = wrapped.handle(request) return if (result != null) { if (failResponse) { - MockResponse().setResponseCode(500).setBody("") + MockResponse().setResponseCode(failResponseCode).setBody("") } else { result } diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/Constants.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/Constants.kt new file mode 100644 index 00000000000..0d74f789f57 --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/route/Constants.kt @@ -0,0 +1,3 @@ +package com.mapbox.navigation.base.internal.route + +const val DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES = 8 diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteAlternativesOptions.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteAlternativesOptions.kt index d459adb435a..8a9a7fe03be 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteAlternativesOptions.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/route/RouteAlternativesOptions.kt @@ -1,6 +1,7 @@ package com.mapbox.navigation.base.route import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.internal.route.DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES import com.mapbox.navigator.RouteAlternativesObserver import java.util.concurrent.TimeUnit @@ -70,7 +71,7 @@ class RouteAlternativesOptions private constructor( */ class Builder { private var intervalMillis: Long = TimeUnit.MINUTES.toMillis(5) - private var avoidManeuverSeconds = 8 + private var avoidManeuverSeconds = DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES /** * Update the route refresh interval in milliseconds. diff --git a/libnavigation-copilot/src/main/assets/open_source_licenses.html b/libnavigation-copilot/src/main/assets/open_source_licenses.html new file mode 100644 index 00000000000..ddd51fada1f --- /dev/null +++ b/libnavigation-copilot/src/main/assets/open_source_licenses.html @@ -0,0 +1,361 @@ + + + + Open source licenses + + +

Notice for packages:

+ + + diff --git a/libnavigation-core/api/current.txt b/libnavigation-core/api/current.txt index 3eefaaa3a7f..dc4210421de 100644 --- a/libnavigation-core/api/current.txt +++ b/libnavigation-core/api/current.txt @@ -733,6 +733,9 @@ package com.mapbox.navigation.core.replay.route { package com.mapbox.navigation.core.reroute { + public final class MapboxRerouteControllerKt { + } + @UiThread public interface NavigationRerouteController extends com.mapbox.navigation.core.reroute.RerouteController { method public void reroute(com.mapbox.navigation.core.reroute.NavigationRerouteController.RoutesCallback callback); } @@ -852,6 +855,15 @@ package com.mapbox.navigation.core.routealternatives { method public void onRouteAlternativesRequestError(com.mapbox.navigation.core.routealternatives.RouteAlternativesError error); } + @com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI public final class OnlineRouteAlternativesSwitch implements com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver { + ctor public OnlineRouteAlternativesSwitch(int connectTimeoutMilliseconds = 10_000, int readTimeoutMilliseconds = 30_000, int minimumRetryInterval = 60_000, int avoidManeuverSeconds = DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES); + method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation); + method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation); + } + + public final class OnlineRouteAlternativesSwitchKt { + } + public final class RouteAlternativesControllerKt { } diff --git a/libnavigation-core/build.gradle b/libnavigation-core/build.gradle index 45df7ab9071..20be4c88c6b 100644 --- a/libnavigation-core/build.gradle +++ b/libnavigation-core/build.gradle @@ -54,6 +54,9 @@ dependencies { implementation dependenciesList.coroutinesAndroid implementation dependenciesList.androidStartup + // TODO: get rid of dependency removing workaround https://github.com/mapbox/mapbox-navigation-android/pull/7245 + implementation dependenciesList.mapboxSdkServices + api dependenciesList.androidXLifecycleRuntime implementation dependenciesList.androidXFragment testImplementation dependenciesList.androidXCore 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 bdee4dfdd50..155794960bb 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 @@ -52,6 +52,8 @@ import com.mapbox.navigation.core.directions.LegacyRouterAdapter import com.mapbox.navigation.core.directions.session.DirectionsSession import com.mapbox.navigation.core.directions.session.RoutesExtra import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams +import com.mapbox.navigation.core.directions.session.SetNavigationRoutesStartedObserver import com.mapbox.navigation.core.directions.session.Utils import com.mapbox.navigation.core.history.MapboxHistoryReader import com.mapbox.navigation.core.history.MapboxHistoryRecorder @@ -511,7 +513,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( tripSession.registerStateObserver(historyRecordingStateHandler) directionsSession = NavigationComponentProvider.createDirectionsSession(moduleRouter) - directionsSession.registerRoutesObserver(navigationSession) + directionsSession.registerSetNavigationRoutesFinishedObserver(navigationSession) if (reachabilityObserverId == null) { reachabilityObserverId = ReachabilityService.addReachabilityObserver( connectivityHandler @@ -1047,6 +1049,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( logD(LOG_CATEGORY) { "setting routes; reason: ${setRoutesInfo.mapToReason()}; IDs: ${routes.map { it.id }}" } + directionsSession.setNavigationRoutesStarted(RoutesSetStartedParams(routes)) when (setRoutesInfo) { SetRoutes.CleanUp, is SetRoutes.NewRoutes, @@ -1082,7 +1085,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( processedRoutes, setRoutesInfo ) - directionsSession.setRoutes(directionsSessionRoutes) + directionsSession.setNavigationRoutesFinished(directionsSessionRoutes) routesSetResult = ExpectedFactory.createValue( RoutesSetSuccess( directionsSessionRoutes.ignoredRoutes.associate { @@ -1217,7 +1220,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( logD("onDestroy", LOG_CATEGORY) billingController.onDestroy() directionsSession.shutdown() - directionsSession.unregisterAllRoutesObservers() + directionsSession.unregisterAllSetNavigationRoutesFinishedObserver() tripSession.stop() tripSession.unregisterAllLocationObservers() tripSession.unregisterAllRouteProgressObservers() @@ -1320,7 +1323,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( fun registerRoutesObserver(routesObserver: RoutesObserver) { threadController.getMainScopeAndRootJob().scope.launch(Dispatchers.Main.immediate) { routeUpdateMutex.withLock { - directionsSession.registerRoutesObserver(routesObserver) + directionsSession.registerSetNavigationRoutesFinishedObserver(routesObserver) } } } @@ -1329,7 +1332,7 @@ class MapboxNavigation @VisibleForTesting internal constructor( * Unregisters [RoutesObserver]. */ fun unregisterRoutesObserver(routesObserver: RoutesObserver) { - directionsSession.unregisterRoutesObserver(routesObserver) + directionsSession.unregisterSetNavigationRoutesFinishedObserver(routesObserver) } /** @@ -1906,6 +1909,16 @@ class MapboxNavigation @VisibleForTesting internal constructor( evDynamicDataHolder.updateData(data) } + internal fun registerOnRoutesSetStartedObserver(observer: SetNavigationRoutesStartedObserver) { + directionsSession.registerSetNavigationRoutesStartedObserver(observer) + } + + internal fun unregisterOnRoutesSetStartedObserver( + observer: SetNavigationRoutesStartedObserver + ) { + directionsSession.unregisterSetNavigationRoutesStartedObserver(observer) + } + private fun createHistoryRecorderHandles(config: ConfigHandle) = NavigatorLoader.createHistoryRecorderHandles( config, diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt index e14f399b65a..c418e625ef3 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/DirectionsSession.kt @@ -13,7 +13,8 @@ internal interface DirectionsSession : RouteRefresh { val initialLegIndex: Int - fun setRoutes(routes: DirectionsSessionRoutes) + fun setNavigationRoutesStarted(params: RoutesSetStartedParams) + fun setNavigationRoutesFinished(routes: DirectionsSessionRoutes) /** * Provide route options for current primary route. @@ -46,17 +47,21 @@ internal interface DirectionsSession : RouteRefresh { /** * Registers [RoutesObserver]. Updated on each change of [routes] */ - fun registerRoutesObserver(routesObserver: RoutesObserver) + fun registerSetNavigationRoutesFinishedObserver(routesObserver: RoutesObserver) /** * Unregisters [RoutesObserver] */ - fun unregisterRoutesObserver(routesObserver: RoutesObserver) + fun unregisterSetNavigationRoutesFinishedObserver(routesObserver: RoutesObserver) /** * Unregisters all [RoutesObserver] */ - fun unregisterAllRoutesObservers() + fun unregisterAllSetNavigationRoutesFinishedObserver() + + fun registerSetNavigationRoutesStartedObserver(observer: SetNavigationRoutesStartedObserver) + + fun unregisterSetNavigationRoutesStartedObserver(observer: SetNavigationRoutesStartedObserver) /** * Interrupts the route-fetching request diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt index 4fee3838d1d..e6b1951623f 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSession.kt @@ -23,7 +23,9 @@ internal class MapboxDirectionsSession( private val router: NavigationRouterV2, ) : DirectionsSession { - private val routesObservers = CopyOnWriteArraySet() + private val onSetNavigationRoutesFinishedObservers = CopyOnWriteArraySet() + private val onSetNavigationRoutesStartedObservers = + CopyOnWriteArraySet() override var routesUpdatedResult: RoutesUpdatedResult? = null override val routes: List @@ -36,7 +38,7 @@ internal class MapboxDirectionsSession( internal const val DEFAULT_INITIAL_LEG_INDEX = 0 } - override fun setRoutes(routes: DirectionsSessionRoutes) { + override fun setNavigationRoutesFinished(routes: DirectionsSessionRoutes) { this.initialLegIndex = routes.setRoutesInfo.initialLegIndex() if ( routesUpdatedResult?.navigationRoutes?.isEmpty() == true && @@ -47,11 +49,15 @@ internal class MapboxDirectionsSession( RouteCompatibilityCache.setDirectionsSessionResult(routes.acceptedRoutes) val result = routes.toRoutesUpdatedResult().also { routesUpdatedResult = it } - routesObservers.forEach { + onSetNavigationRoutesFinishedObservers.forEach { it.onRoutesChanged(result) } } + override fun setNavigationRoutesStarted(params: RoutesSetStartedParams) { + onSetNavigationRoutesStartedObservers.forEach { it.onRoutesSetStarted(params) } + } + /** * Provide route options for current primary route. */ @@ -93,7 +99,7 @@ internal class MapboxDirectionsSession( * * @param routeOptions RouteOptions * @param routerCallback Callback that gets notified with the results of the request(optional), - * see [registerRoutesObserver] + * see [registerSetNavigationRoutesFinishedObserver] * * @return requestID, see [cancelRouteRequest] */ @@ -111,23 +117,35 @@ internal class MapboxDirectionsSession( /** * Registers [RoutesObserver]. Updated on each change of [routesUpdatedResult] */ - override fun registerRoutesObserver(routesObserver: RoutesObserver) { - routesObservers.add(routesObserver) + override fun registerSetNavigationRoutesFinishedObserver(routesObserver: RoutesObserver) { + onSetNavigationRoutesFinishedObservers.add(routesObserver) routesUpdatedResult?.let { routesObserver.onRoutesChanged(it) } } /** * Unregisters [RoutesObserver] */ - override fun unregisterRoutesObserver(routesObserver: RoutesObserver) { - routesObservers.remove(routesObserver) + override fun unregisterSetNavigationRoutesFinishedObserver(routesObserver: RoutesObserver) { + onSetNavigationRoutesFinishedObservers.remove(routesObserver) } /** * Unregisters all [RoutesObserver] */ - override fun unregisterAllRoutesObservers() { - routesObservers.clear() + override fun unregisterAllSetNavigationRoutesFinishedObserver() { + onSetNavigationRoutesFinishedObservers.clear() + } + + override fun registerSetNavigationRoutesStartedObserver( + observer: SetNavigationRoutesStartedObserver + ) { + onSetNavigationRoutesStartedObservers.add(observer) + } + + override fun unregisterSetNavigationRoutesStartedObserver( + observer: SetNavigationRoutesStartedObserver + ) { + onSetNavigationRoutesStartedObservers.remove(observer) } /** diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/SetNavigationRoutesStartedObserver.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/SetNavigationRoutesStartedObserver.kt new file mode 100644 index 00000000000..9939a904eb6 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/directions/session/SetNavigationRoutesStartedObserver.kt @@ -0,0 +1,11 @@ +package com.mapbox.navigation.core.directions.session + +import com.mapbox.navigation.base.route.NavigationRoute + +internal fun interface SetNavigationRoutesStartedObserver { + fun onRoutesSetStarted(params: RoutesSetStartedParams) +} + +internal data class RoutesSetStartedParams( + val routes: List +) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/extensions/MapboxNavigationExtensions.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/extensions/MapboxNavigationExtensions.kt index f712a965f53..68f2abb02c1 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/extensions/MapboxNavigationExtensions.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/internal/extensions/MapboxNavigationExtensions.kt @@ -12,7 +12,9 @@ import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.arrival.ArrivalObserver import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams import com.mapbox.navigation.core.directions.session.RoutesUpdatedResult +import com.mapbox.navigation.core.directions.session.SetNavigationRoutesStartedObserver import com.mapbox.navigation.core.history.MapboxHistoryRecorder import com.mapbox.navigation.core.internal.HistoryRecordingStateChangeObserver import com.mapbox.navigation.core.routealternatives.NavigationRouteAlternativesObserver @@ -146,6 +148,7 @@ fun MapboxNavigation.flowOnWaypointArrival(): Flow = callbackFlow override fun onWaypointArrival(routeProgress: RouteProgress) { trySend(routeProgress) } + override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) = Unit override fun onFinalDestinationArrival(routeProgress: RouteProgress) = Unit } @@ -160,6 +163,7 @@ fun MapboxNavigation.flowOnNextRouteLegStart(): Flow = callbac override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) { trySend(routeLegProgress) } + override fun onFinalDestinationArrival(routeProgress: RouteProgress) = Unit } registerArrivalObserver(observer) @@ -194,3 +198,11 @@ fun MapboxNavigation.flowRouteAlternativeObserver(): registerRouteAlternativesObserver(alternativesObserver) awaitClose { unregisterRouteAlternativesObserver(alternativesObserver) } } + +@OptIn(ExperimentalCoroutinesApi::class) +internal fun MapboxNavigation.flowSetNavigationRoutesStarted(): Flow = + callbackFlow { + val observer = SetNavigationRoutesStartedObserver { trySend(it) } + registerOnRoutesSetStartedObserver(observer) + awaitClose { unregisterOnRoutesSetStartedObserver(observer) } + } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt index 96afe97b569..169ebbe88e3 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/reroute/MapboxRerouteController.kt @@ -14,6 +14,7 @@ import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.base.route.toDirectionsRoutes import com.mapbox.navigation.core.directions.session.DirectionsSession import com.mapbox.navigation.core.ev.EVDynamicDataHolder +import com.mapbox.navigation.core.reroute.MapboxRerouteController.Companion.applyRerouteOptions import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.utils.internal.JobControl @@ -73,11 +74,6 @@ internal class MapboxRerouteController @VisibleForTesting constructor( private companion object { private const val LOG_CATEGORY = "MapboxRerouteController" - /** - * Max dangerous maneuvers radius meters. See [RouteOptions.avoidManeuverRadius] - */ - private const val MAX_DANGEROUS_MANEUVERS_RADIUS = 1000.0 - /** * Apply reroute options. Speed must be provided as **m/s** */ @@ -88,21 +84,10 @@ internal class MapboxRerouteController @VisibleForTesting constructor( if (this == null || speed == null) { return this } - - val builder = toBuilder() - - if (this.profile() == DirectionsCriteria.PROFILE_DRIVING || - this.profile() == DirectionsCriteria.PROFILE_DRIVING_TRAFFIC - ) { - val avoidManeuverRadius = rerouteOptions.avoidManeuverSeconds - .let { speed * it }.toDouble() - .takeIf { it >= 1 } - ?.coerceAtMost(MAX_DANGEROUS_MANEUVERS_RADIUS) - - builder.avoidManeuverRadius(avoidManeuverRadius) - } - - return builder.build() + return this.applyAvoidManeuvers( + rerouteOptions.avoidManeuverSeconds, + speed + ) } } @@ -290,3 +275,28 @@ private sealed class RouteRequestResult { object Cancellation : RouteRequestResult() } + +/** + * Max dangerous maneuvers radius meters. See [RouteOptions.avoidManeuverRadius] + */ +private const val MAX_DANGEROUS_MANEUVERS_RADIUS = 1000.0 + +internal fun RouteOptions.applyAvoidManeuvers( + avoidManeuverSeconds: Int, + speed: Float +): RouteOptions { + val builder = toBuilder() + + if (this.profile() == DirectionsCriteria.PROFILE_DRIVING || + this.profile() == DirectionsCriteria.PROFILE_DRIVING_TRAFFIC + ) { + val avoidManeuverRadius = avoidManeuverSeconds + .let { speed * it }.toDouble() + .takeIf { it >= 1 } + ?.coerceAtMost(MAX_DANGEROUS_MANEUVERS_RADIUS) + + builder.avoidManeuverRadius(avoidManeuverRadius) + } + + return builder.build() +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitch.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitch.kt new file mode 100644 index 00000000000..96c010a1087 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitch.kt @@ -0,0 +1,339 @@ +package com.mapbox.navigation.core.routealternatives + +import com.mapbox.api.directions.v5.MapboxDirections +import com.mapbox.api.directions.v5.models.DirectionsResponse +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider +import com.mapbox.navigation.base.internal.route.DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.RouterOrigin.Offboard +import com.mapbox.navigation.base.route.RouterOrigin.Onboard +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams +import com.mapbox.navigation.core.internal.accounts.MapboxNavigationAccounts +import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult +import com.mapbox.navigation.core.internal.extensions.flowRouteProgress +import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated +import com.mapbox.navigation.core.internal.extensions.flowSetNavigationRoutesStarted +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver +import com.mapbox.navigation.core.reroute.applyAvoidManeuvers +import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.utils.internal.logE +import com.mapbox.navigation.utils.internal.logI +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.util.concurrent.CancellationException +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume + +private const val LOG_CATEGORY = "OnlineRouteAlternativesSwitch" + +/*** + * This components replaces offline route by an online alternative when it's available. Under the + * hood it's requesting routes in cycle from the current position until the first successful result. + * Normally this job should be done by the SDK. The problem is that it currently relies on platform + * reachability/connectivity notifications. However some environments + * don't have working reachability/connectivity API. This class is a temporary workaround for + * those environments until we reimplement route alternatives logic so that it handles + * offline-online switch even if platform's reachability API doesn't work. + * + * To enable automatic online-offline switch functionality, attach an instance of this class + * to [MapboxNavigation] using [MapboxNavigationApp.registerObserver]. If you want to disable + * automatic switch at runtime, detach it by invoking [MapboxNavigationApp.unregisterObserver]. + * + * Example: + * ```kotlin + * MapboxNavigationApp.registerObserver(OnlineRouteAlternativesSwitch()) + * ``` + * + * If you don't use [MapboxNavigationApp], you can enable the switch logic by calling + * [OnlineRouteAlternativesSwitch.onAttached] when `MapboxNavigation` instance is created, and then + * [OnlineRouteAlternativesSwitch.onDetached] when it is destroyed. + * + * Known limitations: this class doesn't use user provided router. + * Warning: this is a temporary solution and will be removed in the next versions of the SDK. + * + * @param connectTimeoutMilliseconds - is forwarded to OkHttp as is + * @param readTimeoutMilliseconds - is forwarded to OkHttp as is + * @param minimumRetryInterval - defines an interval that limits how often routes will be requested. + * Route requests won't happen more often than this interval. + */ +@ExperimentalMapboxNavigationAPI +class OnlineRouteAlternativesSwitch( + private val connectTimeoutMilliseconds: Int = 10_000, + private val readTimeoutMilliseconds: Int = 30_000, + private val minimumRetryInterval: Int = 60_000, + private val avoidManeuverSeconds: Int = DEFAULT_AVOID_MANEUVER_SECONDS_FOR_ROUTE_ALTERNATIVES +) : MapboxNavigationObserver { + + private lateinit var mapboxNavigationScope: CoroutineScope + + override fun onAttached(mapboxNavigation: MapboxNavigation) { + mapboxNavigationScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + val accessToken = mapboxNavigation.navigationOptions.accessToken ?: return + mapboxNavigationScope.launch { + requestOnlineRoutes( + mapboxNavigation.flowRoutesUpdated() + .map { it.navigationRoutes }, + mapboxNavigation.flowLocationMatcherResult(), + mapboxNavigation.flowRouteProgress(), + mapboxNavigation.flowSetNavigationRoutesStarted(), + routeRequestMechanism = { options -> + requestRoutes( + options, + accessToken, + MapboxNavigationAccounts, + connectTimeoutMilliseconds = connectTimeoutMilliseconds, + readTimeoutMilliseconds = readTimeoutMilliseconds, + ) + }, + minimumRetryInterval = minimumRetryInterval.toLong(), + navigationRouteSerializationDispatcher = Dispatchers.Default, + avoidManeuverSeconds = avoidManeuverSeconds + ) + .collectLatest { + mapboxNavigation.setNavigationRoutes(it) + } + } + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + mapboxNavigationScope.cancel() + } +} + +internal sealed class DirectionsRequestResult { + data class SuccessfulResponse(val body: DirectionsResponse) : DirectionsRequestResult() + sealed class ErrorResponse : DirectionsRequestResult() { + object RetryableError : ErrorResponse() + object NotRetryableError : ErrorResponse() + data class RetryableErrorWithDelay(val delayMilliseconds: Long) : ErrorResponse() + } +} + +internal typealias RouteRequestMechanism = suspend (RouteOptions) -> DirectionsRequestResult + +@OptIn(ExperimentalCoroutinesApi::class) +internal fun requestOnlineRoutes( + routesUpdatedEvents: Flow>, + matchingResults: Flow, + routeProgressUpdates: Flow, + setNavigaitonRoutesStartedEvents: Flow, + routeRequestMechanism: RouteRequestMechanism, + minimumRetryInterval: Long, + navigationRouteSerializationDispatcher: CoroutineDispatcher, + avoidManeuverSeconds: Int +): Flow> = + merge( + setNavigaitonRoutesStartedEvents.map { + emptyList() + }, + routesUpdatedEvents + ).mapLatest { + if (it.isEmpty()) { + null + } else { + val primaryRoute = it.first() + if (primaryRoute.origin == Onboard) { + logI(LOG_CATEGORY) { "Current route is offline, requesting online routes" } + requestOnlineRouteWithRetryOrNull( + matchingResults, + primaryRoute, + routeProgressUpdates, + routeRequestMechanism, + navigationRouteSerializationDispatcher, + minimumRetryInterval, + avoidManeuverSeconds, + ) + } else { + null + } + } + }.filterNotNull() + +private suspend fun requestOnlineRouteWithRetryOrNull( + matchingResults: Flow, + primaryRoute: NavigationRoute, + routeProgress: Flow, + routeRequestMechanism: RouteRequestMechanism, + navigationRouteSerializationDispatcher: CoroutineDispatcher, + minimumRetryInterval: Long, + avoidManeuverSeconds: Int +): List? = coroutineScope { + while (true) { + val retryLimiter = launch { delay(minimumRetryInterval) } + try { + val latestMatchingResult = matchingResults.first() + val latestRouteProgress = routeProgress.first() + logI(LOG_CATEGORY) { "Requesting routes from ${latestMatchingResult.enhancedLocation}" } + val newRouteOptions = updateRouteOptionsOrNull( + primaryRoute, + latestRouteProgress, + latestMatchingResult + )?.applyAvoidManeuvers( + avoidManeuverSeconds, + latestMatchingResult.enhancedLocation.speed + ) + if (newRouteOptions == null) { + logE(LOG_CATEGORY) { + "Error calculating route options for online route, will retry later" + } + retryLimiter.join() + continue + } + + val routeRequestResult = try { + routeRequestMechanism(newRouteOptions) + } catch (ce: CancellationException) { + throw ce + } catch (t: Throwable) { + DirectionsRequestResult.ErrorResponse.RetryableError + } + when (routeRequestResult) { + is DirectionsRequestResult.SuccessfulResponse -> { + val navigationRoutes = withContext(navigationRouteSerializationDispatcher) { + NavigationRoute.create( + routeRequestResult.body, + newRouteOptions, + Offboard + ) + } + return@coroutineScope navigationRoutes + } + is DirectionsRequestResult.ErrorResponse.NotRetryableError -> { + logE(LOG_CATEGORY) { + "Not retryable error has occurred, " + + "won't retry until new offline route is set" + } + return@coroutineScope null + } + is DirectionsRequestResult.ErrorResponse.RetryableErrorWithDelay -> { + logI(LOG_CATEGORY) { + "Error requesting route. " + + "Applying additional delay of ${routeRequestResult.delayMilliseconds}" + + " milliseconds" + } + delay(routeRequestResult.delayMilliseconds) + retryLimiter.join() + } + else -> { + logI(LOG_CATEGORY) { "failed to receive an online route, will retry later" } + retryLimiter.join() + } + } + } finally { + retryLimiter.cancel() + } + } + null +} + +private fun updateRouteOptionsOrNull( + primaryRoute: NavigationRoute, + routeProgress: RouteProgress, + locations: LocationMatcherResult +): RouteOptions? { + val routeOptionsUpdateResult = RouteOptionsUpdater().update( + primaryRoute.routeOptions, + routeProgress, + locations + ) + val newRouteOptions = when (routeOptionsUpdateResult) { + is RouteOptionsUpdater.RouteOptionsResult.Error -> { + logE(LOG_CATEGORY) { "Can't update route options: ${routeOptionsUpdateResult.error}" } + null + } + is RouteOptionsUpdater.RouteOptionsResult.Success -> { + routeOptionsUpdateResult.routeOptions + } + } + return newRouteOptions +} + +private suspend fun requestRoutes( + routeOptions: RouteOptions, + accessToken: String, + urlSkuTokenProvider: UrlSkuTokenProvider, + connectTimeoutMilliseconds: Int, + readTimeoutMilliseconds: Int +): DirectionsRequestResult { + val mapboxDirections = MapboxDirections.builder() + .routeOptions(routeOptions) + .accessToken(accessToken) + .interceptor { + val httpUrl = it.request().url + val skuUrl = urlSkuTokenProvider.obtainUrlWithSkuToken(httpUrl.toUrl()) + val request = it.request().newBuilder().url(skuUrl).build() + it.withConnectTimeout(connectTimeoutMilliseconds, TimeUnit.MILLISECONDS) + .withReadTimeout(readTimeoutMilliseconds, TimeUnit.MILLISECONDS) + .proceed(request) + } + .build() + logI(LOG_CATEGORY) { + "Requesting online route: ${routeOptions.toUrl("***")}" + } + return suspendCancellableCoroutine { continuation -> + mapboxDirections.enqueueCall(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + val body = response.body() + val result = if (body != null && response.code() in 200..299) { + DirectionsRequestResult.SuccessfulResponse(body) + } else { + logE(LOG_CATEGORY) { + "Error receiving routes ${response.code()}: ${response.message()}" + } + when (response.code()) { + in 400..499 -> { + DirectionsRequestResult.ErrorResponse.NotRetryableError + } + in 500..599 -> { + DirectionsRequestResult.ErrorResponse.RetryableErrorWithDelay( + 60_000 + ) + } + else -> { + DirectionsRequestResult.ErrorResponse.RetryableError + } + } + } + continuation.resume(result) + } + + override fun onFailure(call: Call, t: Throwable) { + logE(LOG_CATEGORY) { + "Error requesting routes: $t" + } + continuation.resume(DirectionsRequestResult.ErrorResponse.RetryableError) + } + }) + continuation.invokeOnCancellation { + mapboxDirections.cancelCall() + } + } +} 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 161af1b0bab..e71d917b5c9 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 @@ -150,14 +150,14 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { createMapboxNavigation() // + 1 for navigationSession // + 1 for routesCacheClearer - verify(exactly = 3) { directionsSession.registerRoutesObserver(any()) } + verify(exactly = 3) { directionsSession.registerSetNavigationRoutesFinishedObserver(any()) } } @Test fun init_registersRoutesCacheClearerAsObservers() { createMapboxNavigation() verify(exactly = 1) { - directionsSession.registerRoutesObserver(routesCacheClearer) + directionsSession.registerSetNavigationRoutesFinishedObserver(routesCacheClearer) routesPreviewController.registerRoutesPreviewObserver(routesCacheClearer) } } @@ -240,7 +240,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { createMapboxNavigation() mapboxNavigation.onDestroy() - verify(exactly = 1) { directionsSession.unregisterAllRoutesObservers() } + verify(exactly = 1) { directionsSession.unregisterAllSetNavigationRoutesFinishedObserver() } } @Test @@ -307,7 +307,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.onDestroy() verify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( emptyList(), emptyList(), @@ -326,7 +326,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.onDestroy() verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -540,7 +540,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { it.onOffRouteStateChanged(true) } coVerify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( newAcceptedRoutes, newIgnoredRoutes, @@ -572,7 +572,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { it.onOffRouteStateChanged(true) } coVerify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -606,7 +606,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRerouteController(oldController) mapboxNavigation.setRerouteController(navigationRerouteController) coVerify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( newAcceptedRoutes, newIgnoredRoutes, @@ -636,7 +636,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRerouteController(oldController) mapboxNavigation.setRerouteController(navigationRerouteController) coVerify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -659,7 +659,33 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRerouteController(oldController) mapboxNavigation.setRerouteController(rerouteController) coVerify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( + match { it.setRoutesInfo == SetRoutes.Reroute }, + ) + } + } + + @Test + fun `set navigation reroute controller in fetching state sets routes to session`() { + val newRoutes = emptyList() + val oldController = mockk(relaxed = true) { + every { state } returns RerouteState.FetchingRoute + } + val rerouteController: NavigationRerouteController = mockk(relaxed = true) { + every { reroute(any()) } answers { + (firstArg() as NavigationRerouteController.RoutesCallback) + .onNewRoutes(newRoutes, mockk(relaxed = true)) + } + } + coEvery { + tripSession.setRoutes(any(), any()) + } returns NativeSetRouteValue(emptyList(), emptyList()) + + createMapboxNavigation() + mapboxNavigation.setRerouteController(oldController) + mapboxNavigation.setRerouteController(rerouteController) + coVerify(exactly = 1) { + directionsSession.setNavigationRoutesFinished( match { it.setRoutesInfo == SetRoutes.Reroute }, ) } @@ -684,7 +710,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRerouteController(oldController) mapboxNavigation.setRerouteController(rerouteController) coVerify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -714,7 +740,11 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val reason = RoutesExtra.ROUTES_UPDATE_REASON_NEW val routeObserversSlot = mutableListOf() every { tripSession.getState() } returns TripSessionState.STARTED - verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } + verify { + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) + } routeObserversSlot.forEach { it.onRoutesChanged(RoutesUpdatedResult(routes, ignoredRoutes, reason)) @@ -734,7 +764,11 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val reason = RoutesExtra.ROUTES_UPDATE_REASON_CLEAN_UP val routeObserversSlot = mutableListOf() every { tripSession.getState() } returns TripSessionState.STARTED - verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } + verify { + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) + } routeObserversSlot.forEach { it.onRoutesChanged(RoutesUpdatedResult(routes, ignoredRoutes, reason)) @@ -942,7 +976,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setNavigationRoutes(routes, initialLegIndex) verify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( acceptedRoutes, ignoredRoutes, @@ -966,7 +1000,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setNavigationRoutes(emptyList()) verify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( emptyList(), emptyList(), @@ -998,7 +1032,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setNavigationRoutes(routes, initialLegIndex) verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1025,7 +1059,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRoutes(routes, initialLegIndex) verify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( acceptedRoutes, listOf( @@ -1054,7 +1088,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setRoutes(routes, initialLegIndex) verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1099,7 +1133,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { possibleInternalCallbackSlot.captured.onRoutesReady(routes, origin) verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1360,7 +1394,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { verifyOrder { billingController.onExternalRouteSet(routes.first()) - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( routes, emptyList(), @@ -1382,8 +1416,12 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.setNavigationRoutes(listOf(primaryRoute)) verifyOrder { - directionsSession.setRoutes(match { it.setRoutesInfo is SetRoutes.Alternatives }) - directionsSession.setRoutes(match { it.setRoutesInfo is SetRoutes.Alternatives }) + directionsSession.setNavigationRoutesFinished( + match { it.setRoutesInfo is SetRoutes.Alternatives } + ) + directionsSession.setNavigationRoutesFinished( + match { it.setRoutesInfo is SetRoutes.Alternatives } + ) } } @@ -1426,14 +1464,14 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { } verifyOrder { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( longRoutes, emptyList(), SetRoutes.NewRoutes(0) ) ) - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( shortRoutes, emptyList(), @@ -1530,7 +1568,9 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { coroutineRule.runBlockingTest { val routeObserversSlot = mutableListOf() every { - directionsSession.registerRoutesObserver(capture(routeObserversSlot)) + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) } just Runs createMapboxNavigation() mapboxNavigation.setRerouteController(rerouteController) @@ -1586,7 +1626,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { processedRoutes, nativeAlternatives ) - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( processedRoutes, emptyList(), @@ -1620,7 +1660,11 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { ) } returns NativeSetRouteError("some error") - verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } + verify { + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) + } routeObserversSlot.forEach { it.onRoutesChanged( RoutesUpdatedResult(routes, ignoredRoutes, RoutesExtra.ROUTES_UPDATE_REASON_NEW) @@ -1658,7 +1702,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { delay(100) NativeSetRouteValue(inputRoutes, validAlternatives) } - every { directionsSession.setRoutes(any()) } answers { + every { directionsSession.setNavigationRoutesFinished(any()) } answers { every { directionsSession.routes } returns (firstArg() as DirectionsSessionRoutes).acceptedRoutes @@ -1674,7 +1718,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { coVerifyOrder { tripSession.setRoutes(inputRoutes, setRoutesInfo) - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes(acceptedRoutes, ignoredRoutes, setRoutesInfo) ) tripSession.setRoutes(acceptedRoutes, setRoutesInfo) @@ -1682,7 +1726,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val routesSlot = mutableListOf() verify(exactly = 1) { - directionsSession.setRoutes(capture(routesSlot)) + directionsSession.setNavigationRoutesFinished(capture(routesSlot)) } assertEquals(1, routesSlot.size) assertEquals(acceptedRoutes, routesSlot.first().acceptedRoutes) @@ -1699,10 +1743,10 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { mapboxNavigation.stopTripSession() verify(exactly = 1) { - directionsSession.setRoutes(match { it.acceptedRoutes == routes }) + directionsSession.setNavigationRoutesFinished(match { it.acceptedRoutes == routes }) } verify(exactly = 0) { - directionsSession.setRoutes(match { it.acceptedRoutes.isEmpty() }) + directionsSession.setNavigationRoutesFinished(match { it.acceptedRoutes.isEmpty() }) } } @@ -1726,7 +1770,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { tripSession.setRoutes(any(), ofType(SetRoutes.Alternatives::class)) } verify(exactly = 1) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1750,7 +1794,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { tripSession.setRoutes(any(), ofType(SetRoutes.NewRoutes::class)) } verify(exactly = 1) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1774,7 +1818,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { tripSession.setRoutes(any(), any()) } verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } @@ -1787,7 +1831,11 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val reason = RoutesExtra.ROUTES_UPDATE_REASON_NEW val routeObserversSlot = mutableListOf() every { tripSession.getState() } returns TripSessionState.STARTED - verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } + verify { + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) + } val alternativeRoute1 = routeWithId("id#1") val alternativeRoute2 = routeWithId("id#2") @@ -1820,7 +1868,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { ) } verify(exactly = 1) { - directionsSession.setRoutes( + directionsSession.setNavigationRoutesFinished( DirectionsSessionRoutes( acceptedRefreshRoutes, ignoredRefreshRoutes, @@ -1842,7 +1890,11 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { val reason = RoutesExtra.ROUTES_UPDATE_REASON_NEW val routeObserversSlot = mutableListOf() every { tripSession.getState() } returns TripSessionState.STARTED - verify { directionsSession.registerRoutesObserver(capture(routeObserversSlot)) } + verify { + directionsSession.registerSetNavigationRoutesFinishedObserver( + capture(routeObserversSlot) + ) + } val primaryRoute = routeWithId("id#0") val alternativeRoute = routeWithId("id#1") @@ -1869,7 +1921,7 @@ internal class MapboxNavigationTest : MapboxNavigationBaseTest() { ) } verify(exactly = 0) { - directionsSession.setRoutes(any()) + directionsSession.setNavigationRoutesFinished(any()) } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSessionTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSessionTest.kt index bd9e74efb11..4087b29a4dc 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSessionTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/directions/session/MapboxDirectionsSessionTest.kt @@ -154,7 +154,13 @@ class MapboxDirectionsSessionTest { @Test fun getRouteOptions() { - session.setRoutes(DirectionsSessionRoutes(routes, emptyList(), SetRoutes.NewRoutes(0))) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes( + routes, + emptyList(), + SetRoutes.NewRoutes(0) + ) + ) assertEquals(routeOptions, session.getPrimaryRouteOptions()) } @@ -173,7 +179,9 @@ class MapboxDirectionsSessionTest { ) cases.forEach { (setRoutes, expectedInitialLegIndex) -> - session.setRoutes(DirectionsSessionRoutes(routes, emptyList(), setRoutes)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, emptyList(), setRoutes) + ) assertEquals(expectedInitialLegIndex, session.initialLegIndex) } @@ -209,8 +217,10 @@ class MapboxDirectionsSessionTest { val slot = slot() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.registerSetNavigationRoutesFinishedObserver(observer) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) verify(exactly = 1) { observer.onRoutesChanged(slot.captured) } assertEquals(slot.captured.reason, mockSetRoutesInfo.mapToReason()) @@ -221,7 +231,9 @@ class MapboxDirectionsSessionTest { @Test fun `when route set, compatibility cache notified`() { mockkObject(RouteCompatibilityCache) - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) verify(exactly = 1) { RouteCompatibilityCache.setDirectionsSessionResult(routes) } verify(exactly = 0) { RouteCompatibilityCache.cacheCreationResult(routes) } @@ -231,10 +243,14 @@ class MapboxDirectionsSessionTest { @Test fun `when route cleared, compatibility cache notified`() { - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) mockkObject(RouteCompatibilityCache) - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) verify(exactly = 1) { RouteCompatibilityCache.setDirectionsSessionResult(emptyList()) } @@ -243,13 +259,13 @@ class MapboxDirectionsSessionTest { @Test fun `observer notified on subscribe with actual route data`() { - session.setRoutes( + session.setNavigationRoutesFinished( DirectionsSessionRoutes(routes, ignoredRoutes, SetRoutes.NewRoutes(0)) ) val slot = slot() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) + session.registerSetNavigationRoutesFinishedObserver(observer) verify(exactly = 1) { observer.onRoutesChanged(slot.captured) } assertEquals( @@ -263,11 +279,13 @@ class MapboxDirectionsSessionTest { @Test fun `observer notified on subscribe with explicit empty route data`() { - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) val slot = slot() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) + session.registerSetNavigationRoutesFinishedObserver(observer) verify(exactly = 1) { observer.onRoutesChanged(slot.captured) } assertEquals( @@ -287,7 +305,7 @@ class MapboxDirectionsSessionTest { val slot = slot() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) + session.registerSetNavigationRoutesFinishedObserver(observer) verify(exactly = 0) { observer.onRoutesChanged(any()) } } @@ -297,9 +315,13 @@ class MapboxDirectionsSessionTest { val slot = mutableListOf() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, SetRoutes.NewRoutes(0))) - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.registerSetNavigationRoutesFinishedObserver(observer) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, SetRoutes.NewRoutes(0)) + ) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) assertTrue("Number of onRoutesChanged invocations", slot.size == 2) assertEquals( @@ -323,8 +345,10 @@ class MapboxDirectionsSessionTest { val slot = mutableListOf() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.registerSetNavigationRoutesFinishedObserver(observer) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) assertTrue("Number of onRoutesChanged invocations", slot.size == 1) assertEquals( @@ -338,11 +362,15 @@ class MapboxDirectionsSessionTest { @Test fun `when route cleared for the second first time, observer not notified`() { - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) - session.registerRoutesObserver(observer) + session.registerSetNavigationRoutesFinishedObserver(observer) clearMocks(observer) - session.setRoutes(DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(emptyList(), emptyList(), SetRoutes.CleanUp) + ) verify(exactly = 0) { observer.onRoutesChanged(any()) @@ -354,15 +382,17 @@ class MapboxDirectionsSessionTest { val slot = slot() every { observer.onRoutesChanged(capture(slot)) } just runs - session.registerRoutesObserver(observer) - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, SetRoutes.NewRoutes(0))) + session.registerSetNavigationRoutesFinishedObserver(observer) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, SetRoutes.NewRoutes(0)) + ) val newRoutes: List = listOf( mockk { every { directionsRoute } returns mockk() } ) val newIgnoredRoutes = listOf(mockk(relaxed = true)) - session.setRoutes( + session.setNavigationRoutesFinished( DirectionsSessionRoutes(newRoutes, newIgnoredRoutes, SetRoutes.NewRoutes(0)) ) @@ -375,22 +405,28 @@ class MapboxDirectionsSessionTest { @Test fun `setting a route does not impact ongoing route request`() { session.requestRoutes(routeOptions, routerCallback) - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) verify(exactly = 0) { router.cancelAll() } } @Test fun unregisterAllRouteObservers() { - session.registerRoutesObserver(observer) - session.unregisterAllRoutesObservers() - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.registerSetNavigationRoutesFinishedObserver(observer) + session.unregisterAllSetNavigationRoutesFinishedObserver() + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) verify(exactly = 0) { observer.onRoutesChanged(any()) } } @Test fun `routes when routesUpdatedResult is null`() { - session.setRoutes(DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo)) + session.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, ignoredRoutes, mockSetRoutesInfo) + ) assertEquals(routes, session.routes) } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitchTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitchTest.kt new file mode 100644 index 00000000000..c5371828d42 --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/OnlineRouteAlternativesSwitchTest.kt @@ -0,0 +1,799 @@ +package com.mapbox.navigation.core.routealternatives + +import com.mapbox.api.directions.v5.models.Bearing +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.route.NavigationRoute +import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.core.SetRoutes +import com.mapbox.navigation.core.directions.session.DirectionsSessionRoutes +import com.mapbox.navigation.core.directions.session.MapboxDirectionsSession +import com.mapbox.navigation.core.directions.session.RoutesObserver +import com.mapbox.navigation.core.directions.session.RoutesSetStartedParams +import com.mapbox.navigation.core.directions.session.SetNavigationRoutesStartedObserver +import com.mapbox.navigation.core.trip.session.LocationMatcherResult +import com.mapbox.navigation.testing.LoggingFrontendTestRule +import com.mapbox.navigation.testing.NativeRouteParserRule +import com.mapbox.navigation.testing.factories.createBearing +import com.mapbox.navigation.testing.factories.createCoordinatesList +import com.mapbox.navigation.testing.factories.createDirectionsResponse +import com.mapbox.navigation.testing.factories.createNavigationRoutes +import com.mapbox.navigation.testing.factories.createRouteOptions +import com.mapbox.navigation.testing.factories.createWaypoint +import com.mapbox.navigation.utils.internal.toPoint +import com.mapbox.navigator.Waypoint +import com.mapbox.navigator.WaypointType +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Rule +import org.junit.Test + +internal class OnlineRouteAlternativesSwitchTest { + + @get:Rule + val loggerRule = LoggingFrontendTestRule() + + @get:Rule + val nativeRouteParserRule = NativeRouteParserRule() + + @Test + fun `nothing happens for online routes`() = runBlockingTest { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Offboard + ) + val testMapboxNavigation = TestMapboxNavigation() + var routeRequestCount = 0 + + val onlineRoutesDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + testMapboxNavigation.routeProgressEvents, + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = { options -> + routeRequestCount++ + createOnlineRoute(options) + } + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + destroy() + } + + val onlineRoutes = onlineRoutesDeferred.await() + assertEquals(0, onlineRoutes.size) + assertEquals(0, routeRequestCount) + } + + @Test + fun `online route is calculated for an offline route`() = runBlockingTest { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + Point.fromLngLat(2.2, 2.2), + ), + bearings = listOf( + createBearing(angle = 99.0), + null + ), + avoidManeuverRadius = null + ) + val testMapboxNavigation = TestMapboxNavigation() + val newLocation = createLocation( + longitudeValue = 33.0, + latitudeValue = 99.0, + bearingValue = 250.0f, + speedValue = 8.0f + ) + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(testRoutes.first()).toStateFlow(), + newLocation.toStateFlow(), + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + avoidManeuverSeconds = 8 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + destroy() + } + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + assertEquals( + newLocation.enhancedLocation.toPoint(), + onlinePrimaryRoute.routeOptions.coordinatesList().first() + ) + assertEquals( + newLocation.enhancedLocation.bearing.toDouble(), + onlinePrimaryRoute.routeOptions.bearingsList()?.first()?.angle() + ) + assertEquals( + 64.0, + onlinePrimaryRoute.routeOptions.avoidManeuverRadius() + ) + } + + @Test + fun `online route is not calculated for an offline route in case of internal calculation failure`() = + runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Onboard + ) + val routeProgressFlow = createRouteProgress( + testRoutes.first(), + // incorrect reaming waypoints value should cause internal failure + remainingWaypointsValue = -1, + ).toStateFlow() + val testMapboxNavigation = TestMapboxNavigation() + var requestsCount = 0 + + val onlineRoutesEventDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + routeProgressFlow, + createLocation().toStateFlow(), + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = { options -> + requestsCount++ + createOnlineRoute(options) + }, + minimumRetryInterval = 100, + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + } + assertTrue(onlineRoutesEventDeferred.isActive) + advanceTimeBy(200) + assertTrue(onlineRoutesEventDeferred.isActive) + routeProgressFlow.value = createRouteProgress(testRoutes.first()) + testMapboxNavigation.destroy() + val onlineRoutesEvents = onlineRoutesEventDeferred.await() + + assertEquals(1, requestsCount) + assertEquals(1, onlineRoutesEvents.size) + } + + @Test + fun `online route is calculated for an offline route on second leg`() = runBlockingTest { + val testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + Point.fromLngLat(2.2, 2.2), + Point.fromLngLat(3.3, 3.3), + ) + val testRoutes = createTestRoutes( + testCoordinates, + RouterOrigin.Onboard, + + ) + val newLocation = createLocation( + longitudeValue = 2.5, + latitudeValue = 2.5, + bearingValue = 30.0f, + speedValue = 3f + ) + val routeProgress = createRouteProgress( + testRoutes.first(), + remainingWaypointsValue = 1, + ) + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + routeProgress.toStateFlow(), + newLocation.toStateFlow(), + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + avoidManeuverSeconds = 4, + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + destroy() + } + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + assertEquals( + listOf( + newLocation.enhancedLocation.toPoint(), + testRoutes.first().routeOptions.coordinatesList().last() + ), + onlinePrimaryRoute.routeOptions.coordinatesList() + ) + assertEquals( + listOf( + newLocation.enhancedLocation.bearing.toDouble(), + null + ), + onlinePrimaryRoute.routeOptions.bearingsList()?.map { it?.angle() } + ) + assertEquals( + 12.0, + onlinePrimaryRoute.routeOptions.avoidManeuverRadius(), + ) + } + + @Test + fun `offline route is cleaned up before online is calculated`() = runBlockingTest { + val offlineRoutes = createNavigationRoutes( + routerOrigin = RouterOrigin.Onboard, + ) + val onlineRouteCalculated = CompletableDeferred() + val testMapboxNavigation = TestMapboxNavigation() + val onlineRoutesDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(offlineRoutes.first()).toStateFlow(), + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = { options -> + onlineRouteCalculated.await() + createOnlineRoute(options) + }, + ).toList() + } + + assertTrue(onlineRoutesDeferred.isActive) + testMapboxNavigation.apply { + setRoutes(offlineRoutes) + setRoutes(emptyList()) + } + onlineRouteCalculated.complete(Unit) + testMapboxNavigation.destroy() + + val onlineRoutes = onlineRoutesDeferred.await() + assertEquals(0, onlineRoutes.size) + } + + @Test + fun `online route is calculated after routes are cleaned up but before cleanup is processed`() = + runBlockingTest { + val offlineRoute = createNavigationRoutes( + routerOrigin = RouterOrigin.Onboard, + ) + val onlineRouteCalculated = CompletableDeferred() + val testMapboxNavigation = TestMapboxNavigation() + val cleanupProcessing = CompletableDeferred() + val onlineRoutesCollection = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(offlineRoute.first()).toStateFlow(), + routesSetStartedEvents = testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = { options -> + onlineRouteCalculated.await() + createOnlineRoute(options) + }, + ).toList() + } + + assertTrue(onlineRoutesCollection.isActive) + testMapboxNavigation.apply { + setRoutes(offlineRoute) + setRoutes( + emptyList(), + routesProcessing = cleanupProcessing + ) + } + onlineRouteCalculated.complete(Unit) + cleanupProcessing.complete(Unit) + + testMapboxNavigation.destroy() + + onlineRoutesCollection.cancel() + val collectedOnlineRoutes = onlineRoutesCollection.await() + assertEquals(0, collectedOnlineRoutes.size) + } + + @Test + fun `offline route is updated during online route calculation`() = runBlockingTest { + val currentPosition = Point.fromLngLat(1.0, 1.0) + val firstOfflineRoutes = createNavigationRoutes( + routerOrigin = RouterOrigin.Onboard, + options = createRouteOptions( + coordinatesList = listOf( + currentPosition, + Point.fromLngLat(2.0, 2.0) + ) + ) + ) + val secondOfflineRoutes = createNavigationRoutes( + routerOrigin = RouterOrigin.Onboard, + options = createRouteOptions( + coordinatesList = listOf( + currentPosition, + Point.fromLngLat(3.0, 3.0) + ) + ) + ) + val newLocation = createLocation( + latitudeValue = currentPosition.latitude(), + longitudeValue = currentPosition.longitude() + ) + val onlineRouteRequestWaitHandle = CompletableDeferred() + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + testMapboxNavigation.routeProgressEvents, + newLocation.toStateFlow(), + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = { options -> + onlineRouteRequestWaitHandle.await() + createOnlineRoute(options) + } + ).toList() + } + testMapboxNavigation.apply { + setRoutes(firstOfflineRoutes) + setRoutes(secondOfflineRoutes) + onlineRouteRequestWaitHandle.complete(Unit) + destroy() + } + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlineRoutes = onlineRoutesEvents.first() + assertEquals( + secondOfflineRoutes.first().routeOptions.coordinatesList(), + onlineRoutes.first().routeOptions.coordinatesList() + ) + } + + @Test + fun `retry calculating online route with delay in case of immediate failure`() = + runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + Point.fromLngLat(2.2, 2.2), + ) + ) + val locationUpdatesValue = listOf( + createLocation(latitudeValue = 1.1, longitudeValue = 1.1), + createLocation(latitudeValue = 1.2, longitudeValue = 1.1), + createLocation(latitudeValue = 1.3, longitudeValue = 1.1), + ) + val locationUpdatesFlow = locationUpdatesValue.first().toStateFlow() + var routeRequestCount = 0 + val routeRequest: RouteRequestMechanism = { options -> + routeRequestCount++ + if (options.coordinatesList().first().latitude() == 1.3) { + createOnlineRoute(options) + } else { + DirectionsRequestResult.ErrorResponse.RetryableError + } + } + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(testRoutes.first()).toStateFlow(), + locationUpdatesFlow, + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = routeRequest, + minimumRetryInterval = 100 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + } + assertTrue(onlineRoutesEventsDeferred.isActive) + assertEquals(1, routeRequestCount) + advanceTimeBy(500) + assertEquals(6, routeRequestCount) + locationUpdatesFlow.value = locationUpdatesValue[2] + testMapboxNavigation.destroy() + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + assertEquals( + locationUpdatesValue[2].enhancedLocation.toPoint(), + onlinePrimaryRoute.routeOptions.coordinatesList().first() + ) + } + + @Test + fun `retry calculating online route in case of an exception from route request mechanism`() = + runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + Point.fromLngLat(2.2, 2.2), + ) + ) + val locationUpdatesValue = listOf( + createLocation(latitudeValue = 1.1, longitudeValue = 1.1), + createLocation(latitudeValue = 1.3, longitudeValue = 1.1), + ) + val locationUpdatesFlow = locationUpdatesValue.first().toStateFlow() + val routeRequest: RouteRequestMechanism = { options -> + if (options.coordinatesList().first().latitude() == 1.3) { + createOnlineRoute(options) + } else { + error("test error") + } + } + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(testRoutes.first()).toStateFlow(), + locationUpdatesFlow, + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = routeRequest, + minimumRetryInterval = 100 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + } + assertTrue(onlineRoutesEventsDeferred.isActive) + locationUpdatesFlow.value = locationUpdatesValue[1] + testMapboxNavigation.destroy() + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + } + + @Test + fun `retry calculating online route with connection delay`() = runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val testRoutes = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + Point.fromLngLat(2.2, 2.2), + ) + ) + val locationUpdatesValue = listOf( + createLocation(latitudeValue = 1.1, longitudeValue = 1.1), + createLocation(latitudeValue = 1.2, longitudeValue = 1.1), + createLocation(latitudeValue = 1.3, longitudeValue = 1.1), + ) + val locationUpdatesFlow = locationUpdatesValue.first().toStateFlow() + var routeRequestCount = 0 + val routeRequest: RouteRequestMechanism = { options -> + delay(500) + routeRequestCount++ + if (options.coordinatesList().first().latitude() == 1.3) { + createOnlineRoute(options) + } else { + DirectionsRequestResult.ErrorResponse.RetryableError + } + } + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + createRouteProgress(testRoutes.first()).toStateFlow(), + locationUpdatesFlow, + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = routeRequest, + minimumRetryInterval = 100 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(testRoutes) + } + assertTrue(onlineRoutesEventsDeferred.isActive) + advanceTimeBy(1001) + assertEquals(2, routeRequestCount) + locationUpdatesFlow.value = locationUpdatesValue[2] + testMapboxNavigation.destroy() + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + } + + @Test + fun `not retryable error stops retries`() = + runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val brokenDestination = Point.fromLngLat(2.2, 2.2) + val offlineRouteWhereOnlineCanNotBeCalculated = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + brokenDestination, + ) + ) + val normalDestination = Point.fromLngLat(3.3, 3.3) + val offlineRoute = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + testCoordinates = listOf( + Point.fromLngLat(1.1, 1.1), + normalDestination, + ) + ) + val locationUpdatesFlow = createLocation().toStateFlow() + var routeRequestCount = 0 + val routeRequest: RouteRequestMechanism = { options -> + routeRequestCount++ + val destination = options.coordinatesList().last() + when (destination) { + normalDestination -> createOnlineRoute(options) + brokenDestination -> DirectionsRequestResult.ErrorResponse.NotRetryableError + else -> { + fail("not expected destination $destination") + error("not expected destination $destination") + } + } + } + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + testMapboxNavigation.routeProgressEvents, + locationUpdatesFlow, + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = routeRequest, + minimumRetryInterval = 100 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(offlineRouteWhereOnlineCanNotBeCalculated) + } + assertTrue(onlineRoutesEventsDeferred.isActive) + assertEquals(1, routeRequestCount) + advanceTimeBy(500) + assertEquals(1, routeRequestCount) + testMapboxNavigation.apply { + setRoutes(offlineRoute) + testMapboxNavigation.destroy() + } + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + assertEquals( + normalDestination, + onlinePrimaryRoute.routeOptions.coordinatesList().last() + ) + } + + @Test + fun `error that requires delay ignores user set intervals`() = + runBlockingTest( + Job() // https://github.com/Kotlin/kotlinx.coroutines/issues/1910 + ) { + val offlineRoute = createTestRoutes( + routerOrigin = RouterOrigin.Onboard, + ) + val locationUpdatesFlow = createLocation().toStateFlow() + var routeRequestCount = 0 + var returnErrorWithRetryDelay = true + val testRetryDelay = 5_000L + val routeRequest: RouteRequestMechanism = { options -> + routeRequestCount++ + if (returnErrorWithRetryDelay) { + DirectionsRequestResult.ErrorResponse.RetryableErrorWithDelay(testRetryDelay) + } else { + createOnlineRoute(options) + } + } + val testMapboxNavigation = TestMapboxNavigation() + + val onlineRoutesEventsDeferred = async { + requestOnlineRoutesTestWrapperWithDefaultValues( + testMapboxNavigation.flowRoutesUpdated, + testMapboxNavigation.routeProgressEvents, + locationUpdatesFlow, + testMapboxNavigation.flowSetNavigationRoutesStarted, + routeRequestMechanism = routeRequest, + minimumRetryInterval = 100 + ).toList() + } + testMapboxNavigation.apply { + setRoutes(offlineRoute) + } + assertTrue(onlineRoutesEventsDeferred.isActive) + assertEquals(1, routeRequestCount) + advanceTimeBy(3_000) + assertEquals(1, routeRequestCount) + advanceTimeBy(testRetryDelay) + assertEquals(2, routeRequestCount) + returnErrorWithRetryDelay = false + advanceTimeBy(testRetryDelay) + assertEquals(3, routeRequestCount) + + testMapboxNavigation.destroy() + val onlineRoutesEvents = onlineRoutesEventsDeferred.await() + + assertEquals(1, onlineRoutesEvents.size) + val onlinePrimaryRoute = onlineRoutesEvents.first().first() + assertEquals(RouterOrigin.Offboard, onlinePrimaryRoute.origin) + } +} + +private fun createTestRoutes( + testCoordinates: List = createCoordinatesList(2), + routerOrigin: RouterOrigin, + bearings: List? = null, + avoidManeuverRadius: Double? = null +) = + createNavigationRoutes( + routerOrigin = routerOrigin, + response = createDirectionsResponse( + responseWaypoints = testCoordinates.map { + createWaypoint(location = it.coordinates().toDoubleArray()) + } + ), + options = createRouteOptions( + coordinatesList = testCoordinates, + bearingList = bearings, + avoidManeuverRadius = avoidManeuverRadius + ), + waypointsMapper = { _ -> + testCoordinates.mapIndexed { index, coordinate -> + Waypoint( + "waypoint_$index", + coordinate, + null, + null, + null, + WaypointType.REGULAR, + ) + } + } + ) + +private fun createLocation( + latitudeValue: Double = 8.0, + longitudeValue: Double = 8.0, + bearingValue: Float = 8.0f, + speedValue: Float = 10.0f +) = mockk(relaxed = true) { + every { enhancedLocation } returns mockk(relaxed = true) { + every { longitude } returns longitudeValue + every { latitude } returns latitudeValue + every { bearing } returns bearingValue + every { speed } returns speedValue + } +} + +private fun createRouteProgress( + primaryRoute: NavigationRoute, + remainingWaypointsValue: Int = 1, +): RouteProgress = mockk(relaxed = true) { + every { navigationRoute } returns primaryRoute + every { remainingWaypoints } returns remainingWaypointsValue +} + +fun T.toStateFlow(): MutableStateFlow = MutableStateFlow(this) + +internal suspend fun createOnlineRoute(routeOptions: RouteOptions): DirectionsRequestResult { + return DirectionsRequestResult.SuccessfulResponse( + createDirectionsResponse(routeOptions = routeOptions) + ) +} + +// wrapper helps adding new parameters without rewriting every call in tests +private fun requestOnlineRoutesTestWrapperWithDefaultValues( + routesUpdatedEvents: Flow>, + routeProgressUpdate: Flow, + locationUpdates: Flow = createLocation().toStateFlow(), + routesSetStartedEvents: Flow, + routeRequestMechanism: RouteRequestMechanism = ::createOnlineRoute, + minimumRetryInterval: Long = 1_000, + avoidManeuverSeconds: Int = 3, + navigationRouteSerializationDispatcher: CoroutineDispatcher = TestCoroutineDispatcher() +): Flow> { + return requestOnlineRoutes( + routesUpdatedEvents = routesUpdatedEvents, + matchingResults = locationUpdates, + routeProgressUpdates = routeProgressUpdate, + setNavigaitonRoutesStartedEvents = routesSetStartedEvents, + routeRequestMechanism = routeRequestMechanism, + minimumRetryInterval = minimumRetryInterval, + navigationRouteSerializationDispatcher = navigationRouteSerializationDispatcher, + avoidManeuverSeconds = avoidManeuverSeconds + ) +} + +private class TestMapboxNavigation { + + private val directionsSession = MapboxDirectionsSession(mockk()) + private val onClose = mutableListOf<() -> Unit>() + + val flowRoutesUpdated: Flow> + get() = callbackFlow { + onClose.add { + this.close() + } + val observer = RoutesObserver { trySend(it) } + directionsSession.registerSetNavigationRoutesFinishedObserver(observer) + awaitClose { directionsSession.unregisterSetNavigationRoutesFinishedObserver(observer) } + }.map { it.navigationRoutes } + + val flowSetNavigationRoutesStarted: Flow + get() = callbackFlow { + onClose.add { + this.close() + } + val observer = SetNavigationRoutesStartedObserver { trySend(it) } + directionsSession.registerSetNavigationRoutesStartedObserver(observer) + awaitClose { directionsSession.unregisterSetNavigationRoutesStartedObserver(observer) } + } + + val routeProgressEvents: Flow + get() = flowRoutesUpdated.mapNotNull { + if (it.isNotEmpty()) { + createRouteProgress(primaryRoute = it.first()) + } else { + null + } + } + + fun CoroutineScope.setRoutes( + routes: List, + initialLegIndex: Int = 0, + routesProcessing: Deferred? = null + ) { + launch { + directionsSession.setNavigationRoutesStarted(RoutesSetStartedParams(routes)) + routesProcessing?.await() + val setRoutesParams = if (routes.isEmpty()) { + SetRoutes.CleanUp + } else { + SetRoutes.NewRoutes(initialLegIndex) + } + directionsSession.setNavigationRoutesFinished( + DirectionsSessionRoutes(routes, emptyList(), setRoutesParams) + ) + } + } + + fun destroy() { + onClose.forEach { it.invoke() } + } +} diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/DirectionsRouteDiffProviderTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/DirectionsRouteDiffProviderTest.kt index e6451992cc1..fb2b0bfac70 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/DirectionsRouteDiffProviderTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routerefresh/DirectionsRouteDiffProviderTest.kt @@ -6,10 +6,11 @@ import com.mapbox.api.directions.v5.models.DirectionsWaypoint import com.mapbox.api.directions.v5.models.RouteLeg import com.mapbox.navigation.base.route.NavigationRoute import com.mapbox.navigation.testing.factories.createClosure +import com.mapbox.navigation.testing.factories.createDirectionsResponse import com.mapbox.navigation.testing.factories.createDirectionsRoute import com.mapbox.navigation.testing.factories.createIncident import com.mapbox.navigation.testing.factories.createMaxSpeed -import com.mapbox.navigation.testing.factories.createNavigationRoute +import com.mapbox.navigation.testing.factories.createNavigationRoutes import com.mapbox.navigation.testing.factories.createRouteLeg import com.mapbox.navigation.testing.factories.createRouteLegAnnotation import com.mapbox.navigation.testing.factories.createRouteOptions @@ -227,14 +228,19 @@ class DirectionsRouteDiffProviderTest { waypointsPerRoute: Boolean? = null, waypoints: List = emptyList() ): NavigationRoute { - return createNavigationRoute( - createDirectionsRoute( - requestUuid = "testDiff", - legs = legs, - routeOptions = createRouteOptions(waypointsPerRoute = waypointsPerRoute), - waypoints = waypoints + return createNavigationRoutes( + options = createRouteOptions(waypointsPerRoute = waypointsPerRoute), + response = createDirectionsResponse( + uuid = "testDiff", + routes = listOf( + createDirectionsRoute( + legs = legs, + waypoints = if (waypointsPerRoute == true) waypoints else null + ) + ), + responseWaypoints = if (waypointsPerRoute != true) waypoints else null ) - ) + ).first() } private fun createTestLeg( diff --git a/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/NativeRouteParserRule.kt b/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/NativeRouteParserRule.kt index 1a6c67dc8ed..61c34ff0d54 100644 --- a/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/NativeRouteParserRule.kt +++ b/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/NativeRouteParserRule.kt @@ -5,6 +5,8 @@ import com.mapbox.api.directions.v5.models.DirectionsWaypoint import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.bindgen.ExpectedFactory import com.mapbox.navigation.base.internal.NativeRouteParserWrapper +import com.mapbox.navigation.base.internal.utils.mapToNativeRouteOrigin +import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigator.RouteInterface import com.mapbox.navigator.Waypoint import com.mapbox.navigator.WaypointType @@ -28,6 +30,7 @@ class NativeRouteParserRule : TestRule { every { NativeRouteParserWrapper.parseDirectionsResponse(any(), any(), any()) } answers { + val origin = this.thirdArg() val response = JSONObject(this.firstArg()) val routesCount = response.getJSONArray("routes").length() val idBase = if (response.has("uuid")) { @@ -72,7 +75,7 @@ class NativeRouteParserRule : TestRule { every { routeId } returns "$idBase#$it" every { routerOrigin - } returns com.mapbox.navigator.RouterOrigin.CUSTOM + } returns origin.mapToNativeRouteOrigin() every { waypoints } returns parsedWaypoints } ) diff --git a/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/factories/NavigationRouteFactory.kt b/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/factories/NavigationRouteFactory.kt index 476b8f64132..35485fdaed7 100644 --- a/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/factories/NavigationRouteFactory.kt +++ b/libtesting-navigation-base/src/main/java/com/mapbox/navigation/testing/factories/NavigationRouteFactory.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.testing.factories +import com.mapbox.api.directions.v5.models.Bearing import com.mapbox.api.directions.v5.models.DirectionsResponse import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteOptions @@ -16,15 +17,17 @@ import com.mapbox.navigator.Waypoint fun createNavigationRoute( directionsRoute: DirectionsRoute = createDirectionsRoute(), routeInfo: RouteInfo = RouteInfo(emptyList()), - waypoints: List = createWaypoints() + waypoints: List = createWaypoints(), + routerOrigin: RouterOrigin = RouterOrigin.Offboard ): NavigationRoute { return createNavigationRoutes( response = createDirectionsResponse( routes = listOf(directionsRoute), - uuid = directionsRoute.requestUuid() + uuid = directionsRoute.requestUuid(), ), routesInfoMapper = { routeInfo }, waypointsMapper = { waypoints }, + routerOrigin = routerOrigin ).first() } @@ -88,4 +91,12 @@ fun createRouteInterfacesFromDirectionRequestResponse( } } -fun createRouteInfo() = RouteInfo(emptyList()) \ No newline at end of file +fun createRouteInfo() = RouteInfo(emptyList()) + +fun createBearing( + angle: Double = 20.0, + degrees: Double = 45.0 +) = Bearing.builder() + .angle(angle) + .degrees(degrees) + .build() \ No newline at end of file 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 aa35468ff7d..3169741f060 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 @@ -10,6 +10,7 @@ import com.mapbox.api.directions.v5.models.BannerComponents import com.mapbox.api.directions.v5.models.BannerInstructions import com.mapbox.api.directions.v5.models.BannerText import com.mapbox.api.directions.v5.models.BannerView +import com.mapbox.api.directions.v5.models.Bearing import com.mapbox.api.directions.v5.models.DirectionsResponse import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.DirectionsWaypoint @@ -26,15 +27,18 @@ fun createDirectionsResponse( uuid: String? = "testUUID", routes: List = listOf(createDirectionsRoute()), unrecognizedProperties: Map? = null, + responseWaypoints: List? = listOf(createWaypoint(), createWaypoint()), + routeOptions: RouteOptions? = createRouteOptions() ): DirectionsResponse { val processedRoutes = routes.map { - it.toBuilder().requestUuid(uuid).build() + it.toBuilder().requestUuid(uuid).routeOptions(routeOptions).build() } return DirectionsResponse.builder() .uuid(uuid) .code("Ok") .routes(processedRoutes) .unrecognizedJsonProperties(unrecognizedProperties) + .waypoints(responseWaypoints) .build() } @@ -232,6 +236,8 @@ fun createRouteOptions( unrecognizedProperties: Map? = null, enableRefresh: Boolean? = false, waypointsPerRoute: Boolean? = null, + bearingList: List? = null, + avoidManeuverRadius: Double? = null, ): RouteOptions { return RouteOptions .builder() @@ -240,6 +246,8 @@ fun createRouteOptions( .enableRefresh(enableRefresh) .waypointsPerRoute(waypointsPerRoute) .unrecognizedJsonProperties(unrecognizedProperties) + .bearingsList(bearingList) + .avoidManeuverRadius(avoidManeuverRadius) .build() } diff --git a/qa-test-app/src/main/AndroidManifest.xml b/qa-test-app/src/main/AndroidManifest.xml index 800f456784b..cbc8719308c 100644 --- a/qa-test-app/src/main/AndroidManifest.xml +++ b/qa-test-app/src/main/AndroidManifest.xml @@ -49,6 +49,7 @@ + activity.startActivity() }, + TestActivityDescription( + "Switch from offline to online route", + R.string.navigation_view_offline_online_route_switch, + category = CATEGORY_DROP_IN, + launchAfterPermissionResult = false + ) { activity -> + MapboxNavigationViewOfflineOnlineRouteSwitchActivity.startActivity(activity) + } ) fun getTestActivities(category: String): List { diff --git a/qa-test-app/src/main/java/com/mapbox/navigation/qa_test_app/view/MapboxNavigationViewOfflineOnlineRouteSwitchActivity.kt b/qa-test-app/src/main/java/com/mapbox/navigation/qa_test_app/view/MapboxNavigationViewOfflineOnlineRouteSwitchActivity.kt new file mode 100644 index 00000000000..57bd0fa62bf --- /dev/null +++ b/qa-test-app/src/main/java/com/mapbox/navigation/qa_test_app/view/MapboxNavigationViewOfflineOnlineRouteSwitchActivity.kt @@ -0,0 +1,185 @@ +package com.mapbox.navigation.qa_test_app.view + +import android.app.Activity +import android.os.Bundle +import android.view.View +import com.mapbox.bindgen.Value +import com.mapbox.common.TileDataDomain +import com.mapbox.common.TileRegionLoadOptions +import com.mapbox.common.TileStore +import com.mapbox.common.TileStoreOptions +import com.mapbox.geojson.Point +import com.mapbox.geojson.Polygon +import com.mapbox.navigation.base.ExperimentalMapboxNavigationAPI +import com.mapbox.navigation.base.options.NavigationOptions +import com.mapbox.navigation.base.options.RoutingTilesOptions +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.internal.extensions.flowLocationMatcherResult +import com.mapbox.navigation.core.internal.extensions.flowRoutesUpdated +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver +import com.mapbox.navigation.core.routealternatives.OnlineRouteAlternativesSwitch +import com.mapbox.navigation.qa_test_app.R +import com.mapbox.navigation.qa_test_app.databinding.LayoutActivityNavigationOfflineOnlineViewBinding +import com.mapbox.navigation.qa_test_app.databinding.LayoutDrawerMenuNavViewBinding +import com.mapbox.navigation.qa_test_app.utils.startActivity +import com.mapbox.navigation.qa_test_app.view.base.DrawerActivity +import com.mapbox.navigation.qa_test_app.view.customnavview.NavigationViewController +import com.mapbox.navigation.utils.internal.toPoint +import com.mapbox.turf.TurfMeasurement +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMapboxNavigationAPI::class) +class MapboxNavigationViewOfflineOnlineRouteSwitchActivity : DrawerActivity() { + + private lateinit var binding: LayoutActivityNavigationOfflineOnlineViewBinding + private lateinit var menuBinding: LayoutDrawerMenuNavViewBinding + + override fun onCreateContentView(): View { + binding = LayoutActivityNavigationOfflineOnlineViewBinding.inflate(layoutInflater) + return binding.root + } + + override fun onCreateMenuView(): View { + menuBinding = LayoutDrawerMenuNavViewBinding.inflate(layoutInflater) + return menuBinding.root + } + + private lateinit var controller: NavigationViewController + + private var onlineRouteSwitch = OnlineRouteAlternativesSwitch() + + private val tilesLoadingObserver = LoadAreaAroundObserver { state -> + setTitle("${state.requestedTiles}/${state.requestedTiles}, ${state.primaryRouteId}") + } + + override fun onCreate(savedInstanceState: Bundle?) { + val tileStore = TileStore.create() + tileStore.setOption( + TileStoreOptions.MAPBOX_ACCESS_TOKEN, + TileDataDomain.NAVIGATION, + Value.valueOf(getString(R.string.mapbox_access_token)) + ) + MapboxNavigationApp.setup( + NavigationOptions.Builder(applicationContext) + .accessToken(getString(R.string.mapbox_access_token)) + .routingTilesOptions( + RoutingTilesOptions.Builder() + .tileStore(tileStore) + .build() + ) + .build() + ) + super.onCreate(savedInstanceState) + controller = NavigationViewController(this, binding.navigationView) + + menuBinding.toggleReplay.isChecked = binding.navigationView.api.isReplayEnabled() + menuBinding.toggleReplay.setOnCheckedChangeListener { _, isChecked -> + binding.navigationView.api.routeReplayEnabled(isChecked) + } + + MapboxNavigationApp.registerObserver(onlineRouteSwitch) + MapboxNavigationApp.registerObserver(tilesLoadingObserver) + } + + override fun onDestroy() { + super.onDestroy() + MapboxNavigationApp.unregisterObserver(onlineRouteSwitch) + MapboxNavigationApp.unregisterObserver(tilesLoadingObserver) + } + + companion object { + fun startActivity( + parent: Activity, + ) { + parent.startActivity() + } + } +} + +data class UIState(val requestedTiles: Long, val loaded: Long, val primaryRouteId: String) + +class LoadAreaAroundObserver( + val progressCallback: (UIState) -> Unit +) : MapboxNavigationObserver { + + private lateinit var context: CoroutineScope + + override fun onAttached(mapboxNavigation: MapboxNavigation) { + context = CoroutineScope(Dispatchers.Main + SupervisorJob()) + context.launch { + mapboxNavigation.flowLocationMatcherResult() + .map { it.enhancedLocation.toPoint() } + .distinctUntilChanged { old, new -> TurfMeasurement.distance(old, new) < 1 } + .flatMapMerge { + val radius = 2.0 + val halfTheRadius = radius / 2 + val navTilesetDescriptor = mapboxNavigation.tilesetDescriptorFactory.getLatest() + // Do not test it near greenwich or equator + val region = Polygon.fromLngLats( + listOf( + listOf( + Point.fromLngLat( + it.longitude() - halfTheRadius, + it.latitude() - halfTheRadius + ), + Point.fromLngLat( + it.longitude() - halfTheRadius, + it.latitude() + halfTheRadius + ), + Point.fromLngLat( + it.longitude() + halfTheRadius, + it.latitude() + halfTheRadius + ), + Point.fromLngLat( + it.longitude() + halfTheRadius, + it.latitude() - halfTheRadius + ), + ) + ) + ) + val tileRegionLoadOptions = TileRegionLoadOptions.Builder() + .geometry(region) + .descriptors(listOf(navTilesetDescriptor)) + .build() + val tileStore = mapboxNavigation.navigationOptions + .routingTilesOptions + .tileStore!! + callbackFlow { + val cancellation = tileStore.loadTileRegion( + "$it-radius-$radius", + tileRegionLoadOptions, + { progres -> this.trySend(progres) }, + { progres -> this.close() }, + ) + awaitClose { cancellation.cancel() } + } + } + .combine(mapboxNavigation.flowRoutesUpdated()) { progres, routes -> + UIState( + progres.requiredResourceCount, + progres.completedResourceCount, + routes.navigationRoutes.firstOrNull()?.id ?: "null" + ) + } + .collect { + progressCallback(it) + } + } + } + + override fun onDetached(mapboxNavigation: MapboxNavigation) { + context.cancel() + } +} diff --git a/qa-test-app/src/main/res/layout/layout_activity_navigation_offline_online_view.xml b/qa-test-app/src/main/res/layout/layout_activity_navigation_offline_online_view.xml new file mode 100644 index 00000000000..d966cc94c60 --- /dev/null +++ b/qa-test-app/src/main/res/layout/layout_activity_navigation_offline_online_view.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/qa-test-app/src/main/res/values/strings.xml b/qa-test-app/src/main/res/values/strings.xml index 308fa544b18..3d2371eaad6 100644 --- a/qa-test-app/src/main/res/values/strings.xml +++ b/qa-test-app/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ Clear traffic The active leg of the route should be above the inactive leg. \n\nThis example also sets the inactive leg color to more clearly differentiate it from the active leg. \n\nThe active leg including the vansished section of the line should be above the inactive leg and when the puck transitions from the first active leg to the second there should be a visual change that continues to indicate the active leg is above the inactive leg. + Utility for testing offline-online route switching --