Skip to content

50,000 free calls a month, card-free. Get an API key →

Documentation menu
docs / android · raw .md

Android API reference

The public Kotlin surface of ai.mapmap:core 0.3.0, the current release on Maven Central. Install coordinates, credentials and the root-repository Gradle rule are on SDKs & installation; this page is the class-by-class reference.

Requires minSdk 26. The library is a thin, idiomatic Kotlin layer over the Rust navigation core (libsn_nav_core.so, packaged for arm64-v8a, armeabi-v7a and x86_64). The UniFFI-generated bindings live in uniffi.mapmap (and uniffi.ferrostar, served by the same native library); everything under ai.mapmap is the supported surface. Types named below without a package (e.g. TerritoryInfo, AdrProfile, GuidanceUpdate) are generated records/enums in uniffi.mapmap.

PackageClasses
ai.mapmapMapMap
ai.mapmap.territoryTerritoryStore
ai.mapmap.routingOfflineRouting, OfflineRouter, ValhallaMobileRouter, ValhallaConfigWriter
ai.mapmap.adrAdrCompliance
ai.mapmap.navNavigationEngine, NavigationState, GuidanceDisplay, LocationProvider, LocationSample, AndroidLocationProvider, ReplayLocationProvider, VoiceGuidance, VoiceProsody, contributingProbe, ProbeUploader, HttpProbeUploader, ProbeUploadOutcome
ai.mapmap.replayDriveCorpus, DriveHeader, DriveFix, CorpusFormatException

What 0.3.0 added over 0.2.0, all additive: offline search and a local map style on TerritoryStore (openSearch, territoryStyle), a speaking signal on VoiceGuidance (isSpeaking, onSpeakingChanged), and the per-trip probe contribution tap in ai.mapmap.nav. Nothing was removed and no existing signature changed.

ai.mapmap.MapMap

Entry object.

kotlin
object MapMap {
    fun ensureLoaded()
    fun defaultGuidanceConfig(): GuidanceConfig
    fun defaultAdrProfile(): AdrProfile
}
  • ensureLoaded(): eagerly load the native core and verify the bindings match it. Optional (the first native call loads lazily), but calling it at app start surfaces packaging problems as an immediate UnsatisfiedLinkError instead of a mid-drive crash.
  • defaultGuidanceConfig(): a GuidanceConfig with the core's defaults.
  • defaultAdrProfile(): an AdrProfile with the core's defaults: EU maximum authorised dimensions (Council Directive 96/53/EC), no dangerous goods.

ai.mapmap.territory.TerritoryStore

Coroutine-friendly store of signed offline territory packages, wrapping the core's TerritoryManager. Every package is ed25519-signature and BLAKE3-layer-hash verified before promotion; verification is fail-closed (a package that fails leaves no trace on disk). All calls hop to the constructor's dispatcher (Dispatchers.IO by default); installs read whole archives and must never run on the main thread. Implements AutoCloseable.

kotlin
class TerritoryStore(
    rootDir: File,
    verifyingKeyHex: String,               // factory public key, 64 hex; pin at build time
    dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : AutoCloseable {
    val territories: StateFlow<List<TerritoryInfo>>

    suspend fun refresh(): List<TerritoryInfo>
    suspend fun install(snpkg: File): TerritoryInfo
    suspend fun install(snpkg: File, cancel: CancelToken): InstallOutcome
    suspend fun installDirectory(packageDir: File): TerritoryInfo
    suspend fun installDirectory(packageDir: File, cancel: CancelToken): InstallOutcome
    suspend fun setActive(territoryId: String)
    suspend fun active(): String?
    suspend fun activeTerritory(): TerritoryInfo?
    suspend fun layerPath(territoryId: String, kind: MapLayerKind): File
    suspend fun territoryStyle(
        territoryId: String,
        theme: StyleTheme = StyleTheme.LIGHT,
    ): String
    suspend fun openSearch(territoryId: String): TerritorySearch
    suspend fun remove(territoryId: String)
    suspend fun planUpdate(newManifestJson: String): UpdatePlan
    suspend fun checkForUpdate(
        territoryId: String,
        indexJson: String,
        indexSigBase64: String,
        fetcher: LayerFetcher,
    ): AvailableUpdate?
    suspend fun applyUpdate(
        territoryId: String,
        newManifestJson: String,
        newManifestSigBase64: String,
        fetcher: LayerFetcher,
    ): UpdateResult
    suspend fun applyUpdate(
        territoryId: String,
        newManifestJson: String,
        newManifestSigBase64: String,
        fetcher: LayerFetcher,
        cancel: CancelToken,
    ): UpdateOutcome
    override fun close()
}
  • territories: hot StateFlow of the installed set; refreshes after every mutating call and on refresh().

  • install(snpkg): install a packed .snpkg archive; throws NavCoreException.Verification if the signature or any layer hash does not verify. installDirectory installs an unpacked package directory under the same rules.

  • layerPath(id, kind): absolute path of one layer, e.g. MapLayerKind.ValhallaTiles for the on-device router, MapLayerKind.Pmtiles for MapLibre.

  • territoryStyle(id, theme): a complete MapLibre style JSON for an installed territory with local references, mirroring the web SDK's buildStyle(). The tile source is a pmtiles://<absolute path> URL pointing at the installed layer, so the map renders with the radios off. A package with no baked style layer gets the default MapMap theme compiled on the fly. StyleTheme is LIGHT or DARK.

  • openSearch(id): opens the territory's bundled geocode index for fully offline on-device search, returning a TerritorySearch. The handle holds the index open (mmap-backed): create it once per territory and reuse it. Its two calls are synchronous and disk-bound, so keep them off the main thread.

    kotlin
    val search = store.openSearch("uk")
    // Free text: addresses, streets, localities, POIs, UK postcodes.
    // Prefix-tokenised, so search-as-you-type works. `near` biases ranking
    // and fills each result's distanceM.
    val hits: List<Place> = search.search("watford gap", near = null, limit = 10u)
    // Nearest indexed places to a coordinate, nearest first.
    val around: List<Place> = search.reverse(52.2, 0.13, limit = 5u)
    

    Place carries a PlaceKind of Address, Street, Locality, Poi or Postcode. Both calls throw NavCoreException when the territory carries no geocode layer.

  • remove(id): remove an installed territory (clears the active marker if needed).

  • checkForUpdate(...): check the signed update channel. Fail-closed: the channel-index signature is verified before the fetcher is ever invoked. indexJson/indexSigBase64 are the exact served bytes of index.json and index.json.sig. Returns null when already on the channel's latest version.

  • applyUpdate(...): differential update; unchanged layers hard-linked, changed layers fetched through your LayerFetcher, staged package fully re-verified, atomic swap. Any failure leaves the install untouched.

  • The overloads taking a CancelToken can be stopped from any thread with cancel() (and by cancelling the calling coroutine). They return InstallOutcome / UpdateOutcome: Cancelled is an outcome, not an exception, and leaves nothing partial on disk. See Cancelling an install.

  • LayerFetcher is the generated callback interface: one method, fetch(relPath: String, expectedBlake3: String, destPath: String), synchronous by contract (called from a background thread); expectedBlake3 is advisory, the core re-verifies everything.

See the worked install/update flows in territories.

ai.mapmap.routing

OfflineRouting

On-device route computation. The core builds the Valhalla request (merging ADR dangerous-goods and dimensional costing options), the injected router executes it against local tiles, and the core parses the response.

kotlin
object OfflineRouting {
    suspend fun route(
        router: LocalRouter,
        waypoints: List<RoutePoint>,          // at least two, in visit order
        costing: CostingModel = CostingModel.AUTO,
        adr: AdrProfile? = null,
        dispatcher: CoroutineDispatcher = Dispatchers.Default,
    ): RouteResult

    fun buildRequest(
        waypoints: List<RoutePoint>,
        costing: CostingModel = CostingModel.AUTO,
        adr: AdrProfile? = null,
    ): String                                  // raw Valhalla request JSON

    fun parseResponse(responseJson: String): RouteResult
    fun requireCompatibleCosting(costing: CostingModel, adr: AdrProfile?)
}
  • ADR profiles ride on truck costing: supplying adr with any costing other than CostingModel.TRUCK throws IllegalArgumentException.
  • route throws NavCoreException.Routing when the engine returns a Valhalla error body (no route, no tiles for the area).
  • RouteResult.valhallaResponseJson feeds NavigationEngine.

OfflineRouter

The on-device engine interface, the core's LocalRouter callback with a Kotlin name: execute a Valhalla request JSON synchronously, return the raw response JSON.

kotlin
interface OfflineRouter : LocalRouter   // fun route(requestJson: String): String

ValhallaMobileRouter

OfflineRouter backed by valhalla-mobile's on-device engine. valhalla-mobile is a compileOnly dependency: apps using this class must declare io.github.rallista:valhalla-mobile themselves.

kotlin
class ValhallaMobileRouter(configPath: String) : OfflineRouter {
    companion object {
        fun fromConfig(configFile: File): ValhallaMobileRouter
        fun fromTileDir(tileDir: File, scratchDir: File): ValhallaMobileRouter
    }
}

fromTileDir is the usual entry point: point it at the territory's MapLayerKind.ValhallaTiles layer and a scratch dir (e.g. context.cacheDir) and it writes the valhalla.json for you.

ValhallaConfigWriter

Writes the minimal valhalla.json the on-device engine needs.

kotlin
object ValhallaConfigWriter {
    const val DEFAULT_MAX_CACHE_BYTES: Long = 268_435_456  // 256 MiB
    fun configJson(tileDir: File, maxCacheBytes: Long = DEFAULT_MAX_CACHE_BYTES): String
    fun write(tileDir: File, configFile: File,
              maxCacheBytes: Long = DEFAULT_MAX_CACHE_BYTES): File
}

ai.mapmap.adr.AdrCompliance

On-device ADR 8.6.4 dangerous-goods enforcement (pre-route via OfflineRouting.route(adr = …), en-route via checkTunnel).

kotlin
object AdrCompliance {
    fun checkTunnel(profile: AdrProfile, category: AdrTunnelCategory): AdrDecision
    fun forbiddenCategories(profile: AdrProfile): List<AdrTunnelCategory>
    fun isTunnelRestricted(profile: AdrProfile): Boolean
    fun validateProfile(profile: AdrProfile): List<String>
}
  • checkTunnel: may this vehicle pass a tunnel of category? A AdrDecision.Blocked carries a human-readable reason citing ADR 8.6.4.
  • forbiddenCategories: all categories the vehicle is barred from, ascending; empty when unrestricted.
  • validateProfile: physical-plausibility check; returns human-readable problems (empty = valid). Pure Kotlin, safe without the native library.

ai.mapmap.nav

Turn-by-turn guidance over one computed route, adapting the core's Ferrostar-based GuidanceSession to coroutines. An engine is bound to a single route; on off-route, compute a new route and build a new engine.

kotlin
class NavigationEngine(
    routeResponseJson: String,
    config: GuidanceConfig = GuidanceConfig(),
    dispatcher: CoroutineDispatcher = Dispatchers.Default,
) {
    constructor(route: RouteResult,
                config: GuidanceConfig = GuidanceConfig(),
                dispatcher: CoroutineDispatcher = Dispatchers.Default)

    fun navigate(
        provider: LocationProvider,
        manualAdvance: Flow<Unit> = emptyFlow(),
    ): Flow<NavigationState>

    fun totalSteps(): UInt
}
  • navigate is a cold flow (each collection starts a fresh session at the first route step) and completes after arrival. manualAdvance is the tunnel/no-GNSS escape hatch: emit Unit to force-advance a step.
  • Throws NavCoreException if the route response cannot start a session.
kotlin
data class NavigationState(val fix: LocationSample, val update: GuidanceUpdate) {
    val isArrived: Boolean       // update is GuidanceUpdate.Arrived
    val needsReroute: Boolean    // update is GuidanceUpdate.OffRoute
}

object GuidanceDisplay {
    fun instruction(update: GuidanceUpdate): String?
    fun severity(update: GuidanceUpdate): InstructionSeverity
    fun formatDistance(metres: Double): String
    fun summarise(update: GuidanceUpdate): String
}

GuidanceUpdate is the core's sealed state: Navigating (carries currentInstruction, distanceToNextManeuverM, distanceRemainingM, severity and the road-snapped fix snapped), Arrived, or OffRoute (carries deviationM). GuidanceDisplay is pure Kotlin (unit-testable on any JVM).

LocationProvider and LocationSample

kotlin
data class LocationSample(
    val lat: Double,
    val lon: Double,
    val timestampMs: Long,                    // ms since the Unix epoch
    val speedMps: Double? = null,
    val bearingDeg: Double? = null,           // [0, 360), clockwise from true north
    val horizontalAccuracyM: Double? = null,  // 1-sigma; gates step advance
)

interface LocationProvider {
    val locations: Flow<LocationSample>       // collected once per session
}

Any cold Flow of samples in ascending timestamp order qualifies.

AndroidLocationProvider

Live provider on the platform location stack; no Google Play Services dependency. Uses LocationManager.FUSED_PROVIDER on API 31+, falling back to GPS_PROVIDER. Requires ACCESS_FINE_LOCATION granted before the flow is collected; the SDK manifest declares no permissions.

kotlin
class AndroidLocationProvider(
    context: Context,
    minIntervalMs: Long = 1_000L,   // 1 Hz default
    minDistanceM: Float = 0f,
) : LocationProvider

ReplayLocationProvider

Replays a .drive.jsonl corpus (see File formats) as a LocationProvider: the QA/simulator path.

kotlin
class ReplayLocationProvider(
    corpus: DriveCorpus,
    timeScale: Double = 1.0,    // 1.0 real time, 2.0 double speed, 0.0 = as fast as possible
    epochStartMs: Long = 0L,    // epoch ms assigned to the first fix
) : LocationProvider

VoiceGuidance

Speaks guidance updates through Android TextToSpeech, with severity-scaled prosody, earcons and audio focus handled for you. Feed it from the same flow that drives your UI.

kotlin
class VoiceGuidance(
    context: Context,
    language: String? = null,   // BCP 47, e.g. "en-GB"; null = device default
    baseRate: Float = 1.0f,     // base speech rate (1.0 = normal)
) : AutoCloseable {
    var muted: Boolean          // toggle freely mid-drive
    var volume: Float           // 0.0–1.0, speech and earcons
    val isSpeaking: Boolean     // an utterance is being spoken or queued
    var onSpeakingChanged: ((Boolean) -> Unit)?
    fun handle(update: GuidanceUpdate)
    fun handle(state: NavigationState)
    override fun close()        // release TTS engine, earcon player, audio focus
}

isSpeaking and onSpeakingChanged are the speaking signal a UI ducks other audio or animates a voice indicator from: true when the first utterance of a burst is handed to the engine, false once the last outstanding one ends, including on error, stop, mute and close(). End transitions arrive on the TTS engine's own callback thread, so hop to the main thread before touching UI.

VoiceProsody (speech rate/pitch per InstructionSeverity) and the startup/audio-focus helpers (TtsStartup, AudioFocusPolicy) are public for testing and custom voice stacks.

Probe contribution

An optional per-trip tap on the guidance flow that accumulates an aggregate probe batch and uploads it once the trip ends, feeding data collection. Off unless you turn it on: a disabled ProbeConfig makes contributingProbe a transparent pass-through that constructs no collector and uploads nothing, so leaving it off costs nothing.

kotlin
public fun Flow<NavigationState>.contributingProbe(
    config: ProbeConfig,
    uploader: ProbeUploader,
    routeRef: String? = null,       // opaque served-route token
    territory: String? = null,
    vehicleClass: String? = null,   // a costing class only
    now: () -> Long = System::currentTimeMillis,
): Flow<NavigationState>

public interface ProbeUploader {
    fun enqueue(body: String)       // non-blocking; must never throw
}

public class HttpProbeUploader(
    baseUrl: String,                        // e.g. https://api.mapmap.ai
    apiKey: String,
    maxAttempts: Int = 4,                   // total tries, including the first
    backoffMillis: (Int) -> Long = { it * 1_000L },
    executor: ExecutorService = ...,        // default: one daemon thread
    onResult: (ProbeUploadOutcome) -> Unit = {},
) : ProbeUploader, AutoCloseable

public enum class ProbeUploadOutcome {
    ACCEPTED, REFUSED, NOT_ENABLED, REJECTED, GAVE_UP
}
  • Wire it straight onto the guidance stream: engine.navigate(provider).contributingProbe(ProbeConfig(enabled = consentGranted), uploader).collect { … }.
  • Consent is the caller's responsibility. Pass enabled = true only for a key whose operator has set probe_opt_in and, in a consumer app, only with the user's own opt-in.
  • Only aggregates leave the device: the collector cannot emit a trajectory, and the first and last stretch of every trip is discarded on-device before anything is uploaded.
  • HttpProbeUploader POSTs each body to {baseUrl}/v1/probe on a small daemon executor using only the JDK HTTP client, so the SDK takes on no third-party HTTP dependency. Transient failures (IO or 5xx) retry with bounded backoff; 400, 403 and 501 are permanent and dropped. ProbeUploadOutcome names which of those happened, for observability and tests. Close it to release the executor.
  • vehicleClass is a costing class such as "truck_40t" and never a registration, VIN or driver id.

ai.mapmap.replay.DriveCorpus

Parser for the .drive.jsonl replay corpus format; schema in File formats.

kotlin
data class DriveCorpus(val header: DriveHeader, val fixes: List<DriveFix>) {
    companion object {
        const val FORMAT_VERSION: Int = 1
        fun parse(jsonl: String): DriveCorpus   // throws CorpusFormatException
    }
}

data class DriveHeader(val formatVersion: Int, val name: String,
                       val description: String, val routeFixture: String, val seed: Long)

data class DriveFix(val tMs: Long, val lat: Double, val lon: Double,
                    val speedMps: Double?, val bearingDeg: Double?,
                    val horizontalAccuracyM: Double?)

The parser enforces the same invariants as the Rust reference implementation: supported format version, non-empty name, at least one fix, strictly increasing t_ms, sane coordinate/channel ranges.

Putting it together

kotlin
// Territory → router → route → guidance:
val tiles = store.layerPath(territoryId, MapLayerKind.ValhallaTiles)
val router = ValhallaMobileRouter.fromTileDir(tiles, context.cacheDir)

val route = OfflineRouting.route(
    router = router,
    waypoints = listOf(RoutePoint(52.2, 0.13), RoutePoint(52.2045, 0.1388)),
    costing = CostingModel.TRUCK,
    adr = AdrProfile(heightM = 4.0, grossWeightT = 40.0,
                     hazmat = true, tunnelCode = AdrTunnelCode.C),
)

NavigationEngine(route).navigate(AndroidLocationProvider(context))
    .collect { state ->
        ui.instruction = GuidanceDisplay.summarise(state.update)
        if (state.needsReroute) recalculate()
    }

Android Auto (ai.mapmap:car, ai.mapmap:car-maplibre)

Beta modules that put the same guidance state on the car's display: NavigationManagerBridge for the Car App Library's NavigationManager, TurnByTurnNotificationManager, NavigationIntentParser, the NavigationTemplateBuilder template layer, and CarMapViewHost for projecting a MapLibre MapView onto the car surface.

Unlike ai.mapmap:core, these two modules are not published to Maven Central or GitHub Packages: build them from the source checkout until public artefacts land with sdk-v0.7.0, after head-unit hardware validation. Install steps, quickstart, entitlement and Play-review gates on CarPlay & Android Auto.

Next steps