Implements the verifiable pure-Kotlin core of the Android client per
docs/ANDROID_CLIENT_PLAN.md, mirroring the iOS SPM package set as Gradle modules.
Built + reviewed via multi-agent workflow (explore→implement→verify→review),
then review findings fixed with regression tests.
Modules (all pure JVM, kotlin("jvm"); Android-framework modules scaffolded but
gated off in settings — no Android SDK in this env):
- :wire-protocol — frozen wire contract (sealed Client/ServerMessage, enums,
HostEndpoint CSWSH origin derivation, transport interfaces), hand-rolled codec
byte-identical to the server's JSON.stringify + tolerant kotlinx decode.
- :session-core — ReconnectMachine (1→2→4→8→16→30 backoff), PingScheduler,
GateTracker (two-line epoch guard), AwayDigest, UnreadLedger, TitleSanitizer,
KeyByteMap (byte-matches public/keybar.ts).
- :api-client — all REST routes, tolerant decode, Origin-iff-guarded, prefs
unknown-key preservation, pairing probe + host-tier classifier.
- :client-tls — pure half: PKCS#12 parse, X509KeyManager alias logic,
CertificateSummary, provider-agnostic wrong-passphrase classification.
- :test-support — FakeTransport / FakeHttpTransport / virtual-clock fakes.
Verify: ./gradlew clean test → 218 tests, 0 failures (wire-protocol 47,
session-core 60, api-client 69, client-tls 27, test-support 15).
Review (3 lenses, 8/10 each) findings fixed: sessionId JSON-injection escape,
CancellationException propagation in PingScheduler, PairingProbe close-on-cancel
leak, HostEndpoint trim, HostClassifier hoisted to :wire-protocol (type unified),
BouncyCastle-portable wrong-passphrase detection, lone-surrogate escaping.
Known gap (documented, deferred): /push/fcm-token has no server route yet —
plan task A33 (src/push/fcm.ts) delivers it.
96 lines
5.4 KiB
Markdown
96 lines
5.4 KiB
Markdown
# WebTerm — Android client
|
|
|
|
A native Android client for the WebTerm browser-terminal server, targeting functional
|
|
parity with the shipped **iOS** client. See the full design in
|
|
[`docs/ANDROID_CLIENT_PLAN.md`](../docs/ANDROID_CLIENT_PLAN.md) (stack §2, module
|
|
architecture §3, server contract §4, task waves §5).
|
|
|
|
This directory is a **Gradle multi-module** project. The module set mirrors the iOS
|
|
SPM package set and inherits its rule: *dependencies only flow down; nothing points
|
|
upward* (ARCHITECTURE §1).
|
|
|
|
## ⚠️ No-SDK constraint (why only 5 modules build here)
|
|
|
|
The current build environment has **no Android SDK**. Everything that can be pure
|
|
**Kotlin/JVM** (`kotlin("jvm")`) is built and unit-tested now; anything that needs the
|
|
Android framework (`com.android.*` plugins) is **scaffolded but disabled**.
|
|
|
|
- **Enabled now (pure Kotlin/JVM, `./gradlew test`-able):**
|
|
`:wire-protocol`, `:session-core`, `:api-client`, `:client-tls`, `:test-support`.
|
|
- **Scaffolded but COMMENTED OUT** in [`settings.gradle.kts`](settings.gradle.kts)
|
|
(dirs + a `build.gradle.kts` stub exist, marked `// TODO(android-sdk)`):
|
|
`:app`, `:terminal-view`, `:host-registry`, `:client-tls-android`.
|
|
|
|
To bring the Android modules online later: install an SDK, add
|
|
`local.properties` → `sdk.dir`, add the Android Gradle Plugin + `google()` to
|
|
`pluginManagement`, then uncomment the `include(...)` lines and the plugin blocks in
|
|
each stub.
|
|
|
|
## Module map (mirror of the iOS SPM packages — plan §3)
|
|
|
|
| iOS SPM package | Android module | Kind | Status |
|
|
|------------------------|------------------------|-------------------------------|--------|
|
|
| WireProtocol | `:wire-protocol` | pure Kotlin/JVM | ✅ built |
|
|
| SessionCore (reducers) | `:session-core` | pure Kotlin/JVM | ✅ built |
|
|
| APIClient | `:api-client` | pure Kotlin/JVM | ✅ built |
|
|
| ClientTLS (pure half) | `:client-tls` | pure Kotlin/JVM | ✅ built |
|
|
| TestSupport | `:test-support` | pure Kotlin/JVM (fakes) | ✅ built |
|
|
| ClientTLS (fwk half) | `:client-tls-android` | Android (AndroidKeyStore/Tink)| ⏸ SDK-gated |
|
|
| HostRegistry | `:host-registry` | Android (DataStore) | ⏸ SDK-gated |
|
|
| SwiftTerm host view | `:terminal-view` | Android (Termux wrap) | ⏸ SDK-gated |
|
|
| App/WebTerm | `:app` | Android app (Compose/Hilt/FCM)| ⏸ SDK-gated |
|
|
|
|
> Not yet scaffolded: `:transport-okhttp` (OkHttp `TermTransport`/`HttpTransport`
|
|
> impls, JVM) is owned by task **A7** and will be added then. The iOS
|
|
> `URLSession*Transport`s consolidate into it (plan §3 framing note).
|
|
|
|
### Dependency graph (arrows = "depends on")
|
|
|
|
```
|
|
:app (SDK-gated)
|
|
┌───────────────┬───┴────┬──────────────┬───────────────┐
|
|
▼ ▼ ▼ ▼ ▼
|
|
:terminal-view :session-core :api-client :host-registry :client-tls-android
|
|
(SDK-gated) │ │ (SDK-gated) │
|
|
│ │ │ ▼
|
|
│ │ │ :client-tls (pure)
|
|
└──────┬───────┴──────────┴──────────────┬────────────────┘
|
|
▼ ▼
|
|
:wire-protocol ◀──────────── :transport-okhttp (A7, not yet)
|
|
▲
|
|
└──────── :test-support → test source sets only
|
|
```
|
|
|
|
`:wire-protocol` is the **frozen shared contract** (Android analogue of
|
|
`src/types.ts` + WireProtocol) — `ClientMessage`/`ServerMessage`, `MessageCodec`,
|
|
`Validation`, `WireConstants`, `HostEndpoint` (the single Origin/wsURL derivation),
|
|
and the `TermTransport` / `HttpTransport` / `PingableTermTransport` boundary
|
|
interfaces. New wire types are added **only** here (a coordination point).
|
|
|
|
## Toolchain
|
|
|
|
- **Gradle** 9.6.1 (via the committed wrapper — always use `./gradlew`).
|
|
- **Kotlin** 2.3.21 (matches the Kotlin embedded in Gradle 9.6.1).
|
|
- **JVM toolchain** 17 (`jvmToolchain(17)` in every module).
|
|
- Versions are pinned in the version catalog
|
|
[`gradle/libs.versions.toml`](gradle/libs.versions.toml): kotlinx-serialization-json,
|
|
kotlinx-coroutines-core/-test, JUnit5 (Jupiter), Turbine, MockK.
|
|
|
|
Pure modules apply `kotlin("jvm")` + `kotlin("plugin.serialization")`, wire the
|
|
`libs.bundles.unit-test` bundle into `testImplementation`, and run tests on the
|
|
JUnit Platform (`tasks.test { useJUnitPlatform() }`).
|
|
|
|
## Build & test
|
|
|
|
```bash
|
|
# Use the committed wrapper for everything.
|
|
./gradlew help # sanity: the build configures
|
|
./gradlew projects # lists the 5 pure modules
|
|
./gradlew build # compile all pure modules
|
|
./gradlew test # run JVM unit tests (JUnit5 + coroutines-test + Turbine + MockK)
|
|
```
|
|
|
|
> Testing target: **≥80% Kover coverage** on the pure modules (`:wire-protocol`,
|
|
> `:session-core`, `:api-client`, `:client-tls` pure half). TDD, immutable data,
|
|
> small focused files — same discipline as the rest of the repo.
|