eOr Dual-Screen Extension
In developmentTurns a dual-screen Android handheld into a paired-media console: the game on the top panel, and a companion surface on the bottom one that keeps working when the network doesn't.
At a glance
- Outcome: A typed session and paired-media layer over an existing open-source launcher: a persisted Session model, a physical-panel display orchestrator, a lower-panel session deck, and device-authorization pairing to a self-hosted ROM library. Normal single-screen behavior stays intact. Contributing the general parts upstream is the goal; a polished release isn’t there yet.
- Status: In development, and new: my layer is about a week old, landing on an upstream project that has been going far longer. It’s 11,165 added lines across 91 files on top of the upstream base. Device-authorization pairing against a self-hosted RoMM server is implemented and covered by JVM and MockWebServer fixture tests; the last recorded build gate passed 195 tests with zero failures. Browser approval and an authenticated pass against a real library on the Thor are still outstanding.
- Role: Solo, on top of an existing open-source base. eOr is not my project. It’s an existing controller-first Android launcher, and I build the dual-screen and paired-media layer over it.
- Stack & libraries: Kotlin and Android with Gradle, targeting AYN Thor and Anbernic RG DS hardware.
- Source: Private during development.
- Limitations: Profiles exist for AYN Thor and Anbernic RG DS, and an unrecognized device falls back to the layout that never relaunches the Activity. Only the Thor has a hardware record. The RG DS path, offer acceptance, the Companion route, independent Companion stop, and lower-display detach and recovery are covered by unit tests and fakes, not by device evidence.
Where the design came from
The opening move wasn’t code. It was an eight-product competitive review of the dual-screen and handheld-companion category, which concluded that the gap here is not another macro pad, since that space is crowded and solved.
The dual-screen architecture then came from pulling two closed-source competitors’ APKs off the test device with ADB, decompiling them with JADX, and confirming their actual behavior live. Not from guessing at it.
That’s also why the first local build had to be an unchanged upstream APK with its behavior recorded on real hardware before any product work started. You can’t tell what your changes did if you never measured what you started from.
What’s mine and what isn’t
Worth stating plainly, because building on someone else’s work only counts if you’re clear about the seam. Upstream eOr provides the launcher: Android home-app support, controller-first navigation, ROM discovery and metadata, artwork scraping, emulator launch adapters, and baseline dual-screen detection.
My layer is the typed session and the routing above it: panel routing, focus and recovery, the local session deck, the pairing protocol, and the client half of the Catlg companion contract, whose server side isn’t built yet.
The explicit design rule is prefer changes that can be contributed upstream over maintaining an unnecessary hard fork.
A Boolean became a session
This is the load-bearing change.
Upstream eOr tracked one Boolean: whether a game had been placed on the top panel. That’s enough to hide artwork and nothing else.
My layer replaces it with a typed Session. A launch stays pending until Android reports success. Late events from a Session that has already been replaced are ignored as stale. Illegal transitions are rejected rather than applied. An interrupted Companion route recovers without inventing new game state.
What gets persisted is deliberately thin. No live Android object and no authorization value ever reaches the saved state.
The display layer didn’t appear alongside the upstream code either; it replaced it. The commit deletes upstream’s 181-line dual-screen manager and adds a 417-line orchestrator plus its 245-line Android implementation.
Four nouns that are easy to collapse into one
A Session separates the Primary whose lifecycle defines the session, an optional Companion, an Endpoint that can honestly present it, and the Route that actually ran.
Keeping them apart is what lets a Companion stop, fail, or lose its display without ending or relaunching the game. It’s also what stops a phone, television, or cast target being offered merely because its type exists in code.
And the shipped slice is deliberately narrower than the model. The model can represent a phone, television, speaker, provider-native route, or cast target. What’s implemented is authenticated ROM-library metadata rendered locally on the lower panel, with a same-device fallback when there’s no lower panel. A route kind the Android router can’t actually perform returns a failure instead of a pretend success.
That gap is the point of the design, not a hole in it: the nouns exist so those routes can be added without a rewrite, and the router refuses to fake them in the meantime.
The hardware end
This is the closest thing I have to consumer-device work outside firmware: real handhelds with real constraints, where dual-screen presentation is detected from the device rather than configured, launching has to survive the emulator process doing something unexpected, and authenticated pairing had to be designed as an actual device-authorization protocol instead of a shared secret in a config file.