←  All posts

Proving 'vendor-agnostic': adding Android XR without changing a line of your app

With one vendor in production, 'vendor-agnostic' is an assertion nobody can check. So we built the second one and ran it on glasses nobody can buy yet. The code that didn't change, how we verified it without hardware, and what running it taught us that Google's docs don't say.

On 23 July we wrote that a second consumer glasses platform means writing every feature twice, once against Meta's Device Access Toolkit and once against a projected Android XR activity, and that erasing that tax is what Extentos is for.

That's easy to claim and awkward to prove, because with one vendor in production, "vendor-agnostic" is an assertion nobody can check. So on 25 July we built the second one, and proved it end to end on glasses that aren't on sale yet. The app-facing API changed by nothing.

The work that made that possible was done long before Android XR existed as a target: a capability vocabulary the app codes against, a transport seam under it, and a protocol core that neither vendor owns. That groundwork is most of the reason a second vendor is a bounded piece of work rather than a fork, and it is the part of this post worth stealing.

If you're building on Android XR yourself, skip ahead to what running it taught us: a permission API that cannot work as documented, and an emulator that reports a camera it doesn't have.

What "supported" has to mean

Three tests, and a platform passes or it doesn't:

  1. Pick it in the simulator and your app behaves the same.
  2. One integration covers both vendors.
  3. It works on the hardware when the hardware exists.

We score against these at the end.

The code that didn't change

Here is a small app. It watches for a phrase, takes a picture, and says what it sees:

glasses.audio.transcriptions().collect { transcript ->
    if (!transcript.text.contains("what am I looking at", ignoreCase = true)) return@collect

    when (val shot = glasses.camera.capturePhoto()) {
        is ExtentosResult.Ok  -> glasses.audio.speak(describe(shot.value))
        is ExtentosResult.Err -> glasses.audio.speak("I couldn't get a photo.")
    }
}

That is the whole feature, and it is the same code on Meta smart glasses, on Android XR, and against the simulator. Nothing in it names a vendor, a transport, or a device.

Where devices genuinely differ, you ask about the difference rather than the device:

if (glasses.display.isAvailable) {
    showOnLens(route.next)            // Ray-Ban Display
} else {
    glasses.audio.speak(route.next)   // Android XR audio glasses, Ray-Ban Meta, Oakley Meta
}

The guard is not a check for which vendor you're on. It's a question about the hardware in front of you, so the branch written for Meta already covered a vendor that didn't exist when it was written.

The model underneath

Extentos separates three things most glasses SDKs blur together. Vendor is who we integrate against. Device is a hardware identity carrying a capability profile. Capability is what that device can do: camera, microphone, speaker, display.

You code against the third one. Vendor and device are flat dials you can query, never levels you're forced through, and the SDK resolves the connected device's profile at connect time. That gives four rungs: write once, which is nearly all of your code; branch on a capability, which is the first snippet's sibling above; branch on identity for cosmetic tuning; and a fourth rung reserved for a feature with no honest cross-vendor equivalent, which nothing has needed yet.

Rung one is safe because of one rule. One build ships to every device your users own, so capability calls are always callable and never throw. Asking for a display on glasses without one is a no-op, and the control flow is identical in development and production. Only the signal changes: a warning in the event log while you build, a counter on your dashboard in production.

How we proved it without owning glasses

No Android XR glasses exist to buy, and we wanted more than "it compiles". Google ships a glasses emulator, so the goal was a real app connecting over a real projected link.

The rig is two emulators. Android XR glasses development needs the canary Android Studio, which we installed against its own isolated SDK so the toolchain our Meta hardware testing depends on stayed untouched. From there you boot an audio-glasses virtual device and a phone virtual device, because a projected app runs on the phone: the glasses are a display and sensor surface, and your APK never lands on them.

The two emulators then have to be paired, which Google's flow treats as something a human clicks through. We drove it headlessly instead, through the companion device manager and the glasses companion app's own pairing receiver, so the whole loop runs unattended and the pairing survives reboots. Worth knowing before you try this: two emulators at once is heavy, and on a 32 GB machine it wants the phone booted first and the glasses instance memory-capped.

With that running, a host app deploys to the phone, launches its projected activity, and the SDK connects to the glasses. What we got on the live link:

Transport resolution and connection, reporting the right vendor and device identity. Text to speech, spoken on the glasses. Raw 24 kHz PCM played on the glasses speaker, which was the load-bearing unknown, because Google documents text to speech and says nothing about raw audio out. It works, though only from an audio track built inside the projected context. Continuous 16 kHz microphone capture, 283 chunks over six seconds with zero empty reads, behind a real consent prompt.

That's a voice app's entire surface, running on Android XR, before anyone can buy the hardware.

What running it taught us

Two things worth knowing if you build on this platform, both of which cost us a day.

The documented consent path cannot work. ProjectedActivityCompat.requestPermissions is what the docs point you at. Decompiled, it blocks the calling thread waiting for a service connection whose callback is delivered on that same thread. Call it from the main thread, as any permission request naturally is, and it deadlocks by construction, times out, and takes the process down. On our emulator image the underlying service doesn't implement the call at all.

What works is the device-aware permission request made from a host activity, passing the projected virtual device's id. That produces the real dialog and lands a per-device grant. A host-only grant is not a substitute: it unblocks the projected microphone for about 380 milliseconds, then delivery dies.

The camera flags lie. The emulator's feature flags report a camera. The camera list is empty.

So our camera calls return a clear, typed refusal on Android XR rather than an implementation written from documentation we'd just watched be wrong twice. When hardware exists, that refusal becomes a real camera path with no change to your code. On an alpha platform, the docs are a hypothesis and the emulator is the truth.

Isolation, because alpha is contagious

Google's projected library is alpha and needs newer build tooling than our SDK pins, so it lives in its own module. A Meta-only app never inherits a byte of it, and turning it on is one line before you create the SDK:

ExtentosXr.register()

Without that call, transport resolution is byte-identical to what it was before the vendor registry existed, and a test pins it there. The failure we wanted to make impossible is a release build quietly resolving to a preview transport.

What you can do today

Our browser simulator replaces the vendor transport entirely, so it simulates a device identity rather than a vendor SDK. Testing your app as Android XR therefore needs no Google library, no Android XR module, and no extra dependency.

Pick Android XR in the simulator's device picker and your app is told it's on Android XR audio glasses. The transcription handler above keeps running, the photo call keeps returning photos, and the display guard keeps taking its audio branch. The capability set an Android XR session reports and the one a Ray-Ban Meta session reports are identical.

We did leave one device out on purpose. Android XR display glasses stay unsupported in the simulator, because our transport has no display path for them yet and offering it would let someone build against a screen we can't drive.

Where it stands

TestStatus
Pick it in the simulator, same behaviour✅ Today
One integration covers both vendors🟡 One extra module and one line, and the module is unpublished while the platform is in preview
Works on the hardware🟡 Proven on Google's emulator. Camera pending hardware, and no Android XR glasses are buyable

Meta smart glasses remain the production vendor, verified on real hardware. Android XR is preview, and the status page says exactly what that includes.

Doing this before the hardware shipped was the point. The cost of discovering your abstraction doesn't hold is a rewrite, and it is much cheaper to find that out on an emulator than on launch day. The seam held, which is what all the earlier work on the capability model was for, and an app written for glasses you can buy today already runs on glasses that ship this autumn.

We track every platform in this space in the ecosystem reference, and it stays current as the autumn launches land.

Asger Mølgaard
Founder