Skip to content

Commit 0346529

Browse files
committed
Add a MapboxTripStarter
1 parent 1419bb1 commit 0346529

File tree

16 files changed

+304
-146
lines changed

16 files changed

+304
-146
lines changed

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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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, it is recommended to make this
29+
* class a singleton. That will be done automatically if you use [getRegisteredInstance].
30+
*/
31+
@ExperimentalPreviewMapboxNavigationAPI
32+
@SuppressLint("MissingPermission")
33+
class MapboxTripStarter private constructor(): MapboxNavigationObserver {
34+
35+
private val optionsFlow = MutableStateFlow(MapboxTripStarterOptions.Builder().build())
36+
private var replayRouteTripSession: ReplayRouteSession? = null
37+
private var mapboxNavigation: MapboxNavigation? = null
38+
39+
private lateinit var coroutineScope: CoroutineScope
40+
41+
/**
42+
* Signals that the [mapboxNavigation] instance is ready for use.
43+
* @param mapboxNavigation
44+
*/
45+
override fun onAttached(mapboxNavigation: MapboxNavigation) {
46+
this.mapboxNavigation = mapboxNavigation
47+
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
48+
49+
// Initialize the options to be aware of the location permissions
50+
val context = mapboxNavigation.navigationOptions.applicationContext
51+
val granted = PermissionsManager.areLocationPermissionsGranted(context)
52+
optionsFlow.update { it.toBuilder().isLocationPermissionGranted(granted).build() }
53+
54+
// Observe any changes to the options
55+
coroutineScope.launch {
56+
optionsFlow.collect { options ->
57+
onStarterOptionsChanged(mapboxNavigation, options)
58+
}
59+
}
60+
}
61+
62+
/**
63+
* Signals that the [mapboxNavigation] instance is being detached.
64+
* @param mapboxNavigation
65+
*/
66+
override fun onDetached(mapboxNavigation: MapboxNavigation) {
67+
coroutineScope.cancel()
68+
onTripDisabled(mapboxNavigation)
69+
this.mapboxNavigation = null
70+
}
71+
72+
/**
73+
* Get the options that are currently set.
74+
*/
75+
fun getOptions(): MapboxTripStarterOptions = optionsFlow.value
76+
77+
/**
78+
* Set new options.
79+
*/
80+
fun setOptions(options: MapboxTripStarterOptions) = apply {
81+
checkOptions(options)
82+
this.optionsFlow.value = options
83+
}
84+
85+
/**
86+
* Apply changes to existing options.
87+
*/
88+
fun update(function: (MapboxTripStarterOptions.Builder) -> MapboxTripStarterOptions.Builder) {
89+
this.optionsFlow.update {
90+
val nextOptions = function.invoke(it.toBuilder()).build()
91+
checkOptions(nextOptions)
92+
nextOptions
93+
}
94+
}
95+
96+
/**
97+
* Throws an error if location permissions are set to true but the location permissions are
98+
* not actually granted.
99+
*/
100+
private fun checkOptions(options: MapboxTripStarterOptions) {
101+
val mapboxNavigation = this.mapboxNavigation
102+
checkNotNull(mapboxNavigation) {
103+
"MapboxTripStarter cannot be used while MapboxNavigation is detached."
104+
}
105+
val context = mapboxNavigation.navigationOptions.applicationContext
106+
val granted = options.isLocationPermissionGranted
107+
if (granted && !PermissionsManager.areLocationPermissionsGranted(context)) {
108+
error(
109+
"updateLocationPermissions can only be set to true when location permissions" +
110+
" have been accepted."
111+
)
112+
}
113+
}
114+
115+
private fun onStarterOptionsChanged(
116+
mapboxNavigation: MapboxNavigation,
117+
options: MapboxTripStarterOptions
118+
) {
119+
if (options.tripType == MAPBOX_TRIP_STARTER_REPLAY_ROUTE) {
120+
onReplayTripEnabled(mapboxNavigation)
121+
} else if (options.tripType == MAPBOX_TRIP_STARTER_FOLLOW_DEVICE &&
122+
options.isLocationPermissionGranted
123+
) {
124+
onTripSessionEnabled(mapboxNavigation)
125+
} else {
126+
onTripDisabled(mapboxNavigation)
127+
}
128+
}
129+
130+
private fun onReplayTripEnabled(mapboxNavigation: MapboxNavigation) {
131+
replayRouteTripSession?.onDetached(mapboxNavigation)
132+
replayRouteTripSession = ReplayRouteSession().also {
133+
it.onAttached(mapboxNavigation)
134+
}
135+
}
136+
137+
private fun onTripSessionEnabled(mapboxNavigation: MapboxNavigation) {
138+
replayRouteTripSession?.onDetached(mapboxNavigation)
139+
replayRouteTripSession = null
140+
if (mapboxNavigation.getTripSessionState() != TripSessionState.STARTED) {
141+
mapboxNavigation.startTripSession()
142+
}
143+
}
144+
145+
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
146+
replayRouteTripSession?.onDetached(mapboxNavigation)
147+
replayRouteTripSession = null
148+
mapboxNavigation.stopTripSession()
149+
}
150+
151+
companion object {
152+
153+
/**
154+
* Construct an instance without registering to [MapboxNavigationApp].
155+
*/
156+
@JvmStatic
157+
fun create() = MapboxTripStarter()
158+
159+
/**
160+
* Get the registered instance or create one and register it to [MapboxNavigationApp].
161+
*/
162+
@JvmStatic
163+
fun getRegisteredInstance(): MapboxTripStarter = MapboxNavigationApp
164+
.getObservers(MapboxTripStarter::class)
165+
.firstOrNull() ?: create().also { MapboxNavigationApp.registerObserver(it) }
166+
}
167+
}
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
@@ -28,6 +28,8 @@ internal class TripSessionLocationEngine constructor(
2828
) {
2929

3030
val mapboxReplayer: MapboxReplayer by lazy { MapboxReplayer() }
31+
var isReplayEnabled = false
32+
private set
3133

3234
private val replayLocationEngine: ReplayLocationEngine by lazy {
3335
ReplayLocationEngine(mapboxReplayer)
@@ -37,6 +39,7 @@ internal class TripSessionLocationEngine constructor(
3739

3840
@SuppressLint("MissingPermission")
3941
fun startLocationUpdates(isReplayEnabled: Boolean, onRawLocationUpdate: (Location) -> Unit) {
42+
this.isReplayEnabled = isReplayEnabled
4043
logD(LOG_CATEGORY) {
4144
"starting location updates for ${if (isReplayEnabled) "replay " else ""}location engine"
4245
}
@@ -55,6 +58,7 @@ internal class TripSessionLocationEngine constructor(
5558
}
5659

5760
fun stopLocationUpdates() {
61+
isReplayEnabled = false
5862
onRawLocationUpdate = { }
5963
locationEngine.removeLocationUpdates(locationEngineCallback)
6064
}

libnavui-app/src/main/java/com/mapbox/navigation/ui/app/internal/State.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.mapbox.navigation.ui.app.internal.camera.CameraState
77
import com.mapbox.navigation.ui.app.internal.destination.Destination
88
import com.mapbox.navigation.ui.app.internal.navigation.NavigationState
99
import com.mapbox.navigation.ui.app.internal.routefetch.RoutePreviewState
10-
import com.mapbox.navigation.ui.app.internal.tripsession.TripSessionStarterState
1110

1211
/**
1312
* Navigation state for internal use.
@@ -20,5 +19,4 @@ data class State constructor(
2019
val audio: AudioGuidanceState = AudioGuidanceState(),
2120
val routes: List<NavigationRoute> = emptyList(),
2221
val previewRoutes: RoutePreviewState = RoutePreviewState.Empty,
23-
val tripSession: TripSessionStarterState = TripSessionStarterState()
2422
)

libnavui-app/src/main/java/com/mapbox/navigation/ui/app/internal/controller/TripSessionStarterStateController.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package com.mapbox.navigation.ui.app.internal.controller
22

33
import android.annotation.SuppressLint
44
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
5+
import com.mapbox.navigation.core.trip.MapboxTripStarter
6+
import com.mapbox.navigation.core.trip.MapboxTripStarterExtra
57
import com.mapbox.navigation.ui.app.internal.Action
68
import com.mapbox.navigation.ui.app.internal.State
79
import com.mapbox.navigation.ui.app.internal.Store
810
import com.mapbox.navigation.ui.app.internal.tripsession.TripSessionStarterAction
9-
import com.mapbox.navigation.ui.app.internal.tripsession.TripSessionStarterState
11+
import com.mapbox.navigation.core.trip.MapboxTripStarterOptions
1012

1113
/**
1214
* The class is responsible to start and stop the `TripSession` for NavigationView.
@@ -21,26 +23,28 @@ class TripSessionStarterStateController(store: Store) : StateController() {
2123

2224
override fun process(state: State, action: Action): State {
2325
if (action is TripSessionStarterAction) {
24-
return state.copy(
25-
tripSession = processTripSessionAction(state.tripSession, action)
26-
)
26+
processTripSessionAction(action)
2727
}
2828
return state
2929
}
3030

3131
private fun processTripSessionAction(
32-
state: TripSessionStarterState,
3332
action: TripSessionStarterAction
34-
): TripSessionStarterState {
35-
return when (action) {
33+
) {
34+
val tripStarter = MapboxTripStarter.getRegisteredInstance()
35+
when (action) {
3636
is TripSessionStarterAction.OnLocationPermission -> {
37-
state.copy(isLocationPermissionGranted = action.granted)
37+
tripStarter.update { it.isLocationPermissionGranted(action.granted) }
3838
}
3939
TripSessionStarterAction.EnableReplayTripSession -> {
40-
state.copy(isReplayEnabled = true)
40+
tripStarter.update {
41+
it.tripType(MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_REPLAY_ROUTE)
42+
}
4143
}
4244
TripSessionStarterAction.EnableTripSession -> {
43-
state.copy(isReplayEnabled = false)
45+
tripStarter.update {
46+
it.tripType(MapboxTripStarterExtra.MAPBOX_TRIP_STARTER_FOLLOW_DEVICE)
47+
}
4448
}
4549
}
4650
}

libnavui-app/src/main/java/com/mapbox/navigation/ui/app/internal/tripsession/TripSessionStarterState.kt

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)