Skip to content

Commit ae7f79b

Browse files
committed
Add a MapboxTripStarter
1 parent 6c4a1d8 commit ae7f79b

File tree

27 files changed

+676
-548
lines changed

27 files changed

+676
-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: 49 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,54 @@ 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.MapboxTripStarterOptions getOptions();
1030+
method public static com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1031+
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1032+
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1033+
method @kotlin.jvm.Throws(exceptionClasses=IllegalStateException::class) public void setLocationPermissionGranted(boolean granted) throws java.lang.IllegalStateException;
1034+
method public com.mapbox.navigation.core.trip.MapboxTripStarter setOptions(com.mapbox.navigation.core.trip.MapboxTripStarterOptions options);
1035+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarter.Companion Companion;
1036+
}
1037+
1038+
public static final class MapboxTripStarter.Companion {
1039+
method public com.mapbox.navigation.core.trip.MapboxTripStarter create();
1040+
method public com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1041+
}
1042+
1043+
public final class MapboxTripStarterExtra {
1044+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarterExtra INSTANCE;
1045+
field public static final String MAPBOX_TRIP_STARTER_FOLLOW_DEVICE = "MAPBOX_TRIP_STARTER_FOLLOW_DEVICE";
1046+
field public static final String MAPBOX_TRIP_STARTER_REPLAY_ROUTE = "MAPBOX_TRIP_STARTER_REPLAY_ROUTE";
1047+
}
1048+
1049+
@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 {
1050+
}
1051+
1052+
public final class MapboxTripStarterKt {
1053+
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);
1054+
}
1055+
1056+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarterOptions {
1057+
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getReplayRouteSessionOptions();
1058+
method public String getTripType();
1059+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder toBuilder();
1060+
property public final com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions replayRouteSessionOptions;
1061+
property public final String tripType;
1062+
}
1063+
1064+
public static final class MapboxTripStarterOptions.Builder {
1065+
ctor public MapboxTripStarterOptions.Builder();
1066+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions build();
1067+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder replayRouteSessionOptions(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions options);
1068+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder tripType(@com.mapbox.navigation.core.trip.MapboxTripStarterExtra.Type String tripType);
1069+
}
1070+
1071+
}
1072+
10241073
package com.mapbox.navigation.core.trip.session {
10251074

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

0 commit comments

Comments
 (0)