Skip to content

Commit 80c9abd

Browse files
committed
Add a MapboxTripStarter
1 parent 4ea8475 commit 80c9abd

26 files changed

Lines changed: 611 additions & 580 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added `MapboxTripStarter` to simplify the solution for managing the trip session and replaying routes. This also makes it possible to share the replay state between drop-in-ui and android-auto.

libnavigation-core/api/current.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ package com.mapbox.navigation.core {
3232
method public com.mapbox.navigation.core.trip.session.TripSessionState getTripSessionState();
3333
method public Integer? getZLevel();
3434
method public boolean isDestroyed();
35+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public boolean isReplayEnabled();
3536
method public boolean isRunningForegroundService();
3637
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI @kotlin.jvm.Throws(exceptionClasses=IllegalArgumentException::class) public void moveRoutesFromPreviewToNavigator() throws java.lang.IllegalArgumentException;
3738
method public void navigateNextRouteLeg(com.mapbox.navigation.core.trip.session.LegIndexUpdatedCallback callback);
@@ -1021,6 +1022,27 @@ package com.mapbox.navigation.core.telemetry.events {
10211022

10221023
}
10231024

1025+
package com.mapbox.navigation.core.trip {
1026+
1027+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarter implements com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver {
1028+
method public static com.mapbox.navigation.core.trip.MapboxTripStarter create();
1029+
method public com.mapbox.navigation.core.trip.MapboxTripStarter enableMapMatching();
1030+
method public com.mapbox.navigation.core.trip.MapboxTripStarter enableReplayRoute(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions? options = null);
1031+
method public static com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1032+
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getReplayRouteSessionOptions();
1033+
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1034+
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1035+
method public com.mapbox.navigation.core.trip.MapboxTripStarter refreshLocationPermissions();
1036+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarter.Companion Companion;
1037+
}
1038+
1039+
public static final class MapboxTripStarter.Companion {
1040+
method public com.mapbox.navigation.core.trip.MapboxTripStarter create();
1041+
method public com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1042+
}
1043+
1044+
}
1045+
10241046
package com.mapbox.navigation.core.trip.session {
10251047

10261048
public fun interface BannerInstructionsObserver {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,15 @@ class MapboxNavigation @VisibleForTesting internal constructor(
593593
@ExperimentalPreviewMapboxNavigationAPI
594594
val mapboxReplayer: MapboxReplayer by lazy { tripSessionLocationEngine.mapboxReplayer }
595595

596+
/**
597+
* True when [startReplayTripSession] has been called.
598+
* Will be false after [stopTripSession] is called.
599+
*/
600+
@ExperimentalPreviewMapboxNavigationAPI
601+
fun isReplayEnabled(): Boolean {
602+
return tripSessionLocationEngine.isReplayEnabled
603+
}
604+
596605
/**
597606
* Starts listening for location updates and enters an `Active Guidance` state if there's a primary route available
598607
* or a `Free Drive` state otherwise.
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
import android.annotation.SuppressLint
4+
import com.mapbox.android.core.permissions.PermissionsManager
5+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
6+
import com.mapbox.navigation.core.MapboxNavigation
7+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
8+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
9+
import com.mapbox.navigation.core.replay.route.ReplayRouteSession
10+
import com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions
11+
import com.mapbox.navigation.core.trip.MapboxTripStarter.Companion.getRegisteredInstance
12+
import com.mapbox.navigation.utils.internal.logI
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.ExperimentalCoroutinesApi
16+
import kotlinx.coroutines.SupervisorJob
17+
import kotlinx.coroutines.cancel
18+
import kotlinx.coroutines.flow.Flow
19+
import kotlinx.coroutines.flow.MutableStateFlow
20+
import kotlinx.coroutines.flow.flatMapLatest
21+
import kotlinx.coroutines.flow.launchIn
22+
import kotlinx.coroutines.flow.onEach
23+
24+
/**
25+
* The [MapboxTripStarter] makes it simpler to switch between a trip session and replay.
26+
*
27+
* This is not able to observe when location permissions change, so you may need to refresh the
28+
* state with [refreshLocationPermissions]. Location permissions are not required for replay.
29+
*
30+
* There should be one instance of this class at a time. For example, an app Activity and car
31+
* Session will need to use the same instance. That will be done automatically if you use
32+
* [getRegisteredInstance].
33+
*/
34+
@ExperimentalPreviewMapboxNavigationAPI
35+
class MapboxTripStarter internal constructor() : MapboxNavigationObserver {
36+
37+
private val tripType = MutableStateFlow<MapboxTripStarterType>(
38+
MapboxTripStarterType.MapMatching
39+
)
40+
private val replayRouteSessionOptions = MutableStateFlow(
41+
ReplayRouteSessionOptions.Builder().build()
42+
)
43+
private val isLocationPermissionGranted = MutableStateFlow(false)
44+
private var replayRouteTripSession: ReplayRouteSession? = null
45+
private var mapboxNavigation: MapboxNavigation? = null
46+
47+
private lateinit var coroutineScope: CoroutineScope
48+
49+
/**
50+
* Signals that the [mapboxNavigation] instance is ready for use.
51+
* @param mapboxNavigation
52+
*/
53+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
54+
this.mapboxNavigation = mapboxNavigation
55+
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
56+
57+
// Initialize the options to be aware of the location permissions
58+
val context = mapboxNavigation.navigationOptions.applicationContext
59+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
60+
isLocationPermissionGranted.value = granted
61+
62+
// Observe changes to state
63+
observeStateFlow(mapboxNavigation).launchIn(coroutineScope)
64+
}
65+
66+
/**
67+
* Signals that the [mapboxNavigation] instance is being detached.
68+
* @param mapboxNavigation
69+
*/
70+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
71+
coroutineScope.cancel()
72+
onTripDisabled(mapboxNavigation)
73+
this.mapboxNavigation = null
74+
}
75+
76+
/**
77+
* [enableMapMatching] will not work unless location permissions have been granted. Refresh
78+
* the location permissions after they are granted to ensure the trip session will start.
79+
*/
80+
fun refreshLocationPermissions() = apply {
81+
mapboxNavigation?.navigationOptions?.applicationContext?.let { context ->
82+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
83+
isLocationPermissionGranted.value = granted
84+
}
85+
}
86+
87+
/**
88+
* This is the default mode for the [MapboxTripStarter]. This can be used to disable
89+
* [enableReplayRoute]. Make sure location permissions have been accepted or this will have no
90+
* effect on the experience.
91+
*/
92+
fun enableMapMatching() = apply {
93+
if (!isLocationPermissionGranted.value) {
94+
refreshLocationPermissions()
95+
}
96+
tripType.value = MapboxTripStarterType.MapMatching
97+
}
98+
99+
/**
100+
* Get the current [ReplayRouteSessionOptions]. This can be used with [enableReplayRoute] to
101+
* make minor adjustments to the current options.
102+
*/
103+
fun getReplayRouteSessionOptions(): ReplayRouteSessionOptions = replayRouteSessionOptions.value
104+
105+
/**
106+
* Enables a mode where the primary route is simulated by an artificial driver. Set the route
107+
* with [MapboxNavigation.setNavigationRoutes]. Can be used with [getReplayRouteSessionOptions]
108+
* to make minor adjustments to the current options.
109+
*
110+
* @param options optional options to use for route replay.
111+
*/
112+
fun enableReplayRoute(
113+
options: ReplayRouteSessionOptions? = null
114+
) = apply {
115+
options?.let { options -> replayRouteSessionOptions.value = options }
116+
tripType.value = MapboxTripStarterType.ReplayRoute
117+
}
118+
119+
@OptIn(ExperimentalCoroutinesApi::class)
120+
private fun observeStateFlow(mapboxNavigation: MapboxNavigation): Flow<*> {
121+
return tripType.flatMapLatest { tripType ->
122+
when (tripType) {
123+
MapboxTripStarterType.ReplayRoute ->
124+
replayRouteSessionOptions.onEach { options ->
125+
onReplayTripEnabled(mapboxNavigation, options)
126+
}
127+
MapboxTripStarterType.MapMatching ->
128+
isLocationPermissionGranted.onEach { granted ->
129+
onMapMatchingEnabled(mapboxNavigation, granted)
130+
}
131+
}
132+
}
133+
}
134+
135+
/**
136+
* Internally called when the trip type has been set to replay route.
137+
*
138+
* @param mapboxNavigation
139+
* @param options parameters for the [ReplayRouteSession]
140+
*/
141+
private fun onReplayTripEnabled(
142+
mapboxNavigation: MapboxNavigation,
143+
options: ReplayRouteSessionOptions
144+
) {
145+
replayRouteTripSession?.onDetached(mapboxNavigation)
146+
replayRouteTripSession = ReplayRouteSession().also {
147+
it.setOptions(options)
148+
it.onAttached(mapboxNavigation)
149+
}
150+
}
151+
152+
/**
153+
* Internally called when the trip type has been set to map matching.
154+
*
155+
* @param mapboxNavigation
156+
* @param granted true when location permissions are accepted, false otherwise
157+
*/
158+
@SuppressLint("MissingPermission")
159+
private fun onMapMatchingEnabled(mapboxNavigation: MapboxNavigation, granted: Boolean) {
160+
if (granted) {
161+
replayRouteTripSession?.onDetached(mapboxNavigation)
162+
replayRouteTripSession = null
163+
mapboxNavigation.startTripSession()
164+
} else {
165+
logI(LOG_CATEGORY) {
166+
"startTripSession was not called. Accept location permissions and call " +
167+
"mapboxTripStarter.refreshLocationPermissions()"
168+
}
169+
onTripDisabled(mapboxNavigation)
170+
}
171+
}
172+
173+
/**
174+
* Internally called when the trip session needs to be stopped.
175+
*
176+
* @param mapboxNavigation
177+
*/
178+
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
179+
replayRouteTripSession?.onDetached(mapboxNavigation)
180+
replayRouteTripSession = null
181+
mapboxNavigation.stopTripSession()
182+
}
183+
184+
companion object {
185+
private const val LOG_CATEGORY = "MapboxTripStarter"
186+
187+
/**
188+
* Construct an instance without registering to [MapboxNavigationApp].
189+
*/
190+
@JvmStatic
191+
fun create() = MapboxTripStarter()
192+
193+
/**
194+
* Get the registered instance or create one and register it to [MapboxNavigationApp].
195+
*/
196+
@JvmStatic
197+
fun getRegisteredInstance(): MapboxTripStarter = MapboxNavigationApp
198+
.getObservers(MapboxTripStarter::class)
199+
.firstOrNull() ?: MapboxTripStarter().also { MapboxNavigationApp.registerObserver(it) }
200+
}
201+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
/**
4+
* Specifies a trip type for the [MapboxTripStarter].
5+
*/
6+
internal sealed class MapboxTripStarterType {
7+
8+
/**
9+
* The [MapboxTripStarter] will use the best device location for a trip session.
10+
*/
11+
object MapMatching : MapboxTripStarterType()
12+
13+
/**
14+
* The [MapboxTripStarter] will enable replay for the navigation routes.
15+
*/
16+
object ReplayRoute : MapboxTripStarterType()
17+
}

libnavigation-core/src/main/java/com/mapbox/navigation/core/trip/session/TripSessionLocationEngine.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ internal class TripSessionLocationEngine constructor(
3131
) {
3232

3333
val mapboxReplayer: MapboxReplayer by lazy { MapboxReplayer() }
34+
var isReplayEnabled = false
35+
private set
3436

3537
private val replayLocationEngine: LocationEngine by lazy {
3638
replayLocationEngineProvider.invoke(mapboxReplayer)
@@ -66,6 +68,7 @@ internal class TripSessionLocationEngine constructor(
6668
} else {
6769
navigationOptions.locationEngine
6870
}
71+
this.isReplayEnabled = isReplayEnabled
6972
activeLocationEngine?.requestLocationUpdates(
7073
navigationOptions.locationEngineRequest,
7174
locationEngineCallback,
@@ -75,6 +78,7 @@ internal class TripSessionLocationEngine constructor(
7578
}
7679

7780
fun stopLocationUpdates() {
81+
isReplayEnabled = false
7882
onRawLocationUpdate = { }
7983
activeLocationEngine?.removeLocationUpdates(locationEngineCallback)
8084
activeLocationEngine = null

0 commit comments

Comments
 (0)