Skip to content

Commit

Permalink
Add route refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
Kyle Madsen committed Mar 18, 2020
1 parent 244361f commit a164785
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 1 deletion.
25 changes: 25 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,11 @@ License: [The Apache Software License, Version 2.0](http://www.apache.org/licens

===========================================================================

Mapbox Navigation uses portions of the Converter: Gson.
License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)

===========================================================================

Mapbox Navigation uses portions of the Gson.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)

Expand Down Expand Up @@ -1048,6 +1053,21 @@ License: [Mapbox Terms of Service](https://www.mapbox.com/tos/)

===========================================================================

Mapbox Navigation uses portions of the OkHttp.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)

===========================================================================

Mapbox Navigation uses portions of the OkHttp Logging Interceptor.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)

===========================================================================

Mapbox Navigation uses portions of the Okio.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)

===========================================================================

Mapbox Navigation uses portions of the org.jetbrains.kotlin:kotlin-stdlib (Kotlin Standard Library for JVM).
URL: [https://kotlinlang.org/](https://kotlinlang.org/)
License: [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt)
Expand All @@ -1066,6 +1086,11 @@ License: [The Apache License, Version 2.0](http://www.apache.org/licenses/LICENS

===========================================================================

Mapbox Navigation uses portions of the Retrofit.
License: [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt)

===========================================================================




Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class MapboxOffboardRouter(
) {
mapboxDirections = RouteBuilderProvider.getBuilder(accessToken, context, skuTokenProvider)
.routeOptions(routeOptions)
.enableRefresh(routeOptions.isRefreshEnabled())
.build()
mapboxDirections?.enqueueCall(object : Callback<DirectionsResponse> {

Expand Down Expand Up @@ -69,3 +70,7 @@ class MapboxOffboardRouter(
mapboxDirections = null
}
}

private fun RouteOptions.isRefreshEnabled(): Boolean {
return profile().contains(other = "traffic", ignoreCase = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ internal object RouteBuilderProvider {
.accessToken(accessToken)
.voiceInstructions(true)
.bannerInstructions(true)
.enableRefresh(false)
.voiceUnits(context.inferDeviceLocale().getUnitTypeForLocale())
.interceptor {
val httpUrl = it.request().url()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class MapboxOffboardRouterTest : BaseTest() {
every { mockSkuTokenProvider.obtainUrlWithSkuToken("/mock", 1) } returns ("/mock&sku=102jaksdhfj")
every { RouteBuilderProvider.getBuilder(accessToken, context, mockSkuTokenProvider) } returns mapboxDirectionsBuilder
every { mapboxDirectionsBuilder.interceptor(any()) } returns mapboxDirectionsBuilder
every { mapboxDirectionsBuilder.enableRefresh(any()) } returns mapboxDirectionsBuilder
every { mapboxDirectionsBuilder.build() } returns mapboxDirections
every { mapboxDirections.enqueueCall(capture(listener)) } answers {
callback = listener.captured
Expand Down
1 change: 1 addition & 0 deletions libnavigation-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies {
implementation project(':libnavigation-metrics')
implementation dependenciesList.mapboxAndroidAccounts
implementation dependenciesList.mapboxSdkTurf
implementation dependenciesList.mapboxSdkServices

//ktlint
ktlint dependenciesList.ktlint
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.mapbox.navigation.core.directions.session.RoutesRequestCallback
import com.mapbox.navigation.core.fasterroute.FasterRouteController
import com.mapbox.navigation.core.fasterroute.FasterRouteObserver
import com.mapbox.navigation.core.module.NavigationModuleProvider
import com.mapbox.navigation.core.routerefresh.RouteRefreshController
import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry
import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry.TAG
import com.mapbox.navigation.core.telemetry.events.TelemetryUserFeedback
Expand Down Expand Up @@ -136,6 +137,7 @@ constructor(
private val internalRoutesObserver = createInternalRoutesObserver()
private val internalOffRouteObserver = createInternalOffRouteObserver()
private val fasterRouteController: FasterRouteController
private val routeRefreshController: RouteRefreshController

private var notificationChannelField: Field? = null
private val MAPBOX_NAVIGATION_NOTIFICATION_PACKAGE_NAME =
Expand Down Expand Up @@ -192,6 +194,8 @@ constructor(
}

fasterRouteController = FasterRouteController(directionsSession, tripSession)
routeRefreshController = RouteRefreshController(accessToken ?: "", tripSession)
routeRefreshController.start()
}

/**
Expand Down Expand Up @@ -289,6 +293,7 @@ constructor(
tripSession.unregisterAllBannerInstructionsObservers()
tripSession.unregisterAllVoiceInstructionsObservers()
fasterRouteController.stop()
routeRefreshController.stop()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.mapbox.navigation.core.routerefresh

import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.api.directionsrefresh.v1.MapboxDirectionsRefresh
import com.mapbox.navigation.base.trip.model.RouteProgress

internal class RouteRefreshApi(
private val routeRefreshRetrofit: RouteRefreshRetrofit
) {
fun supportsRefresh(route: DirectionsRoute?): Boolean {
val isTrafficProfile = route?.routeOptions()
?.profile()?.contains(other = "traffic", ignoreCase = true)
return isTrafficProfile == true
}

fun refreshRoute(
accessToken: String,
route: DirectionsRoute?,
routeProgress: RouteProgress?,
callback: RouteRefreshCallback
) {
val refreshBuilder = MapboxDirectionsRefresh.builder()
if (accessToken.isNotEmpty()) {
refreshBuilder.accessToken(accessToken)
}

val originalRoute: DirectionsRoute = route ?: run {
callback.onError(RouteRefreshError("No DirectionsRoute to refresh"))
return
}

if (!supportsRefresh(originalRoute)) {
callback.onError(RouteRefreshError("Unsupported profile ${originalRoute.routeOptions()?.profile()}"))
return
}

originalRoute.routeOptions()?.requestUuid()?.let {
refreshBuilder.requestId(it)
}

val legIndex = routeProgress?.currentLegProgress()?.legIndex() ?: 0
refreshBuilder.legIndex(legIndex)

return try {
val mapboxDirectionsRefresh = refreshBuilder.build()
val callbackMapper = RouteRefreshCallbackMapper(originalRoute, legIndex, callback)
routeRefreshRetrofit.enqueueCall(mapboxDirectionsRefresh, callbackMapper)
} catch (throwable: Throwable) {
callback.onError(RouteRefreshError(
message = "Route refresh call failed",
throwable = throwable))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.mapbox.navigation.core.routerefresh

import com.mapbox.api.directions.v5.models.DirectionsRoute

interface RouteRefreshCallback {
fun onRefresh(directionsRoute: DirectionsRoute)

fun onError(error: RouteRefreshError)
}

data class RouteRefreshError(
val message: String? = null,
val throwable: Throwable? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.mapbox.navigation.core.routerefresh

import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

internal class RouteRefreshCallbackMapper(
private val originalRoute: DirectionsRoute,
private val currentLegIndex: Int,
private val callback: RouteRefreshCallback
) : Callback<DirectionsRefreshResponse> {

override fun onResponse(call: Call<DirectionsRefreshResponse>, response: Response<DirectionsRefreshResponse>) {
val routeAnnotations = response.body()?.route()
var errorThrowable: Throwable? = null
val refreshedDirectionsRoute = try {
mapToDirectionsRoute(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))
}
}

override fun onFailure(call: Call<DirectionsRefreshResponse>, t: Throwable) {
callback.onError(RouteRefreshError(throwable = t))
}

private fun mapToDirectionsRoute(routeAnnotations: DirectionsRoute?): DirectionsRoute? {
val validRouteAnnotations = routeAnnotations ?: return null
val refreshedRouteLegs = originalRoute.legs()?.let { oldRouteLegsList ->
val legs = oldRouteLegsList.toMutableList()
for (i in currentLegIndex until legs.size) {
validRouteAnnotations.legs()?.let { annotationHolderRouteLegsList ->
val updatedAnnotation = annotationHolderRouteLegsList[i - currentLegIndex].annotation()
legs[i] = legs[i].toBuilder().annotation(updatedAnnotation).build()
}
}
legs.toList()
}
return originalRoute.toBuilder().legs(refreshedRouteLegs).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.mapbox.navigation.core.routerefresh

import android.util.Log
import com.mapbox.api.directions.v5.models.DirectionsRoute
import com.mapbox.navigation.core.trip.session.TripSession
import com.mapbox.navigation.utils.timer.MapboxTimer
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Job

/**
* This class is responsible for refreshing the current direction route's traffic at a
* specified [intervalSeconds]. This does not support alternative routes.
*
* If the route is route is successfully refreshed, this class will update the [TripSession.route]
*
* [start] and [stop] are attached to the application lifecycle. Observing routes that
* can be refreshed are handled by this class. Calling [start] will restart the refresh timer.
*/
internal class RouteRefreshController(
private var accessToken: String,
private val tripSession: TripSession
) {
private val routerRefreshTimer = MapboxTimer()
private val routeRefreshRetrofit = RouteRefreshRetrofit()
private val routeRefreshApi = RouteRefreshApi(routeRefreshRetrofit)

var intervalSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(routerRefreshTimer.restartAfterMillis)
set(value) {
routerRefreshTimer.restartAfterMillis = TimeUnit.SECONDS.toMillis(value)
field = value
}

fun start(): Job {
stop()
return routerRefreshTimer.startTimer {
if (routeRefreshApi.supportsRefresh(tripSession.route)) {
routeRefreshApi.refreshRoute(
accessToken,
tripSession.route,
tripSession.getRouteProgress(),
routeRefreshCallback)
}
}
}

fun stop() {
routerRefreshTimer.stopJobs()
}

private val routeRefreshCallback = object : RouteRefreshCallback {

override fun onRefresh(directionsRoute: DirectionsRoute) {
Log.i("RouteRefresh", "Successful refresh")
tripSession.route = directionsRoute
}

override fun onError(error: RouteRefreshError) {
if (error.throwable != null) {
Log.e("RouteRefresh", error.message, error.throwable)
} else {
Log.e("RouteRefresh", error.message)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.mapbox.navigation.core.routerefresh

import com.mapbox.api.directionsrefresh.v1.MapboxDirectionsRefresh
import com.mapbox.api.directionsrefresh.v1.models.DirectionsRefreshResponse
import retrofit2.Callback

/**
* This class is used for adding unit tests to [RouteRefreshApi]
*/
internal class RouteRefreshRetrofit {

internal fun enqueueCall(
mapboxDirectionsRefresh: MapboxDirectionsRefresh,
callback: Callback<DirectionsRefreshResponse>
) {
mapboxDirectionsRefresh.enqueueCall(callback)
}
}
Loading

0 comments on commit a164785

Please sign in to comment.