Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/unreleased/features/6794.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
22 changes: 22 additions & 0 deletions libnavigation-core/api/current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ package com.mapbox.navigation.core {
method public com.mapbox.navigation.core.trip.session.TripSessionState getTripSessionState();
method public Integer? getZLevel();
method public boolean isDestroyed();
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public boolean isReplayEnabled();
method public boolean isRunningForegroundService();
method @com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI @kotlin.jvm.Throws(exceptionClasses=IllegalArgumentException::class) public void moveRoutesFromPreviewToNavigator() throws java.lang.IllegalArgumentException;
method public void navigateNextRouteLeg(com.mapbox.navigation.core.trip.session.LegIndexUpdatedCallback callback);
Expand Down Expand Up @@ -1021,6 +1022,27 @@ package com.mapbox.navigation.core.telemetry.events {

}

package com.mapbox.navigation.core.trip {

@com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI public final class MapboxTripStarter implements com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver {
method public static com.mapbox.navigation.core.trip.MapboxTripStarter create();
method public com.mapbox.navigation.core.trip.MapboxTripStarter enableMapMatching();
method public com.mapbox.navigation.core.trip.MapboxTripStarter enableReplayRoute(com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions? options = null);
method public static com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
method public com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions getReplayRouteSessionOptions();
method public void onAttached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
method public void onDetached(com.mapbox.navigation.core.MapboxNavigation mapboxNavigation);
method public com.mapbox.navigation.core.trip.MapboxTripStarter refreshLocationPermissions();
field public static final com.mapbox.navigation.core.trip.MapboxTripStarter.Companion Companion;
}

public static final class MapboxTripStarter.Companion {
method public com.mapbox.navigation.core.trip.MapboxTripStarter create();
method public com.mapbox.navigation.core.trip.MapboxTripStarter getRegisteredInstance();
}

}

package com.mapbox.navigation.core.trip.session {

public fun interface BannerInstructionsObserver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,15 @@ class MapboxNavigation @VisibleForTesting internal constructor(
@ExperimentalPreviewMapboxNavigationAPI
val mapboxReplayer: MapboxReplayer by lazy { tripSessionLocationEngine.mapboxReplayer }

/**
* True when [startReplayTripSession] has been called.
* Will be false after [stopTripSession] is called.
*/
@ExperimentalPreviewMapboxNavigationAPI
fun isReplayEnabled(): Boolean {
return tripSessionLocationEngine.isReplayEnabled
}

/**
* Starts listening for location updates and enters an `Active Guidance` state if there's a primary route available
* or a `Free Drive` state otherwise.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package com.mapbox.navigation.core.trip

import android.annotation.SuppressLint
import com.mapbox.android.core.permissions.PermissionsManager
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
import com.mapbox.navigation.core.MapboxNavigation
import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp
import com.mapbox.navigation.core.lifecycle.MapboxNavigationObserver
import com.mapbox.navigation.core.replay.route.ReplayRouteSession
import com.mapbox.navigation.core.replay.route.ReplayRouteSessionOptions
import com.mapbox.navigation.core.trip.MapboxTripStarter.Companion.getRegisteredInstance
import com.mapbox.navigation.utils.internal.logI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

/**
* The [MapboxTripStarter] makes it simpler to switch between a trip session and replay.
*
* This is not able to observe when location permissions change, so you may need to refresh the
* state with [refreshLocationPermissions]. Location permissions are not required for replay.
*
* There should be one instance of this class at a time. For example, an app Activity and car
* Session will need to use the same instance. That will be done automatically if you use
* [getRegisteredInstance].
*/
@ExperimentalPreviewMapboxNavigationAPI
class MapboxTripStarter internal constructor() : MapboxNavigationObserver {

private val tripType = MutableStateFlow<MapboxTripStarterType>(
MapboxTripStarterType.MapMatching
)
private val replayRouteSessionOptions = MutableStateFlow(
ReplayRouteSessionOptions.Builder().build()
)
private val isLocationPermissionGranted = MutableStateFlow(false)
private var replayRouteTripSession: ReplayRouteSession? = null
private var mapboxNavigation: MapboxNavigation? = null

private lateinit var coroutineScope: CoroutineScope

/**
* Signals that the [mapboxNavigation] instance is ready for use.
*
* @param mapboxNavigation
*/
override fun onAttached(mapboxNavigation: MapboxNavigation) {
this.mapboxNavigation = mapboxNavigation
coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

// Initialize the options to be aware of the location permissions
val context = mapboxNavigation.navigationOptions.applicationContext
val granted = PermissionsManager.areLocationPermissionsGranted(context)
isLocationPermissionGranted.value = granted

// Observe changes to state
observeStateFlow(mapboxNavigation).launchIn(coroutineScope)
}

/**
* Signals that the [mapboxNavigation] instance is being detached.
*
* @param mapboxNavigation
*/
override fun onDetached(mapboxNavigation: MapboxNavigation) {
coroutineScope.cancel()
onTripDisabled(mapboxNavigation)
this.mapboxNavigation = null
}

/**
* [enableMapMatching] will not work unless location permissions have been granted. Refresh
* the location permissions after they are granted to ensure the trip session will start.
*/
fun refreshLocationPermissions() = apply {
mapboxNavigation?.navigationOptions?.applicationContext?.let { context ->
val granted = PermissionsManager.areLocationPermissionsGranted(context)
isLocationPermissionGranted.value = granted
}
}

/**
* This is the default mode for the [MapboxTripStarter]. This can be used to disable
* [enableReplayRoute]. Make sure location permissions have been accepted or this will have no
* effect on the experience.
*/
fun enableMapMatching() = apply {
if (!isLocationPermissionGranted.value) {
refreshLocationPermissions()
}
tripType.value = MapboxTripStarterType.MapMatching
}

/**
* Get the current [ReplayRouteSessionOptions]. This can be used with [enableReplayRoute] to
* make minor adjustments to the current options.
*/
fun getReplayRouteSessionOptions(): ReplayRouteSessionOptions = replayRouteSessionOptions.value

/**
* Enables a mode where the primary route is simulated by an artificial driver. Set the route
* with [MapboxNavigation.setNavigationRoutes]. Can be used with [getReplayRouteSessionOptions]
* to make minor adjustments to the current options.
*
* @param options optional options to use for route replay.
*/
fun enableReplayRoute(
options: ReplayRouteSessionOptions? = null
) = apply {
options?.let { options -> replayRouteSessionOptions.value = options }
tripType.value = MapboxTripStarterType.ReplayRoute
}

@OptIn(ExperimentalCoroutinesApi::class)
private fun observeStateFlow(mapboxNavigation: MapboxNavigation): Flow<*> {
return tripType.flatMapLatest { tripType ->
when (tripType) {
MapboxTripStarterType.ReplayRoute ->
replayRouteSessionOptions.onEach { options ->
onReplayTripEnabled(mapboxNavigation, options)
}
MapboxTripStarterType.MapMatching ->
isLocationPermissionGranted.onEach { granted ->
onMapMatchingEnabled(mapboxNavigation, granted)
}
}
}
}

/**
* Internally called when the trip type has been set to replay route.
*
* @param mapboxNavigation
* @param options parameters for the [ReplayRouteSession]
*/
private fun onReplayTripEnabled(
mapboxNavigation: MapboxNavigation,
options: ReplayRouteSessionOptions
) {
replayRouteTripSession?.onDetached(mapboxNavigation)
replayRouteTripSession = ReplayRouteSession().also {
it.setOptions(options)
it.onAttached(mapboxNavigation)
}
}

/**
* Internally called when the trip type has been set to map matching.
*
* @param mapboxNavigation
* @param granted true when location permissions are accepted, false otherwise
*/
@SuppressLint("MissingPermission")
private fun onMapMatchingEnabled(mapboxNavigation: MapboxNavigation, granted: Boolean) {
if (granted) {
replayRouteTripSession?.onDetached(mapboxNavigation)
replayRouteTripSession = null
mapboxNavigation.startTripSession()
} else {
logI(LOG_CATEGORY) {
"startTripSession was not called. Accept location permissions and call " +
"mapboxTripStarter.refreshLocationPermissions()"
}
onTripDisabled(mapboxNavigation)
}
}

/**
* Internally called when the trip session needs to be stopped.
*
* @param mapboxNavigation
*/
private fun onTripDisabled(mapboxNavigation: MapboxNavigation) {
replayRouteTripSession?.onDetached(mapboxNavigation)
replayRouteTripSession = null
mapboxNavigation.stopTripSession()
}

companion object {
private const val LOG_CATEGORY = "MapboxTripStarter"

/**
* Construct an instance without registering to [MapboxNavigationApp].
*/
@JvmStatic
fun create() = MapboxTripStarter()

/**
* Get the registered instance or create one and register it to [MapboxNavigationApp].
*/
@JvmStatic
fun getRegisteredInstance(): MapboxTripStarter = MapboxNavigationApp
.getObservers(MapboxTripStarter::class)
.firstOrNull() ?: MapboxTripStarter().also { MapboxNavigationApp.registerObserver(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mapbox.navigation.core.trip

/**
* Specifies a trip type for the [MapboxTripStarter].
*/
internal sealed class MapboxTripStarterType {

/**
* The [MapboxTripStarter] will use the best device location for a trip session.
*/
object MapMatching : MapboxTripStarterType()

/**
* The [MapboxTripStarter] will enable replay for the navigation routes.
*/
object ReplayRoute : MapboxTripStarterType()
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ internal class TripSessionLocationEngine constructor(
) {

val mapboxReplayer: MapboxReplayer by lazy { MapboxReplayer() }
var isReplayEnabled = false
private set

private val replayLocationEngine: LocationEngine by lazy {
replayLocationEngineProvider.invoke(mapboxReplayer)
Expand Down Expand Up @@ -66,6 +68,7 @@ internal class TripSessionLocationEngine constructor(
} else {
navigationOptions.locationEngine
}
this.isReplayEnabled = isReplayEnabled
activeLocationEngine?.requestLocationUpdates(
navigationOptions.locationEngineRequest,
locationEngineCallback,
Expand All @@ -75,6 +78,7 @@ internal class TripSessionLocationEngine constructor(
}

fun stopLocationUpdates() {
isReplayEnabled = false
onRawLocationUpdate = { }
activeLocationEngine?.removeLocationUpdates(locationEngineCallback)
activeLocationEngine = null
Expand Down
Loading