Files
message_bus/reports/0010-2026-06-07-unibus-android-native.md
T
egutierrez d43ffae3ae chore: auto-commit (17 archivos)
- reports/0001-2026-06-07-unibus-grafana-monitoring.md
- reports/0008-2026-06-07-unibus-admin-users-wired.md
- reports/0008-2026-06-07-unibus-decentralization-audit.md
- reports/0009-2026-06-07-unibus-cluster-hardening.md
- reports/0010-2026-06-07-unibus-android-native.md
- reports/0011-2026-06-07-unibus-cluster-deploy.md
- reports/0012-2026-06-07-unibus-deploy-gaps-closed.md
- reports/0013-2026-06-07-unibus-admin-panel.md
- reports/0014-2026-06-07-unibus-users-http-admin-api.md
- reports/0015-2026-06-07-unibus-web-wired.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 01:57:00 +02:00

148 lines
7.4 KiB
Markdown

# Report 0010 — unibus app Android nativa (Compose + binding gomobile)
- **Fecha:** 07/06/2026
- **Autor:** Claude (agente Android)
- **Ámbito:** `projects/message_bus/apps/unibus` — binding `mobile/`, app `android/`
- **Estado:** done (mock primero, binding real cableado y listo para enchufar)
- **Rama:** `issue/android-native` (sub-repo `dataforge/unibus`), pusheada. NO mergeada a master.
- **Aislamiento:** todo el trabajo en worktree `/tmp/unibus_android`. Cero cambios en `~/fn_registry/.../apps/unibus` ni en el repo padre.
## Resumen
App Android nativa de unibus en Kotlin/Compose (Material 3, tema oscuro, acento
índigo/violeta) que replica el look & feel de la app web. Construida sobre un
binding gomobile rehecho que delega todo el cifrado de extremo a extremo en
`pkg/client` (el mismo que cualquier otro peer del bus). La iteración 1 corre
sobre datos mock para iterar el diseño; el repositorio real (binding) está
cableado y compilando detrás de la misma interfaz para enchufar el bus después.
Las tres pantallas (Login, lista de rooms, chat estilo Element) se verificaron
en el emulador Pixel_API34: arrancan sin crash y son visualmente equivalentes a
la web. Capturas en `assets/`.
## Cambios
| Qué | Dónde | Por qué |
|---|---|---|
| Binding gomobile rehecho | `mobile/unibus.go` (package `mobile`) | Se borró en la limpieza de frontends; la app lo necesita. API plana sobre `pkg/client`. |
| Script de regeneración del .aar | `mobile/gen_aar.sh` | El `.aar` (38 MB) no se versiona; reproducible con un comando. |
| App Compose | `android/` (Gradle + Kotlin) | App nativa con E2E real en el dispositivo. |
| Capa de repositorio | `android/.../data/Repository.kt` + `BindingRepository.kt` | Aísla la UI; mock para diseño, binding para el bus real, misma interfaz. |
| Pantallas | `LoginScreen.kt`, `RoomListScreen.kt`, `ChatScreen.kt` | Réplica de `web/src` (Login.tsx, Sidebar.tsx, ChatPanel.tsx). |
| Tema | `ui/theme/Theme.kt` | Escala dark.* + brand índigo/violeta igual que el tema Mantine de la web. |
| go.work gitignored | `.gitignore` (raíz del sub-repo) | Resuelve el `replace fn-registry` con path absoluto desde el worktree, sin tocar `go.mod`. |
### 1. Binding gomobile (`mobile/unibus.go`)
Tipos planos gomobile-friendly (string/[]byte/int/bool/error/interfaces). No
reimplementa criptografía — cada método delega en `pkg/client`. API expuesta:
- `GenerateIdentity(path)`, `NewSession(idPath, natsURL, ctrlURL, caPath)` → usa
`client.Connect` con la auth nueva (TLS pineado al CA + nkey si `caPath != ""`).
- `EndpointID`, `ConnectedServer`, `IsConnected`.
- `CreateRoom(subject, mode)`, `Join`, `RefreshSession` (contrato de membresía
issue 0006e: tras crear/unirse/invitar, `RefreshSession` antes de pub/sub).
- `Publish(text)`, `Subscribe(FrameListener)`, `ListRoomsJSON`.
- `Card`, `Invite`, `Kick`, `Request`, `Close`.
`FrameListener` (interfaz implementada en Kotlin) documenta el contrato de hilo:
`OnFrame` llega en una goroutine de NATS, así que la implementación Kotlin salta
al hilo principal (`Handler(Looper.getMainLooper()).post { ... }`) antes de tocar
estado de Compose.
Clases Java generadas: `com.unibus.core.mobile.{Mobile, Session, FrameListener}`.
### 2. App Compose (`android/`)
- Navegación por estado (KISS, sin lib de routing): Login → lista de rooms → chat.
- `AppViewModel` orquesta el estado observable; `UnibusRepository` desacopla la
fuente: `MockUnibusRepository` (en memoria, mock espejo de `mock.ts`) y
`BindingUnibusRepository` (sobre `unibus.aar`, cableado completo y compilando).
- Diseño: tokens de color `dark.6/7/8/9` + dimmed + brand `#6C47E6` vía
`LocalUnibusColors`; avatares con iniciales; candado/hash por política E2E;
badges de no leídos; chat estilo Element (avatar+nombre+hora+texto) + composer.
## Verificación
### Binding compila y .aar generado (sí)
```
$ go build ./mobile/ # con go.work (replace fn-registry absoluto)
BUILD_OK mobile
$ gomobile bind -target=android -androidapi 21 -javapkg com.unibus.core \
-o android/app/libs/unibus.aar ./mobile
# 26 s. Resultado:
android/app/libs/unibus.aar 38 MB
jni/{armeabi-v7a,arm64-v8a,x86,x86_64}/libgojni.so # 4 ABIs
classes.jar -> com/unibus/core/mobile/{Mobile,Session,FrameListener}.java
```
### APK compila (sí)
```
$ cd android && ./gradlew assembleDebug --no-daemon
BUILD SUCCESSFUL in 1m 9s
37 actionable tasks: 37 executed
APK: android/app/build/outputs/apk/debug/app-debug.apk (53 MB)
package: com.unibus.app versionName=0.1.0 launchable-activity .MainActivity
ABIs empaquetadas: lib/{arm64-v8a,armeabi-v7a,x86,x86_64}/libgojni.so
```
Toolchain: AGP 8.5.2, Gradle 8.7, Kotlin 1.9.24, Compose BOM 2024.06.00,
compileSdk 34, minSdk 21 (= `-androidapi 21` del bind), Java 17.
### Verificación visual en emulador (sí — sin crash)
Instalado y lanzado en `emulator-5554` (Pixel_API34, KVM). `adb logcat` sin
`FATAL`/`AndroidRuntime`. Las 3 pantallas son equivalentes a la web:
- `assets/unibus-android-login.png` — Login: candado de marca, "unibus", campos
Identidad/Contraseña, botón Conectar.
- `assets/unibus-android-rooms.png` — lista de rooms: avatar+handle, buscador,
candado/hash, hora, último mensaje, badges de no leídos (3, 1) en violeta.
- `assets/unibus-android-chat.png` — chat estilo Element: header con candado +
"cifrada · E2E", mensajes avatar+nombre+hora+texto (nombre propio en violeta),
composer redondeado + send.
## Ruta del APK
```
/tmp/unibus_android/android/app/build/outputs/apk/debug/app-debug.apk
```
(Worktree efímero. El APK se regenera con `cd android && ./gradlew assembleDebug`
tras `./mobile/gen_aar.sh`.)
## Gaps / pendientes
- **El binding NO está conectado en la UI todavía** — por diseño (la tarea pedía
mock primero). Para activar el bus real: instanciar `BindingUnibusRepository`
en `MainActivity` con las URLs del bus y pasarlo a `AppViewModel`; las
pantallas no cambian. Falta UI de configuración del endpoint (natsURL/ctrlURL)
y de carga del `ca.crt` (hoy se espera en `assets/ca.crt`, opcional → plaintext).
- **`ListMembers` no existe en `pkg/client`** — se expone `ListRoomsJSON`
(vía `ListMyRooms`). Listar miembros requiere primero exponerlo en el cliente.
- **Password no desbloquea la identidad aún** — `LoadOrCreateIdentity` crea/lee
la clave directamente; el campo password es UI-only de momento.
- **Metadata de room en el binding es parcial** — `ListRoomsJSON` da
subject/encrypted/role/epoch; `lastMessage`/`unread`/`messages` los rellena hoy
el mock. Con el bus real habrá que derivarlos del stream + persistencia local.
- **`.aar` no versionado** (38 MB, regenerable) — reviewer debe correr
`./mobile/gen_aar.sh` antes de compilar. Requiere Go + gomobile + NDK.
- **Verificación solo en x86_64 (emulador)** — no probado en hardware ARM físico.
- **`go.work` local** — necesario solo al construir el binding desde un worktree
fuera del árbol del registry; en checkout normal el `replace` relativo resuelve.
## Notas (onboarding)
Para retomar:
1. `./mobile/gen_aar.sh` regenera `android/app/libs/unibus.aar` (Go+gomobile+NDK).
2. `cd android && ./gradlew assembleDebug` → APK en `app/build/outputs/apk/debug/`.
3. Diseño en `android/app/src/main/java/com/unibus/app/ui/` (espejo de `web/src/`).
4. Para enchufar el bus real: cambiar el repo del `AppViewModel` de
`MockUnibusRepository` a `BindingUnibusRepository(context, natsURL, ctrlURL)`.
La interfaz `UnibusRepository` es el único punto de contacto UI↔datos.