diff --git a/LICENSE.md b/LICENSE.md index 73e728158a7..c7fa80b73f4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -212,6 +212,18 @@ License: [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENS =========================================================================== +Mapbox Navigation uses portions of the kotlinx-coroutines-android (Coroutines support libraries for Kotlin). +URL: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) +License: [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + +Mapbox Navigation uses portions of the kotlinx-coroutines-core (Coroutines support libraries for Kotlin). +URL: [https://github.com/Kotlin/kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) +License: [The Apache Software License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) + +=========================================================================== + Mapbox Navigation uses portions of the Mapbox Android Commmon OkHttp SDK. URL: [https://github.com/mapbox/mapbox-sdk](https://github.com/mapbox/mapbox-sdk) License: [BSD](https://opensource.org/licenses/BSD-2-Clause) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt index eced0e35e75..e59021a341c 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteRefreshTest.kt @@ -7,12 +7,14 @@ import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.geojson.Point import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions import com.mapbox.navigation.base.options.NavigationOptions +import com.mapbox.navigation.base.options.RoutingTilesOptions import com.mapbox.navigation.base.route.RouteRefreshOptions import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.MapboxNavigationProvider import com.mapbox.navigation.instrumentation_tests.R import com.mapbox.navigation.instrumentation_tests.activity.EmptyTestActivity import com.mapbox.navigation.instrumentation_tests.utils.MapboxNavigationRule +import com.mapbox.navigation.instrumentation_tests.utils.http.MockAvailableTilesVersionsRequestHandler import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRefreshHandler import com.mapbox.navigation.instrumentation_tests.utils.http.MockDirectionsRequestHandler import com.mapbox.navigation.instrumentation_tests.utils.idling.IdlingPolicyTimeoutRule @@ -27,6 +29,7 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule import org.junit.Test +import java.net.URI import java.util.concurrent.TimeUnit class RouteRefreshTest : BaseTest(EmptyTestActivity::class.java) { @@ -53,6 +56,8 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja @Before fun setup() { + setupMockRequestHandlers(coordinates) + mapboxNavigation = MapboxNavigationProvider.create( NavigationOptions.Builder(activity) .accessToken(getMapboxAccessTokenFromResources(activity)) @@ -61,6 +66,11 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja .intervalMillis(TimeUnit.SECONDS.toMillis(30)) .build() ) + .routingTilesOptions( + RoutingTilesOptions.Builder() + .tilesBaseUri(URI(mockWebServerRule.baseUrl)) + .build() + ) .build() ) } @@ -68,7 +78,6 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja @Test fun expect_route_refresh_to_update_traffic_annotations() { // Request a route. - setupMockRequestHandlers(coordinates) val routes = requestDirectionsRouteSync(coordinates).reversed() // Create an observer resource that captures the routes. @@ -116,6 +125,11 @@ class RouteRefreshTest : BaseTest(EmptyTestActivity::class.ja readRawFileText(activity, R.raw.route_response_route_refresh_annotations) ) ) + mockWebServerRule.requestHandlers.add( + MockAvailableTilesVersionsRequestHandler( + readRawFileText(activity, R.raw.nn_response_available_versions) + ) + ) } private fun requestDirectionsRouteSync(coordinates: List): List { diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/MockAvailableTilesVersionsRequestHandler.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/MockAvailableTilesVersionsRequestHandler.kt new file mode 100644 index 00000000000..ca0eac1ccda --- /dev/null +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/http/MockAvailableTilesVersionsRequestHandler.kt @@ -0,0 +1,23 @@ +package com.mapbox.navigation.instrumentation_tests.utils.http + +import com.mapbox.navigation.testing.ui.http.MockRequestHandler +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest + +/** + * Mocks NN available tiles versions request. + * + * @param jsonResponse the full JSON response + */ +class MockAvailableTilesVersionsRequestHandler( + private val jsonResponse: String +) : MockRequestHandler { + override fun handle(request: RecordedRequest): MockResponse? { + val prefix = """/route-tiles/v2/mapbox/driving-traffic/versions""" + return if (request.path!!.startsWith(prefix)) { + MockResponse().setBody(jsonResponse) + } else { + null + } + } +} diff --git a/instrumentation-tests/src/main/res/raw/nn_response_available_versions.json b/instrumentation-tests/src/main/res/raw/nn_response_available_versions.json new file mode 100644 index 00000000000..31084c6a89f --- /dev/null +++ b/instrumentation-tests/src/main/res/raw/nn_response_available_versions.json @@ -0,0 +1 @@ +{"availableVersions":["2021_10_15-03_00_00","2021_10_09-03_00_00","2021_10_03-03_00_00","2021_09_17-03_00_00","2021_09_12-03_00_00","2021_09_05-03_00_00","2021_09_04-03_00_00","2021_08_23-14_04_46","2021_08_23-03_00_00","2021_08_21-03_00_00","2021_08_16-03_00_00","2021_08_08-03_00_00","2021_07_31-03_00_00","2021_07_25-03_00_00","2021_07_17-03_00_00","2021_07_10-03_00_00","2021_07_04-03_00_00","2021_06_27-03_00_00","2021_06_20-03_00_00","2021_06_13-03_00_00","2021_06_06-03_00_00","2021_06_05-03_00_00","2021_05_30-03_00_00","2021_05_23-03_00_00","2021_05_16-03_00_00","2021_05_09-03_00_00","2021_05_01-03_00_00","2021_04_26-03_00_00","2021_04_25-03_00_00","2021_04_18-03_00_00"],"metadata":{"2021_10_15-03_00_00":{"map":{"tileset_version":"2021_10_15-03_00_00"}},"2021_10_09-03_00_00":{"map":{"tileset_version":"2021_10_09-03_00_00"}},"2021_10_03-03_00_00":{"map":{"tileset_version":"2021_10_03-03_00_00"}},"2021_09_17-03_00_00":{"map":{"tileset_version":"2021_09_17-03_00_00"}},"2021_09_12-03_00_00":{"map":{"tileset_version":"2021_09_12-03_00_00"}},"2021_09_05-03_00_00":{"map":{"tileset_version":"2021_09_05-03_00_00"}},"2021_09_04-03_00_00":{"map":{"tileset_version":"2021_09_04-03_00_00"}},"2021_08_23-14_04_46":{"map":{}},"2021_08_23-03_00_00":{"map":{}},"2021_08_21-03_00_00":{"map":{}},"2021_08_16-03_00_00":{"map":{}},"2021_08_08-03_00_00":{"map":{}},"2021_07_31-03_00_00":{"map":{}},"2021_07_25-03_00_00":{"map":{}},"2021_07_17-03_00_00":{"map":{}},"2021_07_10-03_00_00":{"map":{}},"2021_07_04-03_00_00":{"map":{}},"2021_06_27-03_00_00":{"map":{}},"2021_06_20-03_00_00":{"map":{}},"2021_06_13-03_00_00":{"map":{}},"2021_06_06-03_00_00":{"map":{}},"2021_06_05-03_00_00":{"map":{}},"2021_05_30-03_00_00":{"map":{}},"2021_05_23-03_00_00":{"map":{}},"2021_05_16-03_00_00":{"map":{}},"2021_05_09-03_00_00":{"map":{}},"2021_05_01-03_00_00":{"map":{}},"2021_04_26-03_00_00":{"map":{}},"2021_04_25-03_00_00":{"map":{}},"2021_04_18-03_00_00":{"map":{}}}} \ No newline at end of file diff --git a/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations.json b/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations.json index 0f5bf50b054..58291ce81df 100644 --- a/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations.json +++ b/instrumentation-tests/src/main/res/raw/route_response_route_refresh_annotations.json @@ -1 +1,457 @@ -{"code":"Ok","route":{"legs":[{"annotation":{"maxspeed":[{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"speed":48,"unit":"km/h"},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true}],"congestion":["unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","unknown","low","low","low","low","low","low","unknown","unknown","unknown","low","low","low","low","low","low","low","low","low","low","low","low","unknown","unknown","low","low","low","low","unknown","low","unknown","unknown","unknown"],"speed":[5,5,12.2,12.2,6.1,6.1,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,5.8,1.7,13.3,9.2,9.4,11.1,11.1,5.6,5.6,8.3,8.3,2.5,12.2,12.2,9.7,9.7,9.7,9.7,9.7,11.7,8.6,9.4,11.9,8.3,12.2,12.2,12.2,12.2,13.9,13.9,14.2,9.2,9.4,11.1,11.1,11.1,10.6],"distance":[26.6,8.9,13.3,11.4,11.7,24.5,4,4.6,4.9,4.9,4.8,4.8,4.7,4,5.7,4.7,4.7,4.8,4.7,4.9,4.7,4.8,34.7,11.4,126.6,30,10.4,23.4,62,2.1,2.1,62,65.2,4.9,116.3,35.9,83.8,3.4,3.7,41.6,101,122.1,123.4,74.4,46,48.6,31.5,9.7,30.5,50.8,13.8,61.4,122.1,34.1,29.9,7.8,8,9],"duration":[5.323,1.787,1.087,0.93,1.918,4.003,0.687,0.787,0.837,0.832,0.83,0.816,0.813,0.689,0.972,0.808,0.811,0.823,0.808,0.835,0.806,0.828,20.809,0.855,13.816,3.175,0.94,2.107,11.166,0.382,0.255,7.436,26.078,0.398,9.519,3.688,8.62,0.351,0.376,4.278,8.661,14.175,13.062,6.225,5.52,3.976,2.58,0.79,2.499,3.66,0.993,4.333,13.321,3.608,2.691,0.701,0.723,0.854]}}]}} \ No newline at end of file +{ + "code": "Ok", + "route": { + "legs": [ + { + "annotation": { + "maxspeed": [ + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 40, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "speed": 48, + "unit": "km/h" + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + }, + { + "unknown": true + } + ], + "congestion": [ + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "low", + "low", + "low", + "unknown", + "unknown", + "unknown", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "low", + "unknown", + "unknown", + "low", + "low", + "low", + "low", + "unknown", + "low", + "unknown", + "unknown", + "unknown" + ], + "speed": [ + 5, + 5, + 12.2, + 12.2, + 6.1, + 6.1, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 5.8, + 1.7, + 13.3, + 9.2, + 9.4, + 11.1, + 11.1, + 5.6, + 5.6, + 8.3, + 8.3, + 2.5, + 12.2, + 12.2, + 9.7, + 9.7, + 9.7, + 9.7, + 9.7, + 11.7, + 8.6, + 9.4, + 11.9, + 8.3, + 12.2, + 12.2, + 12.2, + 12.2, + 13.9, + 13.9, + 14.2, + 9.2, + 9.4, + 11.1, + 11.1, + 11.1, + 10.6 + ], + "distance": [ + 26.6, + 8.9, + 13.3, + 11.4, + 11.7, + 24.5, + 4, + 4.6, + 4.9, + 4.9, + 4.8, + 4.8, + 4.7, + 4, + 5.7, + 4.7, + 4.7, + 4.8, + 4.7, + 4.9, + 4.7, + 4.8, + 34.7, + 11.4, + 126.6, + 30, + 10.4, + 23.4, + 62, + 2.1, + 2.1, + 62, + 65.2, + 4.9, + 116.3, + 35.9, + 83.8, + 3.4, + 3.7, + 41.6, + 101, + 122.1, + 123.4, + 74.4, + 46, + 48.6, + 31.5, + 9.7, + 30.5, + 50.8, + 13.8, + 61.4, + 122.1, + 34.1, + 29.9, + 7.8, + 8, + 9 + ], + "duration": [ + 5.323, + 1.787, + 1.087, + 0.93, + 1.918, + 4.003, + 0.687, + 0.787, + 0.837, + 0.832, + 0.83, + 0.816, + 0.813, + 0.689, + 0.972, + 0.808, + 0.811, + 0.823, + 0.808, + 0.835, + 0.806, + 0.828, + 20.809, + 0.855, + 13.816, + 3.175, + 0.94, + 2.107, + 11.166, + 0.382, + 0.255, + 7.436, + 26.078, + 0.398, + 9.519, + 3.688, + 8.62, + 0.351, + 0.376, + 4.278, + 8.661, + 14.175, + 13.062, + 6.225, + 5.52, + 3.976, + 2.58, + 0.79, + 2.499, + 3.66, + 0.993, + 4.333, + 13.321, + 3.608, + 2.691, + 0.701, + 0.723, + 0.854 + ] + } + } + ] + } +} \ No newline at end of file diff --git a/libnavigation-base/build.gradle b/libnavigation-base/build.gradle index addb1ee4266..6d35a977dfe 100644 --- a/libnavigation-base/build.gradle +++ b/libnavigation-base/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation dependenciesList.mapboxNavigator implementation dependenciesList.androidXAnnotation - + implementation dependenciesList.coroutinesAndroid implementation dependenciesList.kotlinStdLib testImplementation project(':libtesting-utils') diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt new file mode 100644 index 00000000000..11ec1b86f07 --- /dev/null +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/internal/utils/DirectionsResponseUtils.kt @@ -0,0 +1,35 @@ +package com.mapbox.navigation.base.internal.utils + +import com.mapbox.api.directions.v5.models.DirectionsResponse +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.RouteOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +suspend fun parseDirectionsResponse( + json: String, + options: RouteOptions?, + onMetadata: (String) -> Unit, +): List = + withContext(Dispatchers.IO) { + val jsonObject = JSONObject(json) + val uuid: String? = if (jsonObject.has(UUID)) { + jsonObject.getString(UUID) + } else { + null + } + + // TODO remove after https://github.com/mapbox/navigation-sdks/issues/1229 + if (jsonObject.has(METADATA)) { + onMetadata(jsonObject.getString(METADATA)) + } + + // TODO simplify when https://github.com/mapbox/mapbox-java/issues/1292 is finished + val response = DirectionsResponse.fromJson(json, options, uuid) + + response.routes() + } + +private const val UUID = "uuid" +private const val METADATA = "metadata" 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 3be4e9c3d32..cbfb7cd4381 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 @@ -6,7 +6,6 @@ package com.mapbox.navigation.core import android.Manifest.permission.ACCESS_COARSE_LOCATION import android.Manifest.permission.ACCESS_FINE_LOCATION -import android.content.Context import androidx.annotation.RequiresPermission import androidx.annotation.UiThread import androidx.annotation.VisibleForTesting @@ -23,7 +22,6 @@ import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions import com.mapbox.navigation.base.extensions.applyLanguageAndVoiceUnitOptions import com.mapbox.navigation.base.formatter.DistanceFormatter -import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider import com.mapbox.navigation.base.options.HistoryRecorderOptions import com.mapbox.navigation.base.options.NavigationOptions import com.mapbox.navigation.base.options.RoutingTilesOptions @@ -47,7 +45,6 @@ import com.mapbox.navigation.core.formatter.MapboxDistanceFormatter import com.mapbox.navigation.core.history.MapboxHistoryReader import com.mapbox.navigation.core.history.MapboxHistoryRecorder import com.mapbox.navigation.core.internal.ReachabilityService -import com.mapbox.navigation.core.internal.accounts.MapboxNavigationAccounts import com.mapbox.navigation.core.internal.utils.InternalUtils import com.mapbox.navigation.core.navigator.TilesetDescriptorFactory import com.mapbox.navigation.core.replay.MapboxReplayer @@ -101,6 +98,7 @@ import com.mapbox.navigator.FallbackVersionsObserver import com.mapbox.navigator.IncidentsOptions import com.mapbox.navigator.NavigatorConfig import com.mapbox.navigator.PollingConfig +import com.mapbox.navigator.RouterInterface import com.mapbox.navigator.TileEndpointConfiguration import com.mapbox.navigator.TilesConfig import kotlinx.coroutines.channels.Channel @@ -1254,17 +1252,14 @@ class MapboxNavigation @VisibleForTesting internal constructor( String::class.java, accessToken ?: throw RuntimeException(MAPBOX_NAVIGATION_TOKEN_EXCEPTION_ROUTER) ), - ModuleProviderArgument(Context::class.java, navigationOptions.applicationContext), ModuleProviderArgument( - UrlSkuTokenProvider::class.java, - MapboxNavigationAccounts + RouterInterface::class.java, + MapboxNativeNavigatorImpl.router ), ModuleProviderArgument( - MapboxNativeNavigator::class.java, - MapboxNativeNavigatorImpl - ), - ModuleProviderArgument(ConnectivityHandler::class.java, connectivityHandler), - ModuleProviderArgument(ThreadController::class.java, threadController), + ThreadController::class.java, + threadController + ) ) MapboxModuleType.NavigationTripNotification -> arrayOf( ModuleProviderArgument(NavigationOptions::class.java, navigationOptions), diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt index a2434be1aa8..27ac8a71148 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt @@ -1,12 +1,17 @@ package com.mapbox.navigation.core.routealternatives import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.base.common.logger.model.Message +import com.mapbox.base.common.logger.model.Tag +import com.mapbox.navigation.base.internal.utils.parseDirectionsResponse import com.mapbox.navigation.base.route.RouteAlternativesOptions import com.mapbox.navigation.base.route.RouterOrigin import com.mapbox.navigation.core.directions.session.DirectionsSession import com.mapbox.navigation.core.directions.session.RoutesExtra.ROUTES_UPDATE_REASON_ALTERNATIVE import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator +import com.mapbox.navigation.utils.internal.logI +import kotlinx.coroutines.runBlocking import java.util.concurrent.CopyOnWriteArraySet internal class RouteAlternativesController constructor( @@ -66,13 +71,19 @@ internal class RouteAlternativesController constructor( changedRoutes.add(currentRoute) // Map the alternatives from nav-native, add the existing RouteOptions. - val alternatives = routeAlternatives.map { routeAlternative -> - DirectionsRoute.fromJson( - routeAlternative.route, - currentRoute.routeOptions(), - null // We don't know the requestUuid at this point - ) + var alternatives: List + // TODO make async + runBlocking { + alternatives = routeAlternatives.map { routeAlternative -> + parseDirectionsResponse( + routeAlternative.route, + currentRoute.routeOptions() + ) { + logI(TAG, Message("Response metadata: $it")) + }.first() + } } + changedRoutes.addAll(alternatives) directionsSession.setRoutes( @@ -96,6 +107,7 @@ internal class RouteAlternativesController constructor( } private companion object { + private val TAG = Tag("MbxRouteAlternativesController") private const val MIN_TIME_BEFORE_MANEUVER_SECONDS = 1.0 private const val LOOK_AHEAD_SECONDS = 1.0 private const val SECONDS_PER_MILLIS = 0.001 diff --git a/libnavigation-core/src/test/resources/route_alternative_from_native.txt b/libnavigation-core/src/test/resources/route_alternative_from_native.txt index eda7eb71b8b..d7a40b7abe3 100644 --- a/libnavigation-core/src/test/resources/route_alternative_from_native.txt +++ b/libnavigation-core/src/test/resources/route_alternative_from_native.txt @@ -1 +1 @@ -{"distance":1006.574,"duration":221.796,"duration_typical":226.731,"geometry":"i|ebgAlixehFfCyIvA_FrAwErVc{@xAeFpAoEfEeO~Ogk@tA{EvA_FnBsGtHuVxIuZvA}EzC`Bx_@nSpDlBdDfBxK~FnRbKnDnB~C`BjBbAt[zPhDhBsAtEuHvW}ErPqFhRgAtDgApD{GxUqI`Zs@bCm@rBq@`CqApEsAvEuFlRgLr`@mAfEcBzF|DtBhE|B","legs":[{"admins":[{"iso_3166_1":"US","iso_3166_1_alpha3":"USA"}],"annotation":{"congestion_numeric":[0,0,0,0,0,null,null,null,null,null,null,17,17,17,null,null,null,27,27,27,27,0,0,0,0,8,8,8,8,8,0,0,0,0,0,0,0,20,20,20,20,20,72,72],"distance":[17,11,10.6,94.5,11.3,10.2,25.4,69.2,10.8,11,13.6,37.5,43.5,10.9,9.7,65.2,11,10.3,25.4,38.7,11,9.9,6.7,57,10.5,10.5,38.9,27.7,30.3,9,8.8,35.8,42.5,6.5,5.7,6.4,10.3,10.6,30.6,52.9,9.8,12.4,11.8,12.6],"duration":[2.271,1.467,2.242,20.012,2.39,1.753,4.347,11.871,1.851,1.165,1.445,7.507,8.693,2.185,2.327,15.642,2.646,1.687,4.164,6.332,1.794,1.781,1.208,10.252,1.899,1.182,4.373,3.118,3.413,1.007,1.173,4.772,5.664,0.865,0.761,0.848,1.374,2.382,6.883,11.898,2.207,2.79,10.602,11.306],"maxspeed":[{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true}],"speed":[7.5,7.5,4.7,4.7,4.7,5.8,5.8,5.8,5.8,9.4,9.4,5,5,5,4.2,4.2,4.2,6.1,6.1,6.1,6.1,5.6,5.6,5.6,5.6,8.9,8.9,8.9,8.9,8.9,7.5,7.5,7.5,7.5,7.5,7.5,7.5,4.4,4.4,4.4,4.4,4.4,1.1,1.1]},"distance":1006.574,"duration":221.796,"duration_typical":226.731,"steps":[{"bannerInstructions":[{"distanceAlongGeometry":377.232,"primary":{"components":[{"text":"Jackson Street","type":"text"}],"modifier":"right","text":"Jackson Street","type":"turn"}}],"distance":377.232,"driving_side":"right","duration":73.322,"duration_typical":73.322,"geometry":"i|ebgAlixehFfCyIvA_FrAwErVc{@xAeFpAoEfEeO~Ogk@tA{EvA_FnBsGtHuVxIuZvA}E","intersections":[{"admin_index":0,"bearings":[116],"duration":3.764,"entry":[true],"geometry_index":0,"is_urban":true,"location":[-122.270375,37.801429],"mapbox_streets_v8":{"class":"primary"},"out":0,"weight":4.423},{"admin_index":0,"bearings":[116,296],"duration":26.584,"entry":[true,false],"geometry_index":2,"in":1,"is_urban":true,"location":[-122.27009,37.801317],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":2.019,"weight":28.864},{"admin_index":0,"bearings":[117,296],"duration":3.722,"entry":[true,false],"geometry_index":5,"in":1,"is_urban":true,"location":[-122.268905,37.800852],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":2.007,"turn_weight":2,"weight":4.014},{"admin_index":0,"bearings":[116,297],"duration":16.286,"entry":[true,false],"geometry_index":6,"in":1,"is_urban":true,"location":[-122.268801,37.800811],"mapbox_streets_v8":{"class":"primary"},"out":0,"weight":19.136},{"admin_index":0,"bearings":[116,296],"duration":1.905,"entry":[true,false],"geometry_index":8,"in":1,"is_urban":true,"location":[-122.267834,37.800439],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.019,"weight":2.216},{"admin_index":0,"bearings":[117,296],"duration":2.654,"entry":[true,false],"geometry_index":9,"in":1,"is_urban":true,"location":[-122.267724,37.800396],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.007,"weight":3.11},{"admin_index":0,"bearings":[117,297],"entry":[true,false],"geometry_index":11,"in":1,"is_urban":true,"location":[-122.26747400000001,37.800296],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.007}],"maneuver":{"bearing_after":116,"bearing_before":0,"instruction":"Drive southeast on 11th Street.","location":[-122.270375,37.801429],"type":"depart"},"mode":"driving","name":"11th Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"Drive southeast on 11th Street. Then, in a quarter mile, Turn right onto Jackson Street.","distanceAlongGeometry":377.232,"ssmlAnnouncement":"Drive southeast on 11th Street. Then, in a quarter mile, Turn right onto Jackson Street."},{"announcement":"Turn right onto Jackson Street.","distanceAlongGeometry":66.667,"ssmlAnnouncement":"Turn right onto Jackson Street."}],"weight":83.383,"weight_typical":83.383},{"bannerInstructions":[{"distanceAlongGeometry":257,"primary":{"components":[{"text":"8th Street","type":"text"}],"modifier":"right","text":"8th Street","type":"turn"}}],"distance":257,"driving_side":"right","duration":58.159,"duration_typical":66.398,"geometry":"g~bbgAxypehFzC`Bx_@nSpDlBdDfBxK~FnRbKnDnB~C`BjBbAt[zPhDhB","intersections":[{"admin_index":0,"bearings":[206,296],"duration":24.745,"entry":[true,false],"geometry_index":14,"in":1,"is_urban":true,"location":[-122.266541,37.799924],"mapbox_streets_v8":{"class":"street"},"out":0,"turn_duration":4.105,"turn_weight":5,"weight":29.252},{"admin_index":0,"bearings":[26,206],"duration":7.898,"entry":[false,true],"geometry_index":17,"in":0,"is_urban":true,"location":[-122.266973,37.799232],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":2.007,"turn_weight":2,"weight":8.922},{"admin_index":0,"bearings":[26,206],"duration":8.189,"entry":[false,true],"geometry_index":19,"in":0,"is_urban":true,"location":[-122.267153,37.798944],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":10.114},{"admin_index":0,"bearings":[27,206],"duration":5.079,"entry":[false,true],"geometry_index":21,"in":0,"is_urban":true,"location":[-122.267403,37.798544],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":2.019,"turn_weight":2,"weight":5.596},{"admin_index":0,"bearings":[26,206],"entry":[false,true],"geometry_index":23,"in":0,"is_urban":true,"location":[-122.267486,37.79841],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":0.007,"turn_weight":0.5}],"maneuver":{"bearing_after":206,"bearing_before":116,"instruction":"Turn right onto Jackson Street.","location":[-122.266541,37.799924],"modifier":"right","type":"turn"},"mode":"driving","name":"Jackson Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"In 800 feet, Turn right onto 8th Street.","distanceAlongGeometry":247,"ssmlAnnouncement":"In 800 feet, Turn right onto 8th Street."},{"announcement":"Turn right onto 8th Street.","distanceAlongGeometry":50,"ssmlAnnouncement":"Turn right onto 8th Street."}],"weight":68.459,"weight_typical":77.974},{"bannerInstructions":[{"distanceAlongGeometry":348,"primary":{"components":[{"text":"Webster Street","type":"text"}],"modifier":"left","text":"Webster Street","type":"turn"},"sub":{"components":[{"active":false,"directions":["left"],"driving_side":"right","text":"","type":"lane"},{"active":true,"active_direction":"left","directions":["left","straight"],"driving_side":"right","text":"","type":"lane"},{"active":false,"directions":["straight"],"driving_side":"right","text":"","type":"lane"},{"active":false,"directions":["straight"],"driving_side":"right","text":"","type":"lane"}],"text":""}}],"distance":348,"driving_side":"right","duration":60.897,"duration_typical":72.761,"geometry":"s}~agA`jsehFsAtEuHvW}ErPqFhRgAtDgApD{GxUqI`Zs@bCm@rBq@`CqApEsAvEuFlRgLr`@mAfEcBzF","intersections":[{"admin_index":0,"bearings":[26,296],"duration":9.721,"entry":[false,true],"geometry_index":25,"in":0,"is_urban":true,"location":[-122.267825,37.797866],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":4.208,"turn_weight":10,"weight":16.339},{"admin_index":0,"bearings":[116,296],"duration":3.157,"entry":[false,true],"geometry_index":27,"in":0,"is_urban":true,"location":[-122.268328,37.798063],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":4.201},{"admin_index":0,"bearings":[116,296],"duration":4.395,"entry":[false,true],"geometry_index":28,"in":0,"is_urban":true,"location":[-122.26861,37.798174],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":5.655},{"admin_index":0,"bearings":[116,297],"duration":6.008,"entry":[false,true],"geometry_index":30,"in":0,"is_urban":true,"location":[-122.26901,37.798331],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.008,"turn_weight":2,"weight":9.05},{"admin_index":0,"bearings":[116,296],"duration":5.607,"entry":[false,true],"geometry_index":32,"in":0,"is_urban":true,"location":[-122.269464,37.798509],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":7.08},{"admin_index":0,"bearings":[116,296],"duration":0.807,"entry":[false,true],"geometry_index":33,"in":0,"is_urban":true,"location":[-122.269897,37.798678],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":1.44},{"admin_index":0,"bearings":[116,297],"duration":0.808,"entry":[false,true],"geometry_index":34,"in":0,"is_urban":true,"location":[-122.269963,37.798704],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.008,"turn_weight":0.5,"weight":1.44},{"admin_index":0,"bearings":[117,296],"duration":2.286,"entry":[false,true],"geometry_index":35,"in":0,"is_urban":true,"location":[-122.270021,37.798727],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.019,"turn_weight":0.5,"weight":3.163},{"admin_index":0,"bearings":[116,296],"duration":23.157,"entry":[false,true],"geometry_index":37,"in":0,"is_urban":true,"location":[-122.270191,37.798793],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":2.007,"turn_weight":2,"weight":26.851},{"admin_index":0,"bearings":[117,296],"entry":[false,true],"geometry_index":40,"in":0,"is_urban":true,"location":[-122.271148,37.79917],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_weight":0.5}],"maneuver":{"bearing_after":296,"bearing_before":206,"instruction":"Turn right onto 8th Street.","location":[-122.267825,37.797866],"modifier":"right","type":"turn"},"mode":"driving","name":"8th Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"In a quarter mile, Turn left onto Webster Street.","distanceAlongGeometry":331.667,"ssmlAnnouncement":"In a quarter mile, Turn left onto Webster Street."},{"announcement":"Turn left onto Webster Street. Then Your destination will be on the right.","distanceAlongGeometry":107.528,"ssmlAnnouncement":"Turn left onto Webster Street. Then Your destination will be on the right."}],"weight":81.537,"weight_typical":95.477},{"bannerInstructions":[{"distanceAlongGeometry":24.342,"primary":{"components":[{"text":"Your destination is on the right","type":"text"}],"modifier":"right","text":"Your destination is on the right","type":"arrive"}}],"distance":24.342,"driving_side":"right","duration":29.418,"duration_typical":14.251,"geometry":"utabgAzgzehF|DtBhE|B","intersections":[{"admin_index":0,"bearings":[117,206],"entry":[false,true],"geometry_index":42,"in":0,"is_urban":true,"lanes":[{"active":false,"indications":["left"],"valid":true,"valid_indication":"left"},{"active":true,"indications":["left","straight"],"valid":true,"valid_indication":"left"},{"active":false,"indications":["straight"],"valid":false},{"active":false,"indications":["straight"],"valid":false}],"location":[-122.271374,37.799259],"mapbox_streets_v8":{"class":"tertiary"},"out":1,"turn_duration":7.51,"turn_weight":15}],"maneuver":{"bearing_after":206,"bearing_before":297,"instruction":"Turn left onto Webster Street.","location":[-122.271374,37.799259],"modifier":"left","type":"turn"},"mode":"driving","name":"Webster Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"Your destination is on the right.","distanceAlongGeometry":24.342,"ssmlAnnouncement":"Your destination is on the right."}],"weight":40.742,"weight_typical":22.921},{"bannerInstructions":[],"distance":0,"driving_side":"right","duration":0,"duration_typical":0,"geometry":"mhabgAnozehF??","intersections":[{"admin_index":0,"bearings":[26],"entry":[true],"geometry_index":44,"in":0,"location":[-122.271496,37.799063]}],"maneuver":{"bearing_after":0,"bearing_before":206,"instruction":"Your destination is on the right.","location":[-122.271496,37.799063],"modifier":"right","type":"arrive"},"mode":"driving","name":"Webster Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[],"weight":0,"weight_typical":0}],"summary":"11th Street, 8th Street","via_waypoints":[],"weight":274.12,"weight_typical":279.754}],"voiceLocale":"en-US","weight":274.12,"weight_name":"auto","weight_typical":279.754} \ No newline at end of file +{"routes":[{"distance":1006.574,"duration":221.796,"duration_typical":226.731,"geometry":"i|ebgAlixehFfCyIvA_FrAwErVc{@xAeFpAoEfEeO~Ogk@tA{EvA_FnBsGtHuVxIuZvA}EzC`Bx_@nSpDlBdDfBxK~FnRbKnDnB~C`BjBbAt[zPhDhBsAtEuHvW}ErPqFhRgAtDgApD{GxUqI`Zs@bCm@rBq@`CqApEsAvEuFlRgLr`@mAfEcBzF|DtBhE|B","legs":[{"admins":[{"iso_3166_1":"US","iso_3166_1_alpha3":"USA"}],"annotation":{"congestion_numeric":[0,0,0,0,0,null,null,null,null,null,null,17,17,17,null,null,null,27,27,27,27,0,0,0,0,8,8,8,8,8,0,0,0,0,0,0,0,20,20,20,20,20,72,72],"distance":[17,11,10.6,94.5,11.3,10.2,25.4,69.2,10.8,11,13.6,37.5,43.5,10.9,9.7,65.2,11,10.3,25.4,38.7,11,9.9,6.7,57,10.5,10.5,38.9,27.7,30.3,9,8.8,35.8,42.5,6.5,5.7,6.4,10.3,10.6,30.6,52.9,9.8,12.4,11.8,12.6],"duration":[2.271,1.467,2.242,20.012,2.39,1.753,4.347,11.871,1.851,1.165,1.445,7.507,8.693,2.185,2.327,15.642,2.646,1.687,4.164,6.332,1.794,1.781,1.208,10.252,1.899,1.182,4.373,3.118,3.413,1.007,1.173,4.772,5.664,0.865,0.761,0.848,1.374,2.382,6.883,11.898,2.207,2.79,10.602,11.306],"maxspeed":[{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"speed":40,"unit":"km/h"},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true},{"unknown":true}],"speed":[7.5,7.5,4.7,4.7,4.7,5.8,5.8,5.8,5.8,9.4,9.4,5,5,5,4.2,4.2,4.2,6.1,6.1,6.1,6.1,5.6,5.6,5.6,5.6,8.9,8.9,8.9,8.9,8.9,7.5,7.5,7.5,7.5,7.5,7.5,7.5,4.4,4.4,4.4,4.4,4.4,1.1,1.1]},"distance":1006.574,"duration":221.796,"duration_typical":226.731,"steps":[{"bannerInstructions":[{"distanceAlongGeometry":377.232,"primary":{"components":[{"text":"Jackson Street","type":"text"}],"modifier":"right","text":"Jackson Street","type":"turn"}}],"distance":377.232,"driving_side":"right","duration":73.322,"duration_typical":73.322,"geometry":"i|ebgAlixehFfCyIvA_FrAwErVc{@xAeFpAoEfEeO~Ogk@tA{EvA_FnBsGtHuVxIuZvA}E","intersections":[{"admin_index":0,"bearings":[116],"duration":3.764,"entry":[true],"geometry_index":0,"is_urban":true,"location":[-122.270375,37.801429],"mapbox_streets_v8":{"class":"primary"},"out":0,"weight":4.423},{"admin_index":0,"bearings":[116,296],"duration":26.584,"entry":[true,false],"geometry_index":2,"in":1,"is_urban":true,"location":[-122.27009,37.801317],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":2.019,"weight":28.864},{"admin_index":0,"bearings":[117,296],"duration":3.722,"entry":[true,false],"geometry_index":5,"in":1,"is_urban":true,"location":[-122.268905,37.800852],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":2.007,"turn_weight":2,"weight":4.014},{"admin_index":0,"bearings":[116,297],"duration":16.286,"entry":[true,false],"geometry_index":6,"in":1,"is_urban":true,"location":[-122.268801,37.800811],"mapbox_streets_v8":{"class":"primary"},"out":0,"weight":19.136},{"admin_index":0,"bearings":[116,296],"duration":1.905,"entry":[true,false],"geometry_index":8,"in":1,"is_urban":true,"location":[-122.267834,37.800439],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.019,"weight":2.216},{"admin_index":0,"bearings":[117,296],"duration":2.654,"entry":[true,false],"geometry_index":9,"in":1,"is_urban":true,"location":[-122.267724,37.800396],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.007,"weight":3.11},{"admin_index":0,"bearings":[117,297],"entry":[true,false],"geometry_index":11,"in":1,"is_urban":true,"location":[-122.26747400000001,37.800296],"mapbox_streets_v8":{"class":"primary"},"out":0,"turn_duration":0.007}],"maneuver":{"bearing_after":116,"bearing_before":0,"instruction":"Drive southeast on 11th Street.","location":[-122.270375,37.801429],"type":"depart"},"mode":"driving","name":"11th Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"Drive southeast on 11th Street. Then, in a quarter mile, Turn right onto Jackson Street.","distanceAlongGeometry":377.232,"ssmlAnnouncement":"Drive southeast on 11th Street. Then, in a quarter mile, Turn right onto Jackson Street."},{"announcement":"Turn right onto Jackson Street.","distanceAlongGeometry":66.667,"ssmlAnnouncement":"Turn right onto Jackson Street."}],"weight":83.383,"weight_typical":83.383},{"bannerInstructions":[{"distanceAlongGeometry":257,"primary":{"components":[{"text":"8th Street","type":"text"}],"modifier":"right","text":"8th Street","type":"turn"}}],"distance":257,"driving_side":"right","duration":58.159,"duration_typical":66.398,"geometry":"g~bbgAxypehFzC`Bx_@nSpDlBdDfBxK~FnRbKnDnB~C`BjBbAt[zPhDhB","intersections":[{"admin_index":0,"bearings":[206,296],"duration":24.745,"entry":[true,false],"geometry_index":14,"in":1,"is_urban":true,"location":[-122.266541,37.799924],"mapbox_streets_v8":{"class":"street"},"out":0,"turn_duration":4.105,"turn_weight":5,"weight":29.252},{"admin_index":0,"bearings":[26,206],"duration":7.898,"entry":[false,true],"geometry_index":17,"in":0,"is_urban":true,"location":[-122.266973,37.799232],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":2.007,"turn_weight":2,"weight":8.922},{"admin_index":0,"bearings":[26,206],"duration":8.189,"entry":[false,true],"geometry_index":19,"in":0,"is_urban":true,"location":[-122.267153,37.798944],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":10.114},{"admin_index":0,"bearings":[27,206],"duration":5.079,"entry":[false,true],"geometry_index":21,"in":0,"is_urban":true,"location":[-122.267403,37.798544],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":2.019,"turn_weight":2,"weight":5.596},{"admin_index":0,"bearings":[26,206],"entry":[false,true],"geometry_index":23,"in":0,"is_urban":true,"location":[-122.267486,37.79841],"mapbox_streets_v8":{"class":"street"},"out":1,"turn_duration":0.007,"turn_weight":0.5}],"maneuver":{"bearing_after":206,"bearing_before":116,"instruction":"Turn right onto Jackson Street.","location":[-122.266541,37.799924],"modifier":"right","type":"turn"},"mode":"driving","name":"Jackson Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"In 800 feet, Turn right onto 8th Street.","distanceAlongGeometry":247,"ssmlAnnouncement":"In 800 feet, Turn right onto 8th Street."},{"announcement":"Turn right onto 8th Street.","distanceAlongGeometry":50,"ssmlAnnouncement":"Turn right onto 8th Street."}],"weight":68.459,"weight_typical":77.974},{"bannerInstructions":[{"distanceAlongGeometry":348,"primary":{"components":[{"text":"Webster Street","type":"text"}],"modifier":"left","text":"Webster Street","type":"turn"},"sub":{"components":[{"active":false,"directions":["left"],"driving_side":"right","text":"","type":"lane"},{"active":true,"active_direction":"left","directions":["left","straight"],"driving_side":"right","text":"","type":"lane"},{"active":false,"directions":["straight"],"driving_side":"right","text":"","type":"lane"},{"active":false,"directions":["straight"],"driving_side":"right","text":"","type":"lane"}],"text":""}}],"distance":348,"driving_side":"right","duration":60.897,"duration_typical":72.761,"geometry":"s}~agA`jsehFsAtEuHvW}ErPqFhRgAtDgApD{GxUqI`Zs@bCm@rBq@`CqApEsAvEuFlRgLr`@mAfEcBzF","intersections":[{"admin_index":0,"bearings":[26,296],"duration":9.721,"entry":[false,true],"geometry_index":25,"in":0,"is_urban":true,"location":[-122.267825,37.797866],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":4.208,"turn_weight":10,"weight":16.339},{"admin_index":0,"bearings":[116,296],"duration":3.157,"entry":[false,true],"geometry_index":27,"in":0,"is_urban":true,"location":[-122.268328,37.798063],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":4.201},{"admin_index":0,"bearings":[116,296],"duration":4.395,"entry":[false,true],"geometry_index":28,"in":0,"is_urban":true,"location":[-122.26861,37.798174],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":5.655},{"admin_index":0,"bearings":[116,297],"duration":6.008,"entry":[false,true],"geometry_index":30,"in":0,"is_urban":true,"location":[-122.26901,37.798331],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.008,"turn_weight":2,"weight":9.05},{"admin_index":0,"bearings":[116,296],"duration":5.607,"entry":[false,true],"geometry_index":32,"in":0,"is_urban":true,"location":[-122.269464,37.798509],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":7.08},{"admin_index":0,"bearings":[116,296],"duration":0.807,"entry":[false,true],"geometry_index":33,"in":0,"is_urban":true,"location":[-122.269897,37.798678],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.007,"turn_weight":0.5,"weight":1.44},{"admin_index":0,"bearings":[116,297],"duration":0.808,"entry":[false,true],"geometry_index":34,"in":0,"is_urban":true,"location":[-122.269963,37.798704],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.008,"turn_weight":0.5,"weight":1.44},{"admin_index":0,"bearings":[117,296],"duration":2.286,"entry":[false,true],"geometry_index":35,"in":0,"is_urban":true,"location":[-122.270021,37.798727],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":0.019,"turn_weight":0.5,"weight":3.163},{"admin_index":0,"bearings":[116,296],"duration":23.157,"entry":[false,true],"geometry_index":37,"in":0,"is_urban":true,"location":[-122.270191,37.798793],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_duration":2.007,"turn_weight":2,"weight":26.851},{"admin_index":0,"bearings":[117,296],"entry":[false,true],"geometry_index":40,"in":0,"is_urban":true,"location":[-122.271148,37.79917],"mapbox_streets_v8":{"class":"secondary"},"out":1,"turn_weight":0.5}],"maneuver":{"bearing_after":296,"bearing_before":206,"instruction":"Turn right onto 8th Street.","location":[-122.267825,37.797866],"modifier":"right","type":"turn"},"mode":"driving","name":"8th Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"In a quarter mile, Turn left onto Webster Street.","distanceAlongGeometry":331.667,"ssmlAnnouncement":"In a quarter mile, Turn left onto Webster Street."},{"announcement":"Turn left onto Webster Street. Then Your destination will be on the right.","distanceAlongGeometry":107.528,"ssmlAnnouncement":"Turn left onto Webster Street. Then Your destination will be on the right."}],"weight":81.537,"weight_typical":95.477},{"bannerInstructions":[{"distanceAlongGeometry":24.342,"primary":{"components":[{"text":"Your destination is on the right","type":"text"}],"modifier":"right","text":"Your destination is on the right","type":"arrive"}}],"distance":24.342,"driving_side":"right","duration":29.418,"duration_typical":14.251,"geometry":"utabgAzgzehF|DtBhE|B","intersections":[{"admin_index":0,"bearings":[117,206],"entry":[false,true],"geometry_index":42,"in":0,"is_urban":true,"lanes":[{"active":false,"indications":["left"],"valid":true,"valid_indication":"left"},{"active":true,"indications":["left","straight"],"valid":true,"valid_indication":"left"},{"active":false,"indications":["straight"],"valid":false},{"active":false,"indications":["straight"],"valid":false}],"location":[-122.271374,37.799259],"mapbox_streets_v8":{"class":"tertiary"},"out":1,"turn_duration":7.51,"turn_weight":15}],"maneuver":{"bearing_after":206,"bearing_before":297,"instruction":"Turn left onto Webster Street.","location":[-122.271374,37.799259],"modifier":"left","type":"turn"},"mode":"driving","name":"Webster Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[{"announcement":"Your destination is on the right.","distanceAlongGeometry":24.342,"ssmlAnnouncement":"Your destination is on the right."}],"weight":40.742,"weight_typical":22.921},{"bannerInstructions":[],"distance":0,"driving_side":"right","duration":0,"duration_typical":0,"geometry":"mhabgAnozehF??","intersections":[{"admin_index":0,"bearings":[26],"entry":[true],"geometry_index":44,"in":0,"location":[-122.271496,37.799063]}],"maneuver":{"bearing_after":0,"bearing_before":206,"instruction":"Your destination is on the right.","location":[-122.271496,37.799063],"modifier":"right","type":"arrive"},"mode":"driving","name":"Webster Street","speedLimitSign":"mutcd","speedLimitUnit":"mph","voiceInstructions":[],"weight":0,"weight_typical":0}],"summary":"11th Street, 8th Street","via_waypoints":[],"weight":274.12,"weight_typical":279.754}],"voiceLocale":"en-US","weight":274.12,"weight_name":"auto","weight_typical":279.754}],"code":"Ok","uuid":"route_alternative_uuid"} \ No newline at end of file diff --git a/libnavigation-router/proguard-rules.pro b/libnavigation-router/proguard-rules.pro index d8e6421aced..c9d2382812e 100644 --- a/libnavigation-router/proguard-rules.pro +++ b/libnavigation-router/proguard-rules.pro @@ -1,5 +1,5 @@ # Modular architecture --keep class com.mapbox.navigation.route.internal.hybrid.MapboxHybridRouter {*;} +-keep class com.mapbox.navigation.route.internal.RouterWrapper {*;} # --- OkHttp --- -dontwarn okhttp3.** \ No newline at end of file diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterMapper.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterMapper.kt new file mode 100644 index 00000000000..27dc261dd81 --- /dev/null +++ b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterMapper.kt @@ -0,0 +1,24 @@ +package com.mapbox.navigation.route.internal + +import com.mapbox.api.directions.v5.DirectionsCriteria +import com.mapbox.navigation.base.route.RouterOrigin.Offboard +import com.mapbox.navigation.base.route.RouterOrigin.Onboard +import com.mapbox.navigator.RouterOrigin +import com.mapbox.navigator.RoutingMode + +internal fun RouterOrigin.mapToSdkRouteOrigin(): com.mapbox.navigation.base.route.RouterOrigin { + return when (this) { + RouterOrigin.ONLINE -> Offboard + RouterOrigin.ONBOARD -> Onboard + } +} + +internal fun String.mapToRoutingMode(): RoutingMode { + return when (this) { + DirectionsCriteria.PROFILE_CYCLING -> RoutingMode.CYCLING + DirectionsCriteria.PROFILE_WALKING -> RoutingMode.WALKING + DirectionsCriteria.PROFILE_DRIVING -> RoutingMode.DRIVING + DirectionsCriteria.PROFILE_DRIVING_TRAFFIC -> RoutingMode.DRIVING_TRAFFIC + else -> throw IllegalArgumentException("Invalid routing profile: $this") + } +} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt new file mode 100644 index 00000000000..d4be0c1da51 --- /dev/null +++ b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/RouterWrapper.kt @@ -0,0 +1,202 @@ +/** + * Tampering with any file that contains billing code is a violation of Mapbox Terms of Service and will result in enforcement of the penalties stipulated in the ToS. + */ + +package com.mapbox.navigation.route.internal + +import com.mapbox.annotation.module.MapboxModule +import com.mapbox.annotation.module.MapboxModuleType +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.base.common.logger.model.Message +import com.mapbox.base.common.logger.model.Tag +import com.mapbox.navigation.base.internal.utils.parseDirectionsResponse +import com.mapbox.navigation.base.route.RouteRefreshCallback +import com.mapbox.navigation.base.route.RouteRefreshError +import com.mapbox.navigation.base.route.Router +import com.mapbox.navigation.base.route.RouterCallback +import com.mapbox.navigation.base.route.RouterFailure +import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM +import com.mapbox.navigation.route.internal.util.redactQueryParam +import com.mapbox.navigation.utils.internal.ThreadController +import com.mapbox.navigation.utils.internal.logI +import com.mapbox.navigation.utils.internal.logW +import com.mapbox.navigator.RouteRefreshOptions +import com.mapbox.navigator.RouterErrorType +import com.mapbox.navigator.RouterInterface +import com.mapbox.navigator.RoutingProfile +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.URL + +@MapboxModule(MapboxModuleType.NavigationRouter) +class RouterWrapper( + private val accessToken: String, + private val router: RouterInterface, + private val threadController: ThreadController, +) : Router { + + private val mainJobControl by lazy { threadController.getMainScopeAndRootJob() } + + override fun getRoute(routeOptions: RouteOptions, callback: RouterCallback): Long { + val routeUrl = routeOptions.toUrl(accessToken).toString() + + return router.getRoute(routeUrl) { result, origin -> + val urlWithoutToken = URL(routeUrl.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM)) + result.fold( + { + mainJobControl.scope.launch { + if (it.type == RouterErrorType.REQUEST_CANCELLED) { + logI( + TAG, + Message( + """ + Route request cancelled: + $routeOptions + $origin + """.trimIndent() + ) + ) + callback.onCanceled(routeOptions, origin.mapToSdkRouteOrigin()) + } else { + val failureReasons = listOf( + RouterFailure( + url = urlWithoutToken, + routerOrigin = origin.mapToSdkRouteOrigin(), + message = it.message, + code = it.code + ) + ) + + logW( + TAG, + Message( + """ + Route request failed with: + $failureReasons + """.trimIndent() + ) + ) + + callback.onFailure(failureReasons, routeOptions) + } + } + }, + { + mainJobControl.scope.launch { + val routes = parseDirectionsResponse(it, routeOptions) { + logI(TAG, Message("Response metadata: $it")) + } + if (routes.isNullOrEmpty()) { + callback.onFailure( + listOf( + RouterFailure( + urlWithoutToken, + origin.mapToSdkRouteOrigin(), + ROUTES_LIST_EMPTY + ) + ), + routeOptions + ) + } else { + callback.onRoutesReady(routes, origin.mapToSdkRouteOrigin()) + } + } + } + ) + } + } + + override fun getRouteRefresh( + route: DirectionsRoute, + legIndex: Int, + callback: RouteRefreshCallback + ): Long { + val routeOptions = route.routeOptions() + val requestUuid = route.requestUuid() + val routeIndex = route.routeIndex()?.toIntOrNull() + if (routeOptions == null || requestUuid == null || routeIndex == null) { + val errorMessage = + """ + Route refresh failed because of a null param: + routeOptions = $routeOptions + requestUuid = $requestUuid + routeIndex = $routeIndex + """.trimIndent() + + logW(TAG, Message(errorMessage)) + + callback.onError( + RouteRefreshError("Route refresh failed", Exception(errorMessage)) + ) + + return REQUEST_FAILURE + } + + val refreshOptions = RouteRefreshOptions( + requestUuid, + routeIndex, + legIndex, + RoutingProfile(routeOptions.profile().mapToRoutingMode(), routeOptions.user()) + ) + + return router.getRouteRefresh(refreshOptions, route.toJson()) { result, _ -> + result.fold( + { + mainJobControl.scope.launch { + val errorMessage = + """ + Route refresh failed. + message = ${it.message} + code = ${it.code} + type = ${it.type} + requestId = ${it.requestId} + legIndex = $legIndex + """.trimIndent() + + logW(TAG, Message(errorMessage)) + + callback.onError( + RouteRefreshError("Route refresh failed", Exception(errorMessage)) + ) + } + }, + { + mainJobControl.scope.launch { + val refreshedRoute = + withContext(ThreadController.IODispatcher) { + DirectionsRoute.fromJson( + it, + routeOptions, + route.requestUuid() + ) + } + callback.onRefresh(refreshedRoute) + } + } + ) + } + } + + override fun cancelRouteRequest(requestId: Long) { + router.cancelRequest(requestId) + } + + override fun cancelRouteRefreshRequest(requestId: Long) { + router.cancelRequest(requestId) + } + + override fun cancelAll() { + router.cancelAll() + } + + override fun shutdown() { + router.cancelAll() + } + + private companion object { + private val TAG = Tag("MbxRouterWrapper") + private const val ROUTES_LIST_EMPTY = "routes list is empty" + private const val REQUEST_FAILURE = -1L + } +} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/HybridRouterHandler.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/HybridRouterHandler.kt deleted file mode 100644 index 3e035f961df..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/HybridRouterHandler.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.mapbox.navigation.route.internal.hybrid - -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.base.common.logger.model.Message -import com.mapbox.base.common.logger.model.Tag -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.Router -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.utils.internal.LoggerProvider - -internal sealed class HybridRouterHandler( - protected val primaryRouter: Router, - protected val fallbackRouter: Router, -) { - - private companion object { - private val TAG = Tag("MbxHybridRouter") - } - - protected val primaryRouterName = primaryRouter::class.qualifiedName - protected val fallbackRouterName = fallbackRouter::class.qualifiedName - - protected var primaryRouterRequestId: Long? = null - set(value) { - if (field != null) { - throw IllegalArgumentException( - "primaryRouterRequestId was already set for this request" - ) - } - field = value - } - protected var fallbackRouterRequestId: Long? = null - set(value) { - if (field != null) { - throw IllegalArgumentException( - "fallbackRouterRequestId was already set for this request" - ) - } - field = value - } - - internal class Directions( - primaryRouter: Router, - fallbackRouter: Router, - ) : HybridRouterHandler(primaryRouter, fallbackRouter) { - - private inner class PrimaryCallback( - private val clientCallback: RouterCallback - ) : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - clientCallback.onRoutesReady(routes, routerOrigin) - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - LoggerProvider.logger.w( - TAG, - Message( - """ - Route request for $primaryRouterName failed with: - $reasons - Trying to fallback to $fallbackRouterName... - """.trimIndent() - ) - ) - - fallbackRouterRequestId = fallbackRouter.getRoute( - routeOptions, - FallbackCallback(clientCallback, reasons) - ) - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - clientCallback.onCanceled(routeOptions, routerOrigin) - } - } - - private inner class FallbackCallback( - private val clientCallback: RouterCallback, - private val primaryFailureReasons: List - ) : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - clientCallback.onRoutesReady(routes, routerOrigin) - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - clientCallback.onFailure( - primaryFailureReasons.map { failure -> - RouterFailure( - url = failure.url, - routerOrigin = failure.routerOrigin, - message = - "Primary router ($primaryRouterName), " + - "origin ${failure.routerOrigin}, " + - "failed with: ${failure.message}", - code = failure.code, - throwable = failure.throwable - ) - }.plus( - reasons.map { failure -> - RouterFailure( - url = failure.url, - routerOrigin = failure.routerOrigin, - message = - "Fallback router ($fallbackRouterName), " + - "origin ${failure.routerOrigin}, " + - "failed with: ${failure.message}", - code = failure.code, - throwable = failure.throwable - ) - } - ), - routeOptions - ) - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - clientCallback.onCanceled(routeOptions, routerOrigin) - } - } - - fun getRoute( - routeOptions: RouteOptions, - callback: RouterCallback - ) { - primaryRouterRequestId = primaryRouter.getRoute( - routeOptions, - PrimaryCallback(callback) - ) - } - - fun cancelRouteRequest() { - primaryRouterRequestId?.let { - primaryRouter.cancelRouteRequest(it) - } - fallbackRouterRequestId?.let { - fallbackRouter.cancelRouteRequest(it) - } - } - } - - internal class Refresh( - primaryRouter: Router, - fallbackRouter: Router, - ) : HybridRouterHandler(primaryRouter, fallbackRouter) { - - private inner class PrimaryCallback( - private val route: DirectionsRoute, - private val legIndex: Int, - private val clientCallback: RouteRefreshCallback - ) : RouteRefreshCallback { - override fun onRefresh(directionsRoute: DirectionsRoute) { - clientCallback.onRefresh(directionsRoute) - } - - override fun onError(error: RouteRefreshError) { - LoggerProvider.logger.w( - TAG, - Message( - """ - Route refresh for $primaryRouterName failed for - UUID = ${route.requestUuid()} - legIndex = $legIndex - - message = ${error.message} - stack = ${error.throwable} - Trying to fallback to $fallbackRouterName... - """.trimIndent() - ) - ) - - fallbackRouterRequestId = fallbackRouter.getRouteRefresh( - route, - legIndex, - FallbackCallback(route, legIndex, clientCallback, error) - ) - } - } - - private inner class FallbackCallback( - private val route: DirectionsRoute, - private val legIndex: Int, - private val clientCallback: RouteRefreshCallback, - private val primaryError: RouteRefreshError - ) : RouteRefreshCallback { - - override fun onRefresh(directionsRoute: DirectionsRoute) { - LoggerProvider.logger.w( - TAG, - Message( - """ - Route refresh successful fallback to a $fallbackRouterName. - """.trimIndent() - ) - ) - clientCallback.onRefresh(directionsRoute) - } - - override fun onError(error: RouteRefreshError) { - LoggerProvider.logger.e( - TAG, - Message( - """ - Fallback route refresh for $fallbackRouterName failed with: - UUID = ${route.requestUuid()} - legIndex = $legIndex - - message = ${error.message} - stack = ${error.throwable} - """.trimIndent() - ) - ) - clientCallback.onError( - RouteRefreshError( - """ - ${primaryError.message} - ${error.message} - """.trimIndent(), - Throwable( - """ - Primary route refresh failed with: - message = ${primaryError.message} - stack = ${primaryError.throwable?.stackTraceToString()} - - Fallback route refresh failed with: - message = ${error.message} - stack = ${error.throwable?.stackTraceToString()} - """.trimIndent() - ) - ) - ) - } - } - - fun getRouteRefresh( - route: DirectionsRoute, - legIndex: Int, - callback: RouteRefreshCallback - ) { - primaryRouterRequestId = primaryRouter.getRouteRefresh( - route, - legIndex, - PrimaryCallback(route, legIndex, callback) - ) - } - - fun cancelRouteRefreshRequest() { - primaryRouterRequestId?.let { - primaryRouter.cancelRouteRefreshRequest(it) - } - fallbackRouterRequestId?.let { - fallbackRouter.cancelRouteRefreshRequest(it) - } - } - } -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouter.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouter.kt deleted file mode 100644 index 5fa462f3a61..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouter.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.mapbox.navigation.route.internal.hybrid - -import android.content.Context -import com.mapbox.annotation.module.MapboxModule -import com.mapbox.annotation.module.MapboxModuleType -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.Router -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator -import com.mapbox.navigation.route.internal.offboard.MapboxOffboardRouter -import com.mapbox.navigation.route.internal.onboard.MapboxOnboardRouter -import com.mapbox.navigation.utils.internal.ConnectivityHandler -import com.mapbox.navigation.utils.internal.RequestMap -import com.mapbox.navigation.utils.internal.ThreadController -import com.mapbox.navigation.utils.internal.monitorChannelWithException -import kotlinx.coroutines.Job - -/** - * MapboxHybridRouter combines onboard and offboard Routers. - * Fetch route based on internet-connection state. - * - * @param onboardRouter Router - * @param offboardRouter Router - */ -@MapboxModule(MapboxModuleType.NavigationRouter) -class MapboxHybridRouter( - private val onboardRouter: Router, - private val offboardRouter: Router, - networkStatusService: ConnectivityHandler, - threadController: ThreadController, -) : Router { - - private val directionRequests = RequestMap() - private val refreshRequests = RequestMap() - - constructor( - accessToken: String, - context: Context, - urlSkuTokenProvider: UrlSkuTokenProvider, - navigatorNative: MapboxNativeNavigator, - networkStatusService: ConnectivityHandler, - threadController: ThreadController, - ) : this( - onboardRouter = MapboxOnboardRouter( - accessToken, - navigatorNative, - context, - threadController = threadController, - ), - offboardRouter = MapboxOffboardRouter( - accessToken, - context, - urlSkuTokenProvider - ), - networkStatusService = networkStatusService, - threadController, - ) - - private val jobControl = threadController.getIOScopeAndRootJob() - private val networkStatusJob: Job - private var isNetworkAvailable = true - - /** - * At init time, the network monitor is setup. isNetworkAvailable represents the current network state. Based - * on that state we use either the off-board or on-board router. - */ - init { - networkStatusJob = jobControl.scope.monitorChannelWithException( - networkStatusService.getNetworkStatusChannel(), - ::onNetworkStatusChanged - ) - } - - internal suspend fun onNetworkStatusChanged(status: Boolean) { - isNetworkAvailable = status - } - - /** - * Fetch route based on [RouteOptions] - * - * @param routeOptions RouteOptions - * @param callback Callback that gets notified with the results of the request - */ - override fun getRoute( - routeOptions: RouteOptions, - callback: RouterCallback - ): Long { - val routerHandler = createDirectionsHandler() - val id = directionRequests.put(routerHandler) - routerHandler.getRoute( - routeOptions, - object : RouterCallback { - override fun onRoutesReady( - routes: List, - routerOrigin: RouterOrigin - ) { - directionRequests.remove(id) - callback.onRoutesReady(routes, routerOrigin) - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - directionRequests.remove(id) - callback.onFailure(reasons, routeOptions) - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - directionRequests.remove(id) - callback.onCanceled(routeOptions, routerOrigin) - } - } - ) - return id - } - - /** - * Cancel a request with ID from [getRoute]. - */ - override fun cancelRouteRequest(requestId: Long) { - directionRequests.remove(requestId)?.cancelRouteRequest() - } - - /** - * Refresh the traffic annotations for a given [DirectionsRoute] - * - * @param route DirectionsRoute the direction route to refresh - * @param legIndex Int the index of the current leg in the route - * @param callback Callback that gets notified with the results of the request - */ - override fun getRouteRefresh( - route: DirectionsRoute, - legIndex: Int, - callback: RouteRefreshCallback - ): Long { - val routerHandler = createRefreshHandler() - val id = refreshRequests.put(routerHandler) - routerHandler.getRouteRefresh( - route, - legIndex, - object : RouteRefreshCallback { - override fun onRefresh(directionsRoute: DirectionsRoute) { - refreshRequests.remove(id) - callback.onRefresh(directionsRoute) - } - - override fun onError(error: RouteRefreshError) { - refreshRequests.remove(id) - callback.onError(error) - } - } - ) - return id - } - - /** - * Cancel a request with ID from [getRouteRefresh]. - */ - override fun cancelRouteRefreshRequest(requestId: Long) { - refreshRequests.remove(requestId)?.cancelRouteRefreshRequest() - } - - /** - * Cancel all running requests. - */ - override fun cancelAll() { - offboardRouter.cancelAll() - onboardRouter.cancelAll() - } - - /** - * Release used resources. - */ - override fun shutdown() { - offboardRouter.shutdown() - onboardRouter.shutdown() - networkStatusJob.cancel() - } - - private fun createDirectionsHandler(): HybridRouterHandler.Directions { - return if (isNetworkAvailable) { - HybridRouterHandler.Directions(offboardRouter, onboardRouter) - } else { - HybridRouterHandler.Directions(onboardRouter, offboardRouter) - } - } - - private fun createRefreshHandler(): HybridRouterHandler.Refresh { - return if (isNetworkAvailable) { - HybridRouterHandler.Refresh(offboardRouter, onboardRouter) - } else { - HybridRouterHandler.Refresh(onboardRouter, offboardRouter) - } - } -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouter.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouter.kt deleted file mode 100644 index 4fbe09831c7..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouter.kt +++ /dev/null @@ -1,282 +0,0 @@ -/** - * Tampering with any file that contains billing code is a violation of Mapbox Terms of Service and will result in enforcement of the penalties stipulated in the ToS. - */ - -package com.mapbox.navigation.route.internal.offboard - -import android.content.Context -import com.mapbox.api.directions.v5.MapboxDirections -import com.mapbox.api.directions.v5.models.DirectionsResponse -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.api.directionsrefresh.v1.MapboxDirectionsRefresh -import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse -import com.mapbox.base.common.logger.Logger -import com.mapbox.base.common.logger.model.Message -import com.mapbox.base.common.logger.model.Tag -import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.Router -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM -import com.mapbox.navigation.route.internal.util.redactQueryParam -import com.mapbox.navigation.route.offboard.RouteBuilderProvider -import com.mapbox.navigation.route.offboard.routerefresh.RouteRefreshCallbackMapper -import com.mapbox.navigation.utils.internal.LoggerProvider -import com.mapbox.navigation.utils.internal.RequestMap -import com.mapbox.navigation.utils.internal.cancelRequest -import okhttp3.Request -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response - -/** - * MapboxOffboardRouter provides online route-fetching - * - * @param accessToken mapboxAccessToken token - * @param context application [Context] - * @param urlSkuTokenProvider [UrlSkuTokenProvider] - */ -class MapboxOffboardRouter( - private val accessToken: String, - private val context: Context, - private val urlSkuTokenProvider: UrlSkuTokenProvider, - private val logger: Logger = LoggerProvider.logger, -) : Router { - - internal companion object { - internal val TAG = Tag("MbxOffboardRouter") - private const val ROUTES_LIST_EMPTY = "routes list is empty" - private const val UNKNOWN = "unknown" - } - - private val directionRequests = RequestMap() - private val refreshRequests = RequestMap() - - /** - * Fetch routes based on [RouteOptions]. - * - * @param routeOptions RouteOptions - * @param callback Callback that gets notified with the results of the request - * - * @return request ID, can be used to cancel the request with [cancelAll] - */ - override fun getRoute( - routeOptions: RouteOptions, - callback: RouterCallback - ): Long { - val mapboxDirections = RouteBuilderProvider - .getBuilder(urlSkuTokenProvider) - .accessToken(accessToken) - .routeOptions(routeOptions) - .build() - val requestId = directionRequests.put(mapboxDirections) - mapboxDirections.enqueueCall( - object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - directionRequests.remove(requestId) - val urlWithoutToken = call.request().url.redactQueryParam( - ACCESS_TOKEN_QUERY_PARAM - ).toUrl() - val body = response.body() - val routes = body?.routes() - val metadata = body?.metadata()?.infoMap() - val code = response.code() - when { - call.isCanceled -> callback.onCanceled( - routeOptions, RouterOrigin.Offboard - ) - response.isSuccessful -> { - logger.i( - TAG, - Message("Successful directions response. Metadata: $metadata"), - ) - if (!routes.isNullOrEmpty()) { - callback.onRoutesReady(routes, RouterOrigin.Offboard) - } else { - callback.onFailure( - listOf( - RouterFailure( - urlWithoutToken, - RouterOrigin.Offboard, - ROUTES_LIST_EMPTY, - code - ) - ), - routeOptions - ) - } - } - else -> callback.onFailure( - listOf( - RouterFailure( - urlWithoutToken, - RouterOrigin.Offboard, - response.errorBody()?.string().toString(), - code - ) - ), - routeOptions - ) - } - } - - override fun onFailure(call: Call, t: Throwable) { - directionRequests.remove(requestId) - if (call.isCanceled) { - callback.onCanceled(routeOptions, RouterOrigin.Offboard) - } else { - callback.onFailure( - listOf( - RouterFailure( - url = call.request().url.redactQueryParam( - ACCESS_TOKEN_QUERY_PARAM - ).toUrl(), - routerOrigin = RouterOrigin.Offboard, - message = t.message ?: UNKNOWN, - code = null, - throwable = t - ) - ), - routeOptions - ) - } - } - } - ) - return requestId - } - - /** - * Cancels a specific route request. - * - * @see [getRoute] - */ - override fun cancelRouteRequest(requestId: Long) { - directionRequests.cancelRequest(requestId, TAG) { - it.cancelCall() - } - } - - /** - * Interrupts a route-fetching request if one is in progress. - */ - override fun cancelAll() { - directionRequests.removeAll().forEach { - it.cancelCall() - } - refreshRequests.removeAll().forEach { - it.cancelCall() - } - } - - /** - * Release used resources. - */ - override fun shutdown() { - cancelAll() - } - - /** - * Refresh the traffic annotations for a given [DirectionsRoute] - * - * @param route DirectionsRoute the direction route to refresh - * @param legIndex Int the index of the current leg in the route - * @param callback Callback that gets notified with the results of the request - */ - override fun getRouteRefresh( - route: DirectionsRoute, - legIndex: Int, - callback: RouteRefreshCallback - ): Long { - val routeOptions = route.routeOptions() - val mapboxDirectionsRefresh = RouteBuilderProvider.getRefreshBuilder() - .accessToken(accessToken) - .baseUrl(routeOptions?.baseUrl()) - .requestId(route.requestUuid()) - .routeIndex(route.routeIndex()?.toIntOrNull() ?: 0) - .legIndex(legIndex) - .interceptor { - val httpUrl = it.request().url - val skuUrl = urlSkuTokenProvider.obtainUrlWithSkuToken(httpUrl.toUrl()) - it.proceed(it.request().newBuilder().url(skuUrl).build()) - } - .build() - val requestId = refreshRequests.put(mapboxDirectionsRefresh) - - mapboxDirectionsRefresh.enqueueCall(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - refreshRequests.remove(requestId) - if (response.isSuccessful) { - val routeAnnotations = response.body()?.route() - var errorThrowable: Throwable? = null - val refreshedDirectionsRoute = try { - RouteRefreshCallbackMapper.mapToDirectionsRoute(route, routeAnnotations) - } catch (t: Throwable) { - errorThrowable = t - null - } - if (refreshedDirectionsRoute != null) { - callback.onRefresh(refreshedDirectionsRoute) - } else { - callback.onError( - RouteRefreshError( - message = "Failed to read refresh response", - throwable = errorThrowable ?: Exception( - "Message=[${response.message()}]; " + - "url = [${(call.request() as Request).url}]" + - "errorBody = [${response.errorBody()}];" + - "refresh route = [$routeAnnotations]" - ) - ) - ) - } - } else { - callback.onError( - RouteRefreshError( - message = "Route refresh failed", - throwable = Exception( - "Message=[${response.message()}]; " + - "url = [${(call.request() as Request).url}]" + - "code = [${response.code()}];" + - "errorBody = [${response.errorBody()}];" - ) - ) - ) - } - } - - override fun onFailure(call: Call, t: Throwable) { - refreshRequests.remove(requestId) - callback.onError( - RouteRefreshError( - "Route refresh failed; " + - "url = [${(call.request() as Request).url}]", - throwable = t - ) - ) - } - }) - return requestId - } - - /** - * Cancels a specific route refresh request. - * - * @see [getRouteRefresh] - */ - override fun cancelRouteRefreshRequest(requestId: Long) { - refreshRequests.cancelRequest(requestId, TAG) { - it.cancelCall() - } - } -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouter.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouter.kt deleted file mode 100644 index 895b4042e14..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouter.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.mapbox.navigation.route.internal.onboard - -import android.content.Context -import com.mapbox.api.directions.v5.models.DirectionsResponse -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.base.common.logger.Logger -import com.mapbox.base.common.logger.model.Message -import com.mapbox.base.common.logger.model.Tag -import com.mapbox.navigation.base.options.RoutingTilesOptions -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.Router -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator -import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM -import com.mapbox.navigation.route.internal.util.httpUrl -import com.mapbox.navigation.route.internal.util.redactQueryParam -import com.mapbox.navigation.route.offboard.RouteBuilderProvider -import com.mapbox.navigation.route.onboard.OfflineRoute -import com.mapbox.navigation.utils.internal.LoggerProvider -import com.mapbox.navigation.utils.internal.RequestMap -import com.mapbox.navigation.utils.internal.ThreadController -import com.mapbox.navigation.utils.internal.cancelRequest -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.net.URL - -/** - * MapboxOnboardRouter provides offline route fetching - * - * It uses offline storage path to store and retrieve data, setup endpoint, - * tiles' version, token. Config is provided via [RoutingTilesOptions]. - * - * @param navigatorNative Native Navigator - * @param context application [Context] - */ -class MapboxOnboardRouter( - private val accessToken: String, - private val navigatorNative: MapboxNativeNavigator, - private val context: Context, - private val logger: Logger = LoggerProvider.logger, - threadController: ThreadController, -) : Router { - - internal companion object { - internal val loggerTag = Tag("MbxOnboardRouter") - } - - private val mainJobControl by lazy { threadController.getMainScopeAndRootJob() } - private val requests = RequestMap() - - /** - * Fetch route based on [RouteOptions] - * - * @param routeOptions RouteOptions - * @param callback Callback that gets notified with the results of the request - */ - override fun getRoute( - routeOptions: RouteOptions, - callback: RouterCallback - ): Long { - val httpUrl = RouteBuilderProvider - .getBuilder(null) - .accessToken(accessToken) - .routeOptions(routeOptions) - .build() - .httpUrl() - - val offlineRoute = OfflineRoute.Builder(httpUrl.toUrl()).build() - - val requestId = requests.generateNextRequestId() - val internalCallback = object : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - requests.remove(requestId) - callback.onRoutesReady(routes, routerOrigin) - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - requests.remove(requestId) - callback.onFailure(reasons, routeOptions) - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - requests.remove(requestId) - callback.onCanceled(routeOptions, routerOrigin) - } - } - requests.put( - requestId, - retrieveRoute(routeOptions, offlineRoute.buildUrl(), internalCallback) - ) - return requestId - } - - override fun cancelRouteRequest(requestId: Long) { - requests.cancelRequest(requestId, loggerTag) { - it.cancel() - } - } - - /** - * Interrupts a route-fetching request if one is in progress. - */ - override fun cancelAll() { - requests.removeAll().forEach { - it.cancel() - } - } - - /** - * Release used resources. - */ - override fun shutdown() { - cancelAll() - } - - /** - * Refresh the traffic annotations for a given [DirectionsRoute]. [MapboxOnboardRouter] is not - * supporting refresh route. - * - * @param route DirectionsRoute the direction route to refresh - * @param legIndex Int the index of the current leg in the route - * @param callback Callback that gets notified with the results of the request - */ - override fun getRouteRefresh( - route: DirectionsRoute, - legIndex: Int, - callback: RouteRefreshCallback - ): Long { - callback.onError( - RouteRefreshError("Route refresh is not available when offline.") - ) - return -1 - } - - override fun cancelRouteRefreshRequest(requestId: Long) { - // Do nothing - } - - private fun retrieveRoute( - routeOptions: RouteOptions, - url: String, - callback: RouterCallback - ): Job { - val javaUrl = URL(url.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM)) - return mainJobControl.scope.launch { - try { - val routerResult = getRoute(url) - if (routerResult.isValue) { - val directions = parseDirectionsResponse(routerResult.value!!) - val metadata = directions.metadata()?.infoMap() - val message = Message("Successful directions response. Metadata: $metadata") - logger.i(loggerTag, message) - val routes = directions.routes().map { - it.toBuilder().routeOptions(routeOptions).build() - } - callback.onRoutesReady(routes, RouterOrigin.Onboard) - } else { - val error = routerResult.error!! - callback.onFailure( - listOf( - RouterFailure( - url = javaUrl, - routerOrigin = RouterOrigin.Onboard, - message = error.message, - code = error.code - ) - ), - routeOptions - ) - } - } catch (e: CancellationException) { - callback.onCanceled(routeOptions, RouterOrigin.Onboard) - } - } - } - - internal suspend fun getRoute(url: String) = withContext(ThreadController.IODispatcher) { - navigatorNative.getRoute(url) - } - - // todo Nav Native serializes route options, it probably shouldn't - private suspend fun parseDirectionsResponse(json: String): DirectionsResponse = - withContext(ThreadController.IODispatcher) { - DirectionsResponse.fromJson(json) - } -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/util/MapboxDirectionsEx.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/util/MapboxDirectionsEx.kt index 7d18cf1bdc1..606560963ef 100644 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/util/MapboxDirectionsEx.kt +++ b/libnavigation-router/src/main/java/com/mapbox/navigation/route/internal/util/MapboxDirectionsEx.kt @@ -1,5 +1,3 @@ -@file:JvmName("MapboxDirectionsEx") - package com.mapbox.navigation.route.internal.util import com.mapbox.api.directions.v5.MapboxDirections diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/RouteBuilderProvider.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/RouteBuilderProvider.kt deleted file mode 100644 index a77c8bc01a5..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/RouteBuilderProvider.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Tampering with any file that contains billing code is a violation of Mapbox Terms of Service and will result in enforcement of the penalties stipulated in the ToS. - */ - -package com.mapbox.navigation.route.offboard - -import com.mapbox.api.directions.v5.MapboxDirections -import com.mapbox.api.directionsrefresh.v1.MapboxDirectionsRefresh -import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider - -internal object RouteBuilderProvider { - - fun getBuilder( - urlSkuTokenProvider: UrlSkuTokenProvider? - ): MapboxDirections.Builder = - MapboxDirections.builder() - .also { builder -> - if (urlSkuTokenProvider != null) { - builder.interceptor { - val httpUrl = it.request().url - val skuUrl = urlSkuTokenProvider.obtainUrlWithSkuToken(httpUrl.toUrl()) - it.proceed(it.request().newBuilder().url(skuUrl).build()) - } - } - } - - fun getRefreshBuilder(): MapboxDirectionsRefresh.Builder = - MapboxDirectionsRefresh.builder() -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapper.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapper.kt deleted file mode 100644 index 2ac1db08d7b..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapper.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.mapbox.navigation.route.offboard.routerefresh - -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.LegAnnotation -import com.mapbox.api.directions.v5.models.RouteLeg -import com.mapbox.api.directionsrefresh.v1.models.DirectionsRouteRefresh - -internal object RouteRefreshCallbackMapper { - fun mapToDirectionsRoute( - originalRoute: DirectionsRoute, - routeAnnotations: DirectionsRouteRefresh? - ): DirectionsRoute? { - val validRouteAnnotations = routeAnnotations ?: return null - val updatedLegs = mutableListOf() - originalRoute.legs()?.let { oldRouteLegsList -> - oldRouteLegsList.forEachIndexed { index, routeLeg -> - val newAnnotation = routeLeg.annotation()?.toBuilder() - ?.congestion( - validRouteAnnotations.annotationOfLeg(index)?.congestion() - ) - ?.distance( - validRouteAnnotations.annotationOfLeg(index)?.distance() - ) - ?.duration( - validRouteAnnotations.annotationOfLeg(index)?.duration() - ) - ?.maxspeed( - validRouteAnnotations.annotationOfLeg(index)?.maxspeed() - ) - ?.speed( - validRouteAnnotations.annotationOfLeg(index)?.speed() - ) - ?.build() - updatedLegs.add(routeLeg.toBuilder().annotation(newAnnotation).build()) - } - } - return originalRoute.toBuilder().legs(updatedLegs).build() - } - - private fun DirectionsRouteRefresh.annotationOfLeg(index: Int): LegAnnotation? = - this.legs()?.getOrNull(index)?.annotation() -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineCriteria.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineCriteria.kt deleted file mode 100644 index 253defc33f4..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineCriteria.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.mapbox.navigation.route.onboard - -/** - * Offline Criteria. Used in Direction API - */ -internal object OfflineCriteria { - - /** - * BicycleType parameter in the Directions API. - * - * @property type - */ - enum class BicycleType(val type: String) { - /** - * Bicycle type for road bike. - */ - ROAD("Road"), - - /** - * Bicycle type for hybrid bike. - */ - HYBRID("Hybrid"), - - /** - * Bicycle type for city bike. - */ - CITY("City"), - - /** - * Bicycle type for cross bike. - */ - CROSS("Cross"), - - /** - * Bicycle type for mountain bike. - */ - MOUNTAIN("Mountain"); - } - - /** - * WaypointType parameter in the Directions API. - * - * @property type - */ - enum class WaypointType(val type: String) { - /** - * Break waypoint type. - */ - BREAK("break"), - - /** - * Through waypoint type. - */ - THROUGH("through") - } -} diff --git a/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineRoute.kt b/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineRoute.kt deleted file mode 100644 index 1ed4c5d7834..00000000000 --- a/libnavigation-router/src/main/java/com/mapbox/navigation/route/onboard/OfflineRoute.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.mapbox.navigation.route.onboard - -import android.net.Uri -import androidx.annotation.FloatRange -import com.mapbox.navigation.utils.internal.ifNonNull -import java.net.URL - -/** - * The [OfflineRoute] class wraps the [routeUrl] with parameters which - * could be set in order for an offline navigation session to successfully begin. - */ -internal class OfflineRoute private constructor( - private val routeUrl: URL, - private val bicycleType: OfflineCriteria.BicycleType?, - private val cyclingSpeed: Float?, - private val cyclewayBias: Float?, - private val hillBias: Float?, - private val ferryBias: Float?, - private val roughSurfaceBias: Float?, - private val waypointTypes: List? -) { - - private companion object { - private const val BICYCLE_TYPE_QUERY_PARAMETER = "bicycle_type" - private const val CYCLING_SPEED_QUERY_PARAMETER = "cycling_speed" - private const val CYCLEWAY_BIAS_QUERY_PARAMETER = "cycleway_bias" - private const val HILL_BIAS_QUERY_PARAMETER = "hill_bias" - private const val FERRY_BIAS_QUERY_PARAMETER = "ferry_bias" - private const val ROUGH_SURFACE_BIAS_QUERY_PARAMETER = "rough_surface_bias" - private const val WAYPOINT_TYPES_QUERY_PARAMETER = "waypoint_types" - } - - /** - * @return builder matching the one used to create this instance - */ - fun toBuilder() = Builder(routeUrl) - .bicycleType(bicycleType) - .cyclingSpeed(cyclingSpeed) - .cyclewayBias(cyclewayBias) - .hillBias(hillBias) - .ferryBias(ferryBias) - .roughSurfaceBias(roughSurfaceBias) - .waypointTypes(waypointTypes) - - /** - * Builds a URL string for offline. - * - * @return the offline url string - */ - fun buildUrl(): String { - return buildOfflineUrl(routeUrl) - } - - private fun checkWaypointTypes(waypointTypes: List?): String? { - return if (waypointTypes.isNullOrEmpty()) { - null - } else { - formatWaypointTypes(waypointTypes) - } - } - - private fun formatWaypointTypes( - waypointTypesToFormat: List - ): String { - val waypointTypes = waypointTypesToFormat.map { it?.type ?: "" }.toTypedArray() - return waypointTypes.joinTo(StringBuilder(), ";").toString() - } - - private fun buildOfflineUrl(url: URL): String { - val offlineUrlBuilder = Uri.parse(url.toString()).buildUpon() - - offlineUrlBuilder - .appendQueryParamIfNonNull(BICYCLE_TYPE_QUERY_PARAMETER, bicycleType?.type) - offlineUrlBuilder.appendQueryParamIfNonNull(CYCLING_SPEED_QUERY_PARAMETER, cyclingSpeed) - offlineUrlBuilder.appendQueryParamIfNonNull(CYCLEWAY_BIAS_QUERY_PARAMETER, cyclewayBias) - offlineUrlBuilder.appendQueryParamIfNonNull(HILL_BIAS_QUERY_PARAMETER, hillBias) - offlineUrlBuilder.appendQueryParamIfNonNull(FERRY_BIAS_QUERY_PARAMETER, ferryBias) - offlineUrlBuilder.appendQueryParamIfNonNull( - ROUGH_SURFACE_BIAS_QUERY_PARAMETER, - roughSurfaceBias - ) - offlineUrlBuilder.appendQueryParamIfNonNull( - WAYPOINT_TYPES_QUERY_PARAMETER, - checkWaypointTypes(waypointTypes) - ) - - return offlineUrlBuilder.build().toString() - } - - private fun Uri.Builder.appendQueryParamIfNonNull(key: String, value: Any?): Uri.Builder = - apply { - ifNonNull(value) { - appendQueryParameter(key, it.toString()) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as OfflineRoute - - if (routeUrl != other.routeUrl) return false - if (bicycleType != other.bicycleType) return false - if (cyclingSpeed != other.cyclingSpeed) return false - if (cyclewayBias != other.cyclewayBias) return false - if (hillBias != other.hillBias) return false - if (ferryBias != other.ferryBias) return false - if (roughSurfaceBias != other.roughSurfaceBias) return false - if (waypointTypes != other.waypointTypes) return false - - return true - } - - override fun hashCode(): Int { - var result = routeUrl.hashCode() - result = 31 * result + bicycleType.hashCode() - result = 31 * result + cyclingSpeed.hashCode() - result = 31 * result + cyclewayBias.hashCode() - result = 31 * result + hillBias.hashCode() - result = 31 * result + ferryBias.hashCode() - result = 31 * result + roughSurfaceBias.hashCode() - result = 31 * result + waypointTypes.hashCode() - return result - } - - override fun toString(): String { - return "OfflineRoute(" + - "routeUrl=$routeUrl, " + - "bicycleType=$bicycleType, " + - "cyclingSpeed=$cyclingSpeed, " + - "cyclewayBias=$cyclewayBias, " + - "hillBias=$hillBias, " + - "ferryBias=$ferryBias, " + - "roughSurfaceBias=$roughSurfaceBias, " + - "waypointTypes=$waypointTypes" + - ")" - } - - class Builder internal constructor(private val routeUrl: URL) { - private var bicycleType: OfflineCriteria.BicycleType? = null - private var cyclingSpeed: Float? = null - private var cyclewayBias: Float? = null - private var hillBias: Float? = null - private var ferryBias: Float? = null - private var roughSurfaceBias: Float? = null - private var waypointTypes: List? = null - - /** - * The type of bicycle, either Road, Hybrid, City, Cross, Mountain. - * The default type is Hybrid. - * - * @param bicycleType the type of bicycle - * @return this builder for chaining options together - */ - fun bicycleType(bicycleType: OfflineCriteria.BicycleType?): Builder { - this.bicycleType = bicycleType - return this - } - - /** - * Cycling speed is the average travel speed along smooth, flat roads. This is meant to be the - * speed a rider can comfortably maintain over the desired distance of the route. It can be - * modified (in the costing method) by surface type in conjunction with bicycle type and - * (coming soon) by hilliness of the road section. When no speed is specifically provided, the - * default speed is determined by the bicycle type and are as follows: Road = 25 KPH (15.5 MPH), - * Cross = 20 KPH (13 MPH), Hybrid/City = 18 KPH (11.5 MPH), and Mountain = 16 KPH (10 MPH). - * - * @param cyclingSpeed in kmh - * @return this builder for chaining options together - */ - fun cyclingSpeed(@FloatRange(from = 5.0, to = 60.0) cyclingSpeed: Float?): Builder { - this.cyclingSpeed = cyclingSpeed - return this - } - - /** - * A cyclist's propensity to use roads alongside other vehicles. This is a range of values from -1 - * to 1, where -1 attempts to avoid roads and stay on cycleways and paths, and 1 indicates the - * rider is more comfortable riding on roads. Based on the use_roads factor, roads with certain - * classifications and higher speeds are penalized in an attempt to avoid them when finding the - * best path. The default value is 0. - * - * @param cyclewayBias a cyclist's propensity to use roads alongside other vehicles - * @return this builder for chaining options together - */ - fun cyclewayBias(@FloatRange(from = -1.0, to = 1.0) cyclewayBias: Float?): Builder { - this.cyclewayBias = cyclewayBias - return this - } - - /** - * A cyclist's desire to tackle hills in their routes. This is a range of values from -1 to 1, - * where -1 attempts to avoid hills and steep grades even if it means a longer (time and - * distance) path, while 1 indicates the rider does not fear hills and steeper grades. Based on - * the hill bias factor, penalties are applied to roads based on elevation change and grade. - * These penalties help the path avoid hilly roads in favor of flatter roads or less steep - * grades where available. Note that it is not always possible to find alternate paths to avoid - * hills (for example when route locations are in mountainous areas). The default value is 0. - * - * @param hillBias a cyclist's desire to tackle hills in their routes - * @return this builder for chaining options together - */ - fun hillBias(@FloatRange(from = -1.0, to = 1.0) hillBias: Float?): Builder { - this.hillBias = hillBias - return this - } - - /** - * This value indicates the willingness to take ferries. This is a range of values between -1 and 1. - * Values near -1 attempt to avoid ferries and values near 1 will favor ferries. Note that - * sometimes ferries are required to complete a route so values of -1 are not guaranteed to avoid - * ferries entirely. The default value is 0. - * - * @param ferryBias the willingness to take ferries - * @return this builder for chaining options together - */ - fun ferryBias(@FloatRange(from = -1.0, to = 1.0) ferryBias: Float?): Builder { - this.ferryBias = ferryBias - return this - } - - /** - * This value is meant to represent how much a cyclist wants to favor or avoid roads with poor/rough - * surfaces relative to the bicycle type being used. This is a range of values between -1 and 1. - * When the value approaches -1, we attempt to penalize heavier or avoid roads with rough surface types - * so that they are only taken if they significantly improve travel time; only bicycle - * speed on each surface is taken into account. As the value approaches 1, we will favor rough surfaces. - * When the value is equal to -1, all bad surfaces are completely disallowed from routing, - * including start and end points. The default value is 0. - * - * @param roughSurfaceBias how much a cyclist wants to avoid roads with poor surfaces - * @return this builder for chaining options together - */ - fun roughSurfaceBias(@FloatRange(from = -1.0, to = 1.0) roughSurfaceBias: Float?): Builder { - this.roughSurfaceBias = roughSurfaceBias - return this - } - - /** - * The same waypoint types the user originally made when the request was made. - * - * @param waypointTypes break, through or omitted null - * @return this builder for chaining options together - */ - fun waypointTypes(waypointTypes: List?): Builder { - this.waypointTypes = waypointTypes - return this - } - - /** - * This uses the provided parameters set using the [Builder] and adds the required - * settings for offline navigation to work correctly. - * - * @return a new instance of [OfflineRoute] - */ - fun build(): OfflineRoute = OfflineRoute( - routeUrl, - bicycleType, - cyclingSpeed, - cyclewayBias, - hillBias, - ferryBias, - roughSurfaceBias, - waypointTypes - ) - } -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt new file mode 100644 index 00000000000..39452490ba2 --- /dev/null +++ b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/RouterWrapperTests.kt @@ -0,0 +1,438 @@ +package com.mapbox.navigation.route.internal + +import com.mapbox.api.directions.v5.models.DirectionsResponse +import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.bindgen.Expected +import com.mapbox.geojson.Point +import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions +import com.mapbox.navigation.base.extensions.coordinates +import com.mapbox.navigation.base.route.RouteRefreshCallback +import com.mapbox.navigation.base.route.RouteRefreshError +import com.mapbox.navigation.base.route.RouterCallback +import com.mapbox.navigation.base.route.RouterFailure +import com.mapbox.navigation.base.route.RouterOrigin.Offboard +import com.mapbox.navigation.base.route.RouterOrigin.Onboard +import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator +import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM +import com.mapbox.navigation.route.internal.util.TestRouteFixtures +import com.mapbox.navigation.route.internal.util.redactQueryParam +import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.utils.internal.ThreadController +import com.mapbox.navigator.RouteRefreshOptions +import com.mapbox.navigator.RouterError +import com.mapbox.navigator.RouterErrorType +import com.mapbox.navigator.RouterInterface +import com.mapbox.navigator.RouterOrigin +import com.mapbox.navigator.RouterRefreshCallback +import com.mapbox.navigator.RoutingProfile +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@InternalCoroutinesApi +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class RouterWrapperTests { + + @get:Rule + var coroutineRule = MainCoroutineRule() + + private lateinit var routerWrapper: RouterWrapper + private val mapboxNativeNavigator: MapboxNativeNavigator = mockk(relaxed = true) + private val router: RouterInterface = mockk(relaxed = true) + private val accessToken = "pk.123" + private val route: DirectionsRoute = mockk(relaxed = true) + private val routerCallback: RouterCallback = mockk(relaxed = true) + private val routerRefreshCallback: RouteRefreshCallback = mockk(relaxed = true) + private val routerOptions: RouteOptions = provideDefaultRouteOptions() + private val routeUrl = routerOptions.toUrl(accessToken).toString() + + private val testRouteFixtures = TestRouteFixtures() + + private val routerResultSuccess: Expected = mockk { + every { isValue } returns true + every { isError } returns false + every { value } returns testRouteFixtures.loadTwoLegRoute() + every { error } returns null + + val valueSlot = slot>() + every { fold(any(), capture(valueSlot)) } answers { + valueSlot.captured.invoke(this@mockk.value!!) + } + } + private val routerResultFailure: Expected = mockk { + every { isValue } returns false + every { isError } returns true + every { value } returns null + every { error } returns RouterError( + FAILURE_MESSAGE, + FAILURE_CODE, + FAILURE_TYPE, + REQUEST_ID + ) + + val errorSlot = slot>() + every { fold(capture(errorSlot), any()) } answers { + errorSlot.captured.invoke(this@mockk.error!!) + } + } + private val routerResultCancelled: Expected = mockk { + every { isValue } returns false + every { isError } returns true + every { value } returns null + every { error } returns RouterError( + CANCELLED_MESSAGE, + FAILURE_CODE, + CANCELED_TYPE, + REQUEST_ID + ) + + val errorSlot = slot>() + every { fold(capture(errorSlot), any()) } answers { + errorSlot.captured.invoke(this@mockk.error!!) + } + } + private val routerResultSuccessEmptyRoutes: Expected = mockk { + every { isValue } returns true + every { isError } returns false + every { value } returns testRouteFixtures.loadEmptyRoutesResponse() + every { error } returns null + + val valueSlot = slot>() + every { fold(any(), capture(valueSlot)) } answers { + valueSlot.captured.invoke(this@mockk.value!!) + } + } + private val routerRefreshSuccess: Expected = mockk { + every { isValue } returns true + every { isError } returns false + every { value } returns testRouteFixtures.loadRefreshedRoute() + every { error } returns null + + val valueSlot = slot>() + every { fold(any(), capture(valueSlot)) } answers { + valueSlot.captured.invoke(this@mockk.value!!) + } + } + private val nativeOriginOnline: RouterOrigin = RouterOrigin.ONLINE + private val nativeOriginOnboard: RouterOrigin = RouterOrigin.ONBOARD + private val getRouteSlot = slot() + private val refreshRouteSlot = slot() + + @Before + fun setUp() { + mockkObject(ThreadController) + every { ThreadController.IODispatcher } returns coroutineRule.testDispatcher + + every { mapboxNativeNavigator.router } returns router + every { router.getRoute(any(), capture(getRouteSlot)) } returns 0L + every { router.getRouteRefresh(any(), any(), capture(refreshRouteSlot)) } returns 0L + + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns "index" + every { route.routeOptions() } returns routerOptions + + routerWrapper = RouterWrapper( + accessToken, + mapboxNativeNavigator.router, + ThreadController() + ) + } + + @After + fun cleanUp() { + unmockkObject(ThreadController) + } + + @Test + fun generationSanityTest() { + assertNotNull(routerWrapper) + } + + @Test + fun `get route is called with expected url`() { + routerWrapper.getRoute(routerOptions, routerCallback) + + verify { router.getRoute(routeUrl, any()) } + } + + @Test + fun `check callback called on failure with redacted token`() { + routerWrapper.getRoute(routerOptions, routerCallback) + getRouteSlot.captured.run(routerResultFailure, nativeOriginOnline) + + val expected = listOf( + RouterFailure( + url = routeUrl.toHttpUrlOrNull()!!.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM) + .toUrl(), + routerOrigin = Offboard, + message = FAILURE_MESSAGE, + code = FAILURE_CODE, + throwable = null + ) + ) + + verify { router.getRoute(routeUrl, any()) } + verify { routerCallback.onFailure(expected, routerOptions) } + } + + @Test + fun `check callback called on success and contains original options`() = runBlockingTest { + routerWrapper.getRoute(routerOptions, routerCallback) + getRouteSlot.captured.run(routerResultSuccess, nativeOriginOnboard) + + verify { router.getRoute(routeUrl, any()) } + + val expected = DirectionsResponse.fromJson( + testRouteFixtures.loadTwoLegRoute(), + routerOptions, + UUID + ).routes() + + verify(exactly = 1) { routerCallback.onRoutesReady(expected, Onboard) } + } + + @Test + fun `check on failure is called on success response with no routes`() { + routerWrapper.getRoute(routerOptions, routerCallback) + getRouteSlot.captured.run(routerResultSuccessEmptyRoutes, nativeOriginOnboard) + + verify { router.getRoute(routeUrl, any()) } + + val expected = listOf( + RouterFailure( + url = routeUrl.toHttpUrlOrNull()!!.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM) + .toUrl(), + routerOrigin = Onboard, + message = ROUTES_LIST_EMPTY, + ) + ) + + verify(exactly = 1) { routerCallback.onFailure(expected, routerOptions) } + } + + @Test + fun `check callback called on cancel`() = coroutineRule.runBlockingTest { + every { router.cancelRequest(any()) } answers { + getRouteSlot.captured.run(routerResultCancelled, nativeOriginOnboard) + } + + routerWrapper.getRoute(routerOptions, routerCallback) + routerWrapper.cancelRouteRequest(REQUEST_ID) + + verify { routerCallback.onCanceled(routerOptions, Onboard) } + } + + @Test + fun `check callback called on cancelAll`() = coroutineRule.runBlockingTest { + every { router.cancelAll() } answers { + getRouteSlot.captured.run(routerResultCancelled, nativeOriginOnline) + } + + routerWrapper.getRoute(routerOptions, routerCallback) + routerWrapper.cancelAll() + + verify { routerCallback.onCanceled(routerOptions, Offboard) } + } + + @Test + fun `cancel a specific route request when multiple are running`() { + val requestIdOne = 1L + val requestIdTwo = 2L + val refreshIdOne = 3L + val refreshIdTwo = 4L + + verify(exactly = 0) { router.cancelRequest(any()) } + + routerWrapper.cancelRouteRequest(requestIdOne) + routerWrapper.cancelRouteRequest(requestIdTwo) + routerWrapper.cancelRouteRequest(refreshIdOne) + routerWrapper.cancelRouteRequest(refreshIdTwo) + + verify(exactly = 1) { router.cancelRequest(requestIdOne) } + verify(exactly = 1) { router.cancelRequest(requestIdTwo) } + verify(exactly = 1) { router.cancelRequest(refreshIdOne) } + verify(exactly = 1) { router.cancelRequest(refreshIdTwo) } + } + + @Test + fun `route refresh fails with null routeOptions`() { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns "1" + every { route.routeOptions() } returns null + + routerWrapper.getRouteRefresh(route, 0, routerRefreshCallback) + + val expectedErrorMessage = + """ + Route refresh failed because of a null param: + routeOptions = null + requestUuid = $UUID + routeIndex = 1 + """.trimIndent() + + val errorSlot = slot() + verify(exactly = 1) { routerRefreshCallback.onError(capture(errorSlot)) } + verify(exactly = 0) { router.getRouteRefresh(any(), any(), any()) } + assertEquals("Route refresh failed", errorSlot.captured.message) + assertEquals(expectedErrorMessage, errorSlot.captured.throwable?.message) + } + + @Test + fun `route refresh fails with null requestUuid`() { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns null + every { route.routeIndex() } returns "1" + every { route.routeOptions() } returns routerOptions + + routerWrapper.getRouteRefresh(route, 0, routerRefreshCallback) + + val expectedErrorMessage = + """ + Route refresh failed because of a null param: + routeOptions = $routerOptions + requestUuid = null + routeIndex = 1 + """.trimIndent() + + val errorSlot = slot() + verify(exactly = 1) { routerRefreshCallback.onError(capture(errorSlot)) } + verify(exactly = 0) { router.getRouteRefresh(any(), any(), any()) } + assertEquals("Route refresh failed", errorSlot.captured.message) + assertEquals(expectedErrorMessage, errorSlot.captured.throwable?.message) + } + + @Test + fun `route refresh fails with null routeIndex`() { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns null + every { route.routeOptions() } returns routerOptions + + routerWrapper.getRouteRefresh(route, 0, routerRefreshCallback) + + val expectedErrorMessage = + """ + Route refresh failed because of a null param: + routeOptions = $routerOptions + requestUuid = $UUID + routeIndex = null + """.trimIndent() + + val errorSlot = slot() + verify(exactly = 1) { routerRefreshCallback.onError(capture(errorSlot)) } + verify(exactly = 0) { router.getRouteRefresh(any(), any(), any()) } + assertEquals("Route refresh failed", errorSlot.captured.message) + assertEquals(expectedErrorMessage, errorSlot.captured.throwable?.message) + } + + @Test + fun `route refresh set right params`() { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns "1" + every { route.routeOptions() } returns routerOptions + + routerWrapper.getRouteRefresh(route, 0, routerRefreshCallback) + + val expectedRefreshOptions = RouteRefreshOptions( + UUID, + 1, + 0, + RoutingProfile(routerOptions.profile().mapToRoutingMode(), routerOptions.user()) + ) + + verify(exactly = 1) { + router.getRouteRefresh( + expectedRefreshOptions, + route.toJson(), + any() + ) + } + } + + @Test + fun `route refresh successful`() = runBlockingTest { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns "1" + every { route.routeOptions() } returns routerOptions + + routerWrapper.getRouteRefresh(route, 0, routerRefreshCallback) + refreshRouteSlot.captured.run(routerRefreshSuccess, nativeOriginOnboard) + + val expected = DirectionsRoute.fromJson( + testRouteFixtures.loadRefreshedRoute(), + routerOptions, + UUID + ) + + verify(exactly = 1) { routerRefreshCallback.onRefresh(expected) } + } + + @Test + fun `route refresh failure`() { + val route: DirectionsRoute = mockk(relaxed = true) + every { route.requestUuid() } returns UUID + every { route.routeIndex() } returns "1" + every { route.routeOptions() } returns routerOptions + + val legIndex = 0 + routerWrapper.getRouteRefresh(route, legIndex, routerRefreshCallback) + refreshRouteSlot.captured.run(routerResultFailure, nativeOriginOnboard) + + val errorMessage = + """ + Route refresh failed. + message = $FAILURE_MESSAGE + code = $FAILURE_CODE + type = $FAILURE_TYPE + requestId = $REQUEST_ID + legIndex = $legIndex + """.trimIndent() + + val errorSlot = slot() + + verify(exactly = 1) { routerRefreshCallback.onError(capture(errorSlot)) } + assertEquals("Route refresh failed", errorSlot.captured.message) + assertEquals(errorMessage, errorSlot.captured.throwable?.message) + } + + private fun provideDefaultRouteOptions(): RouteOptions { + return RouteOptions.builder() + .applyDefaultNavigationOptions() + .apply { + coordinates(Point.fromLngLat(.0, .0), null, Point.fromLngLat(.0, .0)) + }.build() + } + + private companion object { + private const val CANCELLED_MESSAGE = "Cancelled" + private const val FAILURE_MESSAGE = "No suitable edges near location" + private const val FAILURE_CODE = 171 + private val FAILURE_TYPE = RouterErrorType.WRONG_REQUEST + private val CANCELED_TYPE = RouterErrorType.REQUEST_CANCELLED + private const val REQUEST_ID = 19L + private const val UUID = "cjeacbr8s21bk47lggcvce7lv" + private const val ROUTES_LIST_EMPTY = "routes list is empty" + } +} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouterTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouterTest.kt deleted file mode 100644 index 56480d76006..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/hybrid/MapboxHybridRouterTest.kt +++ /dev/null @@ -1,482 +0,0 @@ -package com.mapbox.navigation.route.internal.hybrid - -import android.content.Context -import android.content.Intent -import android.net.ConnectivityManager -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.geojson.Point -import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions -import com.mapbox.navigation.base.extensions.coordinates -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.Router -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.utils.internal.ConnectivityHandler -import com.mapbox.navigation.utils.internal.LoggerProvider -import com.mapbox.navigation.utils.internal.ThreadController -import io.mockk.Ordering -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.slot -import io.mockk.unmockkObject -import io.mockk.verify -import io.mockk.verifySequence -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Before -import org.junit.Test -import java.net.URL - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -class MapboxHybridRouterTest { - - private lateinit var hybridRouter: MapboxHybridRouter - private val onboardRouter: Router = mockk(relaxUnitFun = true) - private val offboardRouter: Router = mockk(relaxUnitFun = true) - private val context: Context = mockk(relaxUnitFun = true) - private val connectivityManager: ConnectivityManager = mockk(relaxUnitFun = true) - private val routerCallback: RouterCallback = mockk(relaxUnitFun = true) - private val routerOptions: RouteOptions = provideDefaultRouteOptions() - private val internalOffboardCallback = slot() - private val internalOnboardCallback = slot() - private val networkStatusService: ConnectivityHandler = mockk(relaxUnitFun = true) - private val channel = Channel(Channel.CONFLATED) - private val internalOffboardRefreshCallback = slot() - private val internalOnboardRefreshCallback = slot() - - @Before - fun setUp() { - mockkObject(LoggerProvider) - every { LoggerProvider.logger } returns mockk(relaxed = true) - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager - every { context.registerReceiver(any(), any()) } returns Intent() - every { onboardRouter.getRoute(routerOptions, capture(internalOnboardCallback)) } returns 1L - every { - offboardRouter.getRoute( - routerOptions, - capture(internalOffboardCallback) - ) - } returns 2L - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager - every { networkStatusService.getNetworkStatusChannel() } returns channel - every { - offboardRouter.getRouteRefresh( - any(), - any(), - capture(internalOffboardRefreshCallback) - ) - } returns 3L - every { - onboardRouter.getRouteRefresh( - any(), - any(), - capture(internalOnboardRefreshCallback) - ) - } returns 4L - - hybridRouter = MapboxHybridRouter( - onboardRouter, - offboardRouter, - networkStatusService, - ThreadController(), - ) - } - - @Test - fun whenNetworkConnectedOffboardRouterUsed() = runBlocking { - enableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { offboardRouter.getRoute(routerOptions, any()) } - } - - @Test - fun `offboardRouterCanceled and request list is clear`() = runBlocking { - enableNetworkConnection() - - val id = hybridRouter.getRoute(routerOptions, routerCallback) - internalOffboardCallback.captured.onCanceled(routerOptions, RouterOrigin.Offboard) - - verify(exactly = 1) { routerCallback.onCanceled(routerOptions, RouterOrigin.Offboard) } - - hybridRouter.cancelRouteRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRequest(any()) } - } - - @Test - fun whenNoNetworkConnectionOnboardRouterUsed() = runBlocking { - disableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { onboardRouter.getRoute(routerOptions, any()) } - } - - @Test - fun `onboardRouterCanceled and request list is clear`() = runBlocking { - disableNetworkConnection() - - val id = hybridRouter.getRoute(routerOptions, routerCallback) - internalOnboardCallback.captured.onCanceled(routerOptions, RouterOrigin.Onboard) - - verify(exactly = 1) { routerCallback.onCanceled(routerOptions, RouterOrigin.Onboard) } - - hybridRouter.cancelRouteRequest(id) - verify(exactly = 0) { onboardRouter.cancelRouteRequest(any()) } - } - - @Test - fun `whenOffboardRouterFailsOnboardRouterIsCalled and request list is clear`() = runBlocking { - enableNetworkConnection() - - val id = hybridRouter.getRoute(routerOptions, routerCallback) - - internalOffboardCallback.captured.onFailure(listOf(mockk(relaxed = true)), routerOptions) - internalOnboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Onboard) - - verify(exactly = 1) { offboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { onboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { routerCallback.onRoutesReady(any(), RouterOrigin.Onboard) } - - hybridRouter.cancelRouteRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRequest(any()) } - verify(exactly = 0) { onboardRouter.cancelRouteRequest(any()) } - } - - @Test - fun `whenOnboardRouterFailsOffboardRouterIsCalled and request list is clear`() = runBlocking { - disableNetworkConnection() - - val id = hybridRouter.getRoute(routerOptions, routerCallback) - - internalOnboardCallback.captured.onFailure(listOf(mockk(relaxed = true)), routerOptions) - internalOffboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Offboard) - - verify(exactly = 1) { - onboardRouter.getRoute( - routerOptions, - internalOnboardCallback.captured - ) - } - verify(exactly = 1) { - offboardRouter.getRoute( - routerOptions, - internalOffboardCallback.captured - ) - } - verify(exactly = 1) { routerCallback.onRoutesReady(any(), RouterOrigin.Offboard) } - - hybridRouter.cancelRouteRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRequest(any()) } - verify(exactly = 0) { onboardRouter.cancelRouteRequest(any()) } - } - - @Test - fun whenOffboardRouterFailsOnboardRouterIsCalledAndOffboardUsedAgain() = runBlocking { - enableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - internalOffboardCallback.captured.onFailure(listOf(mockk(relaxed = true)), routerOptions) - internalOnboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Onboard) - - hybridRouter.getRoute(routerOptions, routerCallback) - - internalOffboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Offboard) - - verify(exactly = 2) { offboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { onboardRouter.getRoute(routerOptions, any()) } - verify(ordering = Ordering.SEQUENCE) { - routerCallback.onRoutesReady(any(), RouterOrigin.Onboard) - routerCallback.onRoutesReady(any(), RouterOrigin.Offboard) - } - verify(exactly = 2) { routerCallback.onRoutesReady(any(), any()) } - } - - @Test - fun whenOnboardRouterFailsOffboardRouterIsCalledAndOnboardUsedAgain() = runBlocking { - disableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - internalOnboardCallback.captured.onFailure(listOf(mockk(relaxed = true)), routerOptions) - internalOffboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Offboard) - - hybridRouter.getRoute(routerOptions, routerCallback) - - internalOnboardCallback.captured.onRoutesReady(emptyList(), RouterOrigin.Onboard) - - verify(exactly = 2) { onboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { offboardRouter.getRoute(routerOptions, any()) } - verify(ordering = Ordering.SEQUENCE) { - routerCallback.onRoutesReady(any(), RouterOrigin.Offboard) - routerCallback.onRoutesReady(any(), RouterOrigin.Onboard) - } - verify(exactly = 2) { routerCallback.onRoutesReady(any(), any()) } - } - - @Test - fun whenConnectionAppearedRoutersSwitched() = runBlocking { - disableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - enableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { onboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { offboardRouter.getRoute(routerOptions, any()) } - } - - @Test - fun whenConnectionDisappearedRoutersSwitched() = runBlocking { - enableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - disableNetworkConnection() - - hybridRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { offboardRouter.getRoute(routerOptions, any()) } - verify(exactly = 1) { onboardRouter.getRoute(routerOptions, any()) } - } - - @Test - fun `route request failure and request list is clear`() = runBlocking { - enableNetworkConnection() - - val id = hybridRouter.getRoute(routerOptions, routerCallback) - - val url1 = mockk() - val throwable1 = mockk() - val routerFailurePrimary = RouterFailure( - url = url1, - message = "message1", - routerOrigin = RouterOrigin.Offboard, - code = 1, - throwable = throwable1 - ) - - val url2 = mockk() - val throwable2 = mockk() - val routerFailureFallback = RouterFailure( - url = url2, - message = "message2", - routerOrigin = RouterOrigin.Onboard, - code = 2, - throwable = throwable2 - ) - - internalOffboardCallback.captured.onFailure(listOf(routerFailurePrimary), routerOptions) - internalOnboardCallback.captured.onFailure(listOf(routerFailureFallback), routerOptions) - - val expected = listOf( - RouterFailure( - url = url1, - routerOrigin = RouterOrigin.Offboard, - message = "Primary router " + - "(${offboardRouter::class.java.canonicalName}), " + - "origin ${RouterOrigin.Offboard}, " + - "failed with: message1", - code = 1, - throwable = throwable1 - ), - RouterFailure( - url = url2, - routerOrigin = RouterOrigin.Onboard, - message = "Fallback router " + - "(${onboardRouter::class.java.canonicalName}), " + - "origin ${RouterOrigin.Onboard}, " + - "failed with: message2", - code = 2, - throwable = throwable2 - ) - ) - verify(exactly = 1) { routerCallback.onFailure(expected, routerOptions) } - - hybridRouter.cancelRouteRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRequest(any()) } - verify(exactly = 0) { onboardRouter.cancelRouteRequest(any()) } - } - - @Test - fun `cancel a specific refresh request when multiple are running`() = runBlocking { - enableNetworkConnection() - - var offboardRequestIds = 0 - every { offboardRouter.getRoute(any(), any()) } answers { - (++offboardRequestIds).toLong() - } - val firstId = hybridRouter.getRoute(routerOptions, routerCallback) - val secondId = hybridRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 0) { offboardRouter.cancelRouteRequest(any()) } - verify(exactly = 0) { onboardRouter.cancelRouteRequest(any()) } - - disableNetworkConnection() - - var onboardRequestIds = 11 - every { onboardRouter.getRoute(any(), any()) } answers { - (++onboardRequestIds).toLong() - } - val thirdId = hybridRouter.getRoute(routerOptions, routerCallback) - val forthId = hybridRouter.getRoute(routerOptions, routerCallback) - - hybridRouter.cancelRouteRequest(firstId) - hybridRouter.cancelRouteRequest(thirdId) - hybridRouter.cancelRouteRequest(secondId) - hybridRouter.cancelRouteRequest(forthId) - - verifySequence { - offboardRouter.getRoute(routerOptions, any()) - offboardRouter.getRoute(routerOptions, any()) - onboardRouter.getRoute(routerOptions, any()) - onboardRouter.getRoute(routerOptions, any()) - - offboardRouter.cancelRouteRequest(1L) - onboardRouter.cancelRouteRequest(12L) - offboardRouter.cancelRouteRequest(2L) - onboardRouter.cancelRouteRequest(13L) - } - } - - @Test - fun `offboard route refresh is successful and request list is clear`() = runBlocking { - enableNetworkConnection() - val original = mockk() - val resulting = mockk() - val callback = mockk(relaxUnitFun = true) - - val id = hybridRouter.getRouteRefresh(original, 0, callback) - internalOffboardRefreshCallback.captured.onRefresh(resulting) - - verify(exactly = 1) { offboardRouter.getRouteRefresh(original, 0, any()) } - verify(exactly = 1) { callback.onRefresh(resulting) } - - hybridRouter.cancelRouteRefreshRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRefreshRequest(any()) } - } - - @Test - fun `route refresh fails and request list is clear`() = runBlocking { - enableNetworkConnection() - val original = mockk { - every { routeOptions() } returns mockk { - every { requestUuid() } returns "uuid" - } - } - val error = mockk { - every { message } returns "mock" - every { throwable } returns mockk(relaxed = true) - } - val callback = mockk(relaxUnitFun = true) - - val id = hybridRouter.getRouteRefresh(original, 0, callback) - internalOffboardRefreshCallback.captured.onError(error) - internalOnboardRefreshCallback.captured.onError(error) - - verify(exactly = 1) { offboardRouter.getRouteRefresh(original, 0, any()) } - verify(exactly = 1) { callback.onError(any()) } - - hybridRouter.cancelRouteRefreshRequest(id) - verify(exactly = 0) { offboardRouter.cancelRouteRefreshRequest(any()) } - verify(exactly = 0) { onboardRouter.cancelRouteRefreshRequest(any()) } - } - - @Test - fun `onboard route refresh is successful and request list is clear`() = runBlocking { - disableNetworkConnection() - val original = mockk() - val resulting = mockk() - val callback = mockk(relaxUnitFun = true) - - val id = hybridRouter.getRouteRefresh(original, 0, callback) - internalOnboardRefreshCallback.captured.onRefresh(resulting) - - verify(exactly = 1) { onboardRouter.getRouteRefresh(original, 0, any()) } - verify(exactly = 1) { callback.onRefresh(resulting) } - - hybridRouter.cancelRouteRefreshRequest(id) - verify(exactly = 0) { onboardRouter.cancelRouteRefreshRequest(any()) } - } - - @Test - fun `when network connection is re-established, offboard is used for refresh again`() = - runBlocking { - disableNetworkConnection() - enableNetworkConnection() - val original = mockk() - val resulting = mockk() - val callback = mockk(relaxUnitFun = true) - - hybridRouter.getRouteRefresh(original, 0, callback) - internalOffboardRefreshCallback.captured.onRefresh(resulting) - - verify(exactly = 1) { offboardRouter.getRouteRefresh(original, 0, any()) } - verify(exactly = 1) { callback.onRefresh(resulting) } - } - - @Test - fun `cancel all`() = runBlocking { - var requestIds = 100 - every { offboardRouter.getRoute(any(), any()) } answers { - (++requestIds).toLong() - } - every { onboardRouter.getRoute(any(), any()) } answers { - (++requestIds).toLong() - } - every { offboardRouter.getRouteRefresh(any(), any(), any()) } answers { - (++requestIds).toLong() - } - every { onboardRouter.getRouteRefresh(any(), any(), any()) } answers { - (++requestIds).toLong() - } - - enableNetworkConnection() - hybridRouter.getRoute(mockk(relaxed = true), mockk(relaxed = true)) - hybridRouter.getRoute(mockk(relaxed = true), mockk(relaxed = true)) - hybridRouter.getRouteRefresh(mockk(relaxed = true), 0, mockk(relaxed = true)) - - disableNetworkConnection() - hybridRouter.getRoute(mockk(relaxed = true), mockk(relaxed = true)) - hybridRouter.getRoute(mockk(relaxed = true), mockk(relaxed = true)) - hybridRouter.getRouteRefresh(mockk(relaxed = true), 0, mockk(relaxed = true)) - - hybridRouter.cancelAll() - - verify { offboardRouter.cancelAll() } - verify { onboardRouter.cancelAll() } - } - - @After - fun cleanUp() { - unmockkObject(LoggerProvider) - } - - private suspend fun enableNetworkConnection() = networkConnected(true) - - private suspend fun disableNetworkConnection() = networkConnected(false) - - private suspend fun networkConnected(networkConnected: Boolean) { - hybridRouter.onNetworkStatusChanged(networkConnected) - } - - private fun provideDefaultRouteOptions(): RouteOptions { - return RouteOptions.builder() - .applyDefaultNavigationOptions() - .apply { - coordinates(Point.fromLngLat(.0, .0), null, Point.fromLngLat(.0, .0)) - }.build() - } -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouterTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouterTest.kt deleted file mode 100644 index 0ced7c484c3..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/offboard/MapboxOffboardRouterTest.kt +++ /dev/null @@ -1,562 +0,0 @@ -package com.mapbox.navigation.route.internal.offboard - -import android.content.Context -import com.mapbox.api.directions.v5.MapboxDirections -import com.mapbox.api.directions.v5.models.DirectionsResponse -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.Metadata -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.api.directionsrefresh.v1.MapboxDirectionsRefresh -import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse -import com.mapbox.api.directionsrefresh.v1.models.DirectionsRouteRefresh -import com.mapbox.base.common.logger.Logger -import com.mapbox.base.common.logger.model.Message -import com.mapbox.navigation.base.internal.accounts.UrlSkuTokenProvider -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM -import com.mapbox.navigation.route.internal.util.redactQueryParam -import com.mapbox.navigation.route.offboard.RouteBuilderProvider -import com.mapbox.navigation.route.offboard.base.BaseTest -import com.mapbox.navigation.route.offboard.routerefresh.RouteRefreshCallbackMapper -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.slot -import io.mockk.unmockkObject -import io.mockk.verify -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.ResponseBody -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotNull -import org.junit.Before -import org.junit.Test -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response - -class MapboxOffboardRouterTest : BaseTest() { - - private val mapboxDirections = mockk(relaxed = true) - private val mapboxDirectionsBuilder = mockk(relaxed = true) - private val mapboxDirectionsRefresh = mockk(relaxed = true) - private val mapboxDirectionsRefreshBuilder = mockk() - private val context = mockk() - private val mockBaseUrl = "https://api.mapbox.test.com" - private val accessToken = "pk.1234" - private lateinit var offboardRouter: MapboxOffboardRouter - private lateinit var routeCallback: Callback - private lateinit var refreshCallback: Callback - private val routeOptions: RouteOptions = mockk(relaxed = true) - private val routerOrigin = RouterOrigin.Offboard - private val mockSkuTokenProvider = mockk(relaxed = true) - private val routeCall: Call = mockk() - private val refreshCall: Call = mockk() - private val url = HttpUrl.Builder() - .scheme("https") - .host("api.mapbox.com") - .addPathSegment("test") - .addQueryParameter(ACCESS_TOKEN_QUERY_PARAM, "pk.123") - .build() - private val metadataInfo = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") - private val logger = mockk(relaxUnitFun = true) - - @Before - fun setUp() { - mockkObject(RouteBuilderProvider) - every { - mockSkuTokenProvider.obtainUrlWithSkuToken(any()) - } returns (mockk()) - every { - RouteBuilderProvider.getBuilder(mockSkuTokenProvider) - } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.interceptor(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.routeOptions(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.eventListener(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.accessToken(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.build() } returns mapboxDirections - val routeListener = slot>() - every { mapboxDirections.enqueueCall(capture(routeListener)) } answers { - routeCallback = routeListener.captured - } - every { routeOptions.coordinatesList().size } returns 2 - every { routeCall.isCanceled } returns false - every { routeCall.request().url } returns url - - // refresh - mockkObject(RouteRefreshCallbackMapper) - every { RouteBuilderProvider.getRefreshBuilder() } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.baseUrl(any()) - } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.accessToken(accessToken) - } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.interceptor(any()) - } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.requestId(any()) - } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.legIndex(any()) - } returns mapboxDirectionsRefreshBuilder - every { - mapboxDirectionsRefreshBuilder.routeIndex(any()) - } returns mapboxDirectionsRefreshBuilder - every { mapboxDirectionsRefreshBuilder.build() } returns mapboxDirectionsRefresh - val refreshListener = slot>() - every { mapboxDirectionsRefresh.enqueueCall(capture(refreshListener)) } answers { - refreshCallback = refreshListener.captured - } - - offboardRouter = - MapboxOffboardRouter(accessToken, context, mockSkuTokenProvider, logger) - - every { (refreshCall.request() as Request).url } returns "https://test.com".toHttpUrl() - } - - @After - fun freeRecourse() { - unmockkObject(RouteBuilderProvider) - unmockkObject(RouteRefreshCallbackMapper) - } - - @Test - fun generationSanityTest() { - assertNotNull(offboardRouter) - } - - @Test - fun getRoute_NavigationRouteGetRouteCalled() { - offboardRouter.getRoute(routeOptions, mockk()) - - verify { mapboxDirections.enqueueCall(routeCallback) } - } - - @Test - fun cancelAll_NavigationRouteCancelCallCalled() { - offboardRouter.getRoute(routeOptions, mockk()) - - offboardRouter.cancelAll() - - verify { mapboxDirections.cancelCall() } - } - - @Test - fun `cancel a specific route request when multiple are running`() { - val newMapboxDirections = mockk(relaxed = true) - var firstCall = true - every { mapboxDirectionsBuilder.build() } answers { - if (firstCall) { - firstCall = false - mapboxDirections - } else { - newMapboxDirections - } - } - val firstId = offboardRouter.getRoute(routeOptions, mockk()) - val secondId = offboardRouter.getRoute(routeOptions, mockk()) - verify(exactly = 0) { mapboxDirections.cancelCall() } - verify(exactly = 0) { newMapboxDirections.cancelCall() } - - offboardRouter.cancelRouteRequest(firstId) - verify(exactly = 1) { mapboxDirections.cancelCall() } - verify(exactly = 0) { newMapboxDirections.cancelCall() } - - offboardRouter.cancelRouteRequest(secondId) - verify(exactly = 1) { mapboxDirections.cancelCall() } - verify(exactly = 1) { newMapboxDirections.cancelCall() } - } - - @Test - fun cancel_NavigationRouteCancelCallNotCalled() { - offboardRouter.cancelAll() - - verify(exactly = 0) { mapboxDirections.cancelCall() } - } - - @Test - fun onSuccessfulResponseAndHasRoutes_onRouteReadyCalled() { - val routerCallback = mockk(relaxed = true) - val route = buildMultipleLegRoute() - val response = buildRouteResponse(listOf(route), true) - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onResponse(routeCall, response) - - verify { routerCallback.onRoutesReady(listOf(route), routerOrigin) } - } - - @Test - fun `on successful response metadata is logged`() { - val routerCallback = mockk(relaxed = true) - val response = buildRouteResponse(listOf(), true) - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onResponse(routeCall, response) - - val message = Message("Successful directions response. Metadata: $metadataInfo") - verify { logger.i(MapboxOffboardRouter.TAG, message) } - } - - @Test - fun onUnsuccessfulResponseAndHasRoutes_errorIsProvided() { - val routerCallback = mockk(relaxed = true) - val route = buildMultipleLegRoute() - val response = buildRouteResponse(listOf(route), false) - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onResponse(routeCall, response) - - verify { routerCallback.onFailure(any(), routeOptions) } - } - - @Test - fun onSuccessfulResponseAndNoRoutes_errorIsProvided() { - val routerCallback = mockk(relaxed = true) - val response = buildRouteResponse(listOf(), true) - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onResponse(routeCall, response) - - verify { routerCallback.onFailure(any(), routeOptions) } - } - - @Test - fun onUnsuccessfulResponseAndNoRoutes_errorIsProvided() { - val routerCallback = mockk(relaxed = true) - val response = buildRouteResponse(listOf(), false) - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onResponse(routeCall, response) - - verify { routerCallback.onFailure(any(), routeOptions) } - } - - @Test - fun onFailure_errorIsProvided() { - val routerCallback = mockk(relaxed = true) - val throwable = mockk { - every { message } returns "msg" - } - every { routeCall.request().url } returns url - offboardRouter.getRoute(routeOptions, routerCallback) - - routeCallback.onFailure(routeCall, throwable) - - val expected = listOf( - RouterFailure( - url = url.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM).toUrl(), - routerOrigin = routerOrigin, - message = "msg", - code = null, - throwable = throwable - ) - ) - verify { routerCallback.onFailure(expected, routeOptions) } - } - - @Test - fun onFailure_canceled_onCanceledIsCalled() { - val routerCallback = mockk(relaxed = true) - val throwable = mockk() - offboardRouter.getRoute(routeOptions, routerCallback) - - val call: Call = mockk() - every { call.isCanceled } returns true - - routeCallback.onFailure(call, throwable) - - verify { routerCallback.onCanceled(routeOptions, routerOrigin) } - } - - @Test - fun onSuccess_canceled_onCanceledIsCalled() { - val routerCallback = mockk(relaxed = true) - val route = buildMultipleLegRoute() - val response = buildRouteResponse(listOf(route), true) - offboardRouter.getRoute(routeOptions, routerCallback) - - every { routeCall.isCanceled } returns true - - routeCallback.onResponse(routeCall, response) - - verify { routerCallback.onCanceled(routeOptions, routerOrigin) } - } - - @Test - fun `route request list cleared on response`() { - offboardRouter.getRoute(routeOptions, mockk(relaxUnitFun = true)) - val route = buildMultipleLegRoute() - val response = buildRouteResponse(listOf(route), true) - - routeCallback.onResponse(routeCall, response) - offboardRouter.cancelAll() - - verify(exactly = 0) { mapboxDirections.cancelCall() } - } - - @Test - fun `route request list cleared on failure`() { - offboardRouter.getRoute(routeOptions, mockk(relaxUnitFun = true)) - val throwable = RuntimeException("msg") - - routeCallback.onFailure(routeCall, throwable) - offboardRouter.cancelAll() - - verify(exactly = 0) { mapboxDirections.cancelCall() } - } - - @Test - fun `route refresh set right params`() { - val route = mockRouteForRefresh("uuid_123", "1") - offboardRouter.getRouteRefresh(route, 1, mockk(relaxUnitFun = true)) - - verify(exactly = 1) { - mapboxDirectionsRefreshBuilder.baseUrl(mockBaseUrl) - mapboxDirectionsRefreshBuilder.accessToken(accessToken) - mapboxDirectionsRefreshBuilder.requestId("uuid_123") - mapboxDirectionsRefreshBuilder.routeIndex(1) - mapboxDirectionsRefreshBuilder.legIndex(1) - } - } - - @Test - fun `route refresh set non-valid route index`() { - val route = mockRouteForRefresh("uuid_321", "") - offboardRouter.getRouteRefresh(route, 1, mockk(relaxUnitFun = true)) - - verify(exactly = 1) { - mapboxDirectionsRefreshBuilder.baseUrl(mockBaseUrl) - mapboxDirectionsRefreshBuilder.accessToken(accessToken) - mapboxDirectionsRefreshBuilder.requestId("uuid_321") - mapboxDirectionsRefreshBuilder.routeIndex(0) - mapboxDirectionsRefreshBuilder.legIndex(1) - } - } - - @Test - fun `route refresh successful`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val annotationRoute = mockAnnotationsForRefresh() - val resultingRoute = mockk() - val callback = mockk(relaxUnitFun = true) - val response = buildRefreshResponse(mockk(), true) - every { response.body() } returns mockk { - every { route() } returns annotationRoute - } - every { - RouteRefreshCallbackMapper.mapToDirectionsRoute( - originalRoute, - annotationRoute - ) - } returns resultingRoute - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - - refreshCallback.onResponse(refreshCall, response) - - verify(exactly = 1) { callback.onRefresh(resultingRoute) } - } - - @Test - fun `route refresh failure - failed to parse`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val annotationRoute = mockAnnotationsForRefresh() - val callback = mockk(relaxUnitFun = true) - val response = buildRefreshResponse(mockk(), true) - val ex = mockk() - every { response.body() } returns mockk { - every { route() } returns annotationRoute - } - every { - RouteRefreshCallbackMapper.mapToDirectionsRoute( - originalRoute, - annotationRoute - ) - }.throws(ex) - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - - refreshCallback.onResponse(refreshCall, response) - - verify(exactly = 1) { - callback.onError( - RouteRefreshError("Failed to read refresh response", ex) - ) - } - } - - @Test - fun `route refresh failure - failed response`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val callback = mockk(relaxUnitFun = true) - val slot = slot() - val ex = mockk() - every { callback.onError(capture(slot)) } returns Unit - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - refreshCallback.onFailure(refreshCall, ex) - - assertEquals(slot.captured.throwable, ex) - } - - @Test - fun `route refresh failure - unsuccessful response`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val annotationRoute = mockAnnotationsForRefresh() - val resultingRoute = mockk() - val callback = mockk(relaxUnitFun = true) - val response = buildRefreshResponse(mockk(), false) - every { response.body() } returns mockk { - every { route() } returns annotationRoute - } - every { - RouteRefreshCallbackMapper.mapToDirectionsRoute( - originalRoute, - annotationRoute - ) - } returns resultingRoute - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - - refreshCallback.onResponse(refreshCall, response) - - verify(exactly = 1) { callback.onError(any()) } - } - - @Test - fun `cancel all refresh calls`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - offboardRouter.getRouteRefresh(originalRoute, 0, mockk()) - - offboardRouter.cancelAll() - - verify { mapboxDirectionsRefresh.cancelCall() } - } - - @Test - fun `cancel a specific refresh request when multiple are running`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val newMapboxDirectionsRefresh = mockk(relaxed = true) - var firstCall = true - every { mapboxDirectionsRefreshBuilder.build() } answers { - if (firstCall) { - firstCall = false - mapboxDirectionsRefresh - } else { - newMapboxDirectionsRefresh - } - } - val firstId = offboardRouter.getRouteRefresh(originalRoute, 0, mockk()) - val secondId = offboardRouter.getRouteRefresh(originalRoute, 0, mockk()) - verify(exactly = 0) { mapboxDirectionsRefresh.cancelCall() } - verify(exactly = 0) { newMapboxDirectionsRefresh.cancelCall() } - - offboardRouter.cancelRouteRefreshRequest(firstId) - verify(exactly = 1) { mapboxDirectionsRefresh.cancelCall() } - verify(exactly = 0) { newMapboxDirectionsRefresh.cancelCall() } - - offboardRouter.cancelRouteRefreshRequest(secondId) - verify(exactly = 1) { mapboxDirectionsRefresh.cancelCall() } - verify(exactly = 1) { newMapboxDirectionsRefresh.cancelCall() } - } - - @Test - fun `refresh request list cleared on response`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val annotationRoute = mockAnnotationsForRefresh() - val resultingRoute = mockk() - val callback = mockk(relaxUnitFun = true) - val response = buildRefreshResponse(mockk(), true) - every { response.body() } returns mockk { - every { route() } returns annotationRoute - } - every { - RouteRefreshCallbackMapper.mapToDirectionsRoute( - originalRoute, - annotationRoute - ) - } returns resultingRoute - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - - refreshCallback.onResponse(refreshCall, response) - - offboardRouter.cancelAll() - - verify(exactly = 0) { mapboxDirectionsRefresh.cancelCall() } - } - - @Test - fun `refresh request list cleared on failure`() { - val originalRoute = mockRouteForRefresh("uuid_123", "1") - val callback = mockk(relaxUnitFun = true) - val ex = mockk() - - offboardRouter.getRouteRefresh(originalRoute, 0, callback) - - refreshCallback.onFailure(refreshCall, ex) - - offboardRouter.cancelAll() - - verify(exactly = 0) { mapboxDirectionsRefresh.cancelCall() } - } - - private fun buildRouteResponse( - routeList: List, - isSuccessful: Boolean, - code: Int = 200, - errorBody: ResponseBody? = null - ): Response { - val response = mockk>() - val directionsResponse = mockk() - val metadata = mockk() - every { metadata.infoMap() } returns metadataInfo - every { directionsResponse.routes() } returns routeList - every { directionsResponse.metadata() } returns metadata - every { response.body() } returns directionsResponse - every { response.isSuccessful } returns isSuccessful - every { response.message() } returns "mock" - every { response.code() } returns code - every { response.errorBody() } returns errorBody - - return response - } - - private fun buildRefreshResponse( - route: DirectionsRouteRefresh, - isSuccessful: Boolean - ): Response { - val response = mockk>() - val refreshResponse = mockk() - every { refreshResponse.route() } returns route - every { response.body() } returns refreshResponse - every { response.isSuccessful } returns isSuccessful - every { response.message() } returns "mock" - every { response.code() } returns 123 - every { response.errorBody() } returns mockk() - - return response - } - - private fun mockRouteForRefresh( - mockRequestUuid: String, - mockRouteIndex: String - ): DirectionsRoute = mockk { - every { routeOptions() } returns mockk { - every { baseUrl() } returns mockBaseUrl - every { requestUuid() } returns mockRequestUuid - } - every { routeIndex() } returns mockRouteIndex - } - - private fun mockAnnotationsForRefresh(): DirectionsRouteRefresh = mockk() -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouterTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouterTest.kt deleted file mode 100644 index 8f701555fb6..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/onboard/MapboxOnboardRouterTest.kt +++ /dev/null @@ -1,683 +0,0 @@ -package com.mapbox.navigation.route.internal.onboard - -import android.content.Context -import com.mapbox.api.directions.v5.MapboxDirections -import com.mapbox.api.directions.v5.models.DirectionsResponse -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.base.common.logger.Logger -import com.mapbox.base.common.logger.model.Message -import com.mapbox.bindgen.Expected -import com.mapbox.geojson.Point -import com.mapbox.navigation.base.extensions.applyDefaultNavigationOptions -import com.mapbox.navigation.base.extensions.coordinates -import com.mapbox.navigation.base.route.RouteRefreshCallback -import com.mapbox.navigation.base.route.RouteRefreshError -import com.mapbox.navigation.base.route.RouterCallback -import com.mapbox.navigation.base.route.RouterFailure -import com.mapbox.navigation.base.route.RouterOrigin -import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator -import com.mapbox.navigation.route.internal.util.ACCESS_TOKEN_QUERY_PARAM -import com.mapbox.navigation.route.internal.util.httpUrl -import com.mapbox.navigation.route.internal.util.redactQueryParam -import com.mapbox.navigation.route.offboard.RouteBuilderProvider -import com.mapbox.navigation.testing.MainCoroutineRule -import com.mapbox.navigation.utils.internal.RequestMap -import com.mapbox.navigation.utils.internal.ThreadController -import com.mapbox.navigator.RouterError -import com.mapbox.navigator.RouterErrorType -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkConstructor -import io.mockk.mockkObject -import io.mockk.slot -import io.mockk.unmockkConstructor -import io.mockk.unmockkObject -import io.mockk.verify -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Assert.fail -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -@InternalCoroutinesApi -@ExperimentalCoroutinesApi -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -class MapboxOnboardRouterTest { - - @get:Rule - var coroutineRule = MainCoroutineRule() - - private val navigator: MapboxNativeNavigator = mockk(relaxUnitFun = true) - private val routerCallback: RouterCallback = mockk(relaxUnitFun = true) - private val routerResultSuccess: Expected = mockk { - every { isValue } returns true - every { isError } returns false - every { value } returns SUCCESS_RESPONSE - every { error } returns null - } - private val routerResultFailure: Expected = mockk { - every { isValue } returns false - every { isError } returns true - every { value } returns null - every { error } returns - RouterError(FAILURE_MESSAGE, FAILURE_CODE, RouterErrorType.UNKNOWN, REQUEST_ID) - } - private val routerOptions: RouteOptions = provideDefaultRouteOptions() - private val context = mockk() - private val logger = mockk(relaxUnitFun = true) - private val mapboxDirections = mockk(relaxed = true) - private val mapboxDirectionsBuilder = mockk(relaxed = true) - private val routerOrigin = RouterOrigin.Onboard - private val accessToken = "pk.123" - private val threadController = ThreadController() - - private var onboardRouter = - MapboxOnboardRouter(accessToken, navigator, context, logger, threadController) - - private val url = - MapboxDirections.builder() - .accessToken(accessToken) - .routeOptions(routerOptions) - .build() - .httpUrl() - .toUrl() - - @Before - fun setUp() { - mockkObject(ThreadController) - every { ThreadController.IODispatcher } returns coroutineRule.testDispatcher - - mockkObject(RouteBuilderProvider) - every { - RouteBuilderProvider.getBuilder(null) - } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.interceptor(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.routeOptions(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.accessToken(any()) } returns mapboxDirectionsBuilder - every { mapboxDirectionsBuilder.build() } returns mapboxDirections - every { mapboxDirections.httpUrl() } returns url.toHttpUrlOrNull()!! - } - - @After - fun cleanUp() { - unmockkObject(ThreadController) - unmockkObject(RouteBuilderProvider) - } - - @Test - fun generationSanityTest() { - assertNotNull(onboardRouter) - } - - @Test - fun checkCallbackCalledOnFailureWithRedactedToken() = coroutineRule.runBlockingTest { - coEvery { navigator.getRoute(any()) } returns routerResultFailure - - onboardRouter.getRoute(routerOptions, routerCallback) - - val expected = listOf( - RouterFailure( - url = url.toHttpUrlOrNull()!!.redactQueryParam(ACCESS_TOKEN_QUERY_PARAM).toUrl(), - routerOrigin = routerOrigin, - message = FAILURE_MESSAGE, - code = FAILURE_CODE, - throwable = null - ) - ) - - coVerify { navigator.getRoute(url.toString()) } - verify { routerCallback.onFailure(expected, routerOptions) } - } - - @Test - fun checkCallbackCalledOnSuccess_andContainsOriginalOptions() = coroutineRule.runBlockingTest { - coEvery { navigator.getRoute(any()) } returns routerResultSuccess - - onboardRouter.getRoute(routerOptions, routerCallback) - - coVerify { navigator.getRoute(url.toString()) } - - val fromJson = DirectionsResponse.fromJson(SUCCESS_RESPONSE) - val expected = fromJson.routes().map { - it.toBuilder().routeOptions(routerOptions).build() - } - val message = Message( - "Successful directions response. Metadata: {key1=value1, key2=value2, key3=value3}", - ) - verify(exactly = 1) { - logger.i(MapboxOnboardRouter.loggerTag, message) - routerCallback.onRoutesReady(expected, routerOrigin) - } - } - - @Test - fun checkCallbackCalledOnCancel() { - coEvery { navigator.getRoute(any()) } coAnswers { - awaitCancellation() - } - - val latch = CountDownLatch(1) - val callback: RouterCallback = object : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - fail() - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - fail() - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - latch.countDown() - } - } - onboardRouter.getRoute(routerOptions, callback) - onboardRouter.cancelAll() - - if (!latch.await(5, TimeUnit.SECONDS)) { - fail("onCanceled not called") - } - } - - @Test - fun checkCallbackCalledOnCancel2() = coroutineRule.runBlockingTest { - coEvery { navigator.getRoute(any()) } coAnswers { throw CancellationException() } - - onboardRouter.getRoute(routerOptions, routerCallback) - - verify { routerCallback.onCanceled(routerOptions, routerOrigin) } - } - - @Test - fun checkCallbackCalledOnCancel3() = runBlocking { - // cancellable code should run on a separate dispatcher to allow call `cancel` after launch. - // if we use the same dispatcher, it will be blocked until coroutine finish. `cancel` will have no affect - // job's state will be `isActive == false`, `isCancelled == false`. - every { ThreadController.IODispatcher } returns Dispatchers.Default - - coEvery { navigator.getRoute(any()) } coAnswers { - // delay on a separate dispatcher, it doesn't affect the main thread. - // we just suspend the function until it's cancelled from the main thread. - // if we don't suspend it, the function will return immediately, before `cancel` is called. - // job's state will be `isActive == false`, `isCancelled == false`. - delay(1000) - routerResultSuccess - } - - val scope = coroutineRule.coroutineScope - val job = scope.launch { onboardRouter.getRoute("") } - scope.cancel() - - assertFalse(job.isActive) - assertTrue(job.isCancelled) - } - - @Test - fun `cancel a request when multiple are running`() { - coEvery { navigator.getRoute(any()) } coAnswers { - awaitCancellation() - } - - val firstLatch = CountDownLatch(1) - val firstCallback: RouterCallback = object : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - fail() - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - fail() - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - firstLatch.countDown() - } - } - - val secondLatch = CountDownLatch(1) - val secondCallback: RouterCallback = object : RouterCallback { - override fun onRoutesReady(routes: List, routerOrigin: RouterOrigin) { - fail() - } - - override fun onFailure(reasons: List, routeOptions: RouteOptions) { - fail() - } - - override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) { - secondLatch.countDown() - } - } - - val firstId = onboardRouter.getRoute(routerOptions, firstCallback) - onboardRouter.cancelRouteRequest(firstId) - - if (!firstLatch.await(5, TimeUnit.SECONDS)) { - fail("onCanceled not called on first request") - } - - assertTrue(secondLatch.count > 0) - - val secondId = onboardRouter.getRoute(routerOptions, secondCallback) - onboardRouter.cancelRouteRequest(secondId) - - if (!secondLatch.await(5, TimeUnit.SECONDS)) { - fail("onCanceled not called on second request") - } - } - - @Test - fun `request list cleared on success`() = coroutineRule.runBlockingTest { - mockkConstructor(RequestMap::class) - val idSlot = slot() - every { anyConstructed>().put(capture(idSlot), any()) } just Runs - onboardRouter = MapboxOnboardRouter( - accessToken, - navigator, - context, - threadController = threadController, - ) - coEvery { navigator.getRoute(any()) } returns routerResultSuccess - - onboardRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { anyConstructed>().remove(idSlot.captured) } - unmockkConstructor(RequestMap::class) - } - - @Test - fun `request list cleared on failure`() = coroutineRule.runBlockingTest { - mockkConstructor(RequestMap::class) - val idSlot = slot() - every { anyConstructed>().put(capture(idSlot), any()) } just Runs - onboardRouter = MapboxOnboardRouter( - accessToken, - navigator, - context, - threadController = threadController, - ) - coEvery { navigator.getRoute(any()) } returns routerResultFailure - - onboardRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { anyConstructed>().remove(idSlot.captured) } - unmockkConstructor(RequestMap::class) - } - - @Test - fun `request list cleared on cancel`() = coroutineRule.runBlockingTest { - mockkConstructor(RequestMap::class) - val idSlot = slot() - every { anyConstructed>().put(capture(idSlot), any()) } just Runs - onboardRouter = MapboxOnboardRouter( - accessToken, - navigator, - context, - threadController = threadController, - ) - coEvery { navigator.getRoute(any()) } coAnswers { throw CancellationException() } - - onboardRouter.getRoute(routerOptions, routerCallback) - - verify(exactly = 1) { anyConstructed>().remove(idSlot.captured) } - unmockkConstructor(RequestMap::class) - } - - @Test - fun checkModelOnSuccess() = coroutineRule.runBlockingTest { - val routesSlot = slot>() - coEvery { navigator.getRoute(any()) } returns routerResultSuccess - - onboardRouter.getRoute(routerOptions, routerCallback) - - verify { routerCallback.onRoutesReady(capture(routesSlot), routerOrigin) } - - val delta = 0.000001 - // route - assertEquals(ROUTE_SIZE, routesSlot.captured.size) - val route = routesSlot.captured[0] - assertEquals(ROUTE_DISTANCE, route.distance(), delta) - assertEquals(ROUTE_DURATION, route.duration(), delta) - assertEquals(ROUTE_WEIGHT_NAME, route.weightName()) - assertEquals(ROUTE_VOICE_LANGUAGE, route.voiceLanguage()) - assertEquals(ROUTE_WEIGHT, route.weight()) - - // leg - assertEquals(LEG_SIZE, route.legs()!!.size) - val leg = route.legs()!![0] - assertEquals(LEG_SUMMARY, leg.summary()) - assertEquals(LEG_DURATION, leg.duration()) - assertEquals(LEG_DISTANCE, leg.distance()) - - // step - assertEquals(STEP_SIZE, leg.steps()!!.size) - val step = leg.steps()!![0] - assertEquals(STEP_DRIVING_SIDE, step.drivingSide()) - assertEquals(STEP_GEOMETRY, step.geometry()) - assertEquals(STEP_MODE, step.mode()) - assertEquals(STEP_WEIGHT, step.weight(), delta) - assertEquals(STEP_DURATION, step.duration(), delta) - assertEquals(STEP_NAME, step.name()) - assertEquals(STEP_DISTANCE, step.distance(), delta) - - // intersection - assertEquals(INTERSECTION_SIZE, step.intersections()!!.size) - val intersection = step.intersections()!![0] - assertEquals(INTERSECTION_OUT, intersection.out()) - assertEquals(INTERSECTION_ENTRY, intersection.entry()!![0]) - assertEquals(INTERSECTION_BEARINGS, intersection.bearings()!![0]) - assertEquals(INTERSECTION_LONGITUDE, intersection.location().longitude(), delta) - assertEquals(INTERSECTION_LATITUDE, intersection.location().latitude(), delta) - - // maneuver - val maneuver = step.maneuver() - assertEquals(MANEUVER_BEARING_AFTER, maneuver.bearingAfter()) - assertEquals(MANEUVER_BEARING_BEFORE, maneuver.bearingBefore()) - assertEquals(MANEUVER_MODIFIER, maneuver.modifier()) - assertEquals(MANEUVER_TYPE, maneuver.type()) - assertEquals(MANEUVER_INSTRUCTION, maneuver.instruction()) - assertEquals(MANEUVER_LONGITUDE, maneuver.location().longitude(), delta) - assertEquals(MANEUVER_LATITUDE, maneuver.location().latitude(), delta) - - // voiceInstructions - assertEquals(VOICE_INSTRUCTIONS_SIZE, step.voiceInstructions()!!.size) - val voiceInstruction = step.voiceInstructions()!![0] - assertEquals(VOICE_INSTRUCTIONS_DISTANCE, voiceInstruction.distanceAlongGeometry()) - assertEquals(VOICE_INSTRUCTIONS_ANNOUNCEMENT, voiceInstruction.announcement()) - assertEquals(VOICE_INSTRUCTIONS_SSML_ANNOUNCEMENT, voiceInstruction.ssmlAnnouncement()) - - // bannerInstructions - assertEquals(BANNER_INSTRUCTIONS_SIZE, step.bannerInstructions()!!.size) - val bannerInstruction = step.bannerInstructions()!![0] - assertEquals(BANNER_INSTRUCTIONS_DISTANCE, bannerInstruction.distanceAlongGeometry(), delta) - assertEquals(BANNER_INSTRUCTIONS_SECONDARY, bannerInstruction.secondary()) - assertEquals(BANNER_INSTRUCTIONS_TYPE, bannerInstruction.primary().type()) - assertEquals(BANNER_INSTRUCTIONS_MODIFIER, bannerInstruction.primary().modifier()) - assertEquals(BANNER_INSTRUCTIONS_TEXT, bannerInstruction.primary().text()) - - // components - assertEquals(COMPONENT_SIZE, bannerInstruction.primary().components()!!.size) - val component = bannerInstruction.primary().components()!![0] - assertEquals(COMPONENT_ABBREVIATION, component.abbreviation()) - assertEquals(COMPONENT_ABBREVIATION_PRIORITY, component.abbreviationPriority()) - assertEquals(COMPONENT_TEXT, component.text()) - assertEquals(COMPONENT_TYPE, component.type()) - } - - @Test - fun `route refresh is disabled`() { - val callback = mockk(relaxUnitFun = true) - onboardRouter.getRouteRefresh(mockk(), 0, callback) - - verify { - callback.onError( - RouteRefreshError(message = "Route refresh is not available when offline.") - ) - } - } - - private fun provideDefaultRouteOptions(): RouteOptions { - return RouteOptions.builder() - .applyDefaultNavigationOptions() - .apply { - coordinates(origin, waypoints, destination) - }.build() - } - - companion object { - private const val ACCESS_TOKEN = "pk.1234" - - private val origin = Point.fromLngLat(.0, .0) - private val waypoints = listOf(Point.fromLngLat(.42, .11)) - private val destination = Point.fromLngLat(1.83, 1232.01) - - private const val TILE_PATH = "tiles" - private const val TAG = "MbxOnboardRouter" - - private const val ROUTE_SIZE = 1 - private const val ROUTE_DISTANCE = 50369.7 - private const val ROUTE_DURATION = 2248.5 - private const val ROUTE_WEIGHT_NAME = "routability" - private const val ROUTE_VOICE_LANGUAGE = "en-US" - private const val ROUTE_WEIGHT = 2383.1 - - private const val LEG_SIZE = 1 - private const val LEG_SUMMARY = "PA 283 West, Fishburn Road" - private const val LEG_DURATION = 2248.5 - private const val LEG_DISTANCE = 50369.7 - - private const val STEP_SIZE = 1 - private const val STEP_DRIVING_SIDE = "right" - private const val STEP_GEOMETRY = "s`_kkA`y|opCcAsReEcy@Eq@]oDa@gEEu@uKgwB" - private const val STEP_MODE = "driving" - private const val STEP_WEIGHT = 63.8 - private const val STEP_DURATION = 50.8 - private const val STEP_NAME = "East Fulton Street" - private const val STEP_DISTANCE = 293.2 - - private const val INTERSECTION_SIZE = 3 - private const val INTERSECTION_OUT = 0 - private const val INTERSECTION_ENTRY = true - private const val INTERSECTION_BEARINGS = 82 - private const val INTERSECTION_LONGITUDE = -76.299169 - private const val INTERSECTION_LATITUDE = 40.042522 - - private const val MANEUVER_BEARING_AFTER = 82.0 - private const val MANEUVER_BEARING_BEFORE = 0.0 - private const val MANEUVER_MODIFIER = "left" - private const val MANEUVER_TYPE = "depart" - private const val MANEUVER_INSTRUCTION = "Head east on East Fulton Street" - private const val MANEUVER_LONGITUDE = -76.299169 - private const val MANEUVER_LATITUDE = 40.042522 - - private const val VOICE_INSTRUCTIONS_SIZE = 2 - private const val VOICE_INSTRUCTIONS_DISTANCE = 293.2 - private const val VOICE_INSTRUCTIONS_ANNOUNCEMENT = - "Head east on East Fulton Street, then turn right onto North Ann Street" - private const val VOICE_INSTRUCTIONS_SSML_ANNOUNCEMENT = - "Head east on East Fulton" + - " Street, then turn right onto North Ann Street" - - private const val BANNER_INSTRUCTIONS_SIZE = 1 - private const val BANNER_INSTRUCTIONS_DISTANCE = 293.2 - private val BANNER_INSTRUCTIONS_SECONDARY = null - private const val BANNER_INSTRUCTIONS_TYPE = "turn" - private const val BANNER_INSTRUCTIONS_MODIFIER = "right" - private const val BANNER_INSTRUCTIONS_TEXT = "North Ann Street" - - private const val COMPONENT_SIZE = 2 - private const val COMPONENT_ABBREVIATION = "N" - private const val COMPONENT_ABBREVIATION_PRIORITY = 1 - private const val COMPONENT_TEXT = "North" - private const val COMPONENT_TYPE = "text" - - private const val ERROR_MESSAGE = - "Error occurred fetching offline route: No suitable edges near location - Code: 171" - private const val FAILURE_MESSAGE = "No suitable edges near location" - private const val FAILURE_CODE = 171 - private const val REQUEST_ID = 19L - private const val SUCCESS_RESPONSE = """ - { - "routes": [ - { - "geometry": "", - "legs": [ - { - "summary": "PA 283 West, Fishburn Road", - "weight": 2383.1, - "duration": 2248.5, - "steps": [ - { - "intersections": [ - { - "out": 0, - "entry": [ - true - ], - "bearings": [ - 82 - ], - "location": [ - -76.299169, - 40.042522 - ] - }, - { - "out": 0, - "in": 1, - "entry": [ - true, - false, - true - ], - "bearings": [ - 75, - 255, - 345 - ], - "location": [ - -76.298855, - 40.042556 - ] - }, - { - "out": 0, - "in": 2, - "entry": [ - true, - true, - false, - true - ], - "bearings": [ - 75, - 165, - 255, - 345 - ], - "location": [ - -76.297812, - 40.042673 - ] - } - ], - "driving_side": "right", - "geometry": "s`_kkA`y|opCcAsReEcy@Eq@]oDa@gEEu@uKgwB", - "mode": "driving", - "maneuver": { - "bearing_after": 82, - "bearing_before": 0, - "location": [ - -76.299169, - 40.042522 - ], - "modifier": "left", - "type": "depart", - "instruction": "Head east on East Fulton Street" - }, - "weight": 63.8, - "duration": 50.8, - "name": "East Fulton Street", - "distance": 293.2, - "voiceInstructions": [ - { - "distanceAlongGeometry": 293.2, - "announcement": "Head east on East Fulton Street, then turn right onto North Ann Street", - "ssmlAnnouncement": "Head east on East Fulton Street, then turn right onto North Ann Street" - }, - { - "distanceAlongGeometry": 86.6, - "announcement": "Turn right onto North Ann Street, then turn left onto East Chestnut Street (PA 23 East)", - "ssmlAnnouncement": "Turn right onto North Ann Street, then turn left onto East Chestnut Street (PA 23 East)" - } - ], - "bannerInstructions": [ - { - "distanceAlongGeometry": 293.2, - "primary": { - "type": "turn", - "modifier": "right", - "components": [ - { - "text": "North", - "type": "text", - "abbr": "N", - "abbr_priority": 1 - }, - { - "text": "Ann Street", - "type": "text", - "abbr": "Ann St", - "abbr_priority": 0 - } - ], - "text": "North Ann Street" - }, - "secondary": null - } - ] - } - ], - "distance": 50369.7 - } - ], - "weight_name": "routability", - "weight": 2383.1, - "duration": 2248.5, - "distance": 50369.7, - "voiceLocale": "en-US" - } - ], - "waypoints": [ - { - "name": "East Fulton Street", - "location": [ - -76.299169, - 40.042522 - ] - }, - { - "name": "West Chocolate Avenue", - "location": [ - -76.654634, - 40.283943 - ] - } - ], - "code": "Ok", - "uuid": "cjeacbr8s21bk47lggcvce7lv", - "metadata": { - "map": { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } - } - } - """ - } -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/util/TestRouteFixtures.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/util/TestRouteFixtures.kt new file mode 100644 index 00000000000..7581152d279 --- /dev/null +++ b/libnavigation-router/src/test/java/com/mapbox/navigation/route/internal/util/TestRouteFixtures.kt @@ -0,0 +1,23 @@ +package com.mapbox.navigation.route.internal.util + +import java.util.Scanner + +class TestRouteFixtures { + fun loadTwoLegRoute() = loadJsonFixture(RESPONSE_MULTI_LEG_ROUTE_FIXTURE) + fun loadRefreshedRoute() = loadJsonFixture(REFRESHED_ROUTE_FIXTURE) + fun loadEmptyRoutesResponse() = loadJsonFixture(RESPONSE_EMPTY_ROUTES_FIXTURE) + + private fun loadJsonFixture(filename: String): String { + val inputStream = javaClass.classLoader?.getResourceAsStream(filename) + return inputStream?.let { + val scanner = Scanner(it, "UTF-8").useDelimiter("\\A") + if (scanner.hasNext()) scanner.next() else "" + } ?: "" + } + + private companion object { + const val RESPONSE_MULTI_LEG_ROUTE_FIXTURE = "directions_response_two_leg_route.json" + const val RESPONSE_EMPTY_ROUTES_FIXTURE = "directions_response_empty_routes.json" + const val REFRESHED_ROUTE_FIXTURE = "refreshed_route.json" + } +} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/base/BaseTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/base/BaseTest.kt deleted file mode 100644 index 3fbb913ecb0..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/base/BaseTest.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.mapbox.navigation.route.offboard.base - -import com.google.gson.GsonBuilder -import com.mapbox.api.directions.v5.DirectionsAdapterFactory -import com.mapbox.api.directions.v5.models.DirectionsResponse -import com.mapbox.api.directions.v5.models.DirectionsRoute -import java.io.IOException -import java.util.Scanner - -open class BaseTest { - - companion object { - private const val MULTI_LEG_ROUTE_FIXTURE = "directions_two_leg_route.json" - } - - @Throws(IOException::class) - protected fun loadJsonFixture(filename: String): String { - val inputStream = javaClass.classLoader?.getResourceAsStream(filename) - return inputStream?.let { - val scanner = Scanner(it, "UTF-8").useDelimiter("\\A") - if (scanner.hasNext()) scanner.next() else "" - } ?: "" - } - - @Throws(Exception::class) - protected fun buildMultipleLegRoute(): DirectionsRoute { - val body = loadJsonFixture(MULTI_LEG_ROUTE_FIXTURE) - val gson = GsonBuilder() - .registerTypeAdapterFactory(DirectionsAdapterFactory.create()) - .create() - val response = gson.fromJson(body, DirectionsResponse::class.java) - return response.routes()[0] - } -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapperTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapperTest.kt deleted file mode 100644 index 03b3aa31871..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/offboard/routerefresh/RouteRefreshCallbackMapperTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.mapbox.navigation.route.offboard.routerefresh - -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.api.directions.v5.models.LegAnnotation -import com.mapbox.api.directions.v5.models.RouteLeg -import com.mapbox.api.directionsrefresh.v1.models.DirectionsRouteRefresh -import com.mapbox.api.directionsrefresh.v1.models.RouteLegRefresh -import io.mockk.mockk -import org.junit.Assert.assertEquals -import org.junit.Test - -class RouteRefreshCallbackMapperTest { - - @Test - fun testResponse() { - val originalRoute = originalRoute() - val refreshRoute = refreshRoute() - val result = RouteRefreshCallbackMapper.mapToDirectionsRoute( - originalRoute, - refreshRoute - ) - - for (i in 0..1) { - assertEquals( - refreshRoute.legs()!![i].annotation(), - result!!.legs()!![i].annotation() - ) - } - } - - private fun refreshRoute(): DirectionsRouteRefresh = - DirectionsRouteRefresh.builder() - .legs( - listOf( - RouteLegRefresh.builder() - .annotation( - LegAnnotation.builder() - .congestion(listOf("congestion5", "congestion10")) - .distance(listOf(44.0, 55.0)) - .build() - ) - .build(), - RouteLegRefresh.builder() - .annotation( - LegAnnotation.builder() - .congestion( - listOf( - "congestion333", - "congestion999", - "congestion1000" - ) - ) - .distance(listOf(1.0, 2.0, 3.0)) - .duration(listOf(4.0, 5.0, 6.0)) - .maxspeed(listOf(mockk(), mockk(), mockk())) - .speed(listOf(7.0, 8.0, 9.0)) - .build() - ) - .build() - ) - ) - .build() - - private fun originalRoute(): DirectionsRoute = - DirectionsRoute.builder() - .distance(10.0) - .duration(20.0) - .legs( - listOf( - RouteLeg.builder() - .annotation( - LegAnnotation.builder() - .congestion(listOf("congestion1", "congestion2")) - .distance(listOf(11.0, 21.0)) - .build() - ) - .build(), - RouteLeg.builder() - .annotation( - LegAnnotation.builder() - .congestion(listOf("congestion11", "congestion22", "congestion33")) - .distance(listOf(111.0, 211.0, 311.0)) - .build() - ) - .build() - ) - ) - .build() -} diff --git a/libnavigation-router/src/test/java/com/mapbox/navigation/route/onboard/OfflineRouteTest.kt b/libnavigation-router/src/test/java/com/mapbox/navigation/route/onboard/OfflineRouteTest.kt deleted file mode 100644 index 74a61c6ec53..00000000000 --- a/libnavigation-router/src/test/java/com/mapbox/navigation/route/onboard/OfflineRouteTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.mapbox.navigation.route.onboard - -import com.mapbox.api.directions.v5.DirectionsCriteria -import com.mapbox.api.directions.v5.MapboxDirections -import com.mapbox.api.directions.v5.models.RouteOptions -import com.mapbox.geojson.Point -import com.mapbox.navigation.base.extensions.coordinates -import com.mapbox.navigation.route.internal.util.httpUrl -import com.mapbox.navigation.testing.BuilderTest -import io.mockk.mockk -import org.junit.Assert.assertTrue -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.io.UnsupportedEncodingException -import java.net.URL -import java.net.URLDecoder -import kotlin.reflect.KClass - -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -internal class OfflineRouteTest : BuilderTest() { - - override fun getImplementationClass(): KClass = OfflineRoute::class - - override fun getFilledUpBuilder(): OfflineRoute.Builder { - return OfflineRoute.Builder(mockk(relaxed = true)) - .bicycleType(mockk(relaxed = true)) - .cyclingSpeed(123f) - .cyclewayBias(456f) - .hillBias(789f) - .ferryBias(101112f) - .roughSurfaceBias(131415f) - .waypointTypes(mockk(relaxed = true)) - } - - @Test - override fun trigger() { - // only used to trigger JUnit4 to run this class if all test cases come from the parent - } - - @Test - fun addBicycleTypeIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .bicycleType(OfflineCriteria.BicycleType.ROAD).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("bicycle_type=Road")) - } - - @Test - fun addCyclingSpeedIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .cyclingSpeed(10.0f).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("cycling_speed=10.0")) - } - - @Test - fun addCyclewayBiasIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .cyclewayBias(0.0f).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("cycleway_bias=0.0")) - } - - @Test - fun addHillBiasIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .hillBias(0.0f).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("hill_bias=0.0")) - } - - @Test - fun addFerryBiasIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .ferryBias(0.0f).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("ferry_bias=0.0")) - } - - @Test - fun addRoughSurfaceBiasIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val offlineRoute = OfflineRoute.Builder(routeUrl) - .roughSurfaceBias(0.0f).build() - - val offlineUrl = offlineRoute.buildUrl() - - assertTrue(offlineUrl.contains("rough_surface_bias=0.0")) - } - - @Test - @Throws(UnsupportedEncodingException::class) - fun addWaypointTypesIncludedInRequest() { - val routeUrl = provideOnlineRouteBuilder() - val waypointTypes = listOf( - OfflineCriteria.WaypointType.BREAK, - OfflineCriteria.WaypointType.THROUGH, - null, - OfflineCriteria.WaypointType.BREAK - ) - val offlineRoute = OfflineRoute.Builder(routeUrl) - .waypointTypes(waypointTypes).build() - val offlineUrl = offlineRoute.buildUrl() - - val offlineUrlDecoded = URLDecoder.decode(offlineUrl, "UTF-8") - - assertTrue(offlineUrlDecoded.contains("break;through;;break")) - } - - private fun provideOnlineRouteBuilder(): URL = - MapboxDirections.builder() - .accessToken("pk.XXX") - .routeOptions( - RouteOptions.builder() - .profile(DirectionsCriteria.PROFILE_CYCLING) - .coordinates( - origin = Point.fromLngLat(1.0, 2.0), - waypoints = listOf(Point.fromLngLat(3.0, 2.0)), - destination = Point.fromLngLat(1.0, 5.0) - ) - .build() - ) - .build() - .httpUrl() - .toUrl() -} diff --git a/libnavigation-router/src/test/resources/directions_response_empty_routes.json b/libnavigation-router/src/test/resources/directions_response_empty_routes.json new file mode 100644 index 00000000000..5bac30d7fbb --- /dev/null +++ b/libnavigation-router/src/test/resources/directions_response_empty_routes.json @@ -0,0 +1,4 @@ +{ + "routes": [], + "code": "Ok" +} \ No newline at end of file diff --git a/libnavigation-router/src/test/resources/directions_two_leg_route.json b/libnavigation-router/src/test/resources/directions_response_two_leg_route.json similarity index 99% rename from libnavigation-router/src/test/resources/directions_two_leg_route.json rename to libnavigation-router/src/test/resources/directions_response_two_leg_route.json index 49d88fe466d..4790b62cff4 100644 --- a/libnavigation-router/src/test/resources/directions_two_leg_route.json +++ b/libnavigation-router/src/test/resources/directions_response_two_leg_route.json @@ -6099,5 +6099,6 @@ "duration": 2858.1000000000004 } ], - "code": "Ok" + "code": "Ok", + "uuid": "cjeacbr8s21bk47lggcvce7lv" } diff --git a/libnavigation-router/src/test/resources/refreshed_route.json b/libnavigation-router/src/test/resources/refreshed_route.json new file mode 100644 index 00000000000..ba3f54b65d9 --- /dev/null +++ b/libnavigation-router/src/test/resources/refreshed_route.json @@ -0,0 +1,128 @@ +{ + "geometry": "", + "legs": [ + { + "summary": "Fishburn Road", + "weight": 111.1, + "duration": 222.5, + "steps": [ + { + "intersections": [ + { + "out": 0, + "entry": [ + true + ], + "bearings": [ + 82 + ], + "location": [ + -76.299169, + 40.042522 + ] + }, + { + "out": 0, + "in": 1, + "entry": [ + true, + false, + true + ], + "bearings": [ + 75, + 255, + 345 + ], + "location": [ + -76.298855, + 40.042556 + ] + }, + { + "out": 0, + "in": 2, + "entry": [ + true, + true, + false, + true + ], + "bearings": [ + 75, + 165, + 255, + 345 + ], + "location": [ + -76.297812, + 40.042673 + ] + } + ], + "driving_side": "right", + "geometry": "s`_kkA`y|opCcAsReEcy@Eq@]oDa@gEEu@uKgwB", + "mode": "driving", + "maneuver": { + "bearing_after": 82, + "bearing_before": 0, + "location": [ + -76.299169, + 40.042522 + ], + "modifier": "left", + "type": "depart", + "instruction": "Head east on East Fulton Street" + }, + "weight": 63.8, + "duration": 50.8, + "name": "East Fulton Street", + "distance": 293.2, + "voiceInstructions": [ + { + "distanceAlongGeometry": 293.2, + "announcement": "Head east on East Fulton Street, then turn right onto North Ann Street", + "ssmlAnnouncement": "Head east on East Fulton Street, then turn right onto North Ann Street" + }, + { + "distanceAlongGeometry": 86.6, + "announcement": "Turn right onto North Ann Street, then turn left onto East Chestnut Street (PA 23 East)", + "ssmlAnnouncement": "Turn right onto North Ann Street, then turn left onto East Chestnut Street (PA 23 East)" + } + ], + "bannerInstructions": [ + { + "distanceAlongGeometry": 293.2, + "primary": { + "type": "turn", + "modifier": "right", + "components": [ + { + "text": "North", + "type": "text", + "abbr": "N", + "abbr_priority": 1 + }, + { + "text": "Ann Street", + "type": "text", + "abbr": "Ann St", + "abbr_priority": 0 + } + ], + "text": "North Ann Street" + }, + "secondary": null + } + ] + } + ], + "distance": 111222.7 + } + ], + "weight_name": "routability", + "weight": 123.1, + "duration": 444.5, + "distance": 111222.7, + "voiceLocale": "en-US" +} \ No newline at end of file diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt index c9c3c09d0eb..c888f8f9c19 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigator.kt @@ -26,6 +26,7 @@ import com.mapbox.navigator.RoadObjectsStoreObserver import com.mapbox.navigator.RouteAlternativesControllerInterface import com.mapbox.navigator.RouteInfo import com.mapbox.navigator.RouterError +import com.mapbox.navigator.RouterInterface import com.mapbox.navigator.TilesConfig /** @@ -224,4 +225,6 @@ interface MapboxNativeNavigator { val roadObjectMatcher: RoadObjectMatcher? val experimental: Experimental + + val router: RouterInterface } diff --git a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt index aadd5cc7ca5..7ffaa417a04 100644 --- a/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt +++ b/libnavigator/src/main/java/com/mapbox/navigation/navigator/internal/MapboxNativeNavigatorImpl.kt @@ -36,6 +36,9 @@ import com.mapbox.navigator.RouteAlternativesControllerInterface import com.mapbox.navigator.RouteInfo import com.mapbox.navigator.Router import com.mapbox.navigator.RouterError +import com.mapbox.navigator.RouterFactory +import com.mapbox.navigator.RouterInterface +import com.mapbox.navigator.RouterType import com.mapbox.navigator.TilesConfig import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher @@ -69,6 +72,7 @@ object MapboxNativeNavigatorImpl : MapboxNativeNavigator { override var roadObjectsStore: RoadObjectsStore? = null override lateinit var experimental: Experimental override lateinit var cache: CacheHandle + override lateinit var router: RouterInterface private var logger: Logger? = null private val nativeNavigatorRecreationObservers = CopyOnWriteArraySet() @@ -107,6 +111,7 @@ object MapboxNativeNavigatorImpl : MapboxNativeNavigator { roadObjectsStore = nativeComponents.navigator.roadObjectStore() experimental = nativeComponents.navigator.experimental cache = nativeComponents.cache + router = RouterFactory.build(RouterType.HYBRID, cache, historyRecorderHandle) this.logger = logger this.accessToken = accessToken return this diff --git a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt index f960d59f4cc..39965ec6d26 100644 --- a/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt +++ b/libtesting-ui/src/main/java/com/mapbox/navigation/testing/ui/http/MockWebServerRule.kt @@ -21,7 +21,7 @@ class MockWebServerRule : TestWatcher() { /** * @see [MockWebServer.url] */ - val baseUrl = webServer.url("").toString() + val baseUrl = webServer.url("").toString().dropLast(1) // drop the last `/`, RouteOptions::toUrl() will add it /** * Add [MockRequestHandler]s to this list for each request that should be handled.