# Android API reference

The public Kotlin surface of `ai.mapmap:core` **0.2.0**. Install
coordinates, credentials and the root-repository Gradle rule are on
[SDKs & installation](/docs/sdks#android-kotlin); 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`.

| Package | Classes |
|---|---|
| `ai.mapmap` | `MapMap` |
| `ai.mapmap.territory` | `TerritoryStore` |
| `ai.mapmap.routing` | `OfflineRouting`, `OfflineRouter`, `ValhallaMobileRouter`, `ValhallaConfigWriter` |
| `ai.mapmap.adr` | `AdrCompliance` |
| `ai.mapmap.nav` | `NavigationEngine`, `NavigationState`, `GuidanceDisplay`, `LocationProvider`, `LocationSample`, `AndroidLocationProvider`, `ReplayLocationProvider`, `VoiceGuidance`, `VoiceProsody` |
| `ai.mapmap.replay` | `DriveCorpus`, `DriveHeader`, `DriveFix`, `CorpusFormatException` |

## `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 installDirectory(packageDir: File): TerritoryInfo
    suspend fun setActive(territoryId: String)
    suspend fun active(): String?
    suspend fun activeTerritory(): TerritoryInfo?
    suspend fun layerPath(territoryId: String, kind: MapLayerKind): File
    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
    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.
- `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.
- `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](/docs/territories#device-flow).

## `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`

### `NavigationEngine`

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.

### `NavigationState` and `GuidanceDisplay`

```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](/docs/formats#drivejsonl-replay-corpus)) 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
    fun handle(update: GuidanceUpdate)
    fun handle(state: NavigationState)
    override fun close()        // release TTS engine, earcon player, audio focus
}
```

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

## `ai.mapmap.replay.DriveCorpus`

Parser for the `.drive.jsonl` replay corpus format; schema in
[File formats](/docs/formats#drivejsonl-replay-corpus).

```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 (0.2.0) 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. Quickstart, entitlement and Play-review gates on
[CarPlay & Android Auto](/docs/car).

## Next steps

- [SDKs & installation](/docs/sdks): install coordinates, credentials, the verifying-key trust anchor
- [Territories](/docs/territories): the signed package and update-channel model this SDK consumes
- [File formats](/docs/formats): channel index, package layout, `.drive.jsonl`
- [CarPlay & Android Auto](/docs/car): the in-car surface
