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:
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+
+
+
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:
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+
+
+
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
- --