Android proporciona un potente framework basado en el portapapeles para las funciones de copiar y pegar. Admite tipos de datos simples y complejos, como cadenas de texto, estructuras de datos complejas, datos de transmisión binarios y de texto, y recursos de aplicaciones. Los datos de texto simples se almacenan directamente en el portapapeles, mientras que los datos complejos se almacenan como una referencia que la aplicación de pegado resuelve con un proveedor de contenido. Las acciones de copiar y pegar funcionan tanto dentro de una aplicación como entre aplicaciones que implementan el framework.
Dado que una parte del framework usa proveedores de contenido, este documento supone cierta familiaridad con la API de Android Content Provider, que se describe en Proveedores de contenido.
Los usuarios esperan alguna clase de comentario cuando copian contenido al portapapeles, por lo que, además del framework que permite las acciones de copiar y pegar, Android muestra una IU predeterminada a los usuarios cuando copian contenido en Android 13 (nivel de API 33) y versiones posteriores. Debido a esta función, existe el riesgo de que se dupliquen las notificaciones. Puedes obtener más información sobre este caso límite en la sección Evita las notificaciones duplicadas.
Proporcionar comentarios a los usuarios de forma manual cuando copian contenido en Android 12L (nivel de API 32) y versiones anteriores Consulta las recomendaciones para esto en este documento.
Framework del portapapeles
Cuando uses el framework del portapapeles, coloca los datos en un objeto de clip y, luego, coloca el objeto de clip en el portapapeles de todo el sistema. El objeto de clip puede tomar una de tres formas:
- Texto
- Es una string de texto. Coloca la cadena directamente en el objeto ClipData, que luego colocarás en el portapapeles. Para pegar la cadena, obtén el objeto de clip del portapapeles y copia la cadena en el almacenamiento de tu aplicación.
- URI
-
Un objeto
Urique representa cualquier forma de URI. Esto se usa principalmente para copiar datos complejos de un proveedor de contenido. Para copiar datos, coloca un objetoUrien un objeto de clip y coloca el objeto de clip en el portapapeles. Para pegar los datos, obtén el objeto de clip, obtén el objetoUri, resuélvelo en una fuente de datos, como un proveedor de contenido, y copia los datos de la fuente en el almacenamiento de tu aplicación. - Intent
-
Es un
Intent. Esto admite la copia de accesos directos de la aplicación. Para copiar datos, crea unIntent, colócalo en un objeto de clip y coloca el objeto de clip en el portapapeles. Para pegar los datos, obtén el objeto de clip y, luego, copia el objetoIntenten el área de memoria de tu aplicación.
El portapapeles retiene solo un objeto de clip a la vez. Cuando una aplicación coloca un objeto de clip en el portapapeles, el objeto de clip anterior desaparece.
Si quieres permitir que los usuarios peguen datos en tu aplicación, no es necesario que manejes todos los tipos de datos. Puedes examinar los datos en el portapapeles antes de darles a los usuarios la opción de pegarlos. Además de tener un formato de datos determinado, el objeto Clip también contiene metadatos que te indican qué tipos de MIME están disponibles. Estos metadatos te ayudan a decidir si tu aplicación puede hacer algo útil con los datos del portapapeles. Por ejemplo, si tienes una aplicación que controla principalmente texto, es posible que quieras ignorar los objetos de clip que contienen un URI o un intent.
También puedes permitir que los usuarios peguen texto, independientemente de la forma de los datos en el portapapeles. Para ello, fuerza los datos del portapapeles a una representación de texto y, luego, pega este texto. Esto se describe en la sección Cómo forzar el portapapeles a texto.
Clases del portapapeles
En esta sección, se describen las clases que usa el framework del portapapeles.
ClipboardManager
El portapapeles del sistema Android se representa con la clase global ClipboardManager.
No crees una instancia de esta clase directamente. En su lugar, invoca getSystemService(CLIPBOARD_SERVICE) para obtener una referencia a él.
ClipData, ClipData.Item y ClipDescription
Para agregar datos al portapapeles, crea un objeto ClipData que contenga una descripción de los datos y los datos en sí. El portapapeles contiene un ClipData a la vez. Un ClipData contiene un objeto ClipDescription y uno o más objetos ClipData.Item.
Un objeto ClipDescription contiene metadatos sobre el clip. En particular, contiene un array de tipos de MIME disponibles para los datos del clip. Además, en Android 12 (nivel de API 31) y versiones posteriores, los metadatos incluyen información sobre si el objeto contiene texto estilizado y sobre el tipo de texto en el objeto.
Cuando colocas un clip en el portapapeles, esta información está disponible para las aplicaciones que pegan, que pueden examinar si pueden controlar los datos del clip.
Un objeto ClipData.Item contiene los datos de texto, URI o intent:
- Texto
-
A
CharSequence. - URI
-
A
Uri. Por lo general, contiene un URI de proveedor de contenido, aunque se permite cualquier URI. La aplicación que proporciona los datos coloca el URI en el portapapeles. Las aplicaciones que quieren pegar los datos obtienen el URI del portapapeles y lo usan para acceder al proveedor de contenido o a otra fuente de datos, y recuperar los datos. - Intent
-
Es un
Intent. Este tipo de datos te permite copiar un acceso directo de la aplicación en el portapapeles. Los usuarios pueden pegar el acceso directo en sus aplicaciones para usarlo más tarde.
Puedes agregar más de un objeto ClipData.Item a un clip. Esto permite que los usuarios copien y peguen varias selecciones como un solo clip. Por ejemplo, si tienes un widget de lista que permite al usuario seleccionar más de un elemento a la vez, puedes copiar todos los elementos al portapapeles a la vez. Para ello, crea un ClipData.Item independiente para cada elemento de la lista y, luego, agrega los objetos ClipData.Item al objeto ClipData.
Métodos de conveniencia de ClipData
La clase ClipData proporciona métodos estáticos de conveniencia para crear un objeto ClipData con un solo objeto ClipData.Item y un objeto ClipDescription simple:
-
newPlainText(label, text) - Devuelve un objeto
ClipDatacuyo único objetoClipData.Itemcontiene una cadena de texto. La etiqueta del objetoClipDescriptionse establece enlabel. El único tipo de MIME enClipDescriptionesMIMETYPE_TEXT_PLAIN.Usa
newPlainText()para crear un clip a partir de una cadena de texto. -
newUri(resolver, label, URI) - Devuelve un objeto
ClipDatacuyo único objetoClipData.Itemcontiene un URI. La etiqueta del objetoClipDescriptionse establece enlabel. Si el URI es un URI de contenido, es decir, siUri.getScheme()devuelvecontent:, el método usa el objetoContentResolverproporcionado enresolverpara recuperar los tipos de MIME disponibles del proveedor de contenido. Luego, los almacena enClipDescription. Para un URI que no sea un URIcontent:, el método establece el tipo de MIME enMIMETYPE_TEXT_URILIST.Usa
newUri()para crear un clip a partir de un URI, en particular, un URI decontent:. -
newIntent(label, intent) - Devuelve un objeto
ClipDatacuyo único objetoClipData.Itemcontiene unIntent. La etiqueta del objetoClipDescriptionse establece enlabel. El tipo de MIME se establece enMIMETYPE_TEXT_INTENT.Usa
newIntent()para crear un clip a partir de un objetoIntent.
Cómo convertir los datos del portapapeles en texto
Incluso si tu aplicación solo controla texto, puedes copiar datos que no sean de texto del portapapeles convirtiéndolos con el método ClipData.Item.coerceToText().
Este método convierte los datos de ClipData.Item en texto y devuelve un
CharSequence. El valor que devuelve ClipData.Item.coerceToText() se basa
en la forma de los datos en ClipData.Item:
- Texto
-
Si
ClipData.Itemes texto, es decir, sigetText()no es nulo, coerceToText() devuelve el texto. - URI
-
Si
ClipData.Itemes un URI, es decir, sigetUri()no es nulo,coerceToText()intenta usarlo como un URI de contenido.- Si el URI es un URI de contenido y el proveedor puede devolver un flujo de texto,
coerceToText()devuelve un flujo de texto. - Si el URI es un URI de contenido, pero el proveedor no ofrece un flujo de texto,
coerceToText()devuelve una representación del URI. La representación es la misma que la que muestraUri.toString(). - Si el URI no es un URI de contenido,
coerceToText()devuelve una representación del URI. La representación es la misma que muestraUri.toString().
- Si el URI es un URI de contenido y el proveedor puede devolver un flujo de texto,
- Intent
- Si
ClipData.Itemes unIntent, es decir, sigetIntent()no es nulo,coerceToText()lo convierte en un URI de intent y lo devuelve. La representación es la misma que la que muestraIntent.toUri(URI_INTENT_SCHEME).
El framework del portapapeles se resume en la figura 2. Para copiar datos, una aplicación coloca un objeto ClipData en el portapapeles global ClipboardManager. ClipData contiene uno o más objetos ClipData.Item y un objeto ClipDescription. Para pegar datos, una aplicación obtiene el objeto ClipData,
su tipo de MIME de ClipDescription y los datos de
ClipData.Item o del proveedor de contenido al que hace referencia
ClipData.Item.
Copiar en el portapapeles
Para copiar datos en el portapapeles, obtén un controlador para el objeto ClipboardManager global, crea un objeto ClipData y agrégale un objeto ClipDescription y uno o más objetos ClipData.Item. Luego, agrega el objeto ClipData terminado al objeto ClipboardManager. Esto se describe con más detalle en el siguiente procedimiento:
- Si copias datos con un URI de contenido, configura un proveedor de contenido.
- Obtén el portapapeles del sistema:
Kotlin
when(menuItem.itemId) { ... R.id.menu_copy -> { // if the user selects copy // Gets a handle to the clipboard service. val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } }
Java
... // If the user selects copy. case R.id.menu_copy: // Gets a handle to the clipboard service. ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
-
Copia los datos a un nuevo objeto
ClipData:-
Para texto
Kotlin
// Creates a new text clip to put on the clipboard. val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")
Java
// Creates a new text clip to put on the clipboard. ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
-
Para un URI
Este fragmento construye un URI codificando un ID de registro en el URI de contenido para el proveedor. Esta técnica se explica con más detalle en la sección Cómo codificar un identificador en el URI.
Kotlin
// Creates a Uri using a base Uri and a record ID based on the contact's last // name. Declares the base URI string. const val CONTACTS = "content://com.example.contacts" // Declares a path string for URIs, used to copy data. const val COPY_PATH = "/copy" // Declares the Uri to paste to the clipboard. val copyUri: Uri = Uri.parse("$CONTACTS$COPY_PATH/$lastName") ... // Creates a new URI clip object. The system uses the anonymous // getContentResolver() object to get MIME types from provider. The clip object's // label is "URI", and its data is the Uri previously created. val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri)
Java
// Creates a Uri using a base Uri and a record ID based on the contact's last // name. Declares the base URI string. private static final String CONTACTS = "content://com.example.contacts"; // Declares a path string for URIs, used to copy data. private static final String COPY_PATH = "/copy"; // Declares the Uri to paste to the clipboard. Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName); ... // Creates a new URI clip object. The system uses the anonymous // getContentResolver() object to get MIME types from provider. The clip object's // label is "URI", and its data is the Uri previously created. ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
-
Para un intent
En este fragmento, se construye un
Intentpara una aplicación y, luego, se coloca en el objeto de clip:Kotlin
// Creates the Intent. val appIntent = Intent(this, com.example.demo.myapplication::class.java) ... // Creates a clip object with the Intent in it. Its label is "Intent" // and its data is the Intent object created previously. val clip: ClipData = ClipData.newIntent("Intent", appIntent)
Java
// Creates the Intent. Intent appIntent = new Intent(this, com.example.demo.myapplication.class); ... // Creates a clip object with the Intent in it. Its label is "Intent" // and its data is the Intent object created previously. ClipData clip = ClipData.newIntent("Intent", appIntent);
-
Para texto
-
Coloca el nuevo objeto de clip en el portapapeles:
Kotlin
// Set the clipboard's primary clip. clipboard.setPrimaryClip(clip)
Java
// Set the clipboard's primary clip. clipboard.setPrimaryClip(clip);
Cómo hacer comentarios cuando se copia contenido al portapapeles
Los usuarios esperan alguna clase de comentario visual cuando una app copia contenido al portapapeles. Esta acción se realiza automáticamente para los usuarios de Android 13 y versiones posteriores, pero debe implementarse de forma manual en las versiones anteriores.
A partir de Android 13, el sistema muestra una confirmación visual estándar cuando se agrega contenido al portapapeles. La nueva confirmación hace lo siguiente:
- Confirma que el contenido se copió de forma correcta.
- Proporciona una vista previa del contenido copiado.
En Android 12L (nivel de API 32) y versiones anteriores, es posible que los usuarios no sepan con seguridad si copiaron contenido de forma correcta o el contenido que copiaron. Esta función estandariza las varias notificaciones que muestran las apps después de copiar y les ofrece a los usuarios más control sobre el portapapeles.
Cómo evitar notificaciones duplicadas
En Android 12L (nivel de API 32) y versiones anteriores, te recomendamos que alertes a los usuarios cuando copien contenido correctamente con comentarios visuales en la app, a través de un widget como un Toast o una Snackbar, después de la acción.
Para evitar que se muestre información duplicada, te recomendamos que quites los mensajes de aviso o las barras de notificaciones que se muestran después de una copia en la app para Android 13 y versiones posteriores.
A continuación, se muestra un ejemplo para hacerlo:
fun textCopyThenPost(textCopied:String) { val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager // When setting the clipboard text. clipboardManager.setPrimaryClip(ClipData.newPlainText ("", textCopied)) // Only show a toast for Android 12 and lower. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) Toast.makeText(context, “Copied”, Toast.LENGTH_SHORT).show() }
Cómo agregar contenido sensible al portapapeles
Si tu app permite que los usuarios copien contenido sensible, como contraseñas o información de tarjetas de crédito, en el portapapeles, debes agregar una marca a ClipDescription en ClipData antes de llamar a ClipboardManager.setPrimaryClip(). Si agregas esta marca, evitarás que aparezca contenido sensible en la confirmación visual del contenido copiado en Android 13 y versiones posteriores.
Para marcar contenido sensible, agrega un valor booleano adicional a ClipDescription. Todas las apps deben hacerlo, independientemente del nivel de API objetivo.
// If your app is compiled with the API level 33 SDK or higher. clipData.apply { description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } } // If your app is compiled with a lower SDK. clipData.apply { description.extras = PersistableBundle().apply { putBoolean("android.content.extra.IS_SENSITIVE", true) } }
Pegar desde el portapapeles
Como se describió anteriormente, puedes pegar datos desde el portapapeles. Para ello, obtén el objeto del portapapeles global y el objeto de clip, mira sus datos y, si es posible, copia los datos del objeto de clip en tu propio almacenamiento. En esta sección, se explica en detalle cómo pegar los tres formatos de datos del portapapeles.
Pegar texto sin formato
Para pegar texto sin formato, obtén el portapapeles global y verifica que pueda devolver texto sin formato. Luego, obtén el objeto de clip y copia su texto en tu propio almacenamiento con getText(), como se describe en el siguiente procedimiento:
- Obtén el objeto
ClipboardManagerglobal congetSystemService(CLIPBOARD_SERVICE). Además, declara una variable global para que contenga el texto pegado:Kotlin
var clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager var pasteData: String = ""
Java
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); String pasteData = "";
- Determina si necesitas habilitar o inhabilitar la opción "pegar" en la actividad actual. Verifica que el portapapeles contenga un clip y que puedas controlar el tipo de datos que representa:
Kotlin
// Gets the ID of the "paste" menu item. val pasteItem: MenuItem = menu.findItem(R.id.menu_paste) // If the clipboard doesn't contain data, disable the paste menu item. // If it does contain data, decide whether you can handle the data. pasteItem.isEnabled = when { !clipboard.hasPrimaryClip() -> { false } !(clipboard.primaryClipDescription.hasMimeType(MIMETYPE_TEXT_PLAIN)) -> { // Disables the paste menu item, since the clipboard has data but it // isn't plain text. false } else -> { // Enables the paste menu item, since the clipboard contains plain text. true } }
Java
// Gets the ID of the "paste" menu item. MenuItem pasteItem = menu.findItem(R.id.menu_paste); // If the clipboard doesn't contain data, disable the paste menu item. // If it does contain data, decide whether you can handle the data. if (!(clipboard.hasPrimaryClip())) { pasteItem.setEnabled(false); } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) { // Disables the paste menu item, since the clipboard has data but // it isn't plain text. pasteItem.setEnabled(false); } else { // Enables the paste menu item, since the clipboard contains plain text. pasteItem.setEnabled(true); }
- Copia los datos del portapapeles. Solo se puede acceder a este punto del código si el elemento de menú "Pegar" está habilitado, por lo que puedes suponer que el portapapeles contiene texto sin formato. Aún no sabes si contiene una cadena de texto o un URI que apunta a texto sin formato.
En el siguiente fragmento de código, se prueba esto, pero solo se muestra el código para controlar el texto sin formato:
Kotlin
when (menuItem.itemId) { ... R.id.menu_paste -> { // Responds to the user selecting "paste". // Examines the item on the clipboard. If getText() doesn't return null, // the clip item contains the text. Assumes that this application can only // handle one item at a time. val item = clipboard.primaryClip.getItemAt(0) // Gets the clipboard as text. pasteData = item.text return if (pasteData != null) { // If the string contains data, then the paste operation is done. true } else { // The clipboard doesn't contain text. If it contains a URI, // attempts to get data from it. val pasteUri: Uri? = item.uri if (pasteUri != null) { // If the URI contains something, try to get text from it. // Calls a routine to resolve the URI and get data from it. // This routine isn't presented here. pasteData = resolveUri(pasteUri) true } else { // Something is wrong. The MIME type was plain text, but the // clipboard doesn't contain text or a Uri. Report an error. Log.e(TAG,"Clipboard contains an invalid data type") false } } } }
Java
// Responds to the user selecting "paste". case R.id.menu_paste: // Examines the item on the clipboard. If getText() does not return null, // the clip item contains the text. Assumes that this application can only // handle one item at a time. ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); // Gets the clipboard as text. pasteData = item.getText(); // If the string contains data, then the paste operation is done. if (pasteData != null) { return true; // The clipboard doesn't contain text. If it contains a URI, attempts to get // data from it. } else { Uri pasteUri = item.getUri(); // If the URI contains something, try to get text from it. if (pasteUri != null) { // Calls a routine to resolve the URI and get data from it. // This routine isn't presented here. pasteData = resolveUri(Uri); return true; } else { // Something is wrong. The MIME type is plain text, but the // clipboard doesn't contain text or a Uri. Report an error. Log.e(TAG, "Clipboard contains an invalid data type"); return false; } }
Cómo pegar datos desde un URI de contenido
Si el objeto ClipData.Item contiene un URI de contenido y determinas que puedes controlar uno de sus tipos de MIME, crea un ContentResolver y llama al método de proveedor de contenido adecuado para recuperar los datos.
En el siguiente procedimiento, se describe cómo obtener datos de un proveedor de contenido según un URI de contenido en el portapapeles. Verifica si el proveedor tiene disponible un tipo de MIME que la aplicación pueda usar.
- Declara una variable global para que incluya un tipo de MIME:
Kotlin
// Declares a MIME type constant to match against the MIME types offered // by the provider. const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"
Java
// Declares a MIME type constant to match against the MIME types offered by // the provider. public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
- Obtén el portapapeles global. Además, obtén un agente de resolución de contenido para poder acceder al proveedor de contenido:
Kotlin
// Gets a handle to the Clipboard Manager. val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager // Gets a content resolver instance. val cr = contentResolver
Java
// Gets a handle to the Clipboard Manager. ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); // Gets a content resolver instance. ContentResolver cr = getContentResolver();
- Obtén el clip principal del portapapeles y su contenido como un URI:
Kotlin
// Gets the clipboard data from the clipboard. val clip: ClipData? = clipboard.primaryClip clip?.run { // Gets the first item from the clipboard data. val item: ClipData.Item = getItemAt(0) // Tries to get the item's contents as a URI. val pasteUri: Uri? = item.uri
Java
// Gets the clipboard data from the clipboard. ClipData clip = clipboard.getPrimaryClip(); if (clip != null) { // Gets the first item from the clipboard data. ClipData.Item item = clip.getItemAt(0); // Tries to get the item's contents as a URI. Uri pasteUri = item.getUri();
- Llama a
getType(Uri)para probar si el URI es un URI de contenido. Este método devuelve un valor nulo siUrino apunta a un proveedor de contenido válido.Kotlin
// If the clipboard contains a URI reference... pasteUri?.let { // ...is this a content URI? val uriMimeType: String? = cr.getType(it)
Java
// If the clipboard contains a URI reference... if (pasteUri != null) { // ...is this a content URI? String uriMimeType = cr.getType(pasteUri);
- Prueba si el proveedor de contenido admite un tipo de MIME que la aplicación comprenda. Si
es así, llama a
ContentResolver.query()para obtener los datos. El valor que se muestra es unCursor.Kotlin
// If the return value isn't null, the Uri is a content Uri. uriMimeType?.takeIf { // Does the content provider offer a MIME type that the current // application can use? it == MIME_TYPE_CONTACT }?.apply { // Get the data from the content provider. cr.query(pasteUri, null, null, null, null)?.use { pasteCursor -> // If the Cursor contains data, move to the first record. if (pasteCursor.moveToFirst()) { // Get the data from the Cursor here. // The code varies according to the format of the data model. } // Kotlin `use` automatically closes the Cursor. } } } }
Java
// If the return value isn't null, the Uri is a content Uri. if (uriMimeType != null) { // Does the content provider offer a MIME type that the current // application can use? if (uriMimeType.equals(MIME_TYPE_CONTACT)) { // Get the data from the content provider. Cursor pasteCursor = cr.query(uri, null, null, null, null); // If the Cursor contains data, move to the first record. if (pasteCursor != null) { if (pasteCursor.moveToFirst()) { // Get the data from the Cursor here. // The code varies according to the format of the data model. } } // Close the Cursor. pasteCursor.close(); } } } }
Cómo pegar un objeto Intent
Para pegar un intent, primero obtén el portapapeles global. Examina el objeto ClipData.Item para ver si contiene un Intent. Luego, llama a getIntent() para copiar el intent en tu propio almacenamiento. Esto se demuestra en el siguiente fragmento:
Kotlin
// Gets a handle to the Clipboard Manager. val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager // Checks whether the clip item contains an Intent by testing whether // getIntent() returns null. val pasteIntent: Intent? = clipboard.primaryClip?.getItemAt(0)?.intent if (pasteIntent != null) { // Handle the Intent. } else { // Ignore the clipboard, or issue an error if // you expect an Intent to be on the clipboard. }
Java
// Gets a handle to the Clipboard Manager. ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); // Checks whether the clip item contains an Intent, by testing whether // getIntent() returns null. Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent(); if (pasteIntent != null) { // Handle the Intent. } else { // Ignore the clipboard, or issue an error if // you expect an Intent to be on the clipboard. }
La notificación del sistema se muestra cuando la app accede a los datos del portapapeles
En Android 12 (nivel de API 31) y versiones posteriores, por lo general, el sistema muestra un mensaje de aviso cuando la app llama a getPrimaryClip().
El texto dentro de este mensaje contiene el siguiente formato:
APP pasted from your clipboard
El sistema no muestra un mensaje de aviso cuando tu app realiza una de las siguientes acciones:
- Accede a
ClipDatadesde tu propia app. - Accede de forma reiterada a
ClipDatadesde una app específica. El mensaje de aviso solo aparece cuando tu app accede a los datos de esa app por primera vez. - Cuando recupera los metadatos para el objeto de clip, por ejemplo, llamando a
getPrimaryClipDescription()en lugar degetPrimaryClip().
Cómo usar proveedores de contenido para copiar datos complejos
Los proveedores de contenido admiten la copia de datos complejos como registros de bases de datos o transmisiones de archivos. Para copiar los datos, coloca un URI de contenido en el portapapeles. Luego, las aplicaciones que pegan obtienen este URI del portapapeles y lo usan para recuperar datos de la base de datos o descriptores de transmisión de archivos.
Dado que la aplicación de pegado solo tiene el URI de contenido para tus datos, necesita saber qué dato recuperar. Puedes proporcionar esta información codificando un identificador para los datos en el URI, o bien puedes proporcionar un URI único que devuelva los datos que deseas copiar. La técnica que elijas dependerá de la organización de tus datos.
En las siguientes secciones, se describe cómo configurar URIs, proporcionar datos complejos y proporcionar flujos de archivos. En las descripciones, se supone que conoces los principios generales del diseño de proveedores de contenido.
Codifica un identificador en el URI
Una técnica útil para copiar datos en el portapapeles con un URI es codificar un identificador para los datos en el URI. Luego, tu proveedor de contenido puede obtener el identificador del URI y usarlo para recuperar los datos. La aplicación que pega no tiene que saber que existe identificador. Solo debe obtener tu "referencia" (el URI más el identificador) del portapapeles, proporcionarla a tu proveedor de contenido y recuperar los datos.
Por lo general, para codificar un identificador en un URI de contenido, se lo concatena al final del URI. Por ejemplo, supón que defines el URI de tu proveedor como la siguiente string:
"content://com.example.contacts"
Si quieres codificar un nombre en este URI, usa el siguiente fragmento de código:
Kotlin
val uriString = "content://com.example.contacts/Smith" // uriString now contains content://com.example.contacts/Smith. // Generates a uri object from the string representation. val copyUri = Uri.parse(uriString)
Java
String uriString = "content://com.example.contacts" + "/" + "Smith"; // uriString now contains content://com.example.contacts/Smith. // Generates a uri object from the string representation. Uri copyUri = Uri.parse(uriString);
Si ya usas un proveedor de contenido, te recomendamos que agregues una nueva ruta de acceso de URI que indique que el URI es para copiar. Por ejemplo, supón que ya tienes las siguientes rutas de URI:
"content://com.example.contacts/people" "content://com.example.contacts/people/detail" "content://com.example.contacts/people/images"
Puedes agregar otra ruta de acceso para copiar los URI:
"content://com.example.contacts/copying"
Luego, puedes detectar un URI de "copia" mediante la coincidencia de patrones y controlarlo con código específico para copiar y pegar.
Por lo general, se usa la técnica de codificación si ya usas un proveedor de contenido, una base de datos interna o una tabla interna para organizar tus datos. En estos casos, tienes varios datos que quieres copiar y, probablemente, un identificador único para cada uno. En respuesta a una consulta de la aplicación que pega, puedes buscar los datos por su identificador y devolverlos.
Si no tienes muchos datos, es probable que no necesites codificar un identificador. Puedes usar un URI que sea único para tu proveedor. En respuesta a una consulta, tu proveedor devuelve los datos que contiene actualmente.
Cómo copiar estructuras de datos
Configura un proveedor de contenido para copiar y pegar datos complejos como una subclase del componente ContentProvider. Codifica el URI que colocaste en el portapapeles para que apunte al registro exacto que deseas
proporcionar. Además, considera el estado actual de tu aplicación:
- Si ya tienes un proveedor de contenido, puedes agregarlo a su funcionalidad. Es posible que solo
necesites modificar su método
query()para controlar los URI que provienen de aplicaciones que desean pegar datos. Es probable que quieras modificar el método para controlar un patrón de URI de "copia". - Si tu aplicación mantiene una base de datos interna, te recomendamos que la muevas a un proveedor de contenido para facilitar la copia desde allí.
- Si no usas una base de datos, puedes implementar un proveedor de contenido simple cuyo único propósito sea ofrecer datos a las aplicaciones que pegan contenido desde el portapapeles.
En el proveedor de contenido, anula al menos los siguientes métodos:
-
query() - Las aplicaciones que pegan suponen que pueden obtener tus datos con este método y el URI que colocaste en el portapapeles. Para admitir la copia, haz que este método detecte los URI que contienen una ruta de acceso especial de "copia". Luego, tu aplicación puede crear un URI de "copia" para colocarlo en el portapapeles, que contenga la ruta de acceso de la copia y un puntero al registro exacto que deseas copiar.
-
getType() - Este método debe devolver los tipos de MIME de los datos que quieres copiar. El método
newUri()llama agetType()para colocar los tipos de MIME en el nuevo objetoClipData.Los tipos de MIME para datos complejos se describen en Proveedores de contenido.
No necesitas tener ninguno de los otros métodos de proveedor de contenido, como insert() o update().
Una aplicación de pegado solo necesita obtener los tipos de MIME compatibles y copiar los datos de tu proveedor.
Si ya tienes esos métodos, no interferirán con las operaciones de copiado.
En los siguientes fragmentos, se muestra cómo configurar tu aplicación para copiar datos complejos:
-
En las constantes globales de tu aplicación, declara una cadena de URI base y una ruta de acceso que identifique las cadenas de URI que usas para copiar datos. También declara un tipo de MIME para los datos copiados.
Kotlin
// Declares the base URI string. private const val CONTACTS = "content://com.example.contacts" // Declares a path string for URIs that you use to copy data. private const val COPY_PATH = "/copy" // Declares a MIME type for the copied data. const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"
Java
// Declares the base URI string. private static final String CONTACTS = "content://com.example.contacts"; // Declares a path string for URIs that you use to copy data. private static final String COPY_PATH = "/copy"; // Declares a MIME type for the copied data. public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
- En la actividad desde la que los usuarios copian datos, configura el código para copiar datos en el portapapeles.
En respuesta a una solicitud de copia, coloca el URI en el portapapeles.
Kotlin
class MyCopyActivity : Activity() { ... when(item.itemId) { R.id.menu_copy -> { // The user has selected a name and is requesting a copy. // Appends the last name to the base URI. // The name is stored in "lastName". uriString = "$CONTACTS$COPY_PATH/$lastName" // Parses the string into a URI. val copyUri: Uri? = Uri.parse(uriString) // Gets a handle to the clipboard service. val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri) // Sets the clipboard's primary clip. clipboard.setPrimaryClip(clip) } }
Java
public class MyCopyActivity extends Activity { ... // The user has selected a name and is requesting a copy. case R.id.menu_copy: // Appends the last name to the base URI. // The name is stored in "lastName". uriString = CONTACTS + COPY_PATH + "/" + lastName; // Parses the string into a URI. Uri copyUri = Uri.parse(uriString); // Gets a handle to the clipboard service. ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri); // Sets the clipboard's primary clip. clipboard.setPrimaryClip(clip);
-
En el alcance global de tu proveedor de contenido, crea un comparador de URIs y agrega un patrón de URI que coincida con los URIs que colocas en el portapapeles.
Kotlin
// A Uri Match object that simplifies matching content URIs to patterns. private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { // Adds a matcher for the content URI. It matches. // "content://com.example.contacts/copy/*" addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT) } // An integer to use in switching based on the incoming URI pattern. private const val GET_SINGLE_CONTACT = 0 ... class MyCopyProvider : ContentProvider() { ... }
Java
public class MyCopyProvider extends ContentProvider { ... // A Uri Match object that simplifies matching content URIs to patterns. private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); // An integer to use in switching based on the incoming URI pattern. private static final int GET_SINGLE_CONTACT = 0; ... // Adds a matcher for the content URI. It matches // "content://com.example.contacts/copy/*" sUriMatcher.addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT);
-
Configura el método
query(). Este método puede controlar diferentes patrones de URI, según cómo lo codifiques, pero solo se muestra el patrón para la operación de copia del portapapeles.Kotlin
// Sets up your provider's query() method. override fun query( uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String? ): Cursor? { ... // When based on the incoming content URI: when(sUriMatcher.match(uri)) { GET_SINGLE_CONTACT -> { // Queries and returns the contact for the requested name. Decodes // the incoming URI, queries the data model based on the last name, // and returns the result as a Cursor. } } ... }
Java
// Sets up your provider's query() method. public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { ... // Switch based on the incoming content URI. switch (sUriMatcher.match(uri)) { case GET_SINGLE_CONTACT: // Queries and returns the contact for the requested name. Decodes the // incoming URI, queries the data model based on the last name, and // returns the result as a Cursor. ... }
-
Configura el método
getType()para que devuelva un tipo de MIME apropiado para los datos copiados:Kotlin
// Sets up your provider's getType() method. override fun getType(uri: Uri): String? { ... return when(sUriMatcher.match(uri)) { GET_SINGLE_CONTACT -> MIME_TYPE_CONTACT ... } }
Java
// Sets up your provider's getType() method. public String getType(Uri uri) { ... switch (sUriMatcher.match(uri)) { case GET_SINGLE_CONTACT: return (MIME_TYPE_CONTACT); ... } }
En la sección Cómo pegar datos desde un URI de contenido, se describe cómo obtener un URI de contenido del portapapeles y usarlo para obtener y pegar datos.
Cómo copiar flujos de datos
Puedes copiar y pegar grandes cantidades de datos binarios y de texto como transmisiones. Los datos pueden tener formas como las siguientes:
- Archivos almacenados en el dispositivo real
- Transmisiones desde sockets
- Grandes cantidades de datos almacenados en el sistema de base de datos subyacente de un proveedor
Un proveedor de contenido para flujos de datos proporciona acceso a sus datos con un objeto descriptor de archivo, como AssetFileDescriptor, en lugar de un objeto Cursor. La aplicación que pega lee el flujo de datos con este descriptor de archivo.
Para configurar tu aplicación de modo que copie un flujo de datos con un proveedor, sigue estos pasos:
-
Configura un URI de contenido para la transmisión de datos que quieras colocar en el portapapeles. Para ello, existen las siguientes opciones:
- Codifica un identificador para el flujo de datos en el URI, como se describe en la sección Codifica un identificador en el URI, y, luego, mantén una tabla en tu proveedor que contenga identificadores y el nombre del flujo correspondiente.
- Codifica el nombre de la transmisión directamente en el URI.
- Usa un URI único que siempre muestre la transmisión actual del proveedor. Si usas esta opción, recuerda actualizar tu proveedor para que apunte a una transmisión diferente cada vez que copies la transmisión en el portapapeles con el URI.
- Proporciona un tipo de MIME para cada tipo de transmisión de datos que planees ofrecer. Las aplicaciones que pegan necesitan esta información para determinar si pueden pegar los datos en el portapapeles.
- Implementa uno de los métodos
ContentProviderque devuelven un descriptor de archivo para un flujo. Si codificas identificadores en el URI de contenido, usa este método para determinar qué flujo abrir. - Para copiar el flujo de datos en el portapapeles, construye el URI de contenido y colócalo en el portapapeles.
Para pegar un flujo de datos, una aplicación obtiene el clip del portapapeles, obtiene el URI y lo usa en una llamada a un método de descriptor de archivos ContentResolver que abre el flujo. El método ContentResolver llama al método ContentProvider correspondiente y le pasa el URI de contenido. Tu proveedor devuelve el descriptor de archivo al método ContentResolver. Luego, la aplicación que pega tiene la responsabilidad de leer los datos del flujo.
En la siguiente lista, se muestran los métodos más importantes del descriptor de archivos para un proveedor de contenido. Cada uno de estos tiene un método ContentResolver correspondiente con la cadena "Descriptor" agregada al nombre del método. Por ejemplo, el análogo de ContentResolver de openAssetFile() es openAssetFileDescriptor().
-
openTypedAssetFile() -
Este método devuelve un descriptor de archivo de recurso, pero solo si el proveedor admite el tipo de MIME proporcionado. El emisor (la aplicación que realiza el pegado) proporciona un patrón de tipo de MIME. El proveedor de contenido de la aplicación que copia un URI en el portapapeles devuelve un controlador de archivos
AssetFileDescriptorsi puede proporcionar ese tipo de MIME y arroja una excepción si no puede hacerlo.Este método maneja las subsecciones de los archivos. Puedes usarlo para leer los recursos que el proveedor de contenido copió en el portapapeles.
-
openAssetFile() -
Este método es una forma más general de
openTypedAssetFile(). No filtra los tipos de MIME permitidos, pero puede leer subsecciones de archivos. -
openFile() -
Esta es una forma más general de
openAssetFile(). No puede leer subsecciones de archivos.
De manera opcional, puedes usar el método
openPipeHelper()
con el método de descriptor de archivo. Esto permite que la aplicación que pega lea los datos de transmisión en un subproceso en segundo plano con una canalización. Para usar este método, implementa la interfaz ContentProvider.PipeDataWriter.
Diseña una funcionalidad eficaz de copiar y pegar
Para diseñar una funcionalidad eficaz de copiar y pegar para tu aplicación, ten en cuenta estos puntos:
- En todo momento, solo hay un clip en el portapapeles. Una nueva operación de copia de cualquier aplicación del sistema reemplaza el clip anterior. Dado que el usuario puede salir de tu aplicación y copiar antes de volver, no puedes suponer que el portapapeles contiene el clip que el usuario copió anteriormente en tu aplicación.
-
El propósito de tener varios objetos
ClipData.Itempor clip es admitir la copia y el pegado de varias selecciones, en lugar de diferentes formas de referencia a una sola selección. Por lo general, querrás que todos los objetosClipData.Itemde un clip tengan la misma forma. Es decir, todos deben ser texto simple, URI de contenido oIntent, y no deben estar mezclados. -
Cuando proporcionas datos, puedes ofrecer diferentes representaciones de MIME. Agrega los tipos de MIME
que admites al
ClipDescriptiony, luego, impleméntalos en tu proveedor de contenido. -
Cuando obtienes datos del portapapeles, tu aplicación es responsable de verificar los tipos de MIME disponibles y, luego, decidir cuál usar, si los hubiera. Incluso si hay un clip en el portapapeles y el usuario solicita pegar, no es necesario que tu aplicación realice la acción. Pega el contenido si el tipo de MIME es compatible. Puedes convertir los datos del portapapeles en texto con
coerceToText(). Si tu aplicación admite más de uno de los tipos de MIME disponibles, puedes permitir que el usuario elija cuál usar.