El Permission Engine vive en server/generator/permissions.js y define 86 entradas en PERMISSION_SPEC, cada una con su permiso de manifiesto, si necesita diálogo runtime, su minSdk, sus dependencias y la implementación nativa que se genera. El mismo objeto alimenta tres cosas: los switches del estudio, el GET /api/permissions/spec y el Audit. Si mañana se añade una entrada al spec, aparece sola en la UI.
La regla que sostiene todo el sistema es que declarar un permiso no es lo mismo que que Android lo conceda. Por eso el Audit mira el ZIP que se va a compilar y no la configuración:
- Manifiesto —
main-manifest.xmlcontiene la líneauses-permission. - Runtime —
NativePermissions.javay su patch enMainActivity.javallaman arequestPermissions()con ese lote. - Puente WebView —
WebChromeClient.onPermissionRequestsólo hacegrant()si el permiso está en el manifiesto y además fue concedido; si no,deny().
Sólo el primer nivel es automático de verdad. Que el diálogo aparezca y que tu web reaccione bien se comprueba en un dispositivo Android real.
POST /api/permissions/audit (o el botón de QA en el estudio) devuelve una fila por permiso marcado:
GENERATED OK— está en el manifiesto y tiene implementación generada. Es el único estado que sirve para compilar.SPEC ONLY, NOT GENERATED— lo pediste pero no se generó. El Audit lo marcafaily bloquea el build.NO GENERADO— reservado paraads: hoy sólo se emite la configuración y elmeta-datade AdMob, no hayAdViewni inicialización.warn— compila, pero algo hay que revisar: permiso especial de Settings,minSdkpor encima deltargetSdk, o provider experimental.fail— bloquea.gpsBackgroundsingpses el caso típico.
Cada fila trae un mechanism: runtime (diálogo normal), background (two-step), special (se concede desde Ajustes), install-time (se concede al instalar) o missing.
Los niveles de verdad son tres y conviene no confundirlos: el Audit prueba que el proyecto contiene el permiso; que Android lo conceda sólo se ve en el dispositivo; y que la función sirva sólo se ve probando la función. Para la matriz por versión de Android está la plantilla lab (Permission Test Lab), que ejecuta cada prueba desde la propia app.
INTERNET, ACCESS_NETWORK_STATE y ACCESS_WIFI_STATE se declaran en todos los proyectos (entrada internet del spec). Sin ellos la web no carga y la app se queda en blanco. No aparecen en el Audit porque no se eligen.
Sólo se activan en Android 13+ (minSdk de la entrada: 33); por debajo el permiso no existía y el sistema no pregunta nada. Lo que se genera: POST_NOTIFICATIONS en el manifiesto, los plugins @capacitor/local-notifications y @capacitor/push-notifications en package.json (el estudio los activa solo al marcar la casilla) y el puente Intee.notifications.schedule({title, body}). Si activas el aviso programado de Ajustes sin marcar este permiso, el manifiesto lo añade igualmente.
La prueba es directa: compila con sólo esta opción, instala en Android 13+, acepta el diálogo y programa un aviso con notifyOnOpen. Con targetSdk < 33 el Audit devuelve warn.
Son tres entradas del spec con el mismo objetivo desde ángulos distintos: mantener el proceso vivo mientras suena audio. foreground añade FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC, FOREGROUND_SERVICE_MEDIA_PLAYBACK y WAKE_LOCK (sin él el PARTIAL_WAKE_LOCK lanza SecurityException y el servicio se cae), y es el único de los tres que genera RadioService.java, AudioBridge.java y el patch de MainActivity que lo arranca al abrir. foregroundService declara las tres líneas de servicio sin WAKE_LOCK, pero no genera servicio alguno: aporta el permiso y nada más. wakeLock añade sólo WAKE_LOCK, sin diálogo. Si activas audio nativo con URL de stream, el motor marca solo foreground y wakeLock, porque sin proceso vivo y con la pantalla apagada el sonido se corta. El spec advierte que Play pide justificación si el servicio no es de audio o sincronización.
En detalle, con el HTML que lo usa, está todo en foreground.md.
Están separados a propósito: Play audita el micrófono por separado de la cámara, y una app que sólo graba vídeo no debería pedir RECORD_AUDIO.
cameraMicgeneraCAMERA,NativePermissions.request(CAMERA)y@capacitor/camera.microphonegeneraRECORD_AUDIOyMODIFY_AUDIO_SETTINGS.
El puente WebView comprueba wants(VIDEO) contra el manifiesto y hasPerm(CAMERA) contra la concesión antes de hacer grant(). Si no marcaste cámara, getUserMedia({video:true}) devuelve denied desde la app: es el comportamiento esperado, no un bug.
Los splits finos del grupo son cameraFlash, cameraAutoFocus, videoCapture y audioRecord; comparten permiso de manifiesto pero se declaran por separado para que el Audit pueda rastrear qué pediste.
Con targetSdk 33 o superior, storage genera READ_MEDIA_IMAGES, READ_MEDIA_VIDEO y READ_MEDIA_AUDIO y omite READ_EXTERNAL_STORAGE para no levantar avisos en Play. Con targetSdk por debajo, cae al permiso legacy. manageExternalStorage (MANAGE_EXTERNAL_STORAGE, API 30+) es el de "todos los archivos" y va por Settings, no por diálogo.
La prueba: un <input type="file" accept="image/*"> debe abrir la galería, y una descarga desde tu web debe aparecer en el gestor de descargas.
gps declara ACCESS_FINE_LOCATION y ACCESS_COARSE_LOCATION sin tocar el segundo plano. gpsBackground declara ACCESS_BACKGROUND_LOCATION y es una entrada aparte deliberadamente: en Android 11+ el sistema ignora la petición de background si va en el mismo lote que el foreground, así que el runtime la pide en dos pasos (requestBackground() después de conceder el foreground).
El Audit devuelve fail si marcas background sin foreground. Play exige además justificación con vídeo demostrando rastreo con la app cerrada; para tiendas, blogs y radios no se marca nunca.
Los splits accessFineLocation, accessCoarseLocation y accessBackgroundLocation existen para quien quiere el nivel de detalle máximo, y advGeo añade tracking con FusedLocationProvider y geofencing (requiere justificación en Play).
En Android 12+ son tres permisos distintos y el estudio los presenta como tres switches: escanear (BLUETOOTH_SCAN), conectar (BLUETOOTH_CONNECT) y anunciar (BLUETOOTH_ADVERTISE), todos con minSdk 31.
bluetooth es la entrada legacy (BLUETOOTH + BLUETOOTH_ADMIN) válida hasta API 30. Si la marcas con targetSdk 31 o superior, normalizeConfig activa bluetoothScan y bluetoothConnect automáticamente en lugar de dejar un permiso muerto. bluetoothPrivileged es de apps del sistema y no sirve en una app normal.
Hay cinco entradas para dos permisos reales, y sólo una debe estar marcada a la vez:
alarmSchedule→SCHEDULE_EXACT_ALARM. Es la recomendada: el usuario puede revocarla desde Ajustes.alarmUse→USE_EXACT_ALARM, sólo para reloj, alarma o calendario. Play lo revisa manualmente.scheduleExactAlarmyuseExactAlarmson los splits de esas mismas dos.alarmes el selector legacy;normalizeConfiglo convierte enscheduleExactAlarm.
El Audit los resuelve a special (van por Settings) y el sistema SpecialAccess.java se genera sólo si marcaste alguno.
phone agrupa CALL_PHONE, READ_PHONE_STATE y READ_CALL_LOG; sms agrupa SEND_SMS y READ_SMS. Los splits son callPhone, answerPhone, readPhoneState, readPhoneNumber, readCallLog, processOutgoingCalls, sendSms, readSms, receiveSms y receiveMms.
Todos llevan la advertencia Play Store restringido en el spec. Compilan sin problema, pero Play sólo los acepta en apps cuya categoría es marcador o mensajería. Si tu app no es de esas, no los marques.
contacts y calendar son los paquetes de lectura+escritura; sus splits son readContacts, writeContacts, readCalendar y writeCalendar.
sensors cubre BODY_SENSORS y HIGH_SAMPLING_RATE_SENSORS; los splits son bodySensors (runtime) y highSamplingRateSensors (install-time, API 31+). activityRecognition necesita API 29+ y envSensors (Droncito) añade acelerómetro, giroscopio, barómetro, luz y proximidad con permisos de API 29.
nearby y nearbyWifiDevices son la misma llave (NEARBY_WIFI_DEVICES, API 33) vista desde dos selectores. changeWifiState y changeNetworkState se conceden al instalar. getAccounts (GET_ACCOUNTS) está marcado como restringido en Play.
systemAlert/systemAlertWindow→SYSTEM_ALERT_WINDOW, se concede conSettings.ACTION_MANAGE_OVERLAY_PERMISSION.installPackages/requestInstallPackages→REQUEST_INSTALL_PACKAGES, concanRequestPackageInstalls.powerMgmt→WAKE_LOCKmásREQUEST_IGNORE_BATTERY_OPTIMIZATIONS, que abre Ajustes para sacar la app del ahorro de batería.
El Audit los marca warn con special aunque el manifiesto esté correcto. Es esperado: el usuario tiene que dar ese permiso a mano.
nfc—NFCsin diálogo, másuses-featurey el filtroTECH_DISCOVEREDconnfc_tech_filter.xmlsi lo marcas.vibration/vibrate—VIBRATE, install-time. Los dos selectores apuntan al mismo permiso.biometric(USE_BIOMETRIC, API 28+) yfingerprint(USE_FINGERPRINT, hasta API 27): marcar legacy activabiometric.infrared—TRANSMIT_IR, sólo en dispositivos con IR.ads— sólo config AdMob: el manifiesto lleva elmeta-data APPLICATION_IDy el plugin, pero no hay código de anuncios. El Audit lo devuelve como NO GENERADO y bloquea builds que lo seleccionen.foregroundService— las mismas tres permisos de manifiesto queforeground, sin el servicio.internet— la entrada base de red (INTERNET,ACCESS_NETWORK_STATE,ACCESS_WIFI_STATE): la incluye todo proyecto, con o sin esta casilla.
El Droncito Pack son 18 entradas del spec (ar, voiceRec, envSensors, aiSuite, powerMgmt, adaptiveNotif, advSecurity, dynamicUI, socialAnalytics, advGeo, dataAnalytics, vr, blockchain, rpa, vulnScan, emoAI, iot, mr) que se resuelven en un único archivo DroncitoBridge.java inyectado por patch, más dependencias de Gradle que se añaden con patch-droncito-gradle.js cuando hace falta (ARCore, ML Kit, SceneView, GVR). Desde la web se usan con Intee.ar.*, Intee.voice.*, Intee.ai.*, Intee.chain.*, Intee.iot.* y compañía.
Tres advertencias que el propio spec recoge: ar, vr y mr suben el peso del APK de forma apreciable (ARCore/GVR) y sólo funcionan en dispositivos compatibles; blockchain guarda claves en el Keystore de Android y conviene probarlo en testnet; iot añade Bluetooth y localización encima de lo que marques.
NativePermissions.javase genera con un arrayBATCH[]exactamente con tus permisos runtime: sin background, sinMANAGE_EXTERNAL_STORAGE.requestAll()pide sólo lo declarado y no concedido.ACCESS_BACKGROUND_LOCATIONva en dos pasos:requestBackground()se llama sólo después de conceder el foreground.SpecialAccess.javaabre Settings para overlay, instalador, alarmas exactas y gestión de almacenamiento. Se genera sólo si lo pediste.patch-permissions.jsinyectaonPermissionRequestcon grant selectivo (VIDEO a cámara, AUDIO a micrófono, GEO a ubicación) ydeny()por defecto.- El manifiesto declara
uses-feature ... required="false"para cámara, Bluetooth LE, GPS, NFC y micrófono, y añade el filtro NFC sólo si toca. - En el log del workflow aparecen
permisos nativos instalados,accesos especiales instaladosyfiltro NFC instalado.
Un permiso por build, en este orden:
- HTML mínimo con sólo
INTERNET: instalar, abrir y comprobar que no pide nada. - Cámara:
getUserMedia({video:true})debe disparar el diálogo. - GPS:
navigator.geolocation.getCurrentPositiondebe pedir ubicación. - Notificaciones en Android 13+: debe preguntar al abrir.
- Bluetooth Scan + Connect:
navigator.bluetooth.requestDevice()debe pedir Bluetooth.
Si un build pide algo que no marcaste, el motor está metiendo de más: abre un issue con el build-config.json y el resultado del Audit.
Dos endpoints sirven para la revisión de release:
POST /api/manifest-diffcompara lo pedido contra lo generado y devuelveMATCHoMISSINGpor permiso, más los inesperados.POST /api/inspect(máximo 30 MB de APK) devuelve la ficha del paquete con un score.
Conviene pasar los dos antes de subir a Play. El resto del flujo está en production.md y los errores frecuentes en troubleshooting.md.