mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-20 21:26:04 +00:00
116 lines
7.9 KiB
Markdown
116 lines
7.9 KiB
Markdown
# HAPI Android Companion
|
|
|
|
Native Android client (Kotlin + Jetpack Compose) for the HAPI hub. Fully
|
|
independent from the web app; shares only the protocol contract
|
|
(`docs/api/`) and the golden fixtures (`shared/fixtures/`).
|
|
|
|
- **applicationId**: `run.hapi.companion` · **minSdk** 26 · **target/compileSdk** 36
|
|
- **Toolchain**: Gradle 8.14.2 (wrapper) · AGP 8.11.1 · Kotlin 2.1.21 · Compose BOM 2025.05.00 · JDK 17+ (CI uses 21)
|
|
|
|
## Modules
|
|
|
|
| Module | Type | Responsibility |
|
|
|---|---|---|
|
|
| `:core:protocol` | **pure Kotlin/JVM** (no Android) | Hub wire types (kotlinx.serialization), chat pipeline port (normalize → reduce → tool groups), message-window/pagination logic, versioned patch application, modes catalog, git output parsers, `BindLink` pairing-link parsing. **M1a landed**: `wire/` (`HapiJson`, `Session`/`SessionPatch`/`SessionSummary`, `DecryptedMessage`, `AgentState`, `Machine`, 13-type `SyncEvent` union via `SyncEvents.parse`, `MessagesResponse`), `catalog/` (flavors + permission/collaboration modes), `patch/SessionPatching.kt` (exact port of `web/src/lib/sessionPatch.ts`), all fixture-verified. |
|
|
| `:core:data` | Android library | Transport + persistence. **M1b landed** — `auth/` (`JwtPeek`, `CredentialStore` interface + `EncryptedPrefsCredentialStore`/in-memory, `HubUrls` origin normalization, `HubRegistry` roster behind a storage seam, `AuthInterceptor` + single-flight `TokenAuthenticator` with `ensureFreshToken()` and terminal `AuthEvents`), `api/` (`HapiApi` — plain OkHttp + kotlinx.serialization, one suspend fun per v1 endpoint incl. generated-image bytes via a 256 MB OkHttp cache and the multipart transcription helper; `ApiError` with `(status, code)`), `HubSession` per-hub factory; MockWebServer-tested. **M1c landed** — `sse/`: `SseEngine` (per-key `global`/`session:<id>` loops, `connection-changed` handshake gate with `ok`/`gap` resume verdict, per-key `Last-Event-ID` cursors advanced only after downstream hand-off (at-least-once), 10 s connect deadline, 90 s watchdog, 1 s→30 s→300 s backoff + jitter, background retry deferral + 45 s foreground stale check, one silent 401 re-auth per cycle), `OkHttpSseTransport` (dedicated client, `readTimeout=0`, incremental gzip decoding pinned by test, `acceptEncodingIdentity` fallback), `SyncEventRouter` → `SyncTargets` seam; virtual-time tested. Still to come: StateFlow stores + AtomicFile JSON snapshots (M2), FCM registration + WorkManager workers (M4). |
|
|
| `:app` | Android application | Compose UI, navigation, deep links (`hapicompanion://bind`), FCM service (M4), hand-rolled DI (`AppGraph`, no Hilt). **M1d landed** — `di/` (`AppGraph` process singletons: Preferences DataStore-backed `HubRegistryStorage`, `EncryptedPrefsCredentialStore`, `HubRegistry`, auth-terminal fan-out; `HubGraph` per active hub: `HubSession` + `SseEngine` wired to `ensureFreshToken`, recreated on hub switch; `LocalAppGraph` CompositionLocal + `viewModelFactory` helper), `feature/pairing/` (landing / zxing `ScanContract` QR scan / manual entry sharing one `PairingViewModel`: health + protocol check → `POST /api/auth` → persist + activate), `feature/home/` placeholder (hub switcher + sign-out), `Navigation.kt` (pairing ⇄ home, auth-terminal → pairing with banner), bind deep-link handling in `MainActivity`. |
|
|
|
|
Dependency direction: `:app` → `:core:data` → `:core:protocol`.
|
|
|
|
## Protocol conformance fixtures
|
|
|
|
`:core:protocol` is the porting target for `web/src/chat/` and is verified
|
|
against golden fixtures generated from the web implementation (track K).
|
|
The test task already passes the fixtures location as a system property:
|
|
|
|
```kotlin
|
|
// core/protocol/build.gradle.kts
|
|
tasks.test {
|
|
systemProperty("hapi.fixtures.dir", rootDir.parentFile.resolve("shared/fixtures").absolutePath)
|
|
}
|
|
```
|
|
|
|
Fixture-driven tests (M2) read `System.getProperty("hapi.fixtures.dir")` —
|
|
no further build changes are needed when `shared/fixtures/**` lands. CI
|
|
re-runs this suite whenever `android/**` or `shared/fixtures/**` change.
|
|
|
|
## Building
|
|
|
|
Requires an Android SDK for `:app`/`:core:data` (set `ANDROID_HOME` or
|
|
`android/local.properties` with `sdk.dir=...`). `:core:protocol` alone needs
|
|
only a JDK.
|
|
|
|
```sh
|
|
cd android
|
|
./gradlew :core:protocol:test # pure JVM protocol tests (fast)
|
|
./gradlew :app:assembleDebug # debug APK
|
|
./gradlew :app:installDebug # install on a connected device
|
|
```
|
|
|
|
Without an Android SDK you can still run the protocol suite by configuring
|
|
only the needed projects:
|
|
|
|
```sh
|
|
./gradlew --no-configuration-cache --configure-on-demand :core:protocol:test
|
|
```
|
|
|
|
CI (`.github/workflows/android.yml`) runs the protocol tests and
|
|
`:app:assembleDebug` on every PR touching `android/**` or `shared/fixtures/**`.
|
|
|
|
## Pairing
|
|
|
|
HAPI is self-hosted: the app talks to a hub **you** run. Pairing = giving the
|
|
app a hub URL plus that hub's access token; the app verifies the hub
|
|
(`GET /health`, protocol version), exchanges the token for a JWT
|
|
(`POST /api/auth`), stores the credentials in `EncryptedSharedPreferences`
|
|
(keyed per hub — multiple hubs can be paired, one active at a time), and
|
|
lands on the session UI. Three entry points:
|
|
|
|
1. **QR scan** — the hub prints two QR codes when started with `--relay`
|
|
(also under web Settings → Companion pairing). The in-app scanner accepts
|
|
both: the companion deeplink (`hapicompanion://bind?hub=…&code=…`) and the
|
|
web direct-access URL (`…?hub=…&token=…`).
|
|
2. **Deep link** — scanning the companion QR with the system camera opens the
|
|
app directly with a confirm screen (`hapicompanion://bind` intent filter).
|
|
3. **Manual entry** — hub URL + access token, for hubs started without
|
|
`--relay`.
|
|
|
|
### Pairing against a local dev hub
|
|
|
|
```sh
|
|
# repo root: start the hub (prints the access token + QR codes)
|
|
bun run dev
|
|
|
|
# emulator: the host machine is 10.0.2.2
|
|
# Hub URL: http://10.0.2.2:3006
|
|
# Access token: from the hub terminal / hub settings.json (CLI_API_TOKEN)
|
|
# physical device: use the machine's LAN IP, e.g. http://192.168.1.10:3006
|
|
adb shell am start -a android.intent.action.VIEW \
|
|
-d "hapicompanion://bind?hub=http%3A%2F%2F10.0.2.2%3A3006&code=<accessToken>" # optional: exercises the deep link
|
|
```
|
|
|
|
Plain-`http` LAN/emulator hubs work in all build types: the manifest opts in
|
|
to cleartext traffic (`android:usesCleartextTraffic="true"`), because
|
|
self-hosted LAN hubs are the primary pairing target and Android cannot scope
|
|
the exemption to local addresses only. Sign-out (home → Sign out) deletes the
|
|
stored credentials for that hub and drops it from the roster.
|
|
|
|
## Milestones (track B of the native-clients plan)
|
|
|
|
- **M0** — this scaffold: modules, version catalog, CI, placeholder screen.
|
|
- **M1** — foundations: wire types + modes catalog; auth + `HapiApi` (MockWebServer-tested); `SseEngine` reconnect state machine + versioned patches (gzip streaming verified); pairing UI + `hapicompanion://bind` deep link.
|
|
- **M2** — read-only chat: chat pipeline port gated on fixtures all-green; session list; `MessageWindowStore` port; Markdown renderer; read-only chat screen (`LazyColumn(reverseLayout = true)`).
|
|
- **M3** — interaction: composer (optimistic send/queue/steer/drafts), permission approvals UX, session controls (mode/model/abort/resume/rename/archive), new session, dictation.
|
|
- **M4** — FCM push (register → notification actions via expedited WorkManager) + files/git viewer, Scratchlist, usage/storage stats.
|
|
- **M5** — polish: zh-CN i18n, OLED/Material You theming, predictive back, LeakCanary pass, Play listing + self-build docs.
|
|
|
|
## Firebase / push (self-build note)
|
|
|
|
M0 deliberately does **not** apply the `com.google.gms.google-services`
|
|
plugin and has no Firebase dependency, so the project builds without any
|
|
`google-services.json`. In M4a the plugin lands together with the FCM
|
|
service: official builds inject the default Firebase project config in CI,
|
|
while self-builders drop in their own `app/google-services.json` (docs will
|
|
accompany M4a; a `PushBinding` seam for hub-provided `FirebaseOptions` is
|
|
planned for v1.x).
|