- 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>
7.4 KiB
Report 0010 — unibus app Android nativa (Compose + binding gomobile)
- Fecha: 07/06/2026
- Autor: Claude (agente Android)
- Ámbito:
projects/message_bus/apps/unibus— bindingmobile/, appandroid/ - Estado: done (mock primero, binding real cableado y listo para enchufar)
- Rama:
issue/android-native(sub-repodataforge/unibus), pusheada. NO mergeada a master. - Aislamiento: todo el trabajo en worktree
/tmp/unibus_android. Cero cambios en~/fn_registry/.../apps/unibusni 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)→ usaclient.Connectcon la auth nueva (TLS pineado al CA + nkey sicaPath != "").EndpointID,ConnectedServer,IsConnected.CreateRoom(subject, mode),Join,RefreshSession(contrato de membresía issue 0006e: tras crear/unirse/invitar,RefreshSessionantes 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.
AppViewModelorquesta el estado observable;UnibusRepositorydesacopla la fuente:MockUnibusRepository(en memoria, mock espejo demock.ts) yBindingUnibusRepository(sobreunibus.aar, cableado completo y compilando).- Diseño: tokens de color
dark.6/7/8/9+ dimmed + brand#6C47E6víaLocalUnibusColors; 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
BindingUnibusRepositoryenMainActivitycon las URLs del bus y pasarlo aAppViewModel; las pantallas no cambian. Falta UI de configuración del endpoint (natsURL/ctrlURL) y de carga delca.crt(hoy se espera enassets/ca.crt, opcional → plaintext). ListMembersno existe enpkg/client— se exponeListRoomsJSON(víaListMyRooms). Listar miembros requiere primero exponerlo en el cliente.- Password no desbloquea la identidad aún —
LoadOrCreateIdentitycrea/lee la clave directamente; el campo password es UI-only de momento. - Metadata de room en el binding es parcial —
ListRoomsJSONda subject/encrypted/role/epoch;lastMessage/unread/messageslos rellena hoy el mock. Con el bus real habrá que derivarlos del stream + persistencia local. .aarno versionado (38 MB, regenerable) — reviewer debe correr./mobile/gen_aar.shantes de compilar. Requiere Go + gomobile + NDK.- Verificación solo en x86_64 (emulador) — no probado en hardware ARM físico.
go.worklocal — necesario solo al construir el binding desde un worktree fuera del árbol del registry; en checkout normal elreplacerelativo resuelve.
Notas (onboarding)
Para retomar:
./mobile/gen_aar.shregeneraandroid/app/libs/unibus.aar(Go+gomobile+NDK).cd android && ./gradlew assembleDebug→ APK enapp/build/outputs/apk/debug/.- Diseño en
android/app/src/main/java/com/unibus/app/ui/(espejo deweb/src/). - Para enchufar el bus real: cambiar el repo del
AppViewModeldeMockUnibusRepositoryaBindingUnibusRepository(context, natsURL, ctrlURL). La interfazUnibusRepositoryes el único punto de contacto UI↔datos.