android: la APK se monta sobre la oficial del dev

La app Android de epsylon trae desde la 1.3.x un puente nativo de audio y
camara (.audio_bridge) para el Telefono y las Salas, y no tiene el codigo
publicado. android/scripts/sobre_oficial.sh coge su APK de la misma version,
le cambia el backend por el de la rama (con su node_modules), le pone el
paquete net.laenre.oasis (y los authorities, el permiso propio y las acciones
de llamada) para que conviva con la oficial, y la monta y firma. apktool va
con version y huella fijas.

Probado en el emulador: el Telefono ofrece llamadas y en una sala el
microfono graba y se corta al salir. README, guia, android/README y los
esquemas lo cuentan; el proyecto Gradle propio queda sin uso.
This commit is contained in:
hacklab 2026-10-19 14:12:18 +02:00
parent 34265c2ed0
commit 6bacafea64
9 changed files with 182 additions and 36 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ node_modules/
# pruebas en local: el node_modules suele ser un enlace y el estado de la llamada lo escribe el movil # pruebas en local: el node_modules suele ser un enlace y el estado de la llamada lo escribe el movil
src/server/node_modules src/server/node_modules
.phone_state.json .phone_state.json
android/.tools/

View file

@ -55,10 +55,11 @@ Ahora mismo hay enganches en `main_views.js`, `tribes_view.js`, `vote_view.js`,
La rama tuvo un módulo de salas y llamadas propio (Karvan). Se quitó el 19/10/2026 La rama tuvo un módulo de salas y llamadas propio (Karvan). Se quitó el 19/10/2026
porque el dev ya trae Teléfono y Salas. En Android el sonido y la cámara pasan por un porque el dev ya trae Teléfono y Salas. En Android el sonido y la cámara pasan por un
puente nativo de la app (`.audio_bridge`, ficheros `src/server/phone_*_android.js`, puente nativo de la app del dev (`.audio_bridge`; los ficheros
que son del dev). La APK oficial lo trae; nuestra app Android todavía no, y es lo `src/server/phone_*_android.js` también son suyos). Por eso la APK se monta sobre la
que falta para que las llamadas suenen. Si falta algo en las llamadas, se arregla en oficial con `android/scripts/sobre_oficial.sh`: su app tal cual, nuestro backend
ese puente o se le pide al dev, no se monta otro módulo. dentro y paquete propio. Si falta algo en las llamadas, se le pide al dev, no se monta
otro módulo.
Ojo al comparar con el dev: para el móvil la referencia es el `nodejs-project` de su Ojo al comparar con el dev: para el móvil la referencia es el `nodejs-project` de su
APK oficial, no su repositorio git. Hay ficheros, como `SSB_server.js` o APK oficial, no su repositorio git. Hay ficheros, como `SSB_server.js` o
@ -89,7 +90,8 @@ Aquí se puede hacer lo que haga falta.
- `src/models/fork_workflows.js`: nuestros workflows. - `src/models/fork_workflows.js`: nuestros workflows.
- `src/client/assets/translations/i18n_fork.js`: nuestros textos. - `src/client/assets/translations/i18n_fork.js`: nuestros textos.
- `src/client/assets/images/ux-*.svg`: nuestras imágenes. - `src/client/assets/images/ux-*.svg`: nuestras imágenes.
- `android/`. - `android/scripts/sobre_oficial.sh`: monta la APK sobre la oficial del dev. El resto
de `android/` (el proyecto Gradle propio) queda como estaba, sin uso.
## Cómo hacer cada cosa ## Cómo hacer cada cosa
@ -160,8 +162,8 @@ nombre es el de la ruta: `/tribes` es `tribes`.
3. Se repite la medición de `docs/FRONTERA_UPSTREAM.md`: qué ha tocado y si ha 3. Se repite la medición de `docs/FRONTERA_UPSTREAM.md`: qué ha tocado y si ha
desaparecido alguna clase que usen nuestras hojas. desaparecido alguna clase que usen nuestras hojas.
4. Se repasan a 412 px las pantallas de la lista de arriba. 4. Se repasan a 412 px las pantallas de la lista de arriba.
5. Se compila la APK con la oficial de esa misma versión como base 5. Se monta la APK sobre la oficial de esa misma versión:
(`android/scripts/release.sh` con `OASIS_APK_BASE`). `OASIS_VERSION=x.y.z android/scripts/sobre_oficial.sh`.
## Probar en local ## Probar en local

View file

@ -35,8 +35,10 @@ Android 7 o superior, 64 bits · se instala junto al Oasis oficial sin pisarlo
parlamento, votaciones y eventos. El resto se enciende en *Módulos* o en parlamento, votaciones y eventos. El resto se enciende en *Módulos* o en
*Ajustes → Workflows*. *Ajustes → Workflows*.
- Las llamadas son las de Oasis. Fuera Karvan, el módulo propio de salas: ahora se usan - Las llamadas son las de Oasis. Fuera Karvan, el módulo propio de salas: ahora se usan
el Teléfono y las Salas del dev. Ojo: en esta APK todavía no suenan, porque a nuestra el Teléfono y las Salas del dev, con su levantar la mano, grabación y emisión.
app Android le falta el puente de audio nativo que sí trae la oficial (ver abajo). - La APK se monta sobre la oficial: se coge la del dev, se le cambia el backend por el
de la rama y se le da otro nombre de paquete. Así trae todo lo nativo del dev (el
puente de audio y cámara, las notificaciones de llamada) y convive con la oficial.
- Votaciones como en asamblea: al crear una se elige Sí/No, semáforo (verde, amarillo, - Votaciones como en asamblea: al crear una se elige Sí/No, semáforo (verde, amarillo,
rojo) o termómetro (del 1 al 5). Los resultados salen en barras. rojo) o termómetro (del 1 al 5). Los resultados salen en barras.
- Borrar pide confirmación. Antes la X de una tribu la borraba de un toque. - Borrar pide confirmación. Antes la X de una tribu la borraba de un toque.
@ -59,12 +61,12 @@ Android 7 o superior, 64 bits · se instala junto al Oasis oficial sin pisarlo
| | Oasis oficial | Esta rama | | | Oasis oficial | Esta rama |
|---|---|---| |---|---|---|
| Llamadas y salas | Teléfono y Salas, con micrófono y cámara | Los mismos; el puente de audio de Android todavía falta | | Llamadas y salas | Teléfono y Salas, con micrófono y cámara | Los mismos, tal cual |
| Interfaz de móvil | El menú de escritorio en una pantalla pequeña | Tema Acampada, lanzador con iconos, fichas, barra de cuatro atajos | | Interfaz de móvil | El menú de escritorio en una pantalla pequeña | Tema Acampada, lanzador con iconos, fichas, barra de cuatro atajos |
| Primer arranque | Casi sesenta módulos encendidos | Modo acampada: lo justo para organizarse | | Primer arranque | Casi sesenta módulos encendidos | Modo acampada: lo justo para organizarse |
| Votaciones | Seis opciones fijas en una tabla | Sí/No, semáforo o termómetro, con barras | | Votaciones | Seis opciones fijas en una tabla | Sí/No, semáforo o termómetro, con barras |
| Varias identidades | Una por instalación | Varias en el mismo móvil, con aviso si dos comparten clave | | Varias identidades | Una por instalación | Varias en el mismo móvil, con aviso si dos comparten clave |
| Envoltorio Android | El del dev, sin código publicado | Proyecto Gradle propio y abierto: permisos, subida de ficheros, servicio que sobrevive | | App Android | La del dev | La del dev, con el backend de la rama y paquete propio (`net.laenre.oasis`) |
| Metadatos de las fotos | Se quitan solo si está `sharp`; en Android nunca | Se quitan siempre: GPS, marca, modelo, número de serie | | Metadatos de las fotos | Se quitan solo si está `sharp`; en Android nunca | Se quitan siempre: GPS, marca, modelo, número de serie |
Los nombres de las categorías, los módulos y el resto de la aplicación son los de Los nombres de las categorías, los módulos y el resto de la aplicación son los de
@ -104,13 +106,15 @@ mensaje en tu nombre ni cambiar lo que dijiste.
![Llamadas: las del dev, con el móvil enchufado](docs/capturas/esquema-llamadas.png) ![Llamadas: las del dev, con el móvil enchufado](docs/capturas/esquema-llamadas.png)
El Teléfono (llamadas de uno a uno, con buzón de voz) y las Salas (hasta cincuenta El Teléfono (llamadas de uno a uno, con buzón de voz) y las Salas (hasta cincuenta
personas, con vídeo, grabación y emisión) son de Oasis. En Android el sonido y la personas, con vídeo, levantar la mano, grabación y emisión) son de Oasis. En Android
cámara no los maneja Node: el servidor se conecta a un puente nativo de la app el sonido y la cámara no los maneja Node: el servidor se conecta a un puente nativo de
(`.audio_bridge`), que graba, reproduce, saca fotogramas de la cámara y emite ficheros. la app (`.audio_bridge`), que graba, reproduce, saca fotogramas de la cámara y emite
ficheros.
La APK oficial del dev trae ese puente. La de esta rama todavía no: nuestra app Ese puente está en la app Android del dev, que no tiene el código publicado. Por eso la
Android es otra (tiene el código publicado) y el puente hay que escribirlo. Hasta APK de esta rama se monta sobre la oficial: se queda con su app tal cual y solo cambia
entonces, en esta APK el Teléfono dice que las llamadas no están disponibles. lo que corre dentro (el backend con nuestra capa) y el nombre del paquete. Probado en
el emulador: se entra en una sala y el micrófono graba.
En la barra de arriba, junto al avatar, está el botón de *No molestar* del Teléfono. En la barra de arriba, junto al avatar, está el botón de *No molestar* del Teléfono.
@ -187,25 +191,23 @@ descomprime el servidor.
### Compilar ### Compilar
Con Android Studio, lo más fácil: La APK se monta sobre la oficial de la misma versión. Hace falta Java 17 o más y las
build-tools de Android (zipalign y apksigner); apktool se baja solo.
```sh
./android/scripts/android-studio.sh # trae el Oasis oficial y su APK, y empaqueta el backend
```
Después, en Android Studio: *File → Open →* la carpeta `android/`, esperar al Gradle
Sync, y *Build → Build App Bundle(s) / APK(s) → Build APK(s)*. Si cambias algo en
`src/` o en `main.js`, vuelve a lanzar el script antes de compilar.
Desde la terminal, paso a paso:
```sh ```sh
cd android cd android
# la primera vez hace falta la APK oficial de la MISMA versión (ahora la 1.3.3): ./scripts/sobre_oficial.sh # baja la oficial, le pone el backend de la rama, la firma
# de ella salen las librerías nativas y el node_modules podado del móvil # para publicar, con la clave del hacklab:
OASIS_APK_BASE=/ruta/oasis-v1.3.3.apk ./scripts/release.sh # compila, verifica y calcula la huella OASIS_KEYSTORE=... OASIS_KEYSTORE_PASS=... OASIS_KEY_ALIAS=... ./scripts/sobre_oficial.sh
``` ```
Sale en `android/build/sobre-oficial/oasis-<versión>-hacklab.apk`. Sin clave se firma
con la de depuración y queda depurable, para poder inspeccionarla desde
`chrome://inspect`.
El proyecto Gradle propio de `android/` (`release.sh`, Android Studio) sigue ahí, pero
ya no es el camino: no tiene el puente de audio y en él no hay llamadas.
![Qué hay dentro de la APK](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/16-arquitectura-movil.png) ![Qué hay dentro de la APK](https://gitea.laenre.net/hacklab/OASIS_MOBILE/wiki/raw/img/esq/16-arquitectura-movil.png)
--- ---

View file

@ -6,8 +6,9 @@ molde.
## Por que existe ## Por que existe
Nota (19/10/2026): desde la 1.3.x la APK oficial ya pide microfono y camara y trae un Nota (19/10/2026): desde la 1.3.x la APK oficial ya pide microfono y camara y trae un
puente nativo de audio y camara (`.audio_bridge`) para el Telefono y las Salas. Esta app puente nativo de audio y camara (`.audio_bridge`) para el Telefono y las Salas. Por eso
aun no lo tiene. Lo de abajo describe la situacion anterior. la APK de la rama se monta ahora sobre la oficial con `scripts/sobre_oficial.sh`, y el
proyecto Gradle de esta carpeta queda sin uso. Lo de abajo describe la situacion anterior.
El APK que se venia usando es el de epsylon re-firmado, y su `classes.dex` **no El APK que se venia usando es el de epsylon re-firmado, y su `classes.dex` **no
implementa `WebChromeClient.onPermissionRequest`**. Sin ese metodo el WebView de implementa `WebChromeClient.onPermissionRequest`**. Sin ese metodo el WebView de

126
android/scripts/sobre_oficial.sh Executable file
View file

@ -0,0 +1,126 @@
#!/usr/bin/env bash
#
# Construye la APK de la rama encima de la APK oficial del dev.
#
# Desde la 1.3.x la app Android de epsylon trae cosas que la nuestra no tiene: el
# puente nativo de audio y camara (.audio_bridge) que usan el Telefono y las Salas,
# las notificaciones de llamada y sus permisos. Su codigo no esta publicado, asi que
# en vez de imitarlo se parte de su APK:
#
# 1. se baja la APK oficial de la misma version (github, epsylon/oasis)
# 2. se desmonta con apktool
# 3. su backend (assets/nodejs-project.zip) se cambia por el de la rama, conservando
# su node_modules, que es el podado para el movil
# 4. se le cambia el paquete a net.laenre.oasis (y lo que depende de el) para que
# conviva con la oficial
# 5. se vuelve a montar, se alinea y se firma
#
# Uso:
# ./scripts/sobre_oficial.sh # firma con la clave de depuracion
# OASIS_KEYSTORE=... OASIS_KEYSTORE_PASS=... OASIS_KEY_ALIAS=... ./scripts/sobre_oficial.sh
#
# Variables:
# OASIS_VERSION version del dev (por defecto la de package.json)
# OASIS_APK_OFICIAL ruta a la APK oficial, si ya se tiene (si no, se baja)
# ANDROID_HOME por defecto ~/Android/Sdk
# JAVA_HOME un JDK 17 o superior
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
REPO="$(cd "$HERE/.." && pwd)"
SDK="${ANDROID_HOME:-$HOME/Android/Sdk}"
JAVA="${JAVA_HOME:+$JAVA_HOME/bin/}java"
TOOLS="$HERE/.tools"
OUT="$HERE/build/sobre-oficial"
PKG_DEV="com.solarnethub.oasis"
PKG="net.laenre.oasis"
APKTOOL_VER="3.0.3"
APKTOOL_SHA="dbf930b076c6b9be08d57c449cacefc3bdd6b71ebd59b3066fc0e1f5b14f9423"
say() { printf '\n== %s\n' "$*"; }
die() { printf '\n!! %s\n' "$*" >&2; exit 1; }
VERSION="${OASIS_VERSION:-$(node -p "require('$REPO/package.json').version")}"
BT="$(ls -d "$SDK"/build-tools/*/ | sort -V | tail -1)"
[ -x "$BT/zipalign" ] || die "no encuentro zipalign en $BT"
mkdir -p "$TOOLS" "$OUT"
# --- apktool, version fija y comprobada ---------------------------------------
APKTOOL="$TOOLS/apktool_$APKTOOL_VER.jar"
if [ ! -f "$APKTOOL" ]; then
say "bajando apktool $APKTOOL_VER"
curl -fsSL -o "$APKTOOL" "https://github.com/iBotPeaches/Apktool/releases/download/v$APKTOOL_VER/apktool_$APKTOOL_VER.jar"
fi
echo "$APKTOOL_SHA $APKTOOL" | sha256sum -c --quiet - || die "apktool no coincide con la huella esperada"
# --- la APK oficial -------------------------------------------------------------
OFICIAL="${OASIS_APK_OFICIAL:-$TOOLS/oasis-v$VERSION.apk}"
if [ ! -f "$OFICIAL" ]; then
say "bajando la APK oficial $VERSION"
curl -fsSL -o "$OFICIAL" "https://github.com/epsylon/oasis/releases/download/oasis-android-v$VERSION/oasis-v$VERSION.apk"
fi
say "APK oficial: $OFICIAL"
"$BT/apksigner" verify --print-certs "$OFICIAL" | grep -m2 -E "certificate DN|SHA-256" || die "la APK oficial no verifica"
# --- desmontar ------------------------------------------------------------------
say "desmontando"
rm -rf "$OUT/dec"
"$JAVA" -jar "$APKTOOL" d -q -f -o "$OUT/dec" "$OFICIAL"
DEC="$OUT/dec"
# --- backend de la rama, con el node_modules del dev ---------------------------
say "cambiando el backend por el de la rama"
work="$(mktemp -d)"
( cd "$work" && unzip -q "$DEC/assets/nodejs-project.zip" )
mv "$work/nodejs-project/src/server/node_modules" "$work/_nm"
rm -rf "$work/nodejs-project/src"
cp -r "$REPO/src" "$work/nodejs-project/src"
rm -rf "$work/nodejs-project/src/server/node_modules"
mv "$work/_nm" "$work/nodejs-project/src/server/node_modules"
cp "$REPO/main.js" "$REPO/package.json" "$REPO/README.md" "$work/nodejs-project/"
rm -f "$work/nodejs-project/.phone_state.json"
rm -f "$DEC/assets/nodejs-project.zip"
( cd "$work" && zip -q -r "$DEC/assets/nodejs-project.zip" nodejs-project )
rm -rf "$work"
# --- paquete propio, para convivir con la oficial ------------------------------
# Las clases se quedan como estan (com.solarnethub.oasis.OasisActivity...); cambia
# el paquete, los authorities, el permiso propio y los nombres de las acciones de
# llamada, que si no se cruzarian con los de la app oficial.
say "paquete $PKG"
sed -i \
-e "s|package=\"$PKG_DEV\"|package=\"$PKG\"|" \
-e "s|$PKG_DEV.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION|$PKG.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION|g" \
-e "s|android:authorities=\"$PKG_DEV.|android:authorities=\"$PKG.|g" \
"$DEC/AndroidManifest.xml"
grep -rl "\"$PKG_DEV\.[A-Z_]*\"" "$DEC"/smali* | xargs -r sed -i -E "s|\"$PKG_DEV\.([A-Z_]+)\"|\"$PKG.\1\"|g"
# las de prueba (firmadas con la clave de depuracion) se marcan depurables: asi se
# puede inspeccionar la WebView desde chrome://inspect. Las de publicar, nunca
if [ -z "${OASIS_KEYSTORE:-}" ]; then
sed -i 's|<application |<application android:debuggable="true" |' "$DEC/AndroidManifest.xml"
fi
# version: la del dev por mil mas los commits de la rama, para que suba siempre
CODE_DEV="$(sed -n 's/^ versionCode: //p' "$DEC/apktool.yml")"
CODE="$(( CODE_DEV * 1000 + $(git -C "$REPO" rev-list --count HEAD) % 1000 ))"
sed -i -e "s/^ versionCode: .*/ versionCode: $CODE/" -e "s/^ versionName: .*/ versionName: $VERSION-hacklab/" "$DEC/apktool.yml"
say "version $VERSION-hacklab (codigo $CODE)"
# --- montar, alinear y firmar --------------------------------------------------
say "montando"
"$JAVA" -jar "$APKTOOL" b -q -o "$OUT/sin-firmar.apk" "$DEC"
"$BT/zipalign" -f -p 4 "$OUT/sin-firmar.apk" "$OUT/alineada.apk"
FINAL="$OUT/oasis-$VERSION-hacklab.apk"
if [ -n "${OASIS_KEYSTORE:-}" ]; then
"$BT/apksigner" sign --ks "$OASIS_KEYSTORE" --ks-pass "env:OASIS_KEYSTORE_PASS" \
--ks-key-alias "${OASIS_KEY_ALIAS:-oasis}" --out "$FINAL" "$OUT/alineada.apk"
else
"$BT/apksigner" sign --ks "$HOME/.android/debug.keystore" --ks-pass pass:android \
--ks-key-alias androiddebugkey --out "$FINAL" "$OUT/alineada.apk"
fi
rm -f "$OUT/sin-firmar.apk" "$OUT/alineada.apk" "$FINAL.idsig"
"$BT/apksigner" verify "$FINAL" || die "la APK firmada no verifica"
say "APK: $FINAL ($(du -h "$FINAL" | cut -f1))"
echo "sha256: $(sha256sum "$FINAL" | cut -d' ' -f1)"

View file

@ -205,3 +205,17 @@ cero con la APK sin Karvan (codigo 102).
oficial. Mientras tanto, en la rama no hay llamadas. oficial. Mientras tanto, en la rama no hay llamadas.
- El avatar por defecto (una calavera con gafas) asusta un poco para quien - El avatar por defecto (una calavera con gafas) asusta un poco para quien
empieza. Es del dev; se puede cambiar solo en el skin. empieza. Es del dev; se puede cambiar solo en el skin.
Con la APK montada sobre la oficial (1.3.3-hacklab, mismo emulador):
- Llamadas: ahora SI. El Telefono ofrece "Nueva llamada" y en una sala el micro
graba de verdad (comprobado en Android: VOICE_COMMUNICATION activo) y se corta
al salir.
- Turnos de palabra: las Salas del dev ya tienen "Levantar la mano". Es la base de
lo que pedia el pad; falta el cronometro.
- Dentro de una sala, los controles (silenciar, camara, levantar la mano, salir)
quedan muy abajo, debajo de la ficha de la sala. Habria que tenerlos a mano
mientras se esta dentro. [pendiente, CSS]
- La app del dev pide el permiso de camara nada mas abrirse, antes de ensenar
nada. Es su app; no se toca.
- El boton de crear sala se tapa con el teclado al escribir el titulo: hay que
cerrar el teclado para verlo.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

View file

@ -163,7 +163,7 @@ def esquema_rama():
((740, 210, 1060, 400), "RUTAS", ((740, 210, 1060, 400), "RUTAS",
["fork_routes.js y", "mobile_layer.js, delante", "del router del dev: lo que", "no atienden pasa al suyo"], NEON), ["fork_routes.js y", "mobile_layer.js, delante", "del router del dev: lo que", "no atienden pasa al suyo"], NEON),
((1080, 210, 1340, 400), "ANDROID", ((1080, 210, 1340, 400), "ANDROID",
["envoltorio propio y", "abierto: permisos,", "ficheros y servicio.", "Falta el puente de audio"], NEON), ["la app del dev tal cual,", "con nuestro backend", "dentro y paquete", "propio"], NEON),
] ]
for box, t, l, r in capa: for box, t, l, r in capa:
caja(d, box, t, l, r) caja(d, box, t, l, r)
@ -199,8 +199,8 @@ def esquema_llamadas():
for a, b in (((362, 315), (398, 315)), ((702, 315), (738, 315)), ((1042, 315), (1078, 315))): for a, b in (((362, 315), (398, 315)), ((702, 315), (738, 315)), ((1042, 315), (1078, 315))):
flecha(d, a, b) flecha(d, a, b)
caja(d, (50, 480, 1350, 630), "DÓNDE ESTÁ CADA COSA", caja(d, (50, 480, 1350, 630), "DÓNDE ESTÁ CADA COSA",
["La APK oficial trae el puente: ahí las llamadas suenan. La app de esta rama todavía no lo tiene,", ["El puente está en la app Android del dev, que no tiene el código publicado. Por eso la APK de la",
"así que en ella el Teléfono dice que no hay llamadas. Escribir ese puente es lo siguiente."], BG) "rama se monta sobre la oficial: su app tal cual, nuestro backend dentro y paquete propio."], BG)
out.save(HERE / "esquema-llamadas.png", optimize=True) out.save(HERE / "esquema-llamadas.png", optimize=True)