diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-03 13:41:30 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-03 14:24:54 +0200 |
| commit | eac753c436d64adf921ae2cbdd0d09e095a5e5a3 (patch) | |
| tree | 63117b0a9439b825b70c73ac28ac358d6249c43b | |
| parent | 1f19ccaa583734951d5fcccebb06b073a2b1a111 (diff) | |
| download | meshbay-eac753c436d64adf921ae2cbdd0d09e095a5e5a3.tar.gz | |
docs: the Android client
Design §11.3/§11.4/§15 state what is built and what the phone found; the user
guide drops 'no Android client'; CLAUDE.md gains the package's locators and
the lessons casting from a phone taught.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 55 | ||||
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 65 | ||||
| -rw-r--r-- | docs/USERGUIDE.md | 17 |
3 files changed, 128 insertions, 9 deletions
@@ -28,7 +28,9 @@ meshbay/ ├── packages/ │ ├── meshbay-common/ # Shared crypto + protocol — python3-meshbay-common RPM │ ├── meshbay-hub/ # Hub server (FastAPI + PostgreSQL) — meshbay-hub RPM -│ └── meshbay-node/ # Node daemon + local UI — meshbay-node RPM +│ ├── meshbay-node/ # Node daemon + local UI — meshbay-node RPM +│ ├── meshbay-client/ # Desktop client (Electron) +│ └── meshbay-android/ # Android client (WebView shell, Kotlin) — README.md ├── poc/ # POC spike scripts (reference, not production) ├── docs/ # Architecture drafts and POC plans ├── packaging/ # RPM spec files, DEB control files, systemd units @@ -882,6 +884,43 @@ do. Read them before writing anything that touches the same mechanism. message later by `update_groups`, which assigned its list verbatim. A limit enforced on one path is not enforced +- **Chromium freezes a hidden page sixty seconds after hiding it**, in an + Android WebView as in a tab, and nothing about the *process* changes that: a + media-playback foreground service, a partial wake lock, a Wi-Fi lock and + `setRendererPriorityPolicy` all failed to keep a casting page alive with the + screen off (spike, measured by a heartbeat and the page's own `freeze` + event). What works is the shell telling the WebView its window is still + visible (`ShellWebView.keepVisible`) — set while a cast runs and only then. + Audible audio also exempts a page; a phone humming in the room is not a fix + +- **A first chunk is not a header.** The relay took the page's first decrypted + chunk as the stream's header. On a real film that chunk was by turns 64 KB + (header plus film), 28 bytes (the ftyp alone, the moov in the next push), or + the header exactly — depending on when the node read ffmpeg's output — so the + same film cast on the third try and not the first. The header is everything + before the first moof; boxes, not chunks, are the unit + +- **A seek's first segments came during its own landing and were thrown away.** + `reinitAt` waits on the SourceBuffer; the new stream's first segments arrived + in that gap and the `awaitingInit` flag dropped them as the old film's. The + local player never showed it — its SourceBuffer kept the header from the + start of the film — and a cast relay restarted at that landing received a + stream with no header. Ordering, not the flag, says which stream a segment + belongs to: everything after `stream_init` is held and replayed + +- **A drop threshold sized for small chunks drops whole segments.** The relay + dropped a fragment once 8 MB waited for a receiver; at a film's bitrate one + fragment is 5–10 MB, so a receiver simply reading at playback speed lost + fragments and the picture froze for each. Nothing logged it until a drop + counter was added. The lead now waits in a spool file + +- **When a receiver stops answering, prove which leg failed before reading code.** + A first cast failed every time and two code fixes changed nothing; the cast + framework's own log said `onSocketConnectionFailed … IO Error` to the + receiver's port 8009, and the phone could not ping the receiver while this + machine could. The receiver was half-crashed; a power cycle fixed it. A + machine that reaches the receiver proves nothing about the phone + **Corrections that used to live here** — `punch_nat()` is not a traversal stack, the node keystore's Argon2id parameters, what group chat actually uses, and what is sealed on the wire — are now design statements in `docs/MESHBAY_DESIGN.md` @@ -989,6 +1028,20 @@ here are kept only where they are a rule about *editing* the code. | Node page | `node-page.js` | §6.7 | | i18n | `i18n.js`, `locales/*.js` | `en.js` is the source; **ten catalogues, and a new key goes in all ten** | +### Android (`packages/meshbay-android/`) + +Built with `./gradlew assembleDebug` / `assembleRelease` (JDK 17+, an Android +SDK); `./gradlew testDebugUnitTest` runs the JVM tests, which pytest also runs +when `ANDROID_HOME` is set (`test_android_shell.py`). + +| Need | File | Note | +|---|---|---| +| The shell, the asset loader, the CSP | `MainActivity.kt`, `shell/UiAssets.kt` | the CSP is `main.js`'s, held equal by `test_android_shell.py` | +| The bridge | `assets/bridge/meshbay-bridge.js` (the preload's counterpart), `bridge/Bridge.kt`, `Channels.kt`, `KeyChannels.kt` | **absent, not refusing**, for what a phone lacks; every channel the shim names is the preload's | +| Keys | `keys/Kdf.kt`, `Keyring.kt`, `Transcripts.kt`, `SecretStore.kt`, `DeviceKey.kt` | **held to `meshbay-hub/tests/vectors/keyring.json`**; regenerate it with `gen_keyring_vectors.js` only for a deliberate format change | +| Downloads | `save/SaveSinks.kt`, `SaveNames.kt` | `OPENABLE` is `downloads.js`'s, compared by `test_android_downloads.py` | +| Casting | `cast/CastRelay.kt`, `CastControl.kt`, `CastChannels.kt`, `CastService.kt`, `shell/ShellWebView.kt` | `MeshBayCast` in logcat says what the receiver and the relay did | + ### Test harnesses that drive the real thing | Need | File | diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 3ce0a67..58bbbdb 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -3343,6 +3343,45 @@ A client, not a host. The platform is hostile to *hosting* a node — background execution, storage, battery — and fine as a *client*, which is one of the reasons enrichment is node-side (§6.5). +**The application is the desktop client's design in a system WebView** +(`packages/meshbay-android/`). What §8.2 depends on is not Electron but an +engine with `RTCPeerConnection`, WebCrypto X25519/Ed25519 and MSE, the +interface loaded from the package, and a privileged side reached through a +narrow bridge — and Android has all three: + +- **The interface is the hub's `static/`, copied at build time** (§8.3) and + served from the APK by an asset loader under + `https://appassets.androidplatform.net` — a secure context, so `crypto.subtle` + exists. Nothing the hub serves is ever loaded into the WebView; the policy is + the desktop's, sent as a header. +- **The bridge offers the desktop preload's `window.meshbay`** wherever it + offers anything. What a phone does not have — the node, shared folders, the + tray — is **absent, not a function that refuses**: `platform.js` decides what + to show from whether an object exists. The bridge answers the packaged + origin's top-level document only (`addWebMessageListener`, `isMainFrame`); a + same-origin child frame does get the port, and is refused there. +- **Keys are held natively** (§3.7, §14.1 #20): device key, bundle key and every + node identity, under an Android Keystore key, never handed to the page. The + Kotlin keyring is a third implementation of the bundle format and the + transcripts, so **`tests/vectors/keyring.json` — generated from the desktop + keyring and checked against the specification — is what every implementation + must reproduce**; a list the vectors cannot hold (the admin operations that + may be signed) is compared by source. +- **Hub calls leave from native code**, to the signed-in hub only, as on the + desktop. **Downloads go to disk** through the Storage Access Framework or the + Downloads collection, as pending files until complete; uploads come through + the system picker. +- **A hidden page is frozen by Chromium sixty seconds after it is hidden** — + measured, and whatever the process's importance: a foreground service, wake + locks and the renderer's priority policy do not prevent it. A cast's pipeline + lives in the page (WebRTC → decrypt → relay), so while one runs the shell + keeps the WebView reported visible and holds a media-playback foreground + service; nowhere else, because a page never hidden is never throttled. + +Release builds are signed with the development key until the release key exists +(Stage D12); §2.3's sentence about who holds a signing key applies to whichever +store distributes them. + ### 11.4 Casting An HTTP relay in the desktop client serves a standard fragmented-MP4 stream that @@ -3357,6 +3396,20 @@ within two. The main process holds what the scan has found and the page polls it waiting on one call for the whole scan; a poll uses the same checked `handle()` door as every other call, where a pushed event would be a second one. +**The Android relay is the desktop's, with three things the phone found.** +(1) **The header is everything before the first moof**, however many chunks it +arrives in: the node's first chunk can be the 28-byte ftyp alone, and served as +the header it left the receiver without a moov. (2) **What a receiver has not +read waits in a spool file, not in memory.** A fragment is a segment — 5 to +10 MB at a film's bitrate — and the page runs ahead of the television by its +whole read-ahead; dropped past the desktop's 8 MB bound, each lost fragment +froze the picture for its length. (3) **A seek's first segments are held while +it lands** (`video-player.js`): they are the new stream, its header first, and +they arrived while `reinitAt` waited on the SourceBuffer and were dropped as the +old film's — the local player never noticed, a relay restarted there did. The +local element keeps playing while a cast runs, because its playhead paces the +stream, and is muted. + **Subtitles are rebased onto the relay's clock before they are sent.** The node extracts a track whole, so its cues carry the film's timeline, and the player can use them unchanged because its SourceBuffer is given `timestampOffset = @@ -3814,7 +3867,9 @@ pause and resume, the group-application framework with Chat, Files, Videos, Music and Photos, cross-group search with source merging, per-account playlists, casting to a Chromecast with subtitles rebased onto the relay's clock, the operator CLI and loopback control API, the desktop client through its identity -and download stages, account recovery, and the Windows port through packaging. +and download stages, account recovery, the Windows port through packaging, and +the Android client — the shell, native keys, downloads and uploads, and casting +(§11.3). The packages install: a machine has been taken from the built artefacts to a running hub and node on **Ubuntu 26.04 (`.deb`), Fedora 44 (`.rpm`) and @@ -3842,13 +3897,19 @@ process runs it — `systemctl --user` on Linux, Task Scheduler on Windows. | — | **Bitmap subtitles** (PGS, VOBSUB — about a fifth of the embedded streams). No WebVTT without OCR; they are not listed rather than listed and blank. Burn-in covers them and costs `-c:v copy`, which is what the eight-slot sizing assumes never happens | | — | Delegation (§3.4) | | — | Tier 3 roster attestation (§3.3) | -| — | Android client | +| — | **Android: phone behaviour and release** — the back button driving the page, recovery from a network handover, keeping a download alive with the screen off, lock-screen media controls, and a release key (§11.3) | | — | **Federation between two hubs.** The protocol is written and switched off in the code (§7.6); what is not built is one run between two machines | ### 15.3 Open, and why each is where it is | Item | Status | |---|---| +| **The desktop cast relay drops whole segments** | `cast-relay.js` drops a fragment once 8 MB wait for a receiver, and a fragment is a segment, 5 to 10 MB at a film's bitrate: the picture freezes for its length. Its backlog is bounded in fragments only. The Android relay spools a receiver's lead to disk and bounds its backlog in bytes (§11.4); the desktop has neither | +| **The desktop cast relay takes the first chunk for the whole header** | The node's first chunk can be the ftyp alone, the moov in the next; the receiver then gives up. Fixed in the Android relay (§11.4), not in `cast-relay.js` | +| **A cast restart may pull far ahead** | Seen once on an emulator with a synthetic film: after the restart's reinit the node reported `duration=None`, and the page pulled most of the film at network speed. Not reproduced on a real film; the suspicion is a read-ahead budget computed without a duration | +| **"Copy stream URL" after picking a receiver casts to that receiver** | The player keeps the last device chosen, so the copy-only path reconnects it instead of only starting the relay | +| **The home page says "No groups yet" when the hub cannot be reached** | An unreachable hub reads as an account with no groups, rather than as an error | +| **`meshbay-node init` says "Settings → Link Node"** | The control is on the Profile page, as QUICKSTART says | | **C4** for accounts with browser access | Closed against operators by the pepper; **open against an active hub**, which holds the pepper and can fetch a bundle with a token it mints — the adversary T3 already concedes for browsers (§3.7) | | **A desktop that never held an identity cannot open its recovery copy** | The application opens a node's passphrase copy, not the recovery copy: after a reset, an identity it never held is recovered from a browser (§3.6). And an identity the application mints gets a recovery copy only when the recovery key is entered on its Profile page with browser access on | | **T3** for browser users | **Accepted permanently.** Removed for native clients, and that removal's value depends on reproducible builds | diff --git a/docs/USERGUIDE.md b/docs/USERGUIDE.md index d631804..ad51982 100644 --- a/docs/USERGUIDE.md +++ b/docs/USERGUIDE.md @@ -392,9 +392,10 @@ resume. ### Casting to a TV -**Desktop application only.** A film playing in the application can be sent to a -cast-capable TV or dongle on the same network: *Cast to device* in the player, -pick one from the list. Devices appear as they answer, usually within a couple +**The desktop and Android applications.** A film playing in the application can +be sent to a cast-capable TV or dongle on the same network: *Cast to device* in +the player, pick one from the list. On a phone, the film goes on playing silently +on the phone while the TV shows it, and the screen can be turned off. Devices appear as they answer, usually within a couple of seconds; the search carries on for a few more, for a TV that is still waking up. @@ -967,12 +968,16 @@ Better to know now than to go looking for it: is nothing to check a download against and no updates through your distribution. Until that ships, take them from the download page and from nowhere else. -- **There is no Android client.** A phone browser works. +- **The Android application is not released yet.** It works — groups, chat, + films, downloads, casting — but is built and installed by hand and signed + with a development key, so there is no store page and no update channel. The + back button and the lock screen do not drive it yet. A phone browser works + too. - **A node cannot be hosted on Android**, and is not planned to be. - **Hubs do not talk to each other yet.** Everyone in a group needs an account on the same hub. -- **Casting reaches Chromecast devices** from the desktop application. Support - for other TV protocols is designed but not built. +- **Casting reaches Chromecast devices** from the desktop and Android + applications. Support for other TV protocols is designed but not built. - **Some subtitle tracks cannot be shown** — the ones stored as images rather than text, roughly one embedded track in five. Displaying them would need text recognition. |