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