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