Saltar al contenido
K
Android: la plataformaPaso a paso30 min de lectura · 5 min de máquina Intermedio

Intents implícitos y filtros

Al terminar: Pedirle al sistema que abra un enlace o comparta un texto con un intent implícito, y recibir enlaces con un filtro.

Al terminar esta lección, Marcador podrá compartir el resultado del partido con las apps del móvil, abrir una web y recibir un enlace: abrirás el detalle desde el ordenador con marcador://partido/2, lanzado con adb.

Importa porque un intent implícito es la forma de pedir cosas a otras apps sin saber cuáles hay instaladas, y un filtro es la forma de que otras apps te las pidan a ti. Tema: intents implícitos y intent filters.

Qué hace un intent implícito

Un intent implícito no nombra una clase: declara una acción (ACTION_SEND, «enviar»; ACTION_VIEW, «ver») y, a veces, una URI (una dirección con forma de enlace, como https://kotlinlang.org o marcador://partido/2). El sistema busca entre las apps instaladas cuáles saben hacerlo; si hay varias, decide el usuario.

ejemplo mínimo: un intent implícito
val implicito = Intent(
    Intent.ACTION_VIEW,                 // la acción: «ver algo»
    Uri.parse("https://kotlinlang.org") // convierte el texto en una dirección (Uri)
)
startActivity(implicito)                // el sistema busca quién sabe abrir un enlace web

El intent filter: cómo se declara «yo sé hacer eso»

Para que una Activity reciba intents implícitos, en el Manifest lleva un <intent-filter>: una plantilla que dice qué acciones, categorías y datos acepta. Android compara cada intent implícito con los filtros de todas las apps instaladas.

Parte del filtroQué significa
<action android:name="android.intent.action.VIEW" />«Sé ver algo»
<category android:name="android.intent.category.DEFAULT" />Una categoría es una etiqueta más del intent. Android añade DEFAULT por su cuenta a todo intent implícito que lanzas con startActivity, así que tu filtro tiene que aceptarla: sin ella, no te encuentra
<category android:name="android.intent.category.BROWSABLE" />Puede abrirse desde un enlace del navegador
<data android:scheme="marcador" android:host="partido" />Acepta enlaces que empiezan por marcador://partido: el esquema es lo que va antes de :// (marcador, https) y el host, lo que va justo después (partido)

Además, android:exported dice si otras apps pueden lanzar ese componente. Con un intent-filter hay que declararlo a mano, y en "true": si no, el navegador o adb no podrían abrirla.

Las piezas, en una app de recetas

Las piezas que vas a usar:

  • Intent.ACTION_SEND, type = "text/plain" (el tipo MIME: una etiqueta estándar que dice qué clase de contenido viaja; text/plain es texto sin formato), Intent.EXTRA_TEXT (la clave estándar del texto a enviar) y Intent.createChooser(intent, título): compartir texto, con el selector de apps siempre visible.
  • .apply { … }: ejecuta el bloque sobre el objeto y lo devuelve; dentro, type = … y putExtra(…) se refieren al Intent que estás creando (lo viste en Funciones de alcance).
  • Funciones de extensión (fun Context.compartir(...), en Extensiones): añaden una función a Context. Como una Activity es un Context, puedes llamar a compartir(texto) desde DetalleActivity sin nada delante.
  • Uri.parse(texto): convierte un texto en una Uri; uri.lastPathSegment es su último trozo (en marcador://partido/2, "2"); intent.data es la URI del intent, o null.
  • texto.removePrefix(prefijo): devuelve el texto sin ese principio, si lo tenía. "marcador://partido/2".removePrefix("marcador://partido/") da "2". Y texto.toIntOrNull() convierte a Int o devuelve null si no es un número; con ?. (de Nulabilidad) encadenas sin fallar.
  • try { … } catch (e: ActivityNotFoundException) { … }: «intenta esto y, si falla con este error, haz esto otro» (lo viste en Errores). ActivityNotFoundException: lo lanza startActivity si nadie sabe hacer lo pedido. Toast.makeText(...).show() muestra un mensaje breve (lo viste en Context y Application). Log.w(tag, mensaje, error) escribe en Logcat como Log.d, pero con nivel W (warning, aviso: algo no fue como se esperaba, sin ser grave) y adjunta el error.

Todas esas piezas, en una app de recetas inventada. No es Marcador: los nombres y los datos son otros, pero la forma es la que vas a escribir.

ejemplo mínimo: intents implícitos y leer un enlace (con comentarios)
// Intent implícito como función de extensión de Context:
fun Context.enviarLista(texto: String) {
    val envio = Intent(Intent.ACTION_SEND).apply {          // qué quieres hacer: enviar
        type = "text/plain"                                 // tipo MIME: texto sin formato
        putExtra(Intent.EXTRA_TEXT, texto)                  // el texto, en la clave estándar
    }
    startActivity(Intent.createChooser(envio, "Enviar lista"))  // con selector siempre visible
}
 
fun Context.abrirMapa(url: String) {
    try {
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))   // Uri.parse: de texto a dirección
    } catch (e: ActivityNotFoundException) {                // nadie sabe hacerlo
        Log.w("Recetas", "Nadie abre $url", e)              // nivel W (aviso), con el error adjunto
        Toast.makeText(this, "Sin app para el mapa", Toast.LENGTH_SHORT).show()  // mensaje breve en pantalla
    }
}
 
// Dentro de RecetaActivity.onCreate: leer el número de un enlace "recetas://plato/7".
val numero = intent.data?.lastPathSegment?.toIntOrNull()    // 7; null si no hay enlace o no es número

Y el filtro del manifiesto para que esa RecetaActivity acepte enlaces recetas://plato/…. Los comentarios van fuera de las etiquetas, porque XML no los admite dentro:

ejemplo mínimo: una activity con intent-filter (con comentarios)
<!-- Va dentro de <application>, junto a las demás <activity>. -->
<!-- exported="true": otras apps (el navegador, adb) pueden abrirla. Obligatorio con intent-filter. -->
<activity
    android:name=".RecetaActivity"
    android:exported="true">
    <intent-filter>
        <!-- VIEW: «sé ver esto» -->
        <action android:name="android.intent.action.VIEW" />
        <!-- DEFAULT: Android la añade a todo startActivity implícito; sin ella no te encuentra -->
        <category android:name="android.intent.category.DEFAULT" />
        <!-- BROWSABLE: se puede abrir desde un enlace del navegador -->
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- acepta direcciones que empiezan por recetas://plato -->
        <data android:scheme="recetas" android:host="plato" />
    </intent-filter>
</activity>

Ejercicio · validar el enlace

Reto · Ejercicio: validar el enlace

En el Kotlin Playground, fuera de tu proyecto. Recrea la validación de DetalleActivity sin Android. Recuerda: removePrefix quita el principio de un texto y startsWith comprueba si lo tiene.

  1. Escribe una función que reciba un enlace como texto y devuelva el número de partido de un enlace como marcador://partido/2, o null si el enlace no empieza por marcador://partido/ o si lo que sigue no es un número.
  2. Llama a la función con estos tres enlaces, imprime lo que devuelve en este orden: marcador://partido/2, marcador://partido/abc y https://kotlinlang.org. Lo que debe salir:
2
null
null
  1. Piensa: ¿por qué esta comprobación es necesaria aunque el enlace lo escribas tú?
Ver solución
solución: validar lo que llega por el enlace
fun numeroDePartido(enlace: String): Int? {
    val prefijo = "marcador://partido/"
    if (!enlace.startsWith(prefijo)) return null
    // removePrefix quita el inicio que ya comprobamos; toIntOrNull da null si no es un número.
    return enlace.removePrefix(prefijo).toIntOrNull()
}
 
fun main() {
    println(numeroDePartido("marcador://partido/2"))    // 2
    println(numeroDePartido("marcador://partido/abc"))  // null
    println(numeroDePartido("https://kotlinlang.org"))  // null
}

Porque la Activity está exportada: cualquier app, o adb, puede lanzarla con lo que quiera. Los datos de un intent son entrada externa, como un formulario, y se validan antes de usarlos.

Paso · Compartir, abrir una web y el enlace marcador://partido/2

Objetivo. Añadir Compartir.kt, dos botones más en MainActivity, un botón «Compartir» en el detalle, y el filtro del Manifest con el que el detalle lee enlaces. Al terminar: «Compartir» abre el selector del sistema; y adb shell am start con el enlace abre el detalle de Lobos 1-2 Águilas.

app → manifests
AndroidManifest.xmlse modifica
app → kotlin+java → com.sebastiannarvaez.marcador
core
domain
Resumen.ktse modifica
uise crea
Compartir.ktse crea
laboratorio
DetalleActivity.ktse modifica
MainActivity.ktse modifica
Un fichero nuevo y cuatro modificados, tal como los ves en la vista Android.

Compartir.kt va en una carpeta nueva, core.ui, hermana de core.domain: es una acción de pantalla que usarán varias pantallas y una notificación. Se crea como core.domain en Intents explícitos y una segunda pantalla: en el panel, app → kotlin+java, clic derecho sobre el paquete com.sebastiannarvaez.marcador (el que no lleva (test) ni (androidTest)) → New → Package, escribe .core.ui al final (con el punto delante) y pulsa Intro. Al tener core dos carpetas dentro, la vista Android deja de juntarlas: verás core con domain y ui dentro. Después, clic derecho sobre ui, dentro de core → New → Kotlin Class/File, elige File, escribe Compartir y pulsa Intro.

El proyecto no usa nada que no hayas instalado ya: Intent, Uri y Toast vienen con el propio Android. No hay que tocar Gradle.

Qué hay que hacer en cada fichero (obligatorio; el código completo está más abajo):

  • Compartir.kt: dos funciones de extensión de Context, compartir y abrirEnlace.
  • MainActivity.kt: dos botones más en la Column, tras el de «Ver detalle». Tienes Intent importado desde antes; añade import com.sebastiannarvaez.marcador.core.ui.compartir e import com.sebastiannarvaez.marcador.core.ui.abrirEnlace (Option+Enter los añade).
  • DetalleActivity.kt: tres import de core (compartir, partidosDeEjemplo y resumen; Option+Enter los añade), un botón «Compartir» y, en lugar de leer solo el extra, una función textoRecibido que mira primero si llegó un enlace.
  • Resumen.kt: el mapa partidosDeEjemplo (número → partido).
  • AndroidManifest.xml: la <activity> de DetalleActivity (con su nombre .laboratorio.DetalleActivity) pasa a tener el <intent-filter> y exported="true".

Ahora, el paso en tu proyecto. Los títulos de los bloques de abajo son las rutas de los ficheros en tu proyecto; en la vista Android están en app → kotlin+java (o app → manifests para el manifiesto).

Intenta escribirlo tú, en este orden:

  1. Compartir.kt: dos funciones que cualquier Context pueda usar, compartir, que ofrece enviar un texto a otra app con el selector siempre visible, y abrirEnlace, que abre una dirección web y, si no hay app que sepa, lo anota en Logcat y avisa con un mensaje breve. En MainActivity, un botón para cada una: «Compartir resultado (ACTION_SEND)» y «Abrir web (ACTION_VIEW)», que abre kotlinlang.org. Resultado esperado: el primero abre el selector del sistema con Rayo FC 2-1 Toros; el segundo deja el navegador en primer plano.
  2. En DetalleActivity, un botón «Compartir» que comparte el texto que muestra. Resultado esperado: el mismo selector, desde el detalle.
  3. El filtro del manifiesto, para que el detalle acepte los enlaces que empiezan por marcador://partido. En Resumen.kt, los partidos por número (el 1 es Rayo contra Toros y el 2, Lobos contra Águilas), y en el detalle, si llega un enlace, el número del final elige el partido; si no lo conoce, dice «No conozco el partido N». Resultado esperado: marcador://partido/2 abre Lobos 1-2 Águilas.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
app/src/main/java/com/sebastiannarvaez/marcador/core/ui/Compartir.kt
package com.sebastiannarvaez.marcador.core.ui
 
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.widget.Toast
 
// Intent IMPLICITO: no dices QUE app, dices QUE quieres hacer (ACTION_SEND con un
// texto) y el sistema ofrece las apps que saben hacerlo (mensajes, correo...).
// `createChooser` fuerza que salga siempre el selector, aunque haya una por defecto.
fun Context.compartir(texto: String) {
    val envio = Intent(Intent.ACTION_SEND).apply {
        type = "text/plain"
        putExtra(Intent.EXTRA_TEXT, texto)
    }
    startActivity(Intent.createChooser(envio, "Compartir resultado"))
}
 
// Otro implicito: ACTION_VIEW con una URL abre el navegador. Si no hubiera ninguna
// app capaz (un emulador sin navegador, por ejemplo) startActivity lanza
// ActivityNotFoundException: hay que atraparla.
fun Context.abrirEnlace(url: String) {
    try {
        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
    } catch (e: ActivityNotFoundException) {
        Log.w("Intents", "Nadie sabe abrir $url", e)
        Toast.makeText(this, "No hay app para abrir el enlace", Toast.LENGTH_SHORT).show()
    }
}

En MainActivity.kt, los dos botones van dentro de la Column, tras el de «Ver detalle».

app/src/main/java/com/sebastiannarvaez/marcador/MainActivity.kt
                    Button(onClick = { compartir(Ejemplo.rayoContraToros.resumen()) }) {
                        Text("Compartir resultado (ACTION_SEND)")
                    }
                    Button(onClick = { abrirEnlace("https://kotlinlang.org") }) {
                        Text("Abrir web (ACTION_VIEW)")
                    }

En DetalleActivity.kt, el botón «Compartir» va justo antes del de «Volver», dentro de la Column. El resto no cambia.

app/src/main/java/com/sebastiannarvaez/marcador/laboratorio/DetalleActivity.kt
                    Button(onClick = { compartir(texto) }) { Text("Compartir") }

En Resumen.kt, el mapa va debajo de los import y hay que añadir un tercero, Ejemplo. El resto no cambia.

app/src/main/java/com/sebastiannarvaez/marcador/core/domain/Resumen.kt
import com.sebastiannarvaez.marcador.torneo.Ejemplo
 
// Los partidos que la app conoce, por numero. Un enlace `marcador://partido/2`
// llega con ese 2 como texto.
val partidosDeEjemplo: Map<Int, Partido> = mapOf(
    1 to Ejemplo.rayoContraToros,
    2 to Ejemplo.lobosContraAguilas,
)

En DetalleActivity.kt, la pantalla se abre ahora de DOS formas: con el intent explícito de antes y con un intent implícito que casa con el filtro (un enlace marcador://partido/2). Hay que añadir import android.content.Intent y los tres import com.sebastiannarvaez.marcador.core.ui.compartir, import com.sebastiannarvaez.marcador.core.domain.partidosDeEjemplo e import com.sebastiannarvaez.marcador.core.domain.resumen (los símbolos ahora viven en core), cambiar la línea que calcula texto en onCreate y añadir la función textoRecibido dentro de la clase, justo antes del companion object.

app/src/main/java/com/sebastiannarvaez/marcador/laboratorio/DetalleActivity.kt
        val texto = textoRecibido(intent)
app/src/main/java/com/sebastiannarvaez/marcador/laboratorio/DetalleActivity.kt
    // Un Intent trae DOS tipos de informacion: lo que se quiere hacer (action,
    // data) y datos sueltos («extras»). Aqui se lee de donde toque. El parametro
    // se llama igual que la propiedad `intent` de la Activity: dentro de la
    // funcion manda el parametro, que recibe justo esa propiedad.
    private fun textoRecibido(intent: Intent): String {
        // Si viene de un enlace, `intent.data` es la URI marcador://partido/2.
        val numero = intent.data?.lastPathSegment?.toIntOrNull()
        if (numero != null) {
            return partidosDeEjemplo[numero]?.resumen() ?: "No conozco el partido $numero"
        }
        // Si viene del intent explícito, el resumen va en un extra.
        return intent.getStringExtra(EXTRA_RESUMEN) ?: "Sin partido"
    }

Por último, el manifiesto: la <activity> de DetalleActivity que escribiste en la lección anterior se sustituye por esta, que sí se deja abrir desde fuera. El resto del fichero no cambia.

app/src/main/AndroidManifest.xml
        <!-- Segunda pantalla. exported="true" porque tiene intent-filter y otras apps
             (el navegador, adb) deben poder lanzarla. Casa con marcador://partido/<n>:
             VIEW = «ver esto»; DEFAULT = imprescindible para los intents implicitos
             de startActivity; BROWSABLE = permite abrirla desde un enlace del navegador. -->
        <activity
            android:name=".laboratorio.DetalleActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="marcador" android:host="partido" />
            </intent-filter>
        </activity>

Probar los dos caminos

Los comandos adb se escriben en la Terminal de Android Studio y están explicados en la Chuleta de adb. Hoy usas am start, que lanza una Activity con el intent que le describas: sus opciones -a (action) y -d (data) rellenan la acción y la URI del Intent. (Si adb no se reconoce, mira Instalar Android Studio y el SDK; el emulador se arranca como en La Activity.) Ejecuta la app con Run, con app elegido arriba. Antes de cada prueba limpia Logcat con adb logcat -c.

1. Intent implícito, desde el ordenador. Con la app instalada, un enlace que casa con el filtro:

adb logcat -c
adb shell am start -a android.intent.action.VIEW -d "marcador://partido/2"

am start imprime en la terminal:

Starting: Intent { act=android.intent.action.VIEW dat=marcador://partido/... }

Y Logcat, con adb logcat -d -s Intents:

D Intents : DetalleActivity abierta con action=android.intent.action.VIEW data=marcador://partido/2 -> Lobos 1-2 Águilas

Android oculta el final de la URI al imprimir el intent (partido/...), pero la Activity recibe la completa. Aquí el texto viene de intent.data, no de un extra: es el mismo mecanismo que usaría un enlace en un navegador (BROWSABLE). Prueba también marcador://partido/9 (No conozco el partido 9) y marcador://partido/abc (toIntOrNull() da null y sale Sin partido).

2. Intent implícito, a otras apps. Compartir resultado (ACTION_SEND) abre el selector del sistema con el texto Rayo FC 2-1 Toros. Qué apps salen, y con qué nombre, depende del dispositivo y de su idioma: en un emulador verás algo como Quick Share, Chrome, Messages, Drive y Copy to clipboard. Abrir web (ACTION_VIEW) deja el navegador en primer plano. Y el botón Compartir del detalle abre el mismo selector.

Opcional: una Activity que ya existe y onNewIntent

Hasta aquí cada intent abre una DetalleActivity nueva. Pero si ya hay una abierta arriba de la pila y le llega otro intent, Android puede reutilizarla en lugar de crear una segunda: entonces no llama a onCreate, sino a onNewIntent(intent), el callback que viste en la tabla de El ciclo de vida de la Activity. Para ello la Activity se declara con android:launchMode="singleTop" en el Manifest (modo de arranque: «si ya soy la pantalla de arriba, reutilízame»).

ejemplo mínimo: recibir un intent en una Activity ya abierta
// En el Manifest: <activity android:name=".laboratorio.DetalleActivity" android:launchMode="singleTop" …>
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)   // sin esto, la propiedad `intent` seguiría siendo el mensaje antiguo
    // … aquí se vuelve a leer intent.data y se actualiza la pantalla …
}

Marcador no lo necesita todavía, así que no hay nada que escribir ni probar hoy. Lo aplicarás de verdad en Deep links en Android, donde un enlace puede llegar con la app ya abierta.

Punto de control

  • Ver detalle abre la segunda pantalla con Rayo FC 2-1 Toros, y Volver regresa a la principal.
  • adb shell am start -a android.intent.action.VIEW -d "marcador://partido/2" abre el detalle con Lobos 1-2 Águilas; con el número 9, No conozco el partido 9.
  • Compartir resultado abre el selector del sistema.
  • En la Terminal de Android Studio, ./gradlew :app:assembleDebug termina en BUILD SUCCESSFUL.

Comprueba lo que entendiste

5 preguntas · necesitas 4 para completar la lección

  1. 1.¿Qué pasa si DetalleActivity declara un <intent-filter> pero no pone android:exported?

  2. 2.Alguien lanza marcador://partido/abc con adb. ¿Por qué DetalleActivity no falla?

  3. 3.¿Para qué sirve la categoría DEFAULT en el filtro de una Activity?

  4. 4.¿Qué aporta Intent.createChooser(envio, "Compartir resultado") al compartir un texto?

  5. 5.En un emulador sin navegador, startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) lanza una excepción. ¿Cuál y qué haces?

Faltan 5 de 5