Skip to content

Commit 8d027f7

Browse files
committed
Add a MapboxTripStarter
1 parent 4ea8475 commit 8d027f7

File tree

26 files changed

+613
-580
lines changed

26 files changed

+613
-580
lines changed
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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
*
52+
* @param mapboxNavigation
53+
*/
54+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
55+
this.mapboxNavigation = mapboxNavigation
56+
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
57+
58+
// Initialize the options to be aware of the location permissions
59+
val context = mapboxNavigation.navigationOptions.applicationContext
60+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
61+
isLocationPermissionGranted.value = granted
62+
63+
// Observe changes to state
64+
observeStateFlow(mapboxNavigation).launchIn(coroutineScope)
65+
}
66+
67+
/**
68+
* Signals that the [mapboxNavigation] instance is being detached.
69+
*
70+
* @param mapboxNavigation
71+
*/
72+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
73+
coroutineScope.cancel()
74+
onTripDisabled(mapboxNavigation)
75+
this.mapboxNavigation = null
76+
}
77+
78+
/**
79+
* [enableMapMatching] will not work unless location permissions have been granted. Refresh
80+
* the location permissions after they are granted to ensure the trip session will start.
81+
*/
82+
fun refreshLocationPermissions() = apply {
83+
mapboxNavigation?.navigationOptions?.applicationContext?.let { context ->
84+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
85+
isLocationPermissionGranted.value = granted
86+
}
87+
}
88+
89+
/**
90+
* This is the default mode for the [MapboxTripStarter]. This can be used to disable
91+
* [enableReplayRoute]. Make sure location permissions have been accepted or this will have no
92+
* effect on the experience.
93+
*/
94+
fun enableMapMatching() = apply {
95+
if (!isLocationPermissionGranted.value) {
96+
refreshLocationPermissions()
97+
}
98+
tripType.value = MapboxTripStarterType.MapMatching
99+
}
100+
101+
/**
102+
* Get the current [ReplayRouteSessionOptions]. This can be used with [enableReplayRoute] to
103+
* make minor adjustments to the current options.
104+
*/
105+
fun getReplayRouteSessionOptions(): ReplayRouteSessionOptions = replayRouteSessionOptions.value
106+
107+
/**
108+
* Enables a mode where the primary route is simulated by an artificial driver. Set the route
109+
* with [MapboxNavigation.setNavigationRoutes]. Can be used with [getReplayRouteSessionOptions]
110+
* to make minor adjustments to the current options.
111+
*
112+
* @param options optional options to use for route replay.
113+
*/
114+
fun enableReplayRoute(
115+
options: ReplayRouteSessionOptions? = null
116+
) = apply {
117+
options?.let { options -> replayRouteSessionOptions.value = options }
118+
tripType.value = MapboxTripStarterType.ReplayRoute
119+
}
120+
121+
@OptIn(ExperimentalCoroutinesApi::class)
122+
private fun observeStateFlow(mapboxNavigation: MapboxNavigation): Flow<*> {
123+
return tripType.flatMapLatest { tripType ->
124+
when (tripType) {
125+
MapboxTripStarterType.ReplayRoute ->
126+
replayRouteSessionOptions.onEach { options ->
127+
onReplayTripEnabled(mapboxNavigation, options)
128+
}
129+
MapboxTripStarterType.MapMatching ->
130+
isLocationPermissionGranted.onEach { granted ->
131+
onMapMatchingEnabled(mapboxNavigation, granted)
132+
}
133+
}
134+
}
135+
}
136+
137+
/**
138+
* Internally called when the trip type has been set to replay route.
139+
*
140+
* @param mapboxNavigation
141+
* @param options parameters for the [ReplayRouteSession]
142+
*/
143+
private fun onReplayTripEnabled(
144+
mapboxNavigation: MapboxNavigation,
145+
options: ReplayRouteSessionOptions
146+
) {
147+
replayRouteTripSession?.onDetached(mapboxNavigation)
148+
replayRouteTripSession = ReplayRouteSession().also {
149+
it.setOptions(options)
150+
it.onAttached(mapboxNavigation)
151+
}
152+
}
153+
154+
/**
155+
* Internally called when the trip type has been set to map matching.
156+
*
157+
* @param mapboxNavigation
158+
* @param granted true when location permissions are accepted, false otherwise
159+
*/
160+
@SuppressLint("MissingPermission")
161+
private fun onMapMatchingEnabled(mapboxNavigation: MapboxNavigation, granted: Boolean) {
162+
if (granted) {
163+
replayRouteTripSession?.onDetached(mapboxNavigation)
164+
replayRouteTripSession = null
165+
mapboxNavigation.startTripSession()
166+
} else {
167+
logI(LOG_CATEGORY) {
168+
"startTripSession was not called. Accept location permissions and call " +
169+
"mapboxTripStarter.refreshLocationPermissions()"
170+
}
171+
onTripDisabled(mapboxNavigation)
172+
}
173+
}
174+
175+
/**
176+
* Internally called when the trip session needs to be stopped.
177+
*
178+
* @param mapboxNavigation
179+
*/
180+
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
181+
replayRouteTripSession?.onDetached(mapboxNavigation)
182+
replayRouteTripSession = null
183+
mapboxNavigation.stopTripSession()
184+
}
185+
186+
companion object {
187+
private const val LOG_CATEGORY = "MapboxTripStarter"
188+
189+
/**
190+
* Construct an instance without registering to [MapboxNavigationApp].
191+
*/
192+
@JvmStatic
193+
fun create() = MapboxTripStarter()
194+
195+
/**
196+
* Get the registered instance or create one and register it to [MapboxNavigationApp].
197+
*/
198+
@JvmStatic
199+
fun getRegisteredInstance(): MapboxTripStarter = MapboxNavigationApp
200+
.getObservers(MapboxTripStarter::class)
201+
.firstOrNull() ?: MapboxTripStarter().also { MapboxNavigationApp.registerObserver(it) }
202+
}
203+
}
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)