Files
web-terminal/android
Yaojia Wang 5e427dcf98 feat(android): device enrollment library + rotation (B4)
Hardware-backed (StrongBox/TEE) key + PKCS#10 CSR + /device/enroll client in
:api-client, presented via the existing X509KeyManager; renew body {csr}-only;
DeviceKeyProvider seam makes the orchestration JVM-testable. api-client tests +
koverVerify 80% gate pass.
2026-07-18 13:32:05 +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).

Build environment (SDK installed — all modules build)

The Android SDK is installed and every module — pure Kotlin/JVM and Android-framework alike — builds and unit-tests here. AGP 9.2.1 (built-in Kotlin) + Gradle 9.6.1 build against SDK 35/36.

  • Pure Kotlin/JVM (./gradlew test): :wire-protocol, :session-core, :api-client, :client-tls, :test-support, :transport-okhttp.
  • Android-framework (online in settings.gradle.kts): :app, :terminal-view, :host-registry, :client-tls-android.

Setup: local.propertiessdk.dir=/usr/local/share/android-commandlinetools; google() is in pluginManagement/dependencyResolutionManagement. Green gate: ./gradlew test :app:assembleDebug koverVerify.

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) built
HostRegistry :host-registry Android (DataStore) built
SwiftTerm host view :terminal-view Android (Termux wrap) built
App/WebTerm :app Android app (Compose/Hilt/FCM) built

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
        ┌───────────────┬───┴────┬──────────────┬───────────────┐
        ▼               ▼        ▼              ▼               ▼
 :terminal-view   :session-core :api-client  :host-registry  :client-tls-android
        │              │          │                               │
        │              │          │                               ▼
        │              │          │                          :client-tls (pure)
        └──────┬───────┴──────────┴──────────────┬────────────────┘
               ▼                                  ▼
         :wire-protocol   ◀────────────  :transport-okhttp
               ▲
               └──────── :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.

Android SDK setup (proven working)

The pure JVM modules need only a JDK + Gradle. The Android-framework modules (:app, :terminal-view, :host-registry, :client-tls-android — plan AW2+) need the Android SDK. This machine is set up and the toolchain is proven (an AGP library module compiled against SDK 35 and produced an AAR):

  • SDK location: /usr/local/share/android-commandlinetools (installed via brew install --cask android-commandlinetools).
  • Installed packages: platform-tools, platforms;android-35, platforms;android-36, build-tools;35.0.0, build-tools;36.0.0. (:app compiles against SDK 36 — the Kotlin-2.3.21-contemporaneous androidx/Compose line refuses SDK 35; platforms;android-37 is not fetchable here as the cmdline-tools are too old to parse the v4 repo XML.)
  • android/local.properties (gitignored) points Gradle at it: sdk.dir=/usr/local/share/android-commandlinetools.
  • Shell env (for sdkmanager/adb): export ANDROID_HOME=/usr/local/share/android-commandlinetools.

Wiring an Android module (the working recipe)

  • Repos: google() is in both pluginManagement and dependencyResolutionManagement in settings.gradle.kts (needed to resolve AGP + androidx).
  • Plugin: AGP 9.2.1 (libs.plugins.android.library / .android.application), compatible with Gradle 9.6.1.
  • Gotcha: AGP 9 has built-in Kotlin — apply ONLY the android plugin. Adding org.jetbrains.kotlin.android errors with "no longer required since AGP 9.0".
  • Module block: android { namespace = "…"; compileSdk = 36; defaultConfig { minSdk = 29 } }. (Framework modules target compileSdk = 36; targetSdk stays 35 per plan §2.)
  • :app UI-stack version matrix (A13, proven :app:assembleDebug green): AGP 9.2.1 · Kotlin 2.3.21 · Compose-compiler plugin org.jetbrains.kotlin.plugin.compose = 2.3.21 · Compose BOM 2025.11.01 (→ material3 1.4.0, ui/foundation 1.9.5, material3.adaptive 1.2.0, material3-adaptive-navigation-suite 1.4.0) · Hilt (dagger) 2.60.1 via KSP 2.3.9 · androidx core-ktx 1.17.0 / activity-compose 1.12.4 / lifecycle 2.10.0. Apply plugins: android.application + kotlin.plugin.compose + ksp + dagger.hilt.android (NEVER kotlin.android). Bump these together with compileSdk 37 once platform 37 is installable.

To add more SDK pieces later (e.g. an emulator image for instrumented tests): sdkmanager "system-images;android-35;google_apis;arm64-v8a" "emulator".