SDKAndroid

Android runtime internals

How the Extentos Android SDK is a thin Kotlin shell over a shared Rust core (uniffi + jniLibs), the four transports and how TransportChoice.Auto resolves, and the Meta DAT bridge.

You don't need this page to build an app — the capability surface is the contract. It's here for when you want to understand what's running underneath, debug a transport selection, or reason about cross-platform parity.

Kotlin shell over a shared Rust core

The Android SDK is deliberately thin. Vendor-agnostic logic — state machines, data shapes, event vocabularies, protocol handling, display strings — lives once in a shared Rust core (extentos-core), not in the Kotlin code. The Kotlin layer holds only what must be platform-bound: Meta DAT calls, Bluetooth SCO/HFP audio, Compose UI, and the Android Context plumbing.

The core reaches Kotlin through uniffi:

  • extentos_core.kt — uniffi-generated Kotlin bindings for the core's types and functions. This file is codegen output — never hand-edit it; the source of truth is the Rust crate, and the bindings are regenerated from it.
  • jniLibs/*.so — the compiled Rust core for each Android ABI, loaded at runtime. The bindings load the native library through JNA.

A bug fixed in the Rust core lands on Android and iOS at once; that's the whole point of keeping the shells thin. The same core compiles to an xcframework for the iOS SDK. (See Architecture for the cross-platform picture.)

Four transports behind one abstraction

Everything above the transport — every capability client — talks to a single GlassesTransport interface. Four implementations swap in behind it:

TransportWhat it isNetwork
SystemAudioTransportThe vendorless baseline: mic + speaker over the OS's own Bluetooth routing. No vendor SDK, so no camera and no display.Bluetooth HFP (via the OS)
RealMetaTransportProduction glasses via Meta DAT (com.meta.wearable, 0.8.0, through com.extentos:glasses-meta).Bluetooth / DAT
BrowserSimTransportWebSocket to the Extentos backend; the hardware surrogate runs in a browser tab.WebSocket
LocalSimTransportIn-process mock (no network), for a fast inner loop and JVM unit tests.None

Because the capability clients only see GlassesTransport, the simulator runs the same library code as production — only the transport and I/O substrate differ. A failure in the simulator is a real failure. (See Transport vs. app behavior.)

How TransportChoice.Auto resolves

Auto (the default) picks a transport at create() time by walking these rules in order, stopping at the first match:

  1. Baked session URL — a BuildConfig.EXTENTOS_SESSION_URL injected at compile time → BrowserSim.
  2. EXTENTOS_SESSION_URL env var — a runtime dev override (handy for emulator runs) → BrowserSim.
  3. A registered vendor claims — vendor modules (e.g. com.extentos:glasses-meta) register themselves at app start and are asked, in order, whether their hardware is present. Meta claims when a Meta/Ray-Ban/Oakley device is bonded over Bluetooth → RealMeta. (That check needs BLUETOOTH_CONNECT; without the grant it returns false and falls through, then self-heals on the next launch once granted.) With no vendor module on the classpath the registry is empty and resolution continues unchanged.
  4. Dev pairingdebug = true with a Context but no URL and no vendor claim → BrowserSim in pairing mode (the connection page shows a short code to type into the simulator). Debug-only: a release build never reaches this rung.
  5. System audio — a real device with a Context and no vendor claim → SystemAudio, the vendorless baseline. Mic and speaker over whatever the OS has routed; the full agent runtime works, camera and display do not.
  6. Fallback — no Context at all (headless JVM tests, CI) → LocalSim.

The net effect with zero host config: a phone with paired glasses resolves to real hardware; an emulator resolves to the simulator; a real phone with none resolves to SystemAudio, the vendorless audio baseline. You can always force a specific transport by setting ExtentosConfig.transport explicitly (SystemAudio, RealMeta, Simulated.Local, or Simulated.Browser(url)). The resolved choice is recorded on runtime.events as a transport.selected log line.

This differs from iOS on purpose. Android can enumerate bonded Bluetooth devices, so rung 3 has a direct signal for "are the glasses actually here?" and rung 5 can safely catch everything else. iOS has no equivalent API, so its .auto chain keys the audio-baseline rung off the app's declared capability footprint instead. The practical difference: an app that declares camera, on a phone with no glasses paired, lands on SystemAudio here and on RealMeta on iOS (where connect() then reports no eligible device). See iOS runtime internals.

The Meta DAT bridge

RealMetaTransport is backed by MetaHardwareBridge, the Kotlin glue to Meta's Device Access Toolkit. It owns the platform-bound work the core can't do:

  • DAT session init / registration (needs a Context and a foreground Activity — supplied via ExtentosConfig.applicationContext and activityProvider).
  • Bluetooth SCO/HFP routing for the glasses mic and speaker (wideband mSBC, 16 kHz duplex), which is why the library declares MODIFY_AUDIO_SETTINGS.
  • Surfacing hardware events (lifecycle, audio route, call state) up into the core's event vocabulary.

The Meta DAT SDK arrives ONLY through com.extentos:glasses-meta, and only if you add that dependency. com.extentos:glasses carries no vendor SDK, so an app that never uses camera or display ships none of it and needs no credentialed repository. (This differs from iOS, where the DAT modules are still unconditional dependencies of GlassesCore — the module split is Android-only today.) You never declare com.meta.wearable yourself either way; it comes in transitively through glasses-meta.

Generated bindings (extentos_core.kt, the uniffi/ output, jniLibs/) are build artifacts. If something looks wrong in them, the fix is in the Rust source, regenerated — not an edit to the generated file.