Files
web-terminal/android
Yaojia Wang 4ea8f7862a feat(android): pure-Kotlin foundation — AW0 scaffold + AW1 modules (218 tests green)
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.
2026-07-08 13:45:07 +02:00
..

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 (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 (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.propertiessdk.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*Transports 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: 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

# 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.