Initialize the Android SDK
Create ExtentosGlasses with ExtentosGlasses.create, configure ExtentosConfig (environment, applicationContext, transport), reach the sub-clients, and pattern-match ExtentosResult.
Camera and display need the Meta vendor module. com.extentos:glasses
carries no vendor SDK, so add implementation("com.extentos:glasses-meta")
alongside it — see install. Without it the build
still succeeds and voice still works, but capabilities.camera is false and
captures return errors. The SDK logs a warning at startup when it spots that
combination.
You create one ExtentosGlasses instance and hold it for the life of your app's glasses session. There is no required Application subclass and no static singleton to register — you call a factory and keep the handle.
Create the instance
import com.extentos.glasses.core.ExtentosGlasses
import com.extentos.glasses.core.ExtentosConfig
import com.extentos.glasses.core.ExtentosEnvironment
val glasses = ExtentosGlasses.create(
ExtentosConfig(
applicationContext = applicationContext,
environment = if (BuildConfig.DEBUG) {
ExtentosEnvironment.DEVELOPMENT
} else {
ExtentosEnvironment.PRODUCTION
},
)
)ExtentosGlasses.create(config: ExtentosConfig = ExtentosConfig()): ExtentosGlasses is the only entry point — the constructor is internal, so always go through create(). Calling it with no argument (ExtentosGlasses.create()) is valid and uses all defaults.
Camera or display apps need two more things, and the order matters. The
snippet above is the voice-app shape. TransportChoice.Auto picks the
transport inside create(...) and keeps it for the life of the handle, so
anything that influences the choice has to be true before you call it:
BLUETOOTH_CONNECTmust already be granted. The vendor arm claims by reading the bonded-device list, which that permission gates. Without it the check can't answer,Autofalls through to the audio baseline, andcapabilities.camerareportsfalsefor the rest of the process — voice works, capture doesn't, and nothing throws.- Pass an
activityProvider. Meta's registration handoff needs a live Activity, and it defaults tonull.
So a camera app requests RECORD_AUDIO + CAMERA + BLUETOOTH_CONNECT from
its Activity, then creates the handle. The worked bootstrap is in the
voice-assistant guide.
Assert the result once at startup — glasses.transportChosen should read
REAL_META, not SYSTEM_AUDIO.
A natural home for the handle is a ViewModel or a small holder scoped to your app's lifetime; create it once and reuse it. When you're done, call glasses.shutdown() (a suspend function) — see Lifecycle.
ExtentosConfig
Every field is defaulted, so the config is as small as you need it.
| Field | Default | Notes |
|---|---|---|
appId | null | Your project id. Falls back to the host app's package name when null — that package is the project identity everywhere else (MCP, dashboard, telemetry), so leaving it null is usually fine. |
accountId | null | Optional account attribution. |
transport | TransportChoice.Auto | How to reach the glasses — see below. |
logLevel | LogLevel.WARN | VERBOSE / DEBUG / INFO / WARN / ERROR. |
debug | false | Enables the dev-loop pairing path under Auto. |
telemetryConsent | true | When false, no events are sent at all. |
dataSharingConsent | true | When false, your own dashboard still works but events are excluded from cross-account aggregates. |
environment | ExtentosEnvironment.DEVELOPMENT | Production apps must set PRODUCTION — see the callout below. Use BETA for Play Console / TestFlight channels. |
telemetryEndpoint | null | Override the default telemetry ingest endpoint. |
applicationContext | null | Required whenever the transport resolves to RealMeta or SystemAudio — i.e. on any real device (Meta DAT needs a Context). Leave null only for headless JVM tests; on a real phone a null Context silently resolves to the in-memory LocalSim mock, which has no audio at all. |
activityProvider | null | A () -> Activity? the SDK calls to get a foreground Activity for Meta registration. |
hasBondedMetaDevice | null | A () -> Boolean the Auto chain uses to decide RealMeta vs. the audio baseline vs. simulator. The SDK has a built-in default, so you usually omit it. |
usedCapabilities | emptySet() | The SDK capabilities your app uses (Set<CapabilityKind>). Values: CapabilityKind.Camera, .Microphone, .Speaker, .Display, .Location, .Notifications, and CapabilityKind.Other(name). Drives the connection page's capability tiles — one tile per declared capability, lit when the connected glasses provide it, dimmed when they don't. The generated bootstrap sets this from your generateConnectionModule(capabilities = [...]). On Android this is display-only — the transport is picked from the bonded-device check, not from this set. On iOS the same field also decides the transport; if you ship both platforms, declare it on both. |
Set
environment = PRODUCTIONfor production builds. It defaults toDEVELOPMENTso thatinstallDebugbuilds never pollute production analytics. If you ship a release build still onDEVELOPMENT, its events land in the dev backend, not your production bucket. TheBuildConfig.DEBUGpattern above is the canonical way to wire it. (As a safety net the SDK also reconciles the declared environment against an on-device classifier and downgrades a mismatchedPRODUCTIONdebug build with a warning — but declare it correctly rather than relying on that.)
Transport choice
sealed interface TransportChoice {
data object Auto : TransportChoice // default — resolves at runtime
data object SystemAudio : TransportChoice // mic + speaker over OS Bluetooth; no vendor SDK
data object RealMeta : TransportChoice // force real glasses via Meta DAT
sealed interface Simulated : TransportChoice {
data object Local : Simulated // in-process mock, no network
data class Browser(val url: String? = null) : Simulated // browser simulator
}
}Auto is almost always right. In order: the browser simulator when a session URL is baked in or set in the environment; then real glasses when a vendor module claims a bonded device; then — only when you set ExtentosConfig.debug = true, which is a config field defaulting to false, not your BuildConfig.DEBUG build type — the dev pairing screen; and otherwise SystemAudio, mic and speaker over whatever the OS has routed, which keeps a voice app working with no vendor SDK at all. A vendor claim beats the pairing screen, so a debug build with bonded glasses still goes to hardware. See Runtime internals for the full resolution order and its caveats.
Reading which transport you got
ExtentosGlasses exposes the resolution outcome directly — useful after an upgrade, or to confirm a release build reached real hardware rather than the audio baseline:
| Property | Type | What it tells you |
|---|---|---|
transportChosen | TransportChosen | Which transport resolved: REAL_META, SYSTEM_AUDIO, BROWSER_SIM, LOCAL_SIM |
selectionSource | TransportSelectionSource | Why it won. All seven: EXPLICIT_CONFIG, BUILD_CONFIG, ENV_VAR, VENDOR_REGISTRY, BONDED_DEVICES, PAIRING, FALLBACK_DEFAULT |
capabilities | GlassesCapabilities | Flat yes/no facts of the connected device: camera, microphone, speaker, display |
Log.i("app", "transport=${glasses.transportChosen} (${glasses.selectionSource})")A camera app that reads SYSTEM_AUDIO here got there for one of three reasons: com.extentos:glasses-meta isn't on the classpath, no Meta glasses are bonded to the phone, or BLUETOOTH_CONNECT wasn't granted before create(...). Check them in that order.
The sub-client surface
ExtentosGlasses exposes typed sub-clients as properties. You call capabilities through them:
| Property | What it does |
|---|---|
connection | Connection state (StateFlow<GlassesState>), registration. |
camera | capturePhoto(), captureVideo(), videoFrames(). |
audio | transcriptions(), audioChunks(), recordDiscrete(...), speak(...). |
display | Render UI on display-capable glasses; guard with display.isAvailable. |
runtime | The on-device event log as runtime.events: Flow<RuntimeEvent>. |
toggles | The runtime capability gates. |
voice | Wake/voice-command hints and matching. |
telemetry | App-level telemetry events. |
observability | Diagnostics surface. |
assistant | The end-to-end voice assistant (1.4.0+). |
connectionConfig | The dashboard-managed connection-page config. |
device | Identity of the connected glasses (device.type, device.vendor). |
capabilities | The flat capability facts of the connected device (camera/microphone/speaker/display). |
val photo = glasses.camera.capturePhoto()
glasses.audio.transcriptions().collect { transcript -> /* ... */ }Handle results with ExtentosResult
Lifecycle and capability operations return ExtentosResult<S, F> instead of throwing. It is a sealed interface with two cases:
import com.extentos.glasses.core.ExtentosResult
when (val r = glasses.camera.capturePhoto()) {
is ExtentosResult.Ok -> usePhoto(r.value)
is ExtentosResult.Err -> handleFailure(r.error)
}The failure side carries concrete, per-operation variants — capturePhoto() / captureVideo() return a CaptureError (NotConnected, PermissionDenied, ThermalCritical, HingesClosed, CoexistenceBlocked, StreamPaused, DisabledByUser, PlatformError — all eight, or your when won't be exhaustive); connect() returns a ConnectError (NoDeviceAvailable, PermissionDenied, PairingFailed, RegistrationRequired, TransportFailure); recordDiscrete() / speak() return an AudioError. See Errors for the full set.
Do not wrap these in
runCatching. They never throw, sorunCatchingwould only catch unrelated exceptions and silently hide the typed failure. Pattern-match the result instead.
Helpers if you don't want a full when:
val value = glasses.camera.capturePhoto().valueOrNull() // S? — null on failure
val error = result.errorOrNull() // F? — null on success
val mapped = result.map { it.uri } // transform the success
val chained = result.flatMap { upload(it) } // chain another resultNext: Lifecycle covers shutdown, the foreground service, and process death; Threading covers the coroutine/Flow model.
Related
Android SDK lifecycle
When to create and shut down ExtentosGlasses, how the bundled foreground service keeps the glasses mic alive in the background, and how Extentos behaves across Activity lifecycle and process death.
Android SDK threading model
How the Extentos Android SDK uses Kotlin coroutines and Flow — suspend functions for one-shot ops, Flow/StateFlow for streams, and which dispatcher to collect on.
Error reference
Every typed error the Extentos SDK can return — ConnectError, CaptureError, AudioError, TransportError, the ExtentosError umbrella, and the Meta-DAT DeviceSessionError — with their payload fields and meaning. Lifecycle operations return ExtentosResult<T, E> with these concrete failure variants rather than throwing; pattern-match them. Generated from the Rust core.
Install the Android SDK
Add the Extentos Android SDK to your Gradle project from Maven Central. Coordinates, minSdk 31, Kotlin/AGP/Gradle floors, and the assistant runtime.
Android manifest setup
Which permissions the Extentos Android SDK auto-merges for you and which capability permissions (CAMERA, RECORD_AUDIO, INTERNET) your app must declare, plus the bundled foreground service.
Android SDK lifecycle
When to create and shut down ExtentosGlasses, how the bundled foreground service keeps the glasses mic alive in the background, and how Extentos behaves across Activity lifecycle and process death.