wandres.dev
INTENTS Y DEEP LINKS · la app en el sistema

Recibir y devolver: resultados de actividad y trampas de seguridad

Pedir algo a otra aplicación y recibir la respuesta parece la operación más simple del sistema de intents y es en realidad la que más veces se ha reescrito, porque el patrón antiguo basado en códigos de petición dependía de un estado que no sobrevivía a la muerte del proceso. Esta lección explica el patrón moderno de resultado de actividad, sus contratos predefinidos, la construcción de contratos propios y la forma correcta de devolver un resultado desde una actividad que otros invocan. Cierra el nivel con el catálogo de trampas de seguridad que rodean a los intents implícitos: la redirección de intents, los intents pendientes mutables, la concesión temporal de permisos sobre contenido y la disciplina de validación que convierte una superficie expuesta en una frontera defendida.

⏱ 20 min

Todo el nivel ha ido en una sola dirección: construir un intent, entregarlo al sistema y desentenderse. Pero una parte enorme de la interacción entre aplicaciones no es un envío sino una pregunta que espera respuesta. Elegir una foto, escanear un código, pedir un permiso, seleccionar un fichero donde guardar, autenticar al usuario contra otra aplicación: en todos esos casos la aplicación que pregunta necesita saber qué pasó, y necesita saberlo aunque entre la pregunta y la respuesta el sistema haya matado su proceso para hacer sitio a la aplicación que estaba contestando, cosa que es exactamente lo que ocurre en los dispositivos con poca memoria. Ese detalle, que suena a caso extremo y es el caso normal, es la razón por la que el mecanismo antiguo se retiró y por la que el actual tiene la forma que tiene.

🎯 Al terminar esta lección sabrás
  • Usar el patrón moderno de resultado de actividad y entender por qué el registro debe ocurrir antes del arranque.
  • Escribir contratos propios que encapsulen la construcción del intent y la interpretación de la respuesta.
  • Devolver resultados desde una actividad invocada por terceros sin filtrar información ni confiar en la entrada.
  • Reconocer y neutralizar la redirección de intents, los intents pendientes mutables y las concesiones de permiso mal acotadas.

El patrón de resultado y por qué se registra antes

El mecanismo moderno separa dos momentos que el antiguo mezclaba: registrar quién atenderá la respuesta y lanzar la petición. El registro debe hacerse durante la creación del componente, antes de que exista ninguna posibilidad de que llegue un resultado, porque la plataforma necesita poder reconstruir la asociación entre petición y destinatario después de haber destruido el proceso entero. Si el registro se hiciera en el momento del lanzamiento, un proceso recreado no tendría a quién entregarle nada, que es precisamente el fallo que el patrón de códigos de petición sufría en silencio.

En Compose el registro se hace con la función recordada correspondiente, que se encarga de mantener el registro estable a través de las recomposiciones y de retirarlo cuando la composición abandona el árbol.

@Composable
fun SelectorDeImagen(alElegir: (Uri) -> Unit) {

    val lanzador = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri ->
        // puede llegar con el proceso recien recreado
        if (uri != null) alElegir(uri)
    }

    Button(onClick = {
        lanzador.launch(
            PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
        )
    }) { Text("Elegir imagen") }
}

Los contratos predefinidos cubren la mayor parte de los casos frecuentes y su valor no es el ahorro de líneas sino el tipado: el contrato declara qué se le pasa y qué devuelve, de modo que desaparecen a la vez la construcción manual del intent y la interpretación manual del resultado, que eran las dos fuentes clásicas de error. Conviene además preferir los contratos que no exigen permiso —el selector de medios frente a la lectura del almacenamiento, la creación de documentos frente a la escritura libre— porque delegan la decisión en el usuario en el momento exacto en que la entiende.

💡
Un contrato propio convierte una integración frágil en una función con tipos

Cuando hablas con una actividad de otra aplicación que espera extras concretos y devuelve extras concretos, ese conocimiento suele quedar esparcido entre el punto de lanzamiento y el de recepción, y se degrada en la primera refactorización. Encapsularlo en un contrato propio lo concentra en un único sitio comprobable: la construcción del intent, el nombre de cada extra y la interpretación del código de resultado dejan de ser detalles repetidos y pasan a ser la implementación privada de una función con una firma clara. El resto de la aplicación pide un valor y recibe un valor.

class EscanearCodigo : ActivityResultContract<Unit, String?>() {

    override fun createIntent(context: Context, input: Unit): Intent =
        Intent("com.ejemplo.accion.ESCANEAR").setPackage(PAQUETE)

    override fun parseResult(resultCode: Int, intent: Intent?): String? =
        if (resultCode == Activity.RESULT_OK) intent?.getStringExtra(EXTRA_CODIGO)
        else null

    private companion object {
        const val PAQUETE = "com.ejemplo.escaner"
        const val EXTRA_CODIGO = "codigo"
    }
}

Devolver desde el otro lado

Si tu actividad es la invocada, devolver es fijar un código de resultado y opcionalmente un intent con los datos antes de terminar. La disciplina que hay que mantener es doble. Por un lado, distinguir siempre la cancelación del éxito, porque quien llama necesita diferenciar entre una respuesta negativa y una ausencia de respuesta. Por otro, y esto se olvida constantemente, devolver la cantidad mínima de información: el intent de vuelta va a otra aplicación cuya identidad no controlas, así que no es sitio para volcar el objeto de dominio completo por comodidad.

private fun devolverSeleccion(elemento: Elemento) {
    val datos = Intent().apply {
        putExtra(EXTRA_ID, elemento.id)        // lo minimo imprescindible
    }
    setResult(Activity.RESULT_OK, datos)
    finish()
}

override fun onBackPressed() {
    setResult(Activity.RESULT_CANCELED)        // ausencia de respuesta, no negativa
    super.onBackPressed()
}
sequenceDiagram
participant A as App que pregunta
participant S as Sistema
participant B as App que responde
A->>A: Registro del contrato al crear el componente
A->>S: launch con la entrada tipada
S->>B: Arranque de la actividad
Note over A: El proceso puede morir aqui
B->>S: setResult mas finish
S->>A: Recreacion y entrega del resultado
A->>A: parseResult devuelve un valor tipado

Las trampas de los intents implícitos

Cierra el nivel el capítulo menos glamuroso y más caro de equivocarse. La primera trampa es la redirección de intents, y consiste en recibir dentro de un extra un intent construido por otro y reenviarlo. Como el reenvío lo hace tu proceso, se ejecuta con tu identidad y tus permisos, de modo que un tercero sin acceso a un componente interno tuyo puede alcanzarlo usándote de intermediario. La regla es tajante: un intent que llega en un extra no se lanza nunca sin comprobar antes que su destino pertenece a un conjunto cerrado y conocido.

La segunda es el intent pendiente mutable. Un intent pendiente es una autorización para actuar en tu nombre en el futuro, y si se crea mutable y con un intent base implícito, quien lo reciba puede rellenar los huecos que dejaste, incluido el componente de destino, y conseguir que tu aplicación ejecute algo que nunca quisiste. Desde el nivel de API 31 la plataforma exige declarar la mutabilidad explícitamente, y la respuesta correcta es inmutable salvo justificación escrita, siempre acompañada de un intent base explícito.

val pendiente = PendingIntent.getActivity(
    context,
    0,
    Intent(context, DetalleActivity::class.java),   // base explicita, no implicita
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)

La tercera es la concesión de permisos sobre contenido. Al enviar un identificador de recurso propio a otra aplicación hay que acompañarlo de la bandera de concesión temporal, que otorga acceso solo a ese recurso y solo mientras dure la tarea. Sustituirlo por exportar el proveedor de contenido, que es el atajo habitual cuando la concesión no funciona a la primera, abre el acceso a todo su contenido para todo el dispositivo y de forma permanente.

Existe una variante de la primera trampa que se cuela incluso en equipos cuidadosos: aceptar una dirección en un extra y abrirla. Aunque no reenvíes un intent completo, cargar una URL recibida en un componente web propio, o pasarla a un visor interno que confía en su origen, reproduce el mismo problema con otro disfraz. La defensa es idéntica en forma y en espíritu: comprobar el esquema y el anfitrión contra una lista cerrada antes de hacer absolutamente nada con la cadena recibida.

La cuarta es de datos y no de mecanismo: enviar información sensible en un intent implícito. Como el destinatario lo decide el sistema o el usuario, cualquier aplicación instalada que declare el filtro adecuado puede acabar recibiéndolo. Los identificadores de sesión, los datos personales y los contenidos privados viajan por intents explícitos o no viajan.

📝

Registrar pronto

El registro ocurre al crear el componente porque el resultado puede llegar a un proceso recreado.

📦

Contrato propio

Encapsula construcción e interpretación en un único sitio tipado y comprobable.

🪞

Nunca reenviar

Un intent recibido en un extra solo se lanza contra un conjunto de destinos cerrado y verificado.

🔒

Inmutable y explícito

Los intents pendientes se crean inmutables y con base explícita salvo justificación escrita.

Cada intent que sale es una delegación, y toda delegación es una decisión de confianza

Al terminar el nivel conviene reunir en una sola idea lo que las cinco lecciones han ido rodeando desde ángulos distintos. Un intent no es una llamada a una función: es una delegación. Cuando emites uno estás pidiendo que algo ocurra sin ejecutarlo tú, y toda delegación arrastra las mismas tres preguntas que arrastra delegar cualquier cosa en cualquier ámbito. En quién delego, qué información le entrego y qué me comprometo a aceptar de vuelta. El sistema de intents es notable porque hace esas tres preguntas explícitas en el código en lugar de dejarlas implícitas en la buena fe del programador, y casi todos los fallos graves de seguridad en Android nacen de responder alguna de ellas por omisión. La primera pregunta se responde al elegir entre explícito e implícito, y esa elección no es de estilo: el explícito nombra al delegado y cierra el conjunto de posibles ejecutores a uno; el implícito lo deja abierto a cualquiera que declare la capacidad, hoy o dentro de dos años, con o sin buenas intenciones. La segunda se responde en lo que metes dentro, y la regla que sobrevive a todos los cambios de API es que un intent implícito es un mensaje publicado, no un sobre cerrado. La tercera es la más olvidada porque parece pasiva: al declarar un filtro o al registrar un contrato te comprometes a aceptar lo que llegue, y lo que llegue puede haber sido fabricado con la única finalidad de encontrar la ruta por la que tu proceso, con tus permisos, hace algo que su emisor no podría hacer por sí mismo. Ahí está el corazón de la redirección de intents y del intent pendiente mutable: ninguno de los dos es un fallo del mecanismo, ambos son delegaciones concedidas sin haber decidido a quién. Hay una consecuencia práctica que resume el nivel entero y que conviene llevarse escrita. Toda frontera de tu aplicación con el exterior —un filtro declarado, un enlace profundo aceptado, un resultado recibido, un intent pendiente entregado— es un punto donde la confianza cambia de manos, y esos puntos deben ser pocos, estar enumerados y ser revisados con el mismo cuidado con que se revisa un endpoint público. La plataforma ha ido endureciendo ese terreno versión tras versión, obligando a declarar la exportación, obligando a declarar la mutabilidad, restringiendo la visibilidad de paquetes, exigiendo verificar los dominios. Cada una de esas rupturas rompió el código de mucha gente, y todas apuntan al mismo sitio: convertir en explícita una decisión de confianza que antes se tomaba sin darse cuenta. Escribir buen código de intents consiste, en última instancia, en tomar esas decisiones antes de que la plataforma te obligue a tomarlas.

⚔️ Cierra el círculo y luego atácalo
  1. Sustituye una integración basada en códigos de petición por un contrato tipado y demuestra con el gestor de dispositivos que sobrevive a la muerte del proceso.
  2. Escribe un contrato propio para una actividad de otra aplicación y reduce el punto de llamada a una única línea sin extras visibles.
  3. Devuelve un resultado desde una actividad tuya distinguiendo éxito, negativa y cancelación, y comprueba desde fuera que cada caso se recibe correctamente.
  4. Construye desde una segunda aplicación un ataque de redirección contra un componente tuyo que reenvíe un intent recibido, y ciérralo con una lista blanca de destinos.
  5. Audita todos los intents pendientes de tu proyecto, conviértelos en inmutables con base explícita y anota la justificación de los que no puedan serlo.