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

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 — 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únLoadOrCreateIdentity crea/lee la clave directamente; el campo password es UI-only de momento.
  • Metadata de room en el binding es parcialListRoomsJSON 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.