El share sheet: entregar contenido al resto del sistema
Compartir es la única forma de interoperabilidad que Android ofrece sin acuerdo previo entre las partes: dos aplicaciones que no se conocen intercambian contenido porque el usuario lo decide en el momento. Esta lección desmonta la anatomía de un envío, del tipo MIME como contrato hasta la diferencia entre adjuntar texto y adjuntar un flujo, y explica por qué una ruta de fichero es hoy una excepción garantizada. Recorre después el selector del sistema, la previsualización que el usuario ve antes de decidir, los objetivos directos que convierten una lista de aplicaciones en una lista de personas, y cierra con el catálogo de fallos que solo aparecen en producción: los tipos mixtos, las concesiones que no cubren el adjunto, la visibilidad de paquetes y el tamaño máximo de una transacción.
Casi todo lo que una aplicación hace con el exterior está negociado de antemano: un permiso que se pide, una API que se documenta, un esquema de enlace que alguien tuvo que declarar. Compartir es la excepción. Cuando el usuario pulsa el botón de compartir, dos aplicaciones que jamás se han visto, escritas por equipos que no se conocen y que no comparten ni una línea de código ni una clave de firma, intercambian un fragmento de contenido con permisos concretos y una duración limitada. No hay acuerdo previo, no hay registro, no hay contrato firmado: hay un tipo MIME, un puñado de extras y una decisión humana tomada en el instante. Ese mecanismo, que parece trivial desde el lado del emisor porque cabe en cinco líneas, es en realidad el protocolo de interoperabilidad más usado del sistema, y las cinco líneas se convierten en veinte en cuanto se quiere que funcione con adjuntos, con varios ficheros, con previsualización decente y sin filtrar nada por el camino.
- Construir envíos de texto, de un fichero y de varios ficheros usando el tipo MIME como contrato real.
- Entender por qué una ruta de fichero está prohibida y qué bandera acompaña obligatoriamente a un adjunto.
- Controlar la previsualización, las acciones propias del selector y los objetivos directos.
- Reconocer los fallos de producción: tipos mixtos, concesiones incompletas, visibilidad de paquetes y transacciones demasiado grandes.
La anatomía de un envío
Un envío es un intent implícito con la acción de compartir, un tipo MIME que declara qué se está entregando y unos extras que llevan el contenido. El tipo MIME no es decorativo ni informativo: es el criterio con el que el sistema decide qué aplicaciones aparecen en la lista. Declarar */* porque no se sabe muy bien qué se manda produce un selector con todo lo instalado y una experiencia inútil; declarar image/png cuando lo que se manda es un JPEG produce receptores que fallan al abrirlo. El tipo debe describir el contenido con la mayor precisión que se pueda sostener.
El contenido viaja por dos caminos distintos según su naturaleza. El texto va como extra literal y se copia entero dentro del intent. Un fichero no: va como referencia, una URI de contenido que el receptor resolverá por su cuenta cuando la necesite. Esa distinción es la que hace que compartir un vídeo de doscientos megabytes cueste lo mismo que compartir una frase.
// texto: viaja dentro del intent
val texto = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "Lo que quiero compartir")
putExtra(Intent.EXTRA_TITLE, "Titulo para la previsualizacion")
}
startActivity(Intent.createChooser(texto, null))
// fichero: viaja como referencia, nunca como ruta
val adjunto = Intent(Intent.ACTION_SEND).apply {
type = "image/jpeg"
putExtra(Intent.EXTRA_STREAM, uriDeContenido)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(adjunto, null))
La regla que no admite excepciones es que esa referencia jamás puede ser una ruta del sistema de ficheros. Desde la API 24 el modo estricto lanza FileUriExposedException en cuanto un intent sale de la aplicación con un esquema de fichero dentro, y la excepción es intencionada: una ruta obliga al receptor a tener permiso de almacenamiento sobre ella, revela la estructura interna del emisor y no caduca nunca. La siguiente lección está dedicada por entero a la pieza que produce la referencia correcta.
Compartir varios elementos usa una acción distinta y una lista de referencias, con el tipo MIME rebajado al ancestro común. Aquí aparece el primer detalle que casi todo el mundo pasa por alto: cuando los elementos son de tipos distintos, el tipo declarado debe ser genérico pero hay que enumerar los tipos reales aparte, porque de lo contrario los receptores capaces de tratar solo imágenes desaparecen del selector.
val varios = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/jpeg", "application/pdf"))
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(varios, null))
El selector, la previsualización y los objetivos directos
Envolver el intent en el selector del sistema no es una formalidad. Sin él, la plataforma puede recordar una elección previa del usuario y enviar directamente a la aplicación por defecto, lo que es correcto para abrir un enlace y desastroso para compartir, porque el destino de un envío cambia en cada ocasión. El selector garantiza además que el usuario vea siempre la lista completa y que la elección no quede fijada por accidente.
El selector muestra una previsualización del contenido antes de que el usuario decida, y esa previsualización se controla desde el emisor. Un título explícito sustituye al texto crudo; una miniatura declarada en los datos de recorte da al bloque una imagen representativa; y en las versiones recientes se pueden añadir acciones propias que aparecen junto a la fila de destinos, para ofrecer alternativas como copiar un enlace o modificar el contenido antes de enviarlo.
val conVistaPrevia = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, enlace)
putExtra(Intent.EXTRA_TITLE, "Informe trimestral")
// la miniatura de la previsualizacion viaja en el clip
clipData = ClipData.newUri(contentResolver, "miniatura", uriMiniatura)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
Los objetivos directos son el salto cualitativo. En lugar de una lista de aplicaciones, el selector muestra una lista de conversaciones o de personas concretas dentro de esas aplicaciones. Para participar en esa fila hay que declarar destinos de compartición en el recurso de accesos directos, asociarlos a la actividad que sabe recibir y publicar accesos dinámicos de larga duración que el sistema pueda seguir mostrando aunque ya se hayan retirado del listado activo.
<!-- res/xml/shortcuts.xml -->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<share-target android:targetClass="com.ejemplo.ReceptorActivity">
<data android:mimeType="image/*" />
<category android:name="com.ejemplo.categoria.CONVERSACION" />
</share-target>
</shortcuts>
val destino = ShortcutInfoCompat.Builder(context, "conversacion_$id")
.setShortLabel(nombre)
.setIcon(IconCompat.createWithAdaptiveBitmap(avatar))
.setCategories(setOf("com.ejemplo.categoria.CONVERSACION"))
.setLongLived(true) // el sistema puede mostrarlo tras retirarlo
.setIntent(Intent(Intent.ACTION_VIEW).setPackage(context.packageName))
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, destino)
El orden final de esa fila no lo decides tú: lo calcula el sistema con su propio modelo de uso y frecuencia, y cualquier intento de forzarlo está condenado. Lo único que está en tu mano es mantener los accesos al día, con etiquetas que signifiquen algo, e informar de cada uso efectivo para que el modelo aprenda.
flowchart TD
A[La app construye ACTION SEND] --> B[Tipo MIME y extras]
B --> C{Lleva adjunto}
C -->|no| D[Solo texto en el extra]
C -->|si| E[Referencia de contenido en el stream]
E --> F[Bandera de concesion de lectura]
D --> G[createChooser]
F --> G
G --> H[El sistema filtra por tipo MIME]
H --> I[Ordena candidatos y objetivos directos]
I --> J[El usuario elige destino]
J --> K[La app receptora abre la referencia]Lo que solo se rompe en producción
El fallo más frecuente es una concesión incompleta. La bandera de lectura cubre la referencia principal del intent y los elementos del clip, pero no cubre por sí sola las referencias metidas a mano en una lista de extras en todas las versiones de la plataforma. Cuando se comparten varios ficheros conviene reforzar la concesión de forma explícita hacia los paquetes candidatos, porque el síntoma contrario es una excepción de seguridad dentro de la aplicación receptora que tú nunca verás en tus registros.
El segundo fallo es asumir que existe alguien al otro lado. Desde la API 30 la visibilidad de paquetes está filtrada, y consultar si hay una actividad capaz de atender un intent devuelve resultados vacíos si no se ha declarado la consulta correspondiente en el manifiesto. Lo correcto en un envío casi nunca es preguntar: es lanzar el selector y capturar la ausencia de actividad como un caso normal.
El tercero es el tamaño. Un intent viaja por una transacción de Binder con un búfer compartido de aproximadamente un megabyte para todo el proceso. Meter una imagen codificada en un extra funciona en el emulador y produce TransactionTooLargeException en cuanto el usuario comparte algo real. Por grande que parezca la tentación, el contenido pesado siempre se comparte por referencia.
El tipo es el filtro
El tipo MIME decide quién aparece en la lista: si mientes, el receptor falla; si generalizas, la lista es inútil.
Nunca una ruta
Un adjunto viaja como referencia de contenido con concesión temporal, jamás como ruta del sistema de ficheros.
Personas, no aplicaciones
Los destinos de compartición convierten la fila superior del selector en conversaciones concretas.
Un megabyte para todos
El búfer de transacción es compartido por el proceso entero: el contenido pesado nunca cabe en un extra.
Merece la pena detenerse en lo que ocurre exactamente en el instante en que el usuario toca un icono del selector. Tu aplicación entrega una referencia a un contenido propio, junto con un permiso de lectura, a un proceso del que no sabe absolutamente nada: ni quién lo escribió, ni qué hará con el contenido, ni si lo copiará, lo subirá a un servidor o lo dejará en un directorio legible por terceros. No hay forma técnica de averiguarlo y, lo que es más importante, no debe haberla, porque el modelo entero está construido sobre la premisa de que quien decide es el usuario y no el desarrollador. Esto tiene tres consecuencias que casi nunca se explicitan. La primera es que el permiso concedido es un límite superior y no una política: reducirlo a lo mínimo imprescindible, solo lectura y solo sobre el elemento concreto, no es una precaución sino la única parte del acuerdo sobre la que conservas control. La segunda es que compartir es irreversible en la práctica; revocar la concesión más tarde impide futuras lecturas pero no deshace ninguna copia ya hecha, de modo que la decisión de qué se puede compartir pertenece al diseño del producto y no al código de la pantalla. Y la tercera, la más incómoda, es que el contenido compartido deja de estar sujeto a tus garantías: si tu aplicación cifra en reposo, borra al cerrar sesión o respeta un plazo de retención, nada de eso viaja con el fichero. Un documento confidencial que sale por el selector sale desnudo. Por eso las aplicaciones serias no tratan compartir como un botón que se pone en la barra por costumbre, sino como una salida controlada del perímetro: deciden explícitamente qué contenidos son compartibles, degradan lo que se entrega cuando tiene sentido hacerlo, marcan de forma visible al usuario que está cruzando una frontera, y aceptan que a partir de ese punto la responsabilidad ya no es técnica sino humana. El share sheet es, en ese sentido, el lugar donde la arquitectura de seguridad de una aplicación se encuentra con su límite real, que es la voluntad de la persona que la usa.
- Comparte texto sin envolver el intent en el selector, elige una aplicación por defecto y observa cómo el segundo envío ya no pregunta.
- Adjunta un fichero pasando una ruta del sistema de ficheros y provoca la excepción de exposición; anota el mensaje exacto.
- Comparte tres elementos de tipos distintos declarando el tipo del primero y comprueba qué receptores desaparecen de la lista.
- Retira la bandera de concesión de lectura y localiza el fallo desde el lado receptor con
adb logcat, no desde el tuyo. - Mete una imagen codificada en un extra, comparte una foto real de la cámara y reproduce el fallo de transacción demasiado grande.