Skip to content

Commit 2de2f2f

Browse files
committed
Add a MapboxTripStarter
1 parent d8eeb04 commit 2de2f2f

File tree

22 files changed

+524
-419
lines changed

22 files changed

+524
-419
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 com.mapbox.navigation.core.trip.MapboxTripStarterOptions getOptions();
1029+
method public static com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1030+
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1031+
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
1032+
method public com.mapbox.navigation.core.trip.MapboxTripStarter setOptions(com.mapbox.navigation.core.trip.MapboxTripStarterOptions options);
1033+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarter.Companion Companion;
1034+
}
1035+
1036+
public static final class MapboxTripStarter.Companion {
1037+
method public com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
1038+
}
1039+
1040+
public final class MapboxTripStarterExtra {
1041+
field public static final com.mapbox.navigation.core.trip.MapboxTripStarterExtra INSTANCE;
1042+
field public static final String MAPBOX_TRIP_STARTER_FOLLOW_DEVICE = "MAPBOX_TRIP_STARTER_FOLLOW_DEVICE";
1043+
field public static final String MAPBOX_TRIP_STARTER_REPLAY_ROUTE = "MAPBOX_TRIP_STARTER_REPLAY_ROUTE";
1044+
}
1045+
1046+
@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 {
1047+
}
1048+
1049+
public final class MapboxTripStarterKt {
1050+
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public static void 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);
1051+
}
1052+
1053+
@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarterOptions {
1054+
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getReplayRouteSessionOptions();
1055+
method public String getTripType();
1056+
method public boolean isLocationPermissionGranted();
1057+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder toBuilder();
1058+
property public final boolean isLocationPermissionGranted;
1059+
property public final com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions replayRouteSessionOptions;
1060+
property public final String tripType;
1061+
}
1062+
1063+
public static final class MapboxTripStarterOptions.Builder {
1064+
ctor public MapboxTripStarterOptions.Builder();
1065+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions build();
1066+
method public com.mapbox.navigation.core.trip.MapboxTripStarterOptions.Builder isLocationPermissionGranted(boolean isLocationPermissionGranted);
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: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_FOLLOW_DEVICE
11+
import com.mapbox.navigation.core.trip.MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_REPLAY_ROUTE
12+
import com.mapbox.navigation.core.trip.session.TripSessionState
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.SupervisorJob
16+
import kotlinx.coroutines.cancel
17+
import kotlinx.coroutines.flow.MutableStateFlow
18+
import kotlinx.coroutines.flow.collect
19+
import kotlinx.coroutines.flow.update
20+
import kotlinx.coroutines.launch
21+
22+
/**
23+
* This makes it simpler to start a MapboxNavigation trip session. It can be used to enable replay
24+
* or to follow the device location. The [MapboxTripStarter] is not able to observe when location
25+
* permissions change, so you must notify it of changes. It will check location permissions upon
26+
* attach. The location permissions are not required for replay sessions.
27+
*
28+
* In order to share the state between an App and Android Auto, the instance of this class should
29+
* be shared. That will be done automatically if you use [getRegisteredInstance].
30+
*/
31+
@ExperimentalPreviewMapboxNavigationAPI
32+
class MapboxTripStarter internal constructor() : MapboxNavigationObserver {
33+
34+
private val optionsFlow = MutableStateFlow(MapboxTripStarterOptions.Builder().build())
35+
private var replayRouteTripSession: ReplayRouteSession? = null
36+
private var mapboxNavigation: MapboxNavigation? = null
37+
38+
private lateinit var coroutineScope: CoroutineScope
39+
40+
/**
41+
* Signals that the [mapboxNavigation] instance is ready for use.
42+
* @param mapboxNavigation
43+
*/
44+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
45+
this.mapboxNavigation = mapboxNavigation
46+
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
47+
48+
// Initialize the options to be aware of the location permissions
49+
val context = mapboxNavigation.navigationOptions.applicationContext
50+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
51+
optionsFlow.update { it.toBuilder().isLocationPermissionGranted(granted).build() }
52+
53+
// Observe any changes to the options
54+
coroutineScope.launch {
55+
optionsFlow.collect { options ->
56+
onStarterOptionsChanged(mapboxNavigation, options)
57+
}
58+
}
59+
}
60+
61+
/**
62+
* Signals that the [mapboxNavigation] instance is being detached.
63+
* @param mapboxNavigation
64+
*/
65+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
66+
coroutineScope.cancel()
67+
onTripDisabled(mapboxNavigation)
68+
this.mapboxNavigation = null
69+
}
70+
71+
/**
72+
* Get the options that are currently set.
73+
*/
74+
fun getOptions(): MapboxTripStarterOptions = optionsFlow.value
75+
76+
/**
77+
* Set new options.
78+
*/
79+
fun setOptions(options: MapboxTripStarterOptions) = apply {
80+
checkOptions(options)
81+
this.optionsFlow.value = options
82+
}
83+
84+
/**
85+
* Throws an error if location permissions are set to true but the location permissions are
86+
* not actually granted.
87+
*/
88+
private fun checkOptions(options: MapboxTripStarterOptions) {
89+
val mapboxNavigation = this.mapboxNavigation
90+
checkNotNull(mapboxNavigation) {
91+
"MapboxTripStarter cannot be used while MapboxNavigation is detached."
92+
}
93+
val context = mapboxNavigation.navigationOptions.applicationContext
94+
val granted = options.isLocationPermissionGranted
95+
if (granted && !PermissionsManager.areLocationPermissionsGranted(context)) {
96+
error(
97+
"updateLocationPermissions can only be set to true when location permissions" +
98+
" are granted."
99+
)
100+
}
101+
}
102+
103+
private fun onStarterOptionsChanged(
104+
mapboxNavigation: MapboxNavigation,
105+
options: MapboxTripStarterOptions
106+
) {
107+
if (options.tripType == MAPBOX_TRIP_STARTER_REPLAY_ROUTE) {
108+
onReplayTripEnabled(mapboxNavigation)
109+
} else if (options.tripType == MAPBOX_TRIP_STARTER_FOLLOW_DEVICE &&
110+
options.isLocationPermissionGranted
111+
) {
112+
onTripSessionEnabled(mapboxNavigation)
113+
} else {
114+
onTripDisabled(mapboxNavigation)
115+
}
116+
}
117+
118+
private fun onReplayTripEnabled(mapboxNavigation: MapboxNavigation) {
119+
if (mapboxNavigation.getTripSessionState() != TripSessionState.STARTED &&
120+
!mapboxNavigation.isReplayEnabled()
121+
) {
122+
replayRouteTripSession?.onDetached(mapboxNavigation)
123+
replayRouteTripSession = ReplayRouteSession().also {
124+
it.onAttached(mapboxNavigation)
125+
}
126+
}
127+
}
128+
129+
@SuppressLint("MissingPermission")
130+
private fun onTripSessionEnabled(mapboxNavigation: MapboxNavigation) {
131+
replayRouteTripSession?.onDetached(mapboxNavigation)
132+
replayRouteTripSession = null
133+
if (mapboxNavigation.getTripSessionState() != TripSessionState.STARTED) {
134+
mapboxNavigation.startTripSession()
135+
}
136+
}
137+
138+
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
139+
replayRouteTripSession?.onDetached(mapboxNavigation)
140+
replayRouteTripSession = null
141+
mapboxNavigation.stopTripSession()
142+
}
143+
144+
companion object {
145+
146+
/**
147+
* Get the registered instance or create one and register it to [MapboxNavigationApp].
148+
*/
149+
@JvmStatic
150+
fun getRegisteredInstance(): MapboxTripStarter = MapboxNavigationApp
151+
.getObservers(MapboxTripStarter::class)
152+
.firstOrNull() ?: MapboxTripStarter().also { MapboxNavigationApp.registerObserver(it) }
153+
}
154+
}
155+
156+
/**
157+
* Apply changes to existing options.
158+
*/
159+
@ExperimentalPreviewMapboxNavigationAPI
160+
fun MapboxTripStarter.update(
161+
function: (MapboxTripStarterOptions.Builder) -> MapboxTripStarterOptions.Builder
162+
) {
163+
setOptions(function.invoke(getOptions().toBuilder()).build())
164+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
import androidx.annotation.StringDef
4+
import com.mapbox.navigation.core.directions.session.RoutesObserver
5+
6+
object MapboxTripStarterExtra {
7+
8+
/**
9+
* The [MapboxTripStarter] will use the best device location for a trip session.
10+
*/
11+
const val MAPBOX_TRIP_STARTER_FOLLOW_DEVICE = "MAPBOX_TRIP_STARTER_FOLLOW_DEVICE"
12+
13+
/**
14+
* The [MapboxTripStarter] will enable replay for the navigation routes.
15+
*/
16+
const val MAPBOX_TRIP_STARTER_REPLAY_ROUTE = "MAPBOX_TRIP_STARTER_REPLAY_ROUTE"
17+
18+
/**
19+
* Reason of Routes update. See [RoutesObserver]
20+
*/
21+
@Retention(AnnotationRetention.BINARY)
22+
@StringDef(
23+
MAPBOX_TRIP_STARTER_FOLLOW_DEVICE,
24+
MAPBOX_TRIP_STARTER_REPLAY_ROUTE,
25+
)
26+
annotation class Type
27+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.mapbox.navigation.core.trip
2+
3+
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
4+
import com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions
5+
6+
/**
7+
* Defines options for the automatic trip starter [MapboxTripStarter].
8+
*
9+
* @param tripType Specify the type of trip to start.
10+
* @param isLocationPermissionGranted True if location permissions are granted.
11+
* @param replayRouteSessionOptions options to use when isReplayRouteEnabled is true
12+
*/
13+
@ExperimentalPreviewMapboxNavigationAPI
14+
class MapboxTripStarterOptions private constructor(
15+
@MapboxTripStarterExtra.Type
16+
val tripType: String,
17+
val isLocationPermissionGranted: Boolean,
18+
val replayRouteSessionOptions: ReplayRouteSessionOptions,
19+
) {
20+
/**
21+
* @return builder matching the one used to create this instance
22+
*/
23+
fun toBuilder(): Builder = Builder()
24+
.tripType(tripType)
25+
.isLocationPermissionGranted(isLocationPermissionGranted)
26+
.replayRouteSessionOptions(replayRouteSessionOptions)
27+
28+
/**
29+
* Build your [MapboxTripStarterOptions].
30+
*/
31+
class Builder {
32+
33+
@MapboxTripStarterExtra.Type
34+
private var tripType: String = MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_FOLLOW_DEVICE
35+
private var isLocationPermissionGranted = false
36+
private var replayRouteSessionOptions: ReplayRouteSessionOptions? = null
37+
38+
/**
39+
* Specify the type of trip to start.
40+
*/
41+
fun tripType(@MapboxTripStarterExtra.Type tripType: String) = apply {
42+
this.tripType = tripType
43+
}
44+
45+
/**
46+
* True if location permissions are granted.
47+
*/
48+
fun isLocationPermissionGranted(isLocationPermissionGranted: Boolean) = apply {
49+
this.isLocationPermissionGranted = isLocationPermissionGranted
50+
}
51+
52+
/**
53+
* True if navigation routes will be simulated.
54+
*/
55+
fun replayRouteSessionOptions(options: ReplayRouteSessionOptions) = apply {
56+
this.replayRouteSessionOptions = options
57+
}
58+
59+
/**
60+
* Build the object.
61+
*/
62+
fun build(): MapboxTripStarterOptions {
63+
return MapboxTripStarterOptions(
64+
tripType = tripType,
65+
isLocationPermissionGranted = isLocationPermissionGranted,
66+
replayRouteSessionOptions = replayRouteSessionOptions
67+
?: ReplayRouteSessionOptions.Builder().build()
68+
)
69+
}
70+
}
71+
}

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)
@@ -56,6 +58,7 @@ internal class TripSessionLocationEngine constructor(
5658

5759
@SuppressLint("MissingPermission")
5860
fun startLocationUpdates(isReplayEnabled: Boolean, onRawLocationUpdate: (Location) -> Unit) {
61+
this.isReplayEnabled = isReplayEnabled
5962
logD(LOG_CATEGORY) {
6063
"starting location updates for ${if (isReplayEnabled) "replay " else ""}location engine"
6164
}
@@ -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)