Skip to content

Commit 3b785e4

Browse files
committed
Add a MapboxTripStarter
1 parent 6c4a1d8 commit 3b785e4

File tree

27 files changed

+693
-548
lines changed

27 files changed

+693
-548
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

libnavigation-core/api/current.txt

Lines changed: 50 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,55 @@ 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 enableReplayRoute(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions? options = null);
1030+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions getOptions();
1031+
method public static com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1032+
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1033+
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1034+
method @kotlin.jvm.Throws(exceptionClasses=IllegalStateException::class) public void setLocationPermissionGranted(boolean granted) throws java.lang.IllegalStateException;
1035+
method public com.mapbox.navigation.core.trip.MapboxTripStarter setOptions(com.mapbox.navigation.core.trip.MapboxTripStarterOptions options);
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+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarterExtra {
1045+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarterExtra INSTANCE;
1046+
field public static final String MAPBOX_TRIP_STARTER_FOLLOW_DEVICE = "MAPBOX_TRIP_STARTER_FOLLOW_DEVICE";
1047+
field public static final String MAPBOX_TRIP_STARTER_REPLAY_ROUTE = "MAPBOX_TRIP_STARTER_REPLAY_ROUTE";
1048+
}
1049+
1050+
@StringDef({com.mapbox.navigation.core.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_FOLLOW_DEVICE, com.mapbox.navigation.core.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_REPLAY_ROUTE}) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public static @interface MapboxTripStarterExtra.Type {
1051+
}
1052+
1053+
public final class MapboxTripStarterKt {
1054+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public static com.mapbox.navigation.core.trip.MapboxTripStarter update(com.mapbox.navigation.core.trip.MapboxTripStarter, kotlin.jvm.functions.Function1<? super com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder,com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder> function);
1055+
}
1056+
1057+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarterOptions {
1058+
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getReplayRouteSessionOptions();
1059+
method public String getTripType();
1060+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder toBuilder();
1061+
property public final com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions replayRouteSessionOptions;
1062+
property public final String tripType;
1063+
}
1064+
1065+
public static final class MapboxTripStarterOptions.Builder {
1066+
ctor public MapboxTripStarterOptions.Builder();
1067+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions build();
1068+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder replayRouteSessionOptions(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions options);
1069+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder tripType(@com.mapbox.navigation.core.trip.MapboxTripStarterExtra.Type String tripType);
1070+
}
1071+
1072+
}
1073+
10241074
package com.mapbox.navigation.core.trip.session {
10251075

10261076
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: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
import android.annotation.SuppressLint
4+
import androidx.annotation.VisibleForTesting
5+
import com.mapbox.android.core.permissions.PermissionsManager
6+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
7+
import com.mapbox.navigation.core.MapboxNavigation
8+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
9+
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
10+
import com.mapbox.navigation.core.replay.route.ReplayRouteSession
11+
import com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions
12+
import com.mapbox.navigation.core.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_FOLLOW_DEVICE
13+
import com.mapbox.navigation.core.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_REPLAY_ROUTE
14+
import com.mapbox.navigation.core.trip.session.TripSessionState
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.SupervisorJob
18+
import kotlinx.coroutines.cancel
19+
import kotlinx.coroutines.flow.MutableStateFlow
20+
import kotlinx.coroutines.flow.collect
21+
import kotlinx.coroutines.flow.combine
22+
import kotlinx.coroutines.flow.update
23+
import kotlinx.coroutines.launch
24+
import kotlin.jvm.Throws
25+
26+
/**
27+
* This makes it simpler to start a MapboxNavigation trip session. It can be used to enable replay
28+
* or to follow the device location. The [MapboxTripStarter] is not able to observe when location
29+
* permissions change, so you must notify it of changes. It will check location permissions upon
30+
* attach. The location permissions are not required for replay.
31+
*
32+
* There should be one instance of this class at a time. For example, an app Activity and car
33+
* Session will need to use the same instance. That will be done automatically if you use
34+
* [getRegisteredInstance].
35+
*/
36+
@ExperimentalPreviewMapboxNavigationAPI
37+
class MapboxTripStarter internal constructor() : MapboxNavigationObserver {
38+
39+
private val optionsFlow = MutableStateFlow(MapboxTripStarterOptions.Builder().build())
40+
private val stateFlow = MutableStateFlow(MapboxTripStarterState())
41+
private var replayRouteTripSession: ReplayRouteSession? = null
42+
private var mapboxNavigation: MapboxNavigation? = null
43+
44+
@VisibleForTesting
45+
internal var coroutineScopeProvider: () -> CoroutineScope = {
46+
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
47+
}
48+
private lateinit var coroutineScope: CoroutineScope
49+
50+
/**
51+
* Signals that the [mapboxNavigation] instance is ready for use.
52+
* @param mapboxNavigation
53+
*/
54+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
55+
this.mapboxNavigation = mapboxNavigation
56+
coroutineScope = coroutineScopeProvider()
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+
stateFlow.update { it.copy(isLocationPermissionGranted = granted) }
62+
63+
// Observe any changes to the options
64+
coroutineScope.launch {
65+
combine(optionsFlow, stateFlow, ::Pair).collect {
66+
onStarterOptionsChanged(mapboxNavigation, it.first, it.second)
67+
}
68+
}
69+
}
70+
71+
/**
72+
* Signals that the [mapboxNavigation] instance is being detached.
73+
* @param mapboxNavigation
74+
*/
75+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
76+
coroutineScope.cancel()
77+
onTripDisabled(mapboxNavigation)
78+
this.mapboxNavigation = null
79+
}
80+
81+
/**
82+
* Get the options that are currently set.
83+
*/
84+
fun getOptions(): MapboxTripStarterOptions = optionsFlow.value
85+
86+
/**
87+
* Set new options.
88+
*/
89+
fun setOptions(options: MapboxTripStarterOptions) = apply {
90+
this.optionsFlow.value = options
91+
}
92+
93+
/**
94+
* Get the location permission state. This may not reflect the actual permissions. Used for
95+
* verifying the tests and will likely be removed.
96+
*/
97+
@VisibleForTesting
98+
internal fun getLocationPermissionGranted(): Boolean =
99+
stateFlow.value.isLocationPermissionGranted
100+
101+
/**
102+
* Set the location permission state. [MAPBOX_TRIP_STARTER_FOLLOW_DEVICE] will not work unless
103+
* location permissions have been granted. If the location permissions are not granted and
104+
* this is set to true, an error will be thrown.
105+
*/
106+
@Throws(IllegalStateException::class)
107+
fun setLocationPermissionGranted(granted: Boolean) {
108+
val nextState = stateFlow.value.copy(isLocationPermissionGranted = granted)
109+
checkState(nextState)
110+
stateFlow.value = nextState
111+
}
112+
113+
/**
114+
* Enables a mode where the primary route is simulated by an artificial driver. Set the route
115+
* with [MapboxNavigation.setNavigationRoutes].
116+
*
117+
* @param options optional options to use for route replay.
118+
*/
119+
fun enableReplayRoute(
120+
options: ReplayRouteSessionOptions? = null
121+
) = apply {
122+
update { builder ->
123+
options?.let { builder.replayRouteSessionOptions(options) }
124+
builder.tripType(MAPBOX_TRIP_STARTER_REPLAY_ROUTE)
125+
}
126+
}
127+
128+
/**
129+
* Throws an error if location permissions are set to true but the location permissions are
130+
* not actually granted.
131+
*/
132+
private fun checkState(state: MapboxTripStarterState) {
133+
val mapboxNavigation = this.mapboxNavigation
134+
checkNotNull(mapboxNavigation) {
135+
"MapboxTripStarter cannot be used while MapboxNavigation is detached."
136+
}
137+
val context = mapboxNavigation.navigationOptions.applicationContext
138+
val granted = state.isLocationPermissionGranted
139+
if (granted && !PermissionsManager.areLocationPermissionsGranted(context)) {
140+
error(
141+
"updateLocationPermissions can only be set to true when location permissions" +
142+
" are granted."
143+
)
144+
}
145+
}
146+
147+
private fun onStarterOptionsChanged(
148+
mapboxNavigation: MapboxNavigation,
149+
options: MapboxTripStarterOptions,
150+
state: MapboxTripStarterState,
151+
) {
152+
if (options.tripType == MAPBOX_TRIP_STARTER_REPLAY_ROUTE) {
153+
onReplayTripEnabled(mapboxNavigation)
154+
} else if (options.tripType == MAPBOX_TRIP_STARTER_FOLLOW_DEVICE &&
155+
state.isLocationPermissionGranted
156+
) {
157+
onTripSessionEnabled(mapboxNavigation)
158+
} else {
159+
onTripDisabled(mapboxNavigation)
160+
}
161+
}
162+
163+
private fun onReplayTripEnabled(mapboxNavigation: MapboxNavigation) {
164+
if (mapboxNavigation.getTripSessionState() != TripSessionState.STARTED ||
165+
!mapboxNavigation.isReplayEnabled()
166+
) {
167+
replayRouteTripSession?.onDetached(mapboxNavigation)
168+
replayRouteTripSession = ReplayRouteSession().also {
169+
it.onAttached(mapboxNavigation)
170+
}
171+
}
172+
}
173+
174+
@SuppressLint("MissingPermission")
175+
private fun onTripSessionEnabled(mapboxNavigation: MapboxNavigation) {
176+
replayRouteTripSession?.onDetached(mapboxNavigation)
177+
replayRouteTripSession = null
178+
mapboxNavigation.startTripSession()
179+
}
180+
181+
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
182+
replayRouteTripSession?.onDetached(mapboxNavigation)
183+
replayRouteTripSession = null
184+
mapboxNavigation.stopTripSession()
185+
}
186+
187+
companion object {
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+
}
204+
205+
/**
206+
* Apply changes to existing options.
207+
*/
208+
@ExperimentalPreviewMapboxNavigationAPI
209+
fun MapboxTripStarter.update(
210+
function: (MapboxTripStarterOptions.Builder) -> MapboxTripStarterOptions.Builder
211+
): MapboxTripStarter = apply {
212+
setOptions(function.invoke(getOptions().toBuilder()).build())
213+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
import androidx.annotation.StringDef
4+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
5+
import com.mapbox.navigation.core.directions.session.RoutesObserver
6+
7+
/**
8+
* Specifies a trip type for the [MapboxTripStarter].
9+
*/
10+
@ExperimentalPreviewMapboxNavigationAPI
11+
object MapboxTripStarterExtra {
12+
13+
/**
14+
* The [MapboxTripStarter] will use the best device location for a trip session.
15+
*/
16+
const val MAPBOX_TRIP_STARTER_FOLLOW_DEVICE = "MAPBOX_TRIP_STARTER_FOLLOW_DEVICE"
17+
18+
/**
19+
* The [MapboxTripStarter] will enable replay for the navigation routes.
20+
*/
21+
const val MAPBOX_TRIP_STARTER_REPLAY_ROUTE = "MAPBOX_TRIP_STARTER_REPLAY_ROUTE"
22+
23+
/**
24+
* Reason of Routes update. See [RoutesObserver]
25+
*/
26+
@Retention(AnnotationRetention.BINARY)
27+
@StringDef(
28+
MAPBOX_TRIP_STARTER_FOLLOW_DEVICE,
29+
MAPBOX_TRIP_STARTER_REPLAY_ROUTE,
30+
)
31+
annotation class Type
32+
}

0 commit comments

Comments
 (0)