SDKAndroid

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.

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.

The Extentos Android SDK is coroutine-native. Its public surface is split cleanly into two shapes:

  • suspend functions for one-shot operations that complete with a result.
  • Flow / StateFlow for continuous streams and observable state.

There are no callbacks to register and no threads to manage — you collect flows and await suspend functions from your own coroutine scopes.

One-shot operations are suspend

Discrete capability calls suspend until they finish and return an ExtentosResult:

suspend fun takeShot() {
    when (val r = glasses.camera.capturePhoto()) {
        is ExtentosResult.Ok  -> show(r.value)
        is ExtentosResult.Err -> report(r.error)
    }
    glasses.audio.speak("Got it.")        // also suspend
}

Examples: camera.capturePhoto(), camera.captureVideo(), audio.recordDiscrete(...), audio.speak(...), audio.cancelSpeak(), and glasses.shutdown(). Call them from a coroutine (lifecycleScope, viewModelScope, or any scope you control). They are cooperative with cancellation — cancelling the calling coroutine cancels the operation.

Streams are Flow

Continuous capabilities return a cold Flow you collect for as long as you want the stream:

// Live transcription
glasses.audio.transcriptions().collect { transcript ->
    updateCaption(transcript.text)
}

// Raw mic chunks / camera frames
glasses.audio.audioChunks().collect { chunk -> /* ... */ }
glasses.camera.videoFrames().collect { frame -> /* ... */ }

These are cold — the underlying capture starts when collection starts and stops when the collecting coroutine is cancelled. Run them on a scope whose lifetime matches the subscription (and start the foreground service if the stream must survive backgrounding).

Observable state is StateFlow / event Flow

Two always-available streams describe the SDK's own state:

StreamTypeWhat it carries
glasses.connection.stateStateFlow<GlassesState>The live connection state — always has a current value.
glasses.runtime.eventsFlow<RuntimeEvent>The on-device event log (toggles, transport, logs, warnings).

Collect connection state lifecycle-aware so the UI tracks it without leaking:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        glasses.connection.state.collect { state ->
            render(state)
        }
    }
}

In Compose, collectAsStateWithLifecycle() is the idiomatic equivalent:

val connState by glasses.connection.state.collectAsStateWithLifecycle()

Because state is a StateFlow, a new collector immediately receives the current value — you don't miss the connection state by subscribing late. The event log additionally offers a one-shot snapshot if you don't want to collect: glasses.runtime.snapshotEvents() (a suspend function returning List<RuntimeEvent>).

Dispatcher guidance

  • You don't pick a dispatcher for the SDK's internals. It runs its own work on a SupervisorJob + Dispatchers.Default scope created at create() and cancelled at shutdown(). Flow emissions and suspend-function bodies execute there.
  • Collect on a UI-aware scope (lifecycleScope, viewModelScope) and touch UI from the main dispatcher. Use flowOn(...) only if your own downstream operators are heavy — the SDK already does its capture work off the main thread.
  • Don't block. Never call a suspend operation from runBlocking on the main thread; launch it on a coroutine scope.

Don't wrap results in runCatching

Capability and lifecycle operations return ExtentosResult and do not throw for expected failures. Pattern-match the result; runCatching would only ever catch unrelated exceptions and would hide the typed failure. See Errors for the failure variants and the valueOrNull() / map / flatMap helpers.