Skip to content

Commit c4b2155

Browse files
authored
Route refresh: added experimental state observer (#6345)
* Route refresh: added experimental state observer
1 parent d43e7b8 commit c4b2155

File tree

10 files changed

+672
-205
lines changed

10 files changed

+672
-205
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone.
88
- Introduced `ViewOptionsCustomization.showCameraDebugInfo` to allow end users to enable camera debug info. [#6356](https://github.com/mapbox/mapbox-navigation-android/pull/6356)
99
- Added `ComponentInstaller` for the `LocationComponent` that offers simplified integration of map LocationPuck. [#6206](https://github.com/mapbox/mapbox-navigation-android/pull/6206)
1010
- Added `ViewStyleCustomization.locationPuck` that allows to define a custom location puck using `NavigationView`. [#6365](https://github.com/mapbox/mapbox-navigation-android/pull/6365)
11+
- Added _Experimental_ `RouteRefreshStatesObserver` that can be used to observe route refresh states. To subscribe and unsubscribe on updates corresponding use `MapboxNavigation#registerRouteRefreshStateObserver` and `MapboxNavigation#unregisterRouteRefreshStateObserver`. [#6345](https://github.com/mapbox/mapbox-navigation-android/pull/6345)
1112
#### Bug fixes and improvements
1213
- Marked `PredictiveCacheController`, `MapboxBuildingView`, `ViewportDataSourceUpdateObserver`, `NavigationScaleGestureHandler`, `NavigationCameraStateChangedObserver`, `NavigationCameraStateTransition`, `NavigationCameraTransition`, `TransitionEndListener`, `MapboxRecenterButton`, `MapboxRouteOverviewButton`, `MapboxJunctionView`, `MapboxSignboardView`, `MapboxRoadNameLabelView`, `MapboxRoadNameView`, `MapboxRouteArrowView`, `MapboxRouteLineView`, `MapboxCameraModeButton` methods and `View.capture` extension with `@UiThread` annotation. [#6235](https://github.com/mapbox/mapbox-navigation-android/pull/6235)
1314
- Marked `Binder`, `MapboxExtendableButton` methods and `MapboxNavigation#installComponents` methods with `@UiThread` annotation. [#6268](https://github.com/mapbox/mapbox-navigation-android/pull/6268)

libnavigation-core/api/current.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ package com.mapbox.navigation.core {
4040
method public void registerRouteAlternativesObserver(com.mapbox.navigation.core.routealternatives.RouteAlternativesObserver routeAlternativesObserver);
4141
method public void registerRouteAlternativesObserver(com.mapbox.navigation.core.routealternatives.NavigationRouteAlternativesObserver routeAlternativesObserver);
4242
method public void registerRouteProgressObserver(com.mapbox.navigation.core.trip.session.RouteProgressObserver routeProgressObserver);
43+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void registerRouteRefreshStateObserver(com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver routeRefreshStatesObserver);
4344
method public void registerRoutesObserver(com.mapbox.navigation.core.directions.session.RoutesObserver routesObserver);
4445
method public void registerTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
4546
method public void registerVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
@@ -77,6 +78,7 @@ package com.mapbox.navigation.core {
7778
method public void unregisterRouteAlternativesObserver(com.mapbox.navigation.core.routealternatives.RouteAlternativesObserver routeAlternativesObserver);
7879
method public void unregisterRouteAlternativesObserver(com.mapbox.navigation.core.routealternatives.NavigationRouteAlternativesObserver routeAlternativesObserver);
7980
method public void unregisterRouteProgressObserver(com.mapbox.navigation.core.trip.session.RouteProgressObserver routeProgressObserver);
81+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public void unregisterRouteRefreshStateObserver(com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver routeRefreshStatesObserver);
8082
method public void unregisterRoutesObserver(com.mapbox.navigation.core.directions.session.RoutesObserver routesObserver);
8183
method public void unregisterTripSessionStateObserver(com.mapbox.navigation.core.trip.session.TripSessionStateObserver tripSessionStateObserver);
8284
method public void unregisterVoiceInstructionsObserver(com.mapbox.navigation.core.trip.session.VoiceInstructionsObserver voiceInstructionsObserver);
@@ -754,6 +756,31 @@ package com.mapbox.navigation.core.routeoptions {
754756

755757
}
756758

759+
package com.mapbox.navigation.core.routerefresh {
760+
761+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class RouteRefreshExtra {
762+
field public static final com.mapbox.navigation.core.routerefresh.RouteRefreshExtra INSTANCE;
763+
field public static final String REFRESH_STATE_FINISHED_FAILED = "FINISHED_FAILED";
764+
field public static final String REFRESH_STATE_FINISHED_SUCCESS = "FINISHED_SUCCESS";
765+
field public static final String REFRESH_STATE_STARTED = "STARTED";
766+
}
767+
768+
@StringDef({com.mapbox.navigation.core.routerefresh.RouteRefreshExtra.REFRESH_STATE_STARTED, com.mapbox.navigation.core.routerefresh.RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS, com.mapbox.navigation.core.routerefresh.RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public static @interface RouteRefreshExtra.RouteRefreshState {
769+
}
770+
771+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class RouteRefreshStateResult {
772+
method public String? getMessage();
773+
method public String getState();
774+
property public final String? message;
775+
property public final String state;
776+
}
777+
778+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public fun interface RouteRefreshStatesObserver {
779+
method public void onNewState(com.mapbox.navigation.core.routerefresh.RouteRefreshStateResult result);
780+
}
781+
782+
}
783+
757784
package com.mapbox.navigation.core.telemetry {
758785

759786
public final class MapboxNavigationTelemetryKt {

libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import com.mapbox.navigation.core.routealternatives.RouteAlternativesRequestCall
8383
import com.mapbox.navigation.core.routeoptions.RouteOptionsUpdater
8484
import com.mapbox.navigation.core.routerefresh.RouteRefreshController
8585
import com.mapbox.navigation.core.routerefresh.RouteRefreshControllerProvider
86+
import com.mapbox.navigation.core.routerefresh.RouteRefreshStatesObserver
8687
import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry
8788
import com.mapbox.navigation.core.telemetry.events.FeedbackEvent
8889
import com.mapbox.navigation.core.telemetry.events.FeedbackHelper
@@ -1050,6 +1051,7 @@ class MapboxNavigation @VisibleForTesting internal constructor(
10501051
ReachabilityService.removeReachabilityObserver(it)
10511052
reachabilityObserverId = null
10521053
}
1054+
routeRefreshController.unregisterAllRouteRefreshStateObservers()
10531055

10541056
isDestroyed = true
10551057
hasInstance = false
@@ -1612,6 +1614,28 @@ class MapboxNavigation @VisibleForTesting internal constructor(
16121614
navigationSession.unregisterNavigationSessionStateObserver(navigationSessionStateObserver)
16131615
}
16141616

1617+
/**
1618+
* Register a [RouteRefreshStatesObserver] to be notified of Route refresh state changes.
1619+
*
1620+
* @param routeRefreshStatesObserver RouteRefreshStatesObserver
1621+
*/
1622+
@ExperimentalPreviewMapboxNavigationAPI
1623+
fun registerRouteRefreshStateObserver(
1624+
routeRefreshStatesObserver: RouteRefreshStatesObserver
1625+
) {
1626+
routeRefreshController.registerRouteRefreshStateObserver(routeRefreshStatesObserver)
1627+
}
1628+
1629+
/**
1630+
* Unregisters a [RouteRefreshStatesObserver].
1631+
*/
1632+
@ExperimentalPreviewMapboxNavigationAPI
1633+
fun unregisterRouteRefreshStateObserver(
1634+
routeRefreshStatesObserver: RouteRefreshStatesObserver
1635+
) {
1636+
routeRefreshController.unregisterRouteRefreshStateObserver(routeRefreshStatesObserver)
1637+
}
1638+
16151639
private fun startSession(withTripService: Boolean, withReplayEnabled: Boolean) {
16161640
runIfNotDestroyed {
16171641
tripSession.start(

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.mapbox.navigation.core.routerefresh
22

3+
import androidx.annotation.VisibleForTesting
34
import com.mapbox.api.directions.v5.models.RouteLeg
5+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
46
import com.mapbox.navigation.base.internal.CurrentIndices
57
import com.mapbox.navigation.base.internal.route.updateDirectionsRouteOnly
68
import com.mapbox.navigation.base.internal.time.parseISO8601DateToLocalTimeOrNull
@@ -12,6 +14,7 @@ import com.mapbox.navigation.core.CurrentIndicesProvider
1214
import com.mapbox.navigation.core.directions.session.RouteRefresh
1315
import com.mapbox.navigation.utils.internal.logE
1416
import com.mapbox.navigation.utils.internal.logI
17+
import kotlinx.coroutines.CancellationException
1518
import kotlinx.coroutines.CompletableDeferred
1619
import kotlinx.coroutines.async
1720
import kotlinx.coroutines.awaitAll
@@ -20,39 +23,105 @@ import kotlinx.coroutines.delay
2023
import kotlinx.coroutines.suspendCancellableCoroutine
2124
import kotlinx.coroutines.withTimeoutOrNull
2225
import java.util.Date
26+
import java.util.concurrent.CopyOnWriteArraySet
2327
import kotlin.coroutines.resume
2428

2529
/**
2630
* This class is responsible for refreshing the current direction route's traffic.
2731
* This does not support alternative routes.
2832
*/
33+
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
2934
internal class RouteRefreshController(
3035
private val routeRefreshOptions: RouteRefreshOptions,
3136
private val routeRefresh: RouteRefresh,
3237
private val currentIndicesProvider: CurrentIndicesProvider,
3338
private val routeDiffProvider: DirectionsRouteDiffProvider = DirectionsRouteDiffProvider(),
34-
private val localDateProvider: () -> Date
39+
private val localDateProvider: () -> Date,
3540
) {
3641

42+
private var state: RouteRefreshStateResult? = null
43+
set(value) {
44+
if (field == value) return
45+
field = value
46+
value?.let { nonNullValue ->
47+
observers.forEach {
48+
it.onNewState(nonNullValue)
49+
}
50+
}
51+
}
52+
53+
private val observers = CopyOnWriteArraySet<RouteRefreshStatesObserver>()
54+
3755
internal companion object {
56+
@VisibleForTesting
3857
internal const val LOG_CATEGORY = "RouteRefreshController"
39-
private const val FAILED_ATTEMPTS_TO_INVALIDATE_EXPIRING_DATA = 3
58+
59+
@VisibleForTesting
60+
internal const val FAILED_ATTEMPTS_TO_INVALIDATE_EXPIRING_DATA = 3
4061
}
4162

4263
suspend fun refresh(routes: List<NavigationRoute>): RefreshedRouteInfo {
43-
return if (routes.isNotEmpty()) {
44-
val routesValidationResults = routes.map { validateRoute(it) }
45-
if (routesValidationResults.any { it is RouteValidationResult.Valid }) {
46-
tryRefreshingRoutesUntilRouteChanges(routes)
64+
try {
65+
return if (routes.isNotEmpty()) {
66+
val routesValidationResults = routes.map { validateRoute(it) }
67+
if (routesValidationResults.any { it is RouteValidationResult.Valid }) {
68+
tryRefreshingRoutesUntilRouteChanges(routes)
69+
} else {
70+
val message = joinValidationErrorMessages(routesValidationResults, routes)
71+
onNewState(
72+
RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED,
73+
"No routes which could be refreshed. $message"
74+
)
75+
waitForever("No routes which could be refreshed. $message")
76+
}
4777
} else {
48-
val message = joinValidationErrorMessages(routesValidationResults, routes)
49-
waitForever("No routes which could be refreshed. $message")
78+
resetState()
79+
waitForever("routes are empty")
5080
}
51-
} else {
52-
waitForever("routes are empty")
81+
} catch (e: CancellationException) {
82+
onNewStateIfCurrentIs(
83+
RouteRefreshExtra.REFRESH_STATE_CANCELED,
84+
current = RouteRefreshExtra.REFRESH_STATE_STARTED,
85+
)
86+
resetState()
87+
throw e
5388
}
5489
}
5590

91+
fun registerRouteRefreshStateObserver(observer: RouteRefreshStatesObserver) {
92+
observers.add(observer)
93+
state?.let { observer.onNewState(it) }
94+
}
95+
96+
fun unregisterRouteRefreshStateObserver(observer: RouteRefreshStatesObserver) {
97+
observers.remove(observer)
98+
}
99+
100+
fun unregisterAllRouteRefreshStateObservers() {
101+
observers.clear()
102+
}
103+
104+
private fun onNewState(
105+
@RouteRefreshExtra.RouteRefreshState state: String,
106+
message: String? = null
107+
) {
108+
this.state = RouteRefreshStateResult(state, message)
109+
}
110+
111+
private fun onNewStateIfCurrentIs(
112+
@RouteRefreshExtra.RouteRefreshState state: String,
113+
message: String? = null,
114+
@RouteRefreshExtra.RouteRefreshState current: String,
115+
) {
116+
if (current == this.state?.state) {
117+
onNewState(state, message)
118+
}
119+
}
120+
121+
private fun resetState() {
122+
this.state = null
123+
}
124+
56125
private fun joinValidationErrorMessages(
57126
routeValidation: List<RouteValidationResult>,
58127
routes: List<NavigationRoute>
@@ -78,10 +147,14 @@ internal class RouteRefreshController(
78147
try {
79148
repeat(FAILED_ATTEMPTS_TO_INVALIDATE_EXPIRING_DATA) {
80149
timeUntilNextAttempt.await()
150+
if (it == 0) {
151+
onNewState(RouteRefreshExtra.REFRESH_STATE_STARTED)
152+
}
81153
timeUntilNextAttempt = async { delay(routeRefreshOptions.intervalMillis) }
82154
val indicesSnapshot = currentIndicesProvider.getFilledIndicesOrWait()
83155
val refreshedRoutes = refreshRoutesOrNull(routes, indicesSnapshot)
84156
if (refreshedRoutes.any { it != null }) {
157+
onNewState(RouteRefreshExtra.REFRESH_STATE_FINISHED_SUCCESS)
85158
return@coroutineScope RefreshedRouteInfo(
86159
refreshedRoutes.mapIndexed { index, navigationRoute ->
87160
navigationRoute ?: routes[index]
@@ -93,6 +166,7 @@ internal class RouteRefreshController(
93166
} finally {
94167
timeUntilNextAttempt.cancel() // otherwise current coroutine will wait for its child
95168
}
169+
onNewState(RouteRefreshExtra.REFRESH_STATE_FINISHED_FAILED)
96170
val indicesSnapshot = currentIndicesProvider.getFilledIndicesOrWait()
97171
RefreshedRouteInfo(
98172
routes.map { removeExpiringDataFromRoute(it, indicesSnapshot.legIndex) },

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshControllerProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ internal object RouteRefreshControllerProvider {
1616
directionsSession,
1717
currentIndicesProvider,
1818
DirectionsRouteDiffProvider(),
19-
{ Date() }
19+
{ Date() },
2020
)
2121
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.mapbox.navigation.core.routerefresh
2+
3+
import androidx.annotation.StringDef
4+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
5+
import com.mapbox.navigation.base.route.RouteRefreshOptions
6+
7+
/**
8+
* Extra data of route refresh
9+
*/
10+
@ExperimentalPreviewMapboxNavigationAPI
11+
object RouteRefreshExtra {
12+
13+
/**
14+
* The state becomes [REFRESH_STATE_STARTED] when route refresh round is started.
15+
*/
16+
const val REFRESH_STATE_STARTED = "STARTED"
17+
18+
/**
19+
* The state becomes [REFRESH_STATE_FINISHED_SUCCESS] when the route is successfully refreshed.
20+
* The state is triggered in case if at least one route is refreshed successfully.
21+
*/
22+
const val REFRESH_STATE_FINISHED_SUCCESS = "FINISHED_SUCCESS"
23+
24+
/**
25+
* The state becomes [REFRESH_STATE_FINISHED_FAILED] when a route refresh failed and the route
26+
* is cleaned up of expired data, see [RouteRefreshOptions] for details.
27+
* The state is triggered in case if every single route refresh of a set of the routes is failed.
28+
*/
29+
const val REFRESH_STATE_FINISHED_FAILED = "FINISHED_FAILED"
30+
31+
/**
32+
* The state becomes [REFRESH_STATE_CANCELED] when a route refresh canceled. It occurs
33+
* when a new set of routes are set, that leads to interrupt route refresh process.
34+
*/
35+
const val REFRESH_STATE_CANCELED = "CANCELED"
36+
37+
/**
38+
* Route refresh states. See [RouteRefreshStatesObserver].
39+
*/
40+
@Retention(AnnotationRetention.BINARY)
41+
@StringDef(
42+
REFRESH_STATE_STARTED,
43+
REFRESH_STATE_FINISHED_SUCCESS,
44+
REFRESH_STATE_FINISHED_FAILED,
45+
REFRESH_STATE_CANCELED,
46+
)
47+
annotation class RouteRefreshState
48+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.mapbox.navigation.core.routerefresh
2+
3+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
4+
5+
/**
6+
* Wrapper of Route refresh state result. See [RouteRefreshStatesObserver]
7+
* @param state route refresh state. See [RouteRefreshExtra.RouteRefreshState]
8+
* @param message string represented message (optional)
9+
*/
10+
@ExperimentalPreviewMapboxNavigationAPI
11+
class RouteRefreshStateResult internal constructor(
12+
@RouteRefreshExtra.RouteRefreshState val state: String,
13+
val message: String? = null
14+
) {
15+
/**
16+
* Regenerate whenever a change is made
17+
*/
18+
override fun equals(other: Any?): Boolean {
19+
if (this === other) return true
20+
if (javaClass != other?.javaClass) return false
21+
22+
other as RouteRefreshStateResult
23+
24+
if (state != other.state) return false
25+
if (message != other.message) return false
26+
27+
return true
28+
}
29+
30+
/**
31+
* Returns a hash code value for the object.
32+
*/
33+
override fun hashCode(): Int {
34+
var result = state.hashCode()
35+
result = 31 * result + message.hashCode()
36+
return result
37+
}
38+
39+
/**
40+
* Returns a string representation of the object.
41+
*/
42+
override fun toString(): String {
43+
return "RouteRefreshStateResult(state='$state', message=$message)"
44+
}
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.mapbox.navigation.core.routerefresh
2+
3+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
4+
5+
/**
6+
* Route refresh state observer.
7+
*/
8+
@ExperimentalPreviewMapboxNavigationAPI
9+
fun interface RouteRefreshStatesObserver {
10+
11+
/**
12+
* Invoked for the current and every new state.
13+
*/
14+
fun onNewState(result: RouteRefreshStateResult)
15+
}

0 commit comments

Comments
 (0)