Architecture
Omni is a single-module Android app built on MVVM with unidirectional data flow: Compose renders state, the BrowserViewModel owns and orchestrates it, and GeckoView does the actual browsing.
The layer map
Compose UI
Stateless screens
BrowserViewModel
State + orchestration
GeckoSession
Web engine
MediaInterceptor
Stream detection
Data flows down as immutable state and up as events. Compose screens never mutate engine objects directly — they call intent methods on the ViewModel, which updates state, which recomposes the UI.
The ViewModel as orchestrator
browser/BrowserViewModel.kt (~6,000 lines) is the central state holder. It owns:
- Tabs — a
SnapshotStateList<TabState>that Compose observes directly. - The GeckoRuntime — a process-wide singleton created lazily via
getGeckoRuntime(). - Cross-cutting concerns — search engines, ad-block domain lists, the Discover news feed, permissions.
Because a single file that large is hard to navigate, the ViewModel is split into Kotlin extension files by concern:
| File | Responsibility |
|---|---|
BrowserViewModel_Session.kt | Session save/restore, tab lifecycle |
BrowserViewModel_Extensions.kt | WebExtension install/toggle/messaging |
BrowserViewModel_Passwords.kt | Credential capture and autofill |
BrowserViewModel_History.kt | History read/write/search |
BrowserViewModel_Bookmarks.kt | Bookmark tree operations |
BrowserViewModel_QrScanner.kt | QR decode orchestration |
BrowserViewModel_FindInPage.kt | Find-in-page state |
State management
Omni leans on Compose’s snapshot system rather than a Redux-style store. Tab state is a value object:
data class TabState(
val id: String,
val url: String,
val title: String,
val isPrivate: Boolean, // drives GeckoView private context
val isDesktopMode: Boolean,
val session: GeckoSession?, // engine handle, not serialized
val progress: Int
)
Mutating a tab means replacing its TabState in the SnapshotStateList; Compose recomposes only the affected rows. The GeckoSession itself is a live engine handle and is never serialized.
Navigation
Screen-level navigation uses a Compose NavHost in MainActivity with named routes — browser, settings, downloads, qr_tools, video_player/{filePath}, locker, and so on. In-browser navigation (back/forward/history) is handled by the GeckoSession and mirrored into TabState.
Media detection: dual path
Stream detection runs on two cooperating paths (see Media Hub):
- Extension path — the Media Grabber WebExtension reports candidates over native messaging.
- Native path —
MediaInterceptorinspects engine-level traffic for HLS/DASH manifests.
Both converge on the ViewModel, which surfaces a grabber affordance in the UI.
Persistence
| Data | Store | Encryption |
|---|---|---|
| Preferences / theme | DataStore | None (non-sensitive) |
| History, bookmarks | Room | None |
| Vault contents | Room + SQLCipher | AES-256 |
| Vault files | EncryptedFile | AES-256-GCM (Keystore) |
| Passwords | Encrypted prefs | Keystore-backed |
Threading notes
- GeckoView callbacks arrive on the main thread; long work is dispatched to
viewModelScopeor IO dispatchers. - The GeckoRuntime is created once and shared — creating multiple runtimes is expensive and unsupported.
- Downloads and FFmpeg extraction run on foreground services to survive process death.
The repository ships docs/ARCHITECTURE.md with the authoritative, versioned diagram. This page summarizes it for the web.