wandres.dev
ANATOMÍA DE UNA APP · manifiesto, recursos y AAB

El manifiesto: el contrato que Android lee primero

AndroidManifest.xml como declaración formal de la app: los componentes que expone, los filtros de intención, los permisos que solicita, el hardware que exige, y por qué el sistema lo procesa durante la instalación, mucho antes de ejecutar una sola línea de Kotlin.

⏱ 17 min

Toda app Android tiene un archivo que el sistema lee antes que ninguna otra cosa: AndroidManifest.xml. No es configuración accesoria ni documentación: es el contrato formal entre tu código y la plataforma. Ahí declaras qué componentes existen, cómo pueden invocarse desde fuera, qué permisos pides, qué hardware necesitas y con qué versiones del sistema te comprometes. Android lo procesa en el momento de instalar, cuando tu proceso todavía no existe. Lo que no esté declarado ahí sencillamente no existe para el sistema, por mucho que lo hayas escrito en Kotlin.

🎯 Al terminar esta lección sabrás
  • Entender por qué el manifiesto se procesa en la instalación y no en tiempo de ejecución.
  • Declarar los cuatro tipos de componentes y comprender el papel de los filtros de intención.
  • Distinguir permisos normales, peligrosos y de firma, y separarlos de los requisitos de hardware.
  • Leer el manifiesto final que Gradle produce al fusionar el tuyo con el de cada dependencia.

El archivo que se lee antes de ejecutar nada

Cuando instalas una app, el servicio del sistema encargado de los paquetes abre el archivo, lo interpreta y construye un registro persistente: esta app tiene estas pantallas, responde a estos tipos de intención, pide estos permisos, exige esta versión mínima. Ese registro vive en el sistema, no en tu proceso. Por eso el lanzador puede dibujar tu icono sin que tu app se haya ejecutado jamás, y por eso otra app puede descubrir que tú sabes abrir un PDF sin que tú estés viva.

El manifiesto que escribes es texto, pero el que viaja dentro del paquete es XML binario compilado por AAPT2: un formato tabulado que el sistema recorre sin parsear texto y sin reservar memoria por nodo. La instalación tiene que ser rápida y ocurrir con la app apagada, así que el formato está optimizado para lectura, no para edición.

flowchart TD
A[Se instala el paquete] --> B[El sistema lee el manifiesto binario]
B --> C[Registra componentes y filtros de intencion]
C --> D[Concede permisos de instalacion]
D --> E[El usuario toca el icono]
E --> F[Zygote clona un proceso nuevo]
F --> G[Recien ahora corre tu primera linea de Kotlin]

La estructura mínima anida todo dentro de un elemento raíz y de un bloque de aplicación. Lo importante no es la sintaxis sino la lectura: cada elemento hijo es una declaración pública ante el sistema.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:name=".MiAplicacion"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:theme="@style/Theme.MiApp">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

Fíjate en un detalle histórico: el identificador del paquete ya no se declara aquí. Desde el plugin moderno de Gradle, el namespace y el applicationId viven en el archivo de build, y la herramienta los inyecta al compilar. El manifiesto quedó reducido a lo que de verdad le concierne: la superficie que la app expone al sistema.

Los cuatro componentes y sus puertas de entrada

Android reconoce cuatro tipos de componente, y cada uno es un punto de entrada distinto a tu proceso. Una actividad es una pantalla; un servicio es trabajo sin interfaz; un receptor responde a anuncios del sistema; un proveedor de contenido expone datos estructurados a otras apps. Todos comparten una propiedad decisiva: el sistema los instancia, no tú. No hay una función principal que orqueste la app —hay cuatro contratos que el sistema puede invocar cuando le convenga.

<service
    android:name=".SincronizacionService"
    android:exported="false"
    android:foregroundServiceType="dataSync" />

<receiver
    android:name=".ArranqueReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

El bloque anidado que ves ahí dentro es un filtro de intención, y es la pieza más profunda del manifiesto. Declara: yo sé atender esta combinación de acción, categoría y datos. Cuando alguien lanza una intención implícita —abrir un enlace, compartir una imagen—, el sistema recorre los filtros de todas las apps instaladas y busca coincidencias. Ese es el mecanismo por el que Android compone aplicaciones que no se conocen entre sí: nadie importa a nadie, todos declaran capacidades y el sistema hace de casamentero.

El emparejamiento no es difuso: sigue reglas exactas sobre tres campos. La acción debe coincidir con alguna de las declaradas; todas las categorías de la intención deben estar presentes en el filtro, aunque el filtro puede declarar más; y los datos se comparan por esquema, host, ruta y tipo de contenido, en ese orden de granularidad. Si tras el filtrado sobrevive un solo candidato, el sistema lo lanza sin preguntar. Si sobreviven varios, aparece el selector que el usuario conoce.

🪟

Actividad

Una pantalla. Es el único componente con interfaz, y el único que el usuario puede invocar de forma directa desde el lanzador o desde otra app.

⚙️

Servicio

Trabajo sin interfaz. Hoy debe declarar además su tipo cuando corre en primer plano, porque el sistema exige justificar por qué merece seguir vivo.

📡

Receptor de anuncios

Responde a eventos del sistema. La mayoría de anuncios implícitos ya no pueden registrarse aquí desde Android 8, precisamente para evitar despertar apps en cadena.

🗄️

Proveedor de contenido

Expone datos estructurados bajo un identificador único. Es el mecanismo con el que una app comparte tablas o archivos con otra sin darle acceso a su almacenamiento.

Cada componente admite además atributos que gobiernan cómo lo trata el sistema. El modo de lanzamiento decide si una actividad se apila o se reutiliza, la afinidad de tarea define en qué pila aparece, y el atributo de proceso permite que un componente corra en un proceso separado del resto de la app. Ese último es una herramienta de doble filo: aísla trabajo que puede caerse, pero duplica la inicialización y multiplica la memoria, así que se usa por razones muy concretas y nunca por defecto.

⚠️
`android:exported` es obligatorio y no es un detalle

Desde Android 12, cualquier componente con un filtro de intención debe declarar explícitamente si está exportado. No hay valor por defecto: si lo omites, la app no instala. La razón es una década de vulnerabilidades causadas por componentes accesibles desde fuera sin que su autor lo supiera, porque el valor implícito antiguo era verdadero en cuanto añadías un filtro. La regla práctica es simple y conviene aplicarla sin excepciones: marca como exportado únicamente lo que otra app deba poder invocar, y para todo lo demás declara el valor falso de forma expresa. Un componente exportado es superficie de ataque, y el manifiesto es donde se mide.

Permisos, hardware y compatibilidad

El manifiesto declara también lo que la app necesita del mundo. Conviene separar dos ejes que suelen confundirse: los permisos son autorizaciones sobre datos o capacidades sensibles, mientras que los requisitos de hardware son condiciones de compatibilidad que filtran la app en la tienda.

🟢

Permisos normales

Como el acceso a internet o la vibración. Se conceden en la instalación sin preguntar al usuario, porque el riesgo es bajo. Basta declararlos.

🔴

Permisos peligrosos

Cámara, ubicación, contactos, micrófono. Declararlos no basta: hay que pedirlos en tiempo de ejecución, y el usuario puede revocarlos después.

🔏

Permisos de firma

Solo se conceden si quien los pide está firmado con el mismo certificado que quien los define. Es el mecanismo para suites de apps del mismo autor.

📷

Requisitos de hardware

El elemento uses-feature declara si necesitas cámara, NFC o giroscopio. Con el atributo de obligatoriedad decides si la tienda oculta la app en dispositivos sin ese componente.

Junto a esto viven las declaraciones de compatibilidad: la versión mínima del sistema, la versión objetivo contra la que prometes comportarte y, en la era moderna, el elemento queries, que declara a qué otras apps quieres poder ver. Desde Android 11 la visibilidad de paquetes dejó de ser universal —consultar qué hay instalado en el dispositivo es un dato sensible—, así que si tu app necesita saber si existe cierta aplicación, debe declararlo aquí. Otra vez el mismo patrón: capacidad declarada, no capacidad tomada.

La fusión de manifiestos

El manifiesto que empaqueta Gradle no es el tuyo. Es el resultado de fusionar tres niveles: el de cada dependencia, el de cada variante de build y el tuyo, que tiene la prioridad más alta. Por eso una librería de análisis puede añadir el permiso de internet sin que tú lo escribas, y por eso una app puede acabar pidiendo permisos que nadie recuerda haber solicitado.

La herramienta resuelve conflictos con reglas de precedencia, y cuando el conflicto es irreconciliable falla la compilación. Para gobernarlo existen los marcadores de fusión, atributos de un espacio de nombres auxiliar que no llegan al resultado final y solo dirigen el proceso.

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission
        android:name="android.permission.ACCESS_COARSE_LOCATION"
        tools:node="remove" />

    <application tools:replace="android:label" android:label="@string/mi_nombre">
        <activity
            android:name="com.libreria.PantallaAjena"
            tools:node="merge"
            android:exported="false" />
    </application>
</manifest>

Los tres marcadores que resuelven casi todo son el que elimina un nodo aportado por una dependencia, el que sustituye un atributo en conflicto declarando cuál gana, y el que marca un nodo como estrictamente obligatorio para que la compilación falle si alguien lo altera. El artefacto de salida, un manifiesto fusionado dentro del directorio de build, es la única verdad; el archivo que editas es apenas tu aportación al resultado.

Conviene añadir una advertencia de higiene: eliminar un permiso que una librería declara no impide que esa librería lo use. Simplemente hará que su llamada falle en tiempo de ejecución cuando el sistema deniegue el acceso. El marcador de eliminación sirve para permisos que la librería pide de forma oportunista y de los que tú no dependes, no como forma de recortar funcionalidad ajena sin leer qué hace.

El manifiesto existe porque el sistema tiene que decidir sin ejecutarte

Detrás de este archivo hay una idea que explica media plataforma. Android necesita responder preguntas sobre tu app cuando tu app no está corriendo: qué icono dibujar en el lanzador, si puedes abrir un enlace que otro acaba de tocar, si tienes derecho a arrancar cuando el dispositivo termina de encender, si eres compatible con este teléfono concreto. Ninguna de esas preguntas puede resolverse ejecutando tu código, porque ejecutar código cuesta memoria y batería, porque tu código podría mentir y porque en un dispositivo con doscientas apps instaladas sería inviable arrancarlas todas para preguntarles nada. La solución es la que adoptan todos los sistemas que gestionan software ajeno con desconfianza: obligar a declarar por adelantado, en un formato que el sistema pueda leer, indexar y auditar sin ceder el control. Eso convierte al manifiesto en algo más que configuración: es la representación de tu app dentro del sistema operativo, y en muchos sentidos es la app —el registro que existe entre instalación y ejecución, y que sobrevive a que tu proceso muera cien veces. De ahí se derivan consecuencias que después parecen arbitrarias: por qué añadir una pantalla exige tocar un XML, por qué un permiso no se puede tomar en caliente, por qué un componente exportado es un riesgo real. Cuando aprendes a leer un manifiesto ajeno, sabes qué hace una app antes de abrir su código; y cuando aprendes a escribirlo con parquedad, estás minimizando deliberadamente la superficie que ofreces al resto del sistema.

⚔️ Audita el manifiesto real de tu app
  1. Compila un proyecto y localiza el manifiesto fusionado dentro del directorio de build; compáralo línea a línea con el que tú escribiste.
  2. Anota cada permiso que aparezca y que tú no hayas declarado, e identifica qué dependencia lo aportó.
  3. Elimina uno de esos permisos con el marcador de fusión correspondiente y verifica que desaparece del resultado.
  4. Cambia un componente sin filtros a exportado y razona quién podría invocarlo desde otra app.
  5. Añade un requisito de hardware no obligatorio y explica en una frase cómo cambia el conjunto de dispositivos compatibles.