---
title: Android runtime internals
description: 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.
type: concept
platform: android
vendor: meta
related:
  - /docs/concepts/architecture
  - /docs/concepts/transport-vs-app
  - /docs/sdk/android/initialization
  - /docs/sdk/android/threading
---

You don't need this page to build an app — the [capability surface](/docs/sdk/android/initialization) 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](https://mozilla.github.io/uniffi-rs/):

- **`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](/docs/concepts/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:

| Transport | What it is | Network |
|---|---|---|
| `SystemAudioTransport` | The 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) |
| `RealMetaTransport` | Production glasses via Meta DAT (`com.meta.wearable`, 0.8.0, through `com.extentos:glasses-meta`). | Bluetooth / DAT |
| `BrowserSimTransport` | WebSocket to the Extentos backend; the hardware surrogate runs in a browser tab. | WebSocket |
| `LocalSimTransport` | In-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](/docs/concepts/transport-vs-app).)

## 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 pairing** — `debug = 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.

<Callout type="info">
  **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](/docs/sdk/ios/runtime-internals).
</Callout>

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