d43ffae3ae
- 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>
148 lines
7.4 KiB
Markdown
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.
|