Testing and development

The Navigation SDK includes functionality that will help you build and test your navigation application while it's in development. You can:

  • Replay routes to preview the navigation experience in a development environment
  • Leverage the Navigation SDK's modularization options to customize certain functionalities without increasing your project's binary-size

Route replay

Testing and previewing the navigation experience is an important part of building with the Navigation SDK. For example, you may want to investigate how turn-by-turn navigation performed during test rides as you develop, test, and demo your work. You can use the SDK's route replay functionality to go back in time and replay real rides.

Implement replay

The Navigation SDK's replay framework is comprised of two core classes:

  • MapboxReplayer handles shifting historical event timestamps into real-time events.
  • ReplayEventBase is an interface for anything with a time unit. The time basis can be months in the past, in the future, or starting at 0.0.

There are also several helper classes you can use to create and listen to replay events:

Record and use history files

Record a history file

To record a trip as a history file:

  1. Start recording using HistoryRecorderOptions:

    mapboxNavigation = MapboxNavigation(
      NavigationOptions.Builder(this)
          ...
          .historyRecorderOptions(
              HistoryRecorderOptions.Builder()
                  .enabled(true)
                  .build()
          )
          ...
          .build()
    )
    
  2. At the end of the recording, save the history string:

    val historyRecorder = mapboxNavigation.historyRecorder()
    historyRecorder.saveHistory { filePath ->
        if (filePath != null) {
            // save to a file or upload to the cloud
        }
    }
    

Retrieve history files

There are two ways to retrieve a recorded history file to be used on a device:

  • Store the JSON file in a directory and read the file. The Navigation SDK test app's ReplayHistoryActivity example uses this approach. The file is stored in the app's assets folder.
  • Host history files somewhere in a cloud-based backend database (for example Firebase, Amazon Web Services, or GitHub Pages).

Replay history data

After you've retrieved the history file, you can replay the history data using MapboxReplayer:

  1. Map the history string to replay events using ReplayHistoryMapper.
  2. Push the events into the MapboxReplayer.pushEvents(..).
  3. Substitute your LocationEngine with a ReplayLocationEngine.
  4. Call MapboxReplayer.play().
  5. When done call MapboxReplayer.finish() to stop and clean up the replayer.
example
Replay history

Replay history using real data by reading a history string from a file and replaying it with the Navigation SDK.

Create custom events

You can create custom events and play them on their own or play them through history files.

Replay custom events

You can replay any event to test on its own (without a history file), for example a map gesture event or zooming in and out of the map.

Start by setting up a custom event that implements ReplayEventBase. ReplayEventBase is a base interface event that can be implemented with any event you want to test:

private data class ReplayEventInitialRoute(
    override val eventTimestamp: Double,
    val coordinates: List<LatLng>
) : ReplayEventBase

Then, set up the MapboxReplayer and ReplayHistoryMapper classes:

val mapboxReplayer = MapboxReplayer()

val replayHistoryMapper = ReplayHistoryMapper(customEventMappper, logger)

Create a ReplayHistoryLocationEngine object and give it to MapboxNavigation via the NavigationOptions when you build a MapboxNavigation object. This way, MapboxNavigation will use the Location objects:

val navigationOptions = NavigationOptions.Builder(context)
    .accessToken("token")
    .locationEngine(ReplayLocationEngine(mapboxReplayer))
    .build()
    
mapboxNavigation = MapboxNavigation(navigationOptions)

Get history events:

val replayEvents = replayHistoryMapper.mapToReplayEvents(rideHistoryExampleJsonString)

Then, you can give the custom event to the MapboxReplayer via MapboxReplayer.pushEvents():

mapboxReplayer.pushEvents(replayEvents)

The MapboxReplayer also has a seekTo() method function to move the player to timestamp that you want to play.

Finally, start replaying the events:

playReplay.setOnClickListener {
    mapboxReplayer.play(context)
}

Replay custom events from history files

Navigation history files have several predefined events, which can be retrieved with the MapboxNavigation class:

val historyString = mapboxNavigation.retrieveHistory()

Custom events can also be created and added to the history file:

mapboxNavigation.addHistoryEvent(eventTypeString, eventJsonProperties)

You must create a custom class that implements the Navigation SDK's CustomEventMapper interface to replay the history file's custom events.

private class ReplayCustomEventMapper : CustomEventMapper {
    override fun map(eventType: String, event: LinkedTreeMap<*, *>): ReplayEventBase? {
        return when (eventType) {
            "initial_route" -> {
                val properties = event["properties"] as LinkedTreeMap<*, *>
                val routeOptions = properties["routeOptions"] as LinkedTreeMap<*, *>
                val coordinates = routeOptions["coordinates"] as List<List<Double>>
                val coordinatesLatLng = coordinates.map { LatLng(it[1], it[0]) }
                ReplayEventInitialRoute(
                        eventTimestamp = event["event_timestamp"] as Double,
                        coordinates = coordinatesLatLng
                )
            }
            else -> null
        }
    }
}

Modularization

The Navigation SDK's architecture follows a build-time modular design, which allows you to completely replace certain functionalities with custom implementations without increasing your project's binary-size.

The goal of modularization is to enable swapping SDK modules during compilation instead of exposing runtime configuration options and relying on third-party tools to remove unused code. The SDK streamlines independent units of code into modules where possible and uses interface contracts to connect the modules. Each module has a corresponding default implementation artifact that is shipped independently and referenced by the SDK. This means default implementations can be replaced with build system tools.

Architecture overview

The Navigation SDK's MapboxNavigation class depends on three interface contracts: Router, TripNotification, and Logger. The SDK composites the implementation of these contracts at runtime to receive or send the required information.

Router

The Navigation SDK's Router interface defines a contract to create a DirectionsRoute. This DirectionsRoute is eventually used to compute turn-by-turn instructions and visualize the route on the map.

By default, the Navigation SDK provides a hybrid router implementation that:

  1. Requests the route from the Mapbox Directions API if the connectivity is available.

  2. Generates the route locally on the device if the SDK had a chance to pre-cache the routing resources. This is a fallback if there's no connectivity.

    group: "com.mapbox.navigation", module: "router"
    

TripNotification

TripNotification is the contract definition that fuels the Navigation SDK's notifications. These notifications are required by the Android-system foreground service that's created whenever a trip session is started. While the default Mapbox-provided implementation does allow for a certain amount of customization, completely replacing the implementation makes it possible to provide a completely custom Notification layout with personalized action buttons.

group: "com.mapbox.navigation", module: "notification"

Logger

By default, the Mapbox-provided implementation of the logger uses the android.util.Log class to log any info, warning, or error level log messages. Replacing the logger implementation allows for sending or processing this information by any third-party platform to provide more value and actionable debug data.

group: "com.mapbox.common", module: "logger"

Replacing a module

The Mapbox Navigation SDK comes bundled with runtime dependencies for all the default module implementations. You don't have to take any action to receive the SDK's default features. If your application requires additional levels of customization, you can remove any module listed from the build and replace it with a custom implementation in three steps.

For example, here's how to use Gradle to replace the logger module:

  1. Exclude the module's default implementation from the Mapbox Navigation SDK artifact's dependencies by referencing the group and artifact IDs:

    implementation("com.mapbox.navigation:core:2.0.0-rc.2.1") {
        exclude group: "com.mapbox.common", module: "logger"
    }
    
  2. Add the Mapbox Annotation Processor dependency:

    compileOnly("com.mapbox.base:annotations:0.5.0")
    kapt("com.mapbox.base:annotations-processor:0.5.0")
    
  3. Provide the custom logger implementation by creating and annotating a class that implements the contract:

    @MapboxModule(MapboxModuleType.CommonLogger)
    @Keep
    object MyLogger : Logger {
    ...
    }
    

Now, compiling the project will generate all the required classes and doesn't require any additional actions.

For more information and advanced usage examples, including testing and dependency injection, read the @MapboxModule annotation's documentation and visit the Mapbox Base Android repository.