# OS Layer The `os/` directory is the simulated Android operating system that hosts every app. It owns the launcher, the task and activity stacks, the back key, the keyboard, intent routing, notifications, content providers, and the system services apps consume in place of native browser APIs. > ๐Ÿช **Mental model.** The OS is the substrate. Apps don't talk to each other directly โ€” they post Intents, register receivers, read from providers. Refresh the browser and you reboot the device: persisted data survives, runtime state resets. ## Top-level layout ``` os/ โ”œโ”€โ”€ OSContext.tsx # Thin React Provider โ€” exposes window.__SIM__ / __OS__ โ”œโ”€โ”€ SystemShell.tsx # Launcher, status bar, recents, gesture surface, app container โ”œโ”€โ”€ TaskManager.ts # Task and Activity stacks (volatile) โ”œโ”€โ”€ BackDispatcher.ts # Priority-based back-key dispatch โ”œโ”€โ”€ IntentResolver.ts # Intent matching, choosers, startActivityForResult โ”œโ”€โ”€ AppNavigatorRegistry.ts # Where each running app's navigator registers itself โ”œโ”€โ”€ AppLifecycle.ts # onResume / onPause / onForeground / onBackground hooks โ”œโ”€โ”€ AppStateRegistry.ts # Snapshot collector for __SIM__.getState() โ”œโ”€โ”€ ContentProvider.ts # ContentResolver-style query/insert/update/delete framework โ”œโ”€โ”€ ContentResolver.ts โ”œโ”€โ”€ BroadcastBus.ts # Inter-app broadcast bus โ”œโ”€โ”€ OsStateStore.ts # Unified Android-style data model (settings/hardware/perms/โ€ฆ) โ”œโ”€โ”€ createOsStore.ts # Zustand factory: persisted + volatile + registry hook โ”œโ”€โ”€ managers/ # Write facades: Connectivity, Battery, Audio, Display โ”œโ”€โ”€ providers/ # Shared content data: Contacts, SMS, Media โ”œโ”€โ”€ keyboard/ # KeyboardService + IME โ”œโ”€โ”€ i18n/ # Locale & string-table machinery โ”œโ”€โ”€ data/ # OS defaults (settings, hardware, contacts, etc.) โ”œโ”€โ”€ types/globals.d.ts # The full window.__SIM__ / __OS__ TypeScript surface โ””โ”€โ”€ (top-level service files) # TimeService.ts, LocationService.ts, NetworkService.ts, # ClipboardService.ts, NotificationService.ts, # FileSystemService.ts, MediaService.ts, AIService.ts, โ€ฆ ``` Everything else (`apps/`, `system/`, `bench_env/`) talks to the OS through these modules. Apps never reach into `os/` internals beyond the documented hooks and globals. ## The big picture ``` Browser (one Vite bundle) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ window.__SIM__ / window.__OS__ / window.__SIM_INPUT__ / โ€ฆ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ OSContext โ”€โ–บ SystemShell (launcher ยท status bar ยท recents) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TaskManager (task + activity stacks ยท volatile) โ”‚ โ”‚ โ”œโ”€โ”€ BackDispatcher (priority chain ยท frame-deduped) โ”‚ โ”‚ โ”œโ”€โ”€ IntentResolver (action + scheme + type โ†’ app/route) โ”‚ โ”‚ โ”œโ”€โ”€ AppLifecycle (onResume / onPause broadcast) โ”‚ โ”‚ โ”œโ”€โ”€ AppNavigatorRegistry (per-app navigate handle) โ”‚ โ”‚ โ”œโ”€โ”€ OsStateStore + managers (persisted Android data model) โ”‚ โ”‚ โ”œโ”€โ”€ providers/ (contacts ยท sms ยท media โ€” shared data) โ”‚ โ”‚ โ””โ”€โ”€ services/ (Time ยท Location ยท Network ยท Clipboard ยท โ”‚ โ”‚ Notification ยท Keyboard ยท FileSystem) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` The OS layer is **not Android byte-for-byte**. It's a behavioral model: realistic enough that an agent trained against it transfers to a real phone (the paper validates this), small enough to fit in a browser tab. ## Tasks and Activities A **Task** is a chain of one or more Activities the user backed into in order โ€” same concept as AOSP. A **launcher tap** creates a Task. Tapping again on a backgrounded app reactivates its existing Task. The recents surface shows one entry per Task. ```ts type Task = { taskId: string; rootAppId: AppId; stack: ActivityInstance[]; // top = currently visible Activity launchedByTaskId?: string; // who started us via startActivity (1-shot pointer) wasExternallyRouted?: boolean; // openApp(id, route) flagged this Task at creation }; type ActivityInstance = { activityId: string; appId: AppId; route: string; // MemoryRouter location at last sync }; ``` Key TaskManager actions: | Operation | What happens | |---|---| | `LAUNCH_APP` | Create a new Task (or reactivate one) and push its root Activity. Tracks `launchedByTaskId` when the caller was not the launcher. | | `PUSH_ACTIVITY` | Push a new Activity onto the **current** Task's stack โ€” used by `startActivity({ newTask: false })` and by the OS when `openApp` pushes onto an existing Task. | | `POP_ACTIVITY` | Pop the top Activity. If `stack.length > 1`, pops within the Task; if `1`, closes the Task and activates `launchedByTaskId` (caller-return). | | `MARK_EXTERNAL_ROUTE` | Set `wasExternallyRouted` so the back chain knows the Task didn't originate at the launcher. | | `consumeLaunchedBy` | One-shot clear of `launchedByTaskId` after a caller-return back. | The store is **volatile**: `createVolatileOsStore` keeps it in memory only, so refreshing the browser is a full reboot. Persistent data lives in `OsStateStore` and the providers, both of which use `createOsStore` (persisted to localStorage). ### Lifecycle Apps stay **mounted while backgrounded** โ€” the SystemShell hides them with `display:none` so React state survives. `AppLifecycle` fires `onResume` / `onPause` / `onForeground` / `onBackground` events so apps can pause timers, refresh data on resume, etc. ## Back-key dispatch `BackDispatcher.handleBack()` walks a priority chain. The first handler to return `true` consumes the event; otherwise the chain falls through to the launcher fallback. ``` priority (high โ†’ low): PermissionDialog (1000) SystemShade (800) Keyboard (700) IntentChooser (900) โ† context-conditional, not always above 700 MediaPicker (600) App back (100) โ† app's own go() / back() handler Activity back (50) โ† OS pops one Activity if stack > 1 finishTopActivity (25) os.returnToLauncherTask (12) โ† caller-return: reactivate launchedByTaskId, reset app to / os.goHomeFallback (0) โ† last resort: just go home, keep Task alive ``` Registration is via `BackDispatcher.register(id, handler, priority)`. Frame-level deduplication (`_backLock`) prevents an edge-swipe and a backdrop click from double-firing in the same frame. > ๐Ÿšซ **Apps must not import `BackDispatcher` directly.** Use the URL/history stack instead โ€” push search params for dialogs, call your app's `go()` for navigation. The hook `useAppNavigationHandler` (which every app uses for routing) registers an app-level back handler at priority `100` on your behalf. ### Returning vs. destroying Two priority-low handlers explicitly **do not** destroy the Task โ€” they match Android's default of keeping Tasks in recents: - `os.returnToLauncherTask` (priority 12) โ€” when the current Task has only one Activity and was launched-by another Task, reset the app's MemoryRouter to `/`, reactivate the caller Task, and consume the one-shot pointer. - `os.goHomeFallback` (priority 0) โ€” reset the current app to `/` and `goHome()`, leaving the Task on the recents. Task destruction is opt-in (e.g. `__OS__.closeApp(id)`). ## Intent system `IntentResolver` matches a posted intent (`action`, `scheme`, `type`, optional explicit `appId`) against each app's manifest filters. Multiple matches yield an in-OS chooser sheet; single matches dispatch directly. `startActivityForResult(intent, callback)` dispatches the target Activity and returns `true` if launch succeeded; the callback receives `{ resultCode, data? }` when the callee finishes. It is used for media-picker flows, payment confirmation, contact selection, etc. Full reference: [`intent-system.md`](intent-system.md). ## OsStateStore + managers + providers A unified Android-style data model lives in `OsStateStore.ts`. The shape: ```ts type OsState = { settings: { global: {โ€ฆ}; // affects all apps โ€” wifi, bluetooth, language system: {โ€ฆ}; // device-wide UI โ€” brightness, volume, font size }; hardware: { battery: {โ€ฆ}; wifi: {โ€ฆ}; cellular: {โ€ฆ}; bluetooth: {โ€ฆ}; storage: {โ€ฆ}; hotspot: {โ€ฆ}; // plus vpn/headset/alarm flags and nearby device presets }; permissions: { [appId: string]: { [perm: string]: 'not_requested' | 'granted' | 'denied' | 'denied_forever' } }; preferences: {โ€ฆ}; }; ``` This is persisted at the `os_state` localStorage key. **Managers** (`os/managers/*Manager.ts`) are write facades. `setPreference('brightness', 80)` flows through `DisplayManager` which clamps to the valid range and pushes a broadcast notification. Turning on airplane mode cascades wifi/bluetooth/cellular off via `ConnectivityManager`. The point of going through managers (rather than calling `setState` directly) is that the constraint logic is centralized. **Providers** (`os/providers/*Provider.ts`) hold **shared content data** โ€” the things that an Android `ContentResolver` would query: contacts, SMS, media. Each provider is its own persisted store. Apps consume them via `ContentResolver.query / insert / update / delete` rather than reaching into the store directly. Providers are deliberately **not** part of the OS `services` snapshot โ€” they're surfaced under `state.os.providers.*` in `__SIM__.getState()`. ## OS-level services Service singletons live at the top of `os/` (not in a subdirectory). They give apps a deterministic, persistable replacement for native browser APIs: | Service file | Purpose | App-visible API surface | |---|---|---| | `TimeService.ts` | Simulated wall clock; benchmark control over "now" | `TimeService.now()` / `.realNow()` / `.fromTimestamp(ms)` | | `LocationService.ts` | Simulated GPS, with preset cities + error simulation | `getCurrentPosition(...)` / `getSimulatedCoords()` | | `NetworkService.ts` | CORS-safe HTTP gateway with per-session cookie jar | `netFetch(url)` / `netJson(url)` / `netText(url)` | | `ClipboardService.ts` | Persisted clipboard | `read()` / `write(item)` | | `NotificationService.ts` | Volatile notification queue | `push({title, body, โ€ฆ})` / `dismiss(id)` | | `FileSystemService.ts` | Virtual filesystem with paths under `/sdcard/โ€ฆ` | `readFile(path)` / `writeFile(path, blob)` | | `MediaService.ts` | Audio/video playback, intent integration | `play(uri)` / `pause()` | | `AIService.ts` | Optional AI features (summarization, etc.) | feature-specific | A non-exhaustive set is documented in detail in [`services/README.md`](services/README.md). The full `__OS__.` surface lives in `os/types/globals.d.ts`. > โš ๏ธ Apps **must** use these services rather than calling browser APIs. ESLint currently rejects raw `Date.now()` and `new Date()` in `os/`, `apps/`, and `system/`; `navigator.geolocation`, `navigator.clipboard`, and absolute external `fetch` are platform rules that code review should enforce. Same-origin `fetch` is allowed; absolute external URLs go through `NetworkService` to dodge CORS and have their cookies survive resets. ## Globals: what apps and the benchmark see The OSContext Provider attaches these to `window` at startup: | Global | Lives in | Purpose | |---|---|---| | `window.__SIM__` | `os/types/globals.d.ts` | Simulator-level: `getState()`, `setState(patch)`, `reset()`, `resetState()`, `waitForData(appIds?)`, preload helpers, `warmUpAllApps()` | | `window.__OS__` | same | Per-feature OS handles: `openApp / closeApp / launchApp / handleBack / goHome / showRecents / getAppRoute` plus the services (`notifications`, `permissions`, `keyboard`, `device`, `clipboard`, `broadcast`, โ€ฆ) | | `window.__SIM_INPUT__` | `os/simInput.ts` | Synthetic gestures used by the benchmark and human debugging โ€” tap, doubleTap, longPress, swipe, type, back, home. | | `window.__SIM_QUERY__` | `os/simInput.ts` (alongside `__SIM_INPUT__`) | Read-only DOM queries: `getRectById / getRectBySelector / getRectByTrigger` | | `window.__SIM_TIME__` | `os/TimeService.ts` | Wall-clock override for reproducible runs | | `window.__SIM_LOCATION__` | `os/LocationService.ts` | GPS override with preset cities | | `window.__SIM_FS__` | `os/FileSystemService.ts` | Virtual filesystem control | | `window.__SIM_MEDIA__` | `os/types/globals.d.ts` | Media-library control and picker simulation | | `window.__SIM_AI__` | `os/types/globals.d.ts` | Optional simulated AI service hooks | | `window.__STORAGE_ISOLATION__` | `os/storageIsolation.ts` | Current storage-isolation mode metadata | | `window.__getScrollMeta__` | `os/SystemShell.tsx` | Auto-detect scrollable containers and read their position/extent | These are **debug / orchestration** APIs, not part of the agent's observation. The agent sees screenshots; everything above is for you, the human operator (or the benchmark). Full reference with method signatures: [`../../api/runtime-api.md`](../../api/runtime-api.md). ## Auto-discovery The OS finds installed apps at **compile time** via `import.meta.glob`: ```ts // os/PackageManagerService.ts โ€” manifests are eager so package metadata is ready at boot const manifestModules = import.meta.glob( ['../apps/*/manifest.ts', '../system/*/manifest.ts'], { eager: true }, ); // os/data/appRegistry.tsx โ€” app components are lazy-loaded when launched const appModules = import.meta.glob( ['../../apps/*/*App.tsx', '../../system/*/*App.tsx'], ); ``` That's why adding an app needs no edits anywhere in `os/`. The conventions every app must satisfy are in [`../app/module-contract.md`](../app/module-contract.md). ## Persistence model ``` localStorage key Contents Persisted? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ os_state OsStateStore: settings/hardware/ yes permissions/preferences __os_scenario_overrides__ build + telephony overrides yes (kept separate so bench scenario injects don't pollute os_state) App's Zustand store (one key per yes installed app; no suffix) launcher Pinned apps + launcher layout yes provider_contacts ContactsProvider store yes provider_sms SmsProvider store yes provider_media MediaProvider store yes os_clipboard_v1 ClipboardService yes (in-memory only) TaskManager, NotificationService, no โ€” reboot KeyboardService, SystemShadeService, TextSelectionService, LocationService* * LocationService is volatile but **seeded from SIMULATOR_CONFIG.location** on every reload โ€” it's the only volatile service that re-initializes from a config preset instead of a blank state. IndexedDB database Contents Persisted? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ mobile-gym file system DB FileSystemService virtual files yes ``` The rule of thumb: **content data is persisted, runtime/UI/session is volatile**. Browser refresh = device reboot. This is what makes `__SIM__.reset()` cheap and deterministic. Permissions are an **Android-aligned exception** to the volatile-runtime rule: once the user grants a runtime permission it persists across reboots (in real Android, in the system's package manager database; here, inside `os_state`). `PermissionService` is a facade over `OsStateStore.permissions`, not its own volatile store. There is currently **no boot-time cleanup** of historical localStorage keys. If you see legacy keys (`os_quick_settings_v2`, `os_device_v1`, etc.) in a browser dump, they're stale from earlier simulator versions โ€” the current code neither writes nor cleans them. ## SIMULATOR_CONFIG layer `os/data/simulatorConfig.ts` exports `SIMULATOR_CONFIG`, a configuration layer that has **no Android counterpart** โ€” it controls the simulator itself. It's intentionally separated from `OS_DEFAULTS` (which mirrors a clean factory Android device) so the device model stays "pure". Sections include `framework`, `time`, `location`, `ai`, `display`, and `intent`. Bench scenarios that need to override fields here (e.g. force a specific simulated location, change the chooser behavior, set theme color) write into `__os_scenario_overrides__` โ€” the same channel as `build` / `telephony` overrides, handled by `os/managers/registry.ts`. See [`../state/os-state.md`](../state/os-state.md) for the per-section breakdown. ## Adding new OS plumbing (briefly) You'll rarely need to. If you do: - **New service** โ€” drop a TS file in `os/` (top level), expose its API on `window.__OS__.` via the OSContext provider, and add the TypeScript surface to `os/types/globals.d.ts`. Use `createOsStore` if it needs persistence, `createVolatileOsStore` otherwise. - **New manager** โ€” file in `os/managers/`, wire it into `OsStateStore` writes and `managers/registry.ts` for preference routing. - **New provider** โ€” file in `os/providers/`, persist its own store, expose via `ContentResolver` queries. Add a `state.os.providers.` entry to `AppStateRegistry`. For all three, follow what the existing examples do โ€” the framework is small, and copying patterns is faster than reading more docs. ## Where to go next - ๐Ÿ“ฑ How an app integrates with all of this โ†’ [`../app/module-contract.md`](../app/module-contract.md) - ๐Ÿ—ƒ๏ธ How state is layered and snapshotted โ†’ [`../state/model.md`](../state/model.md) - ๐ŸŽฏ Cross-app calls and Intent matching โ†’ [`intent-system.md`](intent-system.md) - ๐Ÿงญ Cross-app Task/back-stack traces โ†’ [`cross-app-launch.md`](cross-app-launch.md) - ๐Ÿ›ฐ๏ธ What each service actually does โ†’ [`services/README.md`](services/README.md) - ๐Ÿค– The view from Android โ€” which AOSP concepts we model, which we don't โ†’ [`../android-mapping.md`](../android-mapping.md)