Skip to content

App Android nativa: las tres capas de bloqueo, guia de instalacion y panel web - #1

Open
leocagli wants to merge 2 commits into
mainfrom
app-android-nativa
Open

leocagli wants to merge 2 commits into
mainfrom
app-android-nativa

Conversation

@leocagli

Copy link
Copy Markdown

La demo navegable muestra el flujo completo del producto, pero corre entera en el cliente. Esto es la aplicación real: una sola app con dos modos, tal como quedó definido en el MVP.

Las tres capas

Capa Alcance Dónde no llega
VpnService Todas las apps del equipo, vía DNS DoH, conexiones ya resueltas
AccessibilityService Barra de direcciones del navegador Fuera de los navegadores listados
DeviceAdminReceiver Impide desinstalar la app (no bloquea tráfico)

La capa 2 cubre exactamente los huecos de la capa 1: DNS sobre HTTPS, WebView dentro de otra app, y conexiones con el dominio ya resuelto. No es redundancia.

Decisiones que vale la pena revisar

El motor es puro y sincrónico. Lo llama el VpnService una vez por consulta DNS, así que no puede tocar disco ni red: quien lo construye ya le pasa las listas resueltas, y los servicios se quedan con la última versión en un campo @Volatile. Por eso se testea con JUnit común, sin emulador.

Al tun solo se enruta el tráfico hacia el DNS virtual (addRoute(DNS_VIRTUAL, 32)). La app ve nombres de dominio consultados, no contenido. Eso lo garantiza la ruta, no una promesa del README.

El Device Admin pide una sola política, watch-login. Nada de wipe remoto, bloqueo de pantalla ni políticas de contraseña. Un control parental que puede borrar el teléfono es un control parental que un día borra un teléfono.

Se guarda dominio, regla, capa y momento. No URL completa, ni título de página, ni capturas. Es el historial de navegación de un chico: el mínimo que sirve para que un adulto entienda qué pasó es también el máximo que corresponde guardar. Backup automático de Android excluido, retención de 90 días.

Un solo APK y no dos. Dos aplicaciones significarían dos fichas de Play Store, dos revisiones de política y dos claves de firma para un producto que ya pide tres permisos sensibles. El costo es que el APK del equipo protegido lleva código del modo responsable que nunca ejecuta.

Documentación

  • ARQUITECTURA.md: por qué el producto está partido en tres piezas.
  • INSTALACION.md: los dos teléfonos paso a paso, qué ve y qué no ve la app, y cinco pruebas de verificación. Arranca con qué hablar antes de instalar: la notificación permanente no se puede ocultar y Play exige que la supervisión sea visible.
  • panel/README.md: el panel web con login, modelo de datos y reglas de Firestore.

Verificación

BUILD SUCCESSFUL
tests="10" skipped="0" failures="0" errors="0"
app-debug.apk  11M

./gradlew :app:assembleDebug :app:testDebugUnitTest en verde, sin warnings. JDK 17, SDK 35, AGP 8.7.3, Kotlin 2.0.21.

Lo que falta

Backend (los puntos están marcados con // BACKEND), vinculación real de los dos equipos con código de un solo uso, onboarding de selección de modo, sincronización periódica de la lista oficial con WorkManager, e implementación del panel web.

🤖 Generated with Claude Code

…panel web

La demo navegable muestra el flujo del producto, pero corre entera en el cliente.
Esto es la aplicacion real: una sola app con dos modos, igual que en el MVP.

Motor de bloqueo (core/motor):
Port de evaluar() de assets/app.js, pero puro y sincronico. Lo llama el
VpnService una vez por consulta DNS, asi que no puede tocar disco ni red: quien
lo construye ya le pasa las listas resueltas. Por eso se testea con JUnit comun,
sin emulador. 10 tests, incluido el caso de que nobet365.bet no cae por la regla
bet365.bet sino por el TLD.

Las tres capas, en orden de alcance:

1. VpnService. Levanta una interfaz tun dentro del propio equipo y se declara
   unico DNS. No hay servidor del otro lado. Responde NXDOMAIN a lo bloqueado y
   reenvia el resto a upstream. Es la unica capa que alcanza a todas las apps.
   Al tun solo se enruta el trafico hacia el DNS virtual: la app ve nombres de
   dominio, no contenido, y eso lo garantiza la ruta, no el README.

2. AccessibilityService. Cubre exactamente los huecos de la capa 1: DoH,
   WebView dentro de otra app, y conexiones con el dominio ya resuelto. Limitado
   por lista de paquetes y por id de recurso, sin flagRetrieveInteractiveWindows.

3. DeviceAdminReceiver. No bloquea nada: impide desinstalar la app, que es como
   se apagan las otras dos en dos toques. Pide una sola politica, watch-login.
   Nada de wipe remoto ni politicas de contrasenia.

Persistencia (Room + DataStore): se guarda dominio, regla, capa y momento. No
URL completa, ni titulo de pagina, ni capturas. Es el historial de navegacion de
un chico. El backup automatico de Android esta excluido y la retencion es de 90
dias.

Documentacion:
- ARQUITECTURA.md: por que el producto esta partido en tres piezas.
- INSTALACION.md: los dos telefonos paso a paso, que ve y que no ve la app, y
  las cinco pruebas de verificacion.
- panel/README.md: el panel web con login, modelo de datos y reglas de acceso.

Verificado: assembleDebug y testDebugUnitTest en verde, 10/10 tests, APK de 11 MB.
Falta el backend, la vinculacion real de los dos equipos y el onboarding de modo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Antes era un solo APK con dos modos elegidos en el onboarding. Ahora son dos
aplicaciones instalables distintas, generadas con product flavors de Gradle
desde el mismo codebase.

La razon de fondo no es organizativa, es de permisos. El VpnService, el
AccessibilityService y el DeviceAdminReceiver se declaran en
app/src/dispositivo/AndroidManifest.xml, asi que el APK del adulto no los lleva.
Verificado con aapt2 sobre los binarios compilados: control declara ProtegeApp y
MainActivity, nada mas; dispositivo declara ademas los cinco componentes de la
proteccion. Un adulto que instala la app de control nunca ve el dialogo de VPN
ni la advertencia de accesibilidad, porque su APK no puede dispararlos.

Reparto de fuentes:
- app/src/main         motor, Room, DataStore, notificaciones, tema
- app/src/control      MainActivity propia, PanelResponsable, CodigoVinculacion
- app/src/dispositivo  MainActivity propia, y todo protegido/**

Las dos MainActivity comparten nombre de clase a proposito: el manifiesto
compartido declara .MainActivity y cada flavor compila la suya. Por eso
Notificaciones dejo de nombrar la clase y ahora pide el launcher del propio
paquete con getLaunchIntentForPackage: codigo compartido que no se ata a un
flavor.

Front nuevo del adulto (PanelResponsable): alertas sin leer, lista personalizada
con alta y baja, generacion del codigo de vinculacion y las lineas de ayuda.
Sin backend no hay dispositivos que listar, asi que la pantalla explica que falta
en vez de mostrar una tarjeta vacia. CodigoVinculacion usa SecureRandom: seis
digitos alcanzan porque vence en 10 minutos y se quema al primer uso, pero un
codigo predecible seria un equipo ajeno vinculado a la cuenta de otro.

En la app del equipo protegido se sumo el aviso de supervision en pantalla, que
no se puede ocultar.

Documentacion actualizada: README, ARQUITECTURA.md e INSTALACION.md decian lo
contrario. La guia de instalacion ahora distingue que APK va en cada telefono y
usa como senal el hecho de que la app del adulto nunca pide VPN.

Verificado: assembleDebug genera los dos APK, 10/10 tests en cada flavor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@leocagli

Copy link
Copy Markdown
Author

Cambio de arquitectura: dos aplicaciones en vez de una

El PR arrancaba con un solo APK de dos modos, siguiendo lo que decía el README. Ahora son dos aplicaciones instalables distintas, generadas con product flavors de Gradle desde el mismo codebase.

control dispositivo
Nombre BA Protege+ Control BA Protege+
applicationId ar.gob.buenosaires.protege.control ar.gob.buenosaires.protege
Lo instala El adulto El chico o la chica

La razón de fondo es de permisos, no de organización. Verificado con aapt2 sobre los APK compilados:

control:      ProtegeApp, MainActivity
dispositivo:  ProtegeApp, MainActivity, PantallaCorteActivity,
              ProtegeVpnService, NavegacionAccessibilityService,
              ProtegeDeviceAdminReceiver, ArranqueReceiver

Un adulto que instala la app de control nunca ve el diálogo de VPN ni la advertencia de accesibilidad, porque su APK no declara los servicios que los disparan. Eso importa para la adopción y para la revisión de Play, donde cada aplicación se evalúa por lo que realmente declara.

Front nuevo del adulto

PanelResponsable: alertas sin leer, lista personalizada con alta y baja, generación del código de vinculación y las líneas de ayuda. Sin backend no hay dispositivos que listar, así que la pantalla explica qué falta en lugar de mostrar una tarjeta vacía.

CodigoVinculacion usa SecureRandom. Seis dígitos alcanzan porque vence en 10 minutos y se quema al primer uso, pero un código predecible sería un equipo ajeno vinculado a la cuenta de otro.

Un detalle que puede sorprender al revisar

Las dos MainActivity comparten nombre de clase a propósito: el manifiesto compartido declara .MainActivity y cada flavor compila la suya. Por eso Notificaciones dejó de nombrar la clase y ahora pide el launcher del propio paquete con getLaunchIntentForPackage, para que el código compartido no se ate a un flavor.

Verificación

BUILD SUCCESSFUL
testControlDebugUnitTest       tests="10" failures="0" errors="0"
testDispositivoDebugUnitTest   tests="10" failures="0" errors="0"

app-control-debug.apk       11M
app-dispositivo-debug.apk   11M

README, ARQUITECTURA.md e INSTALACION.md decían lo contrario y quedaron actualizados. La guía de instalación ahora distingue qué APK va en cada teléfono, y usa como señal el hecho de que la app del adulto nunca pide VPN.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant