App Android (Kotlin moderno + Jetpack Compose) para respaldar y compartir archivos mediante varios mecanismos intercambiables: API REST, almacenamiento en la nube (S3 / FTP-SFTP / WebDAV / GCS) y P2P (WebRTC real, con un dashboard estilo BitTorrent). Interopera con la app de escritorio hermana (pepe-desktop-files-administrator), que habla el mismo protocolo P2P.
Forma parte de un sistema de tres repos:
| Repo | Rol |
|---|---|
pepe-android-files-administrator |
Esta app (cliente Android) |
pepe-api-files-administrator |
Backend REST (subida/listado) |
pepe-p2p-orquestrator |
Orquestador de transferencias P2P |
Clean Architecture en una sola capa de módulo, con tres anillos:
ui/ Compose + ViewModels (StateFlow)
├─ theme, i18n (ES/EN), components, navigation
└─ screens/ dashboard · files · uploads · downloads · p2p · settings
domain/ Modelos puros + contratos (interfaces) + casos de uso
├─ model/ FileNode, TransferItem, P2pTransfer, AppSettings…
├─ repository/ SourceRepository, DestinationProvider, TransferRepository…
└─ usecase/ BackupSelectionUseCase, TestConnectionUseCase, SeedSelectionUseCase
data/ Implementaciones
├─ local/ Room (checkpointing por archivo)
├─ settings/ DataStore (preferencias) + secretos cifrados (Keystore AES-GCM)
├─ remote/ Retrofit + OkHttp (subida/descarga con progreso por streaming)
├─ destination/ REST / Cloud / resolver ← mecanismos enchufables
├─ source/ SAF (DocumentFile, scoped-storage)
├─ download/ DownloadStorage (destino en disco de las descargas)
├─ webrtc/ Señalización (WebSocket) + DataChannel (WebRTC real)
└─ repository/ Transfer / P2P / P2pConnectivity
work/ WorkManager (BackupWorker + DownloadWorker en foreground service)
di/ Hilt (App, Database, Repository modules)
Todo destino implementa domain/repository/DestinationProvider:
interface DestinationProvider {
val channel: TransferChannel
suspend fun test(settings): ConnectionResult
suspend fun upload(settings, fileName, sizeBytes, input, onProgress): Result<String>
suspend fun list(settings): Result<List<DownloadItem>>
suspend fun download(settings, item, sink, onProgress): Result<Unit>
}DestinationResolver elige la implementación según AppSettings.remoteType. Añadir un mecanismo nuevo = una clase nueva + un caso en el resolver; ni la UI ni el worker cambian.
RestDestinationProvider— HTTP. Listado vía Retrofit; subida víaProgressRequestBodyy descarga en streaming (GET /v1/files/{id}/content), ambas por chunks de 64 KiB sin OOM en archivos de 200 MB+.CloudDestinationProvider— S3 / GCS / WebDAV / FTP / SFTP reales (streaming, listado, descarga y test por proveedor). S3/GCS usan SigV4 manual (data/destination/cloud/SigV4.kt, GCS vía su endpoint de interoperabilidad S3) sobre OkHttp; WebDAV es HTTP puro (PROPFIND/PUT/GET); FTP usa commons-net y SFTP usa sshj (el esquemasftp://en el host selecciona SFTP).- P2P — WebRTC real:
P2pConnectivityRepository(señalización +WebRtcSessionManager) abre un DataChannel por dispositivo y transfiere archivos;P2pRepositoryImplproyecta la actividad real de ese canal al dashboard estilo BitTorrent (SEED = envíos, LEECH = recepciones, velocidad muestreada a 1 Hz). No es simulación.
- WorkManager + Foreground Service (
BackupWorkersubidas,DownloadWorkerdescargas): sobreviven al cierre de la app. - Checkpointing en Room: cada subida pasa
QUEUED → UPLOADING → DONE/FAILEDy cada descargaAVAILABLE → DOWNLOADING → DOWNLOADED; al reanudar solo se procesa lo pendiente. - Progreso granular (~cada 5 %) y notificación de progreso.
- Archivos bloqueados/en uso → se marcan
FAILEDy la copia continúa. Las descargas guardan en el dir externo propio de la app (sin permisos en runtime) y registran su ruta local.
Storage Access Framework con DocumentFile (SafSourceRepository): el usuario concede una carpeta por volumen (interno / SD / USB-OTG) con OpenDocumentTree, se persiste el permiso, y se recorre el árbol — sin MANAGE_EXTERNAL_STORAGE (compatible con políticas de Google Play).
Kotlin 2.1 · Compose (BOM 2025.01) + Material 3 · Hilt · WorkManager · Room · DataStore · Retrofit/OkHttp + kotlinx.serialization · Navigation Compose · Coroutines/Flow. minSdk 26, targetSdk/compileSdk 35.
6 pantallas con barra de navegación inferior, bilingüe ES/EN (toggle en la barra superior) y acentos de color (azul/teal/púrpura/naranja):
- Inicio — estado de conexión, perfiles de servidor (crear / cambiar / editar / eliminar varios destinos REST o Nube; el activo alimenta la conexión), estadísticas, acciones rápidas y actividad reciente real (solo subidas reales).
- Archivos — pestañas interno/SD, breadcrumbs, selección múltiple → barra de acción Subir o Sembrar (P2P).
- Subidas — historial con filtros por estado, progreso y reintento.
- Descargas — lista real del remoto (refresco al abrir), descarga en streaming con progreso y ruta de guardado.
- P2P — estado de señalización, dispositivos vinculados (conectar), y transferencias reales por WebRTC (SEED/LEECH, velocidades).
- Ajustes — REST vs Nube, prueba de conexión, toggles generales, apariencia.
Requiere Android SDK (platform 35) y JDK 17.
# Generar local.properties con la ruta del SDK si no existe:
# sdk.dir=/ruta/al/Android/Sdk
./gradlew :app:assembleDebug # APK debug
./gradlew installDebug # instalar en dispositivo/emuladorSi clonas sin el wrapper jar, ejecútalo una vez con un Gradle instalado:
gradle wrapper --gradle-version 8.11.1(o abre el proyecto en Android Studio).
- REST: URL base, endpoint de listado (GET), endpoint de subida (POST), token.
- Nube: proveedor, host/bucket, access key, secret key.
- Los secretos se cifran en reposo (ver Notas de seguridad) y se excluyen de los backups del sistema (
data_extraction_rules.xml).
Endurecimientos aplicados (detalle en docs/seguridad.md / docs/plan_seguridad.md):
- Verificación de host key SFTP (H-1):
PinningHostKeyVerifier(TOFU) fija la huella SHA-256 del servidor en la primera conexión (archivosftp_known_hostsprivado de la app) y rechaza una clave distinta después — cierra el MITM que permitía el antiguoPromiscuousVerifier. - Secretos cifrados en reposo (H-2):
token,accessKey,secretKeyy el blob de perfiles se cifran con AES-256-GCM usando una clave del Android Keystore (KeystoreSecretCipher), no en texto plano en DataStore. Migración transparente de valores antiguos. - Token de señalización en cabecera (H-3): el WebSocket manda el token en
Authorization, no en la URL. El modo polling ya lo hacía. - Sin TLS no hay subida segura: usa HTTPS / SFTP / WebDAV-S.
- No se registran rutas completas ni contenido en Logcat.
- Permisos mínimos:
INTERNET,POST_NOTIFICATIONS,FOREGROUND_SERVICE(_DATA_SYNC),WAKE_LOCK.