Copiar y pegar

Android proporciona un potente framework basado en el portapapeles para las funciones de copiar y pegar. Es compatible con tipos de datos simples y complejos, entre los que se incluyen strings de texto, estructuras de datos complejas, datos de transmisión de texto y binarios, e incluso elementos de aplicaciones. Los datos de texto simples se almacenan directamente en el portapapeles, mientras que los complejos se guardan como una referencia que la aplicación de pegado resuelve con un proveedor de contenido. La función de copiar y pegar puede utilizarse tanto en una sola aplicación como entre aplicaciones que implementan un framework.

Dado que una parte del framework usa proveedores de contenido, este tema supone cierta familiaridad con la API de Android Content Provider, que se describe en el tema 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. Cuando se copia contenido en Android 12L (nivel de API 32) y versiones anteriores, deberás seguir proporcionando comentarios a los usuarios de forma manual. Consulta nuestras recomendaciones para hacerlo más adelante en esta guía.

Framework del portapapeles

Cuando usas el framework del portapapeles, colocas los datos en un objeto de clip y, luego, colocas ese 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 string directamente en el objeto de clip, que luego colocas en el portapapeles. Para pegar la string, obtén el objeto de clip del portapapeles y copia la string en el almacenamiento de la aplicación.
URI
Es un objeto Uri que representa cualquier formato de URI. Eso se hace principalmente para copiar datos complejos de un proveedor de contenido. Para copiar datos, coloca un objeto Uri en un objeto de clip, y coloca este último en el portapapeles. Para pegar los datos, obtén el objeto de clip y el Uri, resuélvelo en una fuente de datos (como un proveedor de contenido) y copia los datos de la fuente en el almacenamiento de la aplicación.
Intent
Es un Intent. Eso permite copiar accesos directos a aplicaciones. Para copiar datos, crea un intent, colócalo en un objeto de clip y coloca este último en el portapapeles. Para pegar los datos, obtén el objeto de clip y, luego, copia el objeto intent en el área de memoria de la 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 controles 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 determinado formato de datos, el objeto de clip también contiene metadatos que te indican qué tipos de MIME se encuentran disponibles. Esos metadatos te ayudan a decidir si tu aplicación puede realizar alguna acción ú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 contengan un URI o un Intent.

Es posible que también quieras permitir que los usuarios peguen texto independientemente del formato de datos en el portapapeles. Para ello, puedes forzar los datos del portapapeles en una representación de texto y, luego, pegar ese texto. Eso se describe en la sección Cómo convertir el portapapeles en texto.

Clases del portapapeles

En esta sección, se describen las clases que usa el framework del portapapeles.

ClipboardManager

En el sistema Android, la clase ClipboardManager global representa el portapapeles del sistema. No crees una instancia para esa clase de forma directa. En su lugar, obtén una referencia mediante la invocación de getSystemService(CLIPBOARD_SERVICE).

ClipData, ClipData.Item y ClipDescription

Para agregar datos al portapapeles, crea un objeto ClipData que contenga los datos en sí mismos y una descripción de ellos. El portapapeles retiene solo 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 arreglo de los 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 con estilo y sobre el tipo de texto en el objeto. Cuando colocas un clip en el portapapeles, esta información está disponible para aplicaciones de pegado, que pueden examinarla a fin de ver si puede controlar los datos de clips.

Un objeto ClipData.Item contiene los datos de texto, URI o Intent:

Texto
A CharSequence.
URI
A Uri. Por lo general, eso 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. Aquellas aplicaciones que quieran pegar 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 a las aplicaciones 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. Eso permite que los usuarios copien y peguen varias selecciones como un solo clip. Por ejemplo, si tienes un widget de lista que permite que el usuario seleccione más de un elemento a la vez, puedes copiar todos los elementos en el portapapeles en una sola acción. Para ello, crea un ClipData.Item para cada elemento de la lista y, luego, agrega los objetos ClipData.Item al objeto ClipData.

Métodos de conveniencia ClipData

La clase ClipData proporciona métodos de conveniencia estáticos para crear un objeto ClipData con un único objeto ClipData.Item y un objeto ClipDescription simple:

newPlainText(label, text)
Muestra un objeto ClipData cuyo único objeto ClipData.Item contiene una string de texto. La etiqueta del objeto ClipDescription está configurada como label. El único tipo de MIME en ClipDescription es MIMETYPE_TEXT_PLAIN.

Usa newPlainText() para crear un clip a partir de una string de texto.

newUri(resolver, label, URI)
Muestra un objeto ClipData cuyo único objeto ClipData.Item contiene un URI. La etiqueta del objeto ClipDescription está configurada como label. Si se trata de un URI de contenido (Uri.getScheme() muestra content:), el método usa el objeto ContentResolver proporcionado en resolver para recuperar los tipos de MIME disponibles del proveedor de contenido y almacenarlos en ClipDescription. Para un URI que no es content:, el método configura el tipo de MIME como MIMETYPE_TEXT_URILIST.

Usa newUri() para crear un clip a partir de un URI, en especial un URI content:.

newIntent(label, intent)
Muestra un objeto ClipData cuyo único objeto ClipData.Item contiene un Intent. La etiqueta del objeto ClipDescription está configurada como label. El tipo de MIME está configurado como MIMETYPE_TEXT_INTENT.

Usa newIntent() para crear un clip a partir de un objeto intent.

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. Para ello, debes convertirlos con el método ClipData.Item.coerceToText().

Este método convierte los datos de ClipData.Item en texto y muestra un CharSequence. El valor que muestra ClipData.Item.coerceToText() se basa en el formato de datos de ClipData.Item:

Texto
Si ClipData.Item es texto (getText() no es nulo), coerceToText() muestra el texto.
URI
Si ClipData.Item es un URI (getUri() no es nulo), coerceToText() intenta usarlo como un URI de contenido:
  • Si se trata de un URI de contenido y el proveedor puede mostrar una transmisión de texto, coerceToText() muestra una transmisión de texto.
  • Si se trata de un URI de contenido, pero el proveedor no ofrece una transmisión de texto, coerceToText() muestra una representación del URI. La representación es igual que la que muestra Uri.toString().
  • Si el URI no es de contenido, coerceToText() muestra una representación del URI. La representación es igual que la que muestra Uri.toString().
Intent
Si ClipData.Item es un intent (getIntent() no es nulo), coerceToText() lo convierte en un URI de intent y lo muestra. La representación es igual que la que muestra Intent.toUri(URI_INTENT_SCHEME).

El framework del portapapeles se resume en la Figura 1. 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 ClipData, su tipo de MIME de ClipDescription y los datos del ClipData.Item o del proveedor de contenido al que hace referencia ClipData.Item.

Un diagrama de bloques del framework para copiar y pegar

Figura 1: Marco de trabajo del portapapeles de Android

Cómo copiar en el portapapeles

Como se describió anteriormente, para copiar datos en el portapapeles, obtén un controlador para el objeto ClipboardManager global, crea un objeto ClipData, agrega un ClipDescription y uno o más objetos ClipData.Item, y agrega el objeto ClipData terminado al objeto ClipboardManager. Eso se describe en detalle en el siguiente procedimiento:

  1. Si quieres copiar datos mediante un URI de contenido, configura un proveedor de contenido.

    La aplicación de muestra NotePad es un ejemplo del uso de un proveedor de contenido para copiar y pegar. La clase NotePadProvider implementa el proveedor de contenido. La clase NotePad define un contrato entre el proveedor y otras aplicaciones, incluidos los tipos de MIME compatibles.

  2. 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);
    
  3. 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 crea un URI mediante la codificación de un ID de registro en el URI de contenido del proveedor. Esta técnica se trata con más detalle en la sección Cómo codificar un identificador en el URI:

      Kotlin

      // Creates a Uri based on 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 that you use 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 based on 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 that you use 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

      Este fragmento crea un intent para una aplicación y, luego, lo 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);
      
  4. 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, los usuarios suelen no saber con seguridad cuándo 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 su portapapeles.

Widget de copiar y pegar
En Android 13 y versiones posteriores, se muestra la IU cuando el contenido entra al portapapeles.

Cómo evitar notificaciones duplicadas

En Android 12L (nivel de API 32) y versiones anteriores, sugerimos alertar a los usuarios que copiaron correctamente contenido con un widget visual emergente en la app (como avisos o barras de notificaciones) después de la acción.

A fin de evitar que se muestre información duplicada, te recomendamos que quites los widgets emergentes que se muestran después de una copia en la app para Android 13 y versiones posteriores.

Se muestra una barra de notificaciones después de una copia en la app.
Si muestras una barra de notificaciones de confirmación de copia en Android 13, el usuario verá mensajes duplicados.
Se muestra un aviso después de una copia en la app.
Si muestras una notificación de confirmación de copia en Android 13, el usuario verá mensajes duplicados.

A continuación, se muestra un ejemplo para hacerlo:

fun textCopyThenPost(textCopied:String) {
    val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
    // When setting the clip board 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 de ClipData antes de llamar a ClipboardManager#setPrimaryClip(). Si agregas esta marca, evitarás que aparezca contenido sensible en cualquier programa que permita una vista previa del contenido en la confirmación visual de copia en Android 13 y versiones posteriores.

Vista previa del texto copiado sin marcas en el contenido sensible
Vista previa del texto copiado sin marcas en el contenido sensible.
Vista previa del texto copiado con marcas en el contenido sensible.
Vista previa del texto copiado con marcas en el contenido sensible.

Para marcar contenido sensible, agrega un valor booleano adicional a ClipDescription. Todas las apps deben hacerlo, independientemente del nivel de API objetivo.

// When your app targets API level 33 or higher
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app targets a lower API level
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}

Cómo pegar contenido 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.

Cómo pegar texto sin formato

Para pegar texto sin formato, primero obtén el portapapeles global y verifica que pueda mostrar ese tipo de texto. Luego, obtén el objeto de clip y copia su texto en tu propio almacenamiento mediante getText(), como se describe a continuación:

  1. Obtén el objeto ClipboardManager global mediante getSystemService(CLIPBOARD_SERVICE). Además, declara una variable global para que incluya el texto que se pegó:

    Kotlin

    var clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    var pasteData: String = ""
    

    Java

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
    
  2. A continuación, determina si debes habilitar o inhabilitar la opción "pegar" en el objeto Activity actual. Debes verificar que el portapapeles contenga un clip y que puedas procesar el tipo de datos que representa el clip:

    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 if you can handle the data.
    pasteItem.isEnabled = when {
        !clipboard.hasPrimaryClip() -> {
            false
        }
        !(clipboard.primaryClipDescription.hasMimeType(MIMETYPE_TEXT_PLAIN)) -> {
            // This disables the paste menu item, since the clipboard has data but it is not plain text
            false
        }
        else -> {
            // This 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 if you can handle the data.
    if (!(clipboard.hasPrimaryClip())) {
    
        pasteItem.setEnabled(false);
    
    } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    
        // This disables the paste menu item, since the clipboard has data but it is not plain text
        pasteItem.setEnabled(false);
    } else {
    
        // This enables the paste menu item, since the clipboard contains plain text.
        pasteItem.setEnabled(true);
    }
    
  3. Copia los datos del portapapeles. Solo se puede acceder a este punto del programa si está habilitado el elemento de menú "pegar", por lo que puedes suponer que el portapapeles contiene texto sin formato. Aún no sabes si contiene una string de texto o un URI que apunta al texto sin formato. El siguiente fragmento lo prueba, pero solo muestra el código para procesar 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() does not 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 does not 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 is not
                    // presented here.
                    pasteData = resolveUri(pasteUri)
                    true
                } else {
    
                    // Something is wrong. The MIME type was plain text, but the clipboard does not
                    // contain either 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 does not 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 is not
            // presented here.
            pasteData = resolveUri(Uri);
            return true;
        } else {
    
            // Something is wrong. The MIME type was plain text, but the clipboard does not contain either
            // 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 determinaste que puedes procesar uno de sus tipos de MIME, crea un ContentResolver y, luego, llama al método del proveedor de contenido adecuado para recuperar los datos.

En el siguiente procedimiento, se describe cómo obtener datos de un proveedor de contenido en función de un URI de contenido en el portapapeles. Comprueba que el proveedor tenga disponible un tipo de MIME que la aplicación pueda usar:

  1. 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";
    
  2. 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();
    
  3. 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();
    
  4. Realiza una prueba para ver si el URI es de contenido llamando a getType(Uri). Este método muestra un valor nulo si Uri no 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);
    
  5. Realiza una prueba para ver si el proveedor de contenido admite un tipo de MIME que entienda la aplicación actual. Si lo hace, llama a ContentResolver.query() para obtener los datos. El valor que se muestra es un Cursor:

    Kotlin

            // If the return value is not 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 will vary according to the
                        // format of the data model.
                    }
    
                    // Kotlin `use` will automatically close the Cursor
                }
            }
        }
    }
    

    Java

            // If the return value is not 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 will vary according to the
                        // format of the data model.
                        }
                    }
    
                    // close the Cursor
                    pasteCursor.close();
                 }
             }
         }
    }
    

Cómo pegar un objeto Intent

Para pegar un Intent, primero debes obtener el portapapeles global. Examina el objeto ClipData.Item para ver si incluye un Intent. Luego, llama a getIntent() para copiar el objeto 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 to see if the clip item contains an Intent, by testing to see if 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 your application was expecting an Intent to be
    // on the clipboard
}

Java

// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);

// Checks to see if the clip item contains an Intent, by testing to see if 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 your application was expecting 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 la app realiza una de las siguientes acciones:

  • Cuando accede a ClipData desde tu propia app.
  • Cuando accede reiteradamente a ClipData desde una app específica. El aviso solo aparece cuando la app accede a los datos desde esa app por primera vez.
  • Cuando recupera los metadatos para el objeto de clip, por ejemplo, mediante una llamada a getPrimaryClipDescription(), en lugar de getPrimaryClip().

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 de pegado obtendrán ese URI del portapapeles y lo usarán para recuperar los datos de la base de datos o los descriptores de la transmisión de archivos.

Como la aplicación de pegado solo tiene el URI de contenido de tus datos, necesita saber qué datos se deben recuperar. Puedes proporcionar esta información mediante la codificación de un identificador de los datos del URI, o bien puedes proporcionar un URI único que muestre los datos que quieras copiar. La técnica que elijas dependerá de la organización de tus datos.

En las siguientes secciones, se describe cómo configurar los URI, además de cómo proporcionar datos complejos y transmisiones de archivos. Las descripciones suponen que estás familiarizado con los principios generales de diseño del proveedor de contenido.

Cómo codificar un identificador en el URI

Una técnica útil de copiado de datos en el portapapeles con un URI es codificar un identificador para los datos en el URI mismo. Tu proveedor de contenido puede obtener el identificador del URI y usarlo para recuperar los datos. No es necesario que la aplicación de pegado sepa que existe el identificador, ya que todo lo que tiene que hacer es obtener tu "referencia" (el URI más el identificador) del portapapeles, dársela al 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 deseas codificar un nombre en este URI, usarías el siguiente fragmento:

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, es posible que quieras agregar una nueva ruta 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

Podrías agregar otra que sea específica para copiar URI:

"content://com.example.contacts/copying"

Luego, podrías detectar un URI de "copiado" mediante la coincidencia de patrones y procesarlo con un código específico para copiar y pegar.

Por lo general, utilizas 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 esos casos, tienes varios datos que quieres copiar y, posiblemente, un identificador único para cada dato. En respuesta a una búsqueda de la aplicación de pegado, puedes buscar los datos por su identificador y mostrarlos.

Si no tienes muchos datos, es probable que no necesites codificar un identificador. Simplemente puedes usar un URI que sea exclusivo de tu proveedor. En respuesta a una búsqueda, tu proveedor mostrará los datos que contiene en la actualidad.

En la aplicación de muestra NotePad, se usa un único registro por ID para abrir una nota de la lista de notas. La muestra usa el campo _id de una base de datos SQL, pero puedes tener el identificador numérico o de caracteres que quieras.

Cómo copiar estructuras de datos

Configura un proveedor de contenido para copiar y pegar datos complejos como una subclase del componente ContentProvider. También debes codificar el URI que colocas en el portapapeles para que apunte al registro exacto que quieras proporcionar. Además, debes tener en cuenta el estado actual de tu aplicación:

  • Si ya tienes un proveedor de contenido, puedes agregarlo a su funcionalidad. Quizás solo debas modificar su método query() para procesar los URI que provengan de aplicaciones que quieran pegar datos. Es probable que quieras modificar el método para manejar un patrón de URI "copiar".
  • Si tu aplicación mantiene una base de datos interna, es posible que quieras moverla a un proveedor de contenido para facilitar el copiado desde allí.
  • Si actualmente 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, tendrás que anular al menos los siguientes métodos:

query()
Las aplicaciones de pegado supondrán que pueden obtener tus datos mediante este método con el URI que coloques en el portapapeles. Para admitir el copiado, debes hacer que ese método detecte los URI que incluyen una ruta especial de "copiado". Tu aplicación puede crear un URI de "copia" para colocar en el portapapeles, que incluya la ruta correspondiente y un elemento que apunte al registro exacto que quieras copiar.
getType()
Este método debería mostrar los tipos de MIME de los datos que quieras copiar. El método newUri() llama a getType() para colocar los tipos de MIME en el nuevo objeto ClipData.

Los tipos de MIME de datos complejos se describen en el tema Proveedores de contenido.

Ten en cuenta que no es necesario que tengas ninguno de los otros métodos de proveedor de contenido, como insert() o update(). Una aplicación de pegado solo necesita obtener tus tipos de MIME compatibles y copiar datos del proveedor. Si ya tienes esos métodos, no interferirán con las operaciones de copiado.

En los siguientes fragmentos, se demuestra cómo debes configurar tu aplicación para copiar datos complejos:

  1. En las constantes globales de tu aplicación, declara una string de URI básica y una ruta que identifique las strings 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";
    
  2. En la actividad desde la cual 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)
    
            // Set 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);
    
        // Set the clipboard's primary clip.
        clipboard.setPrimaryClip(clip);
    
  3. En el alcance global de tu proveedor de contenido, crea un buscador de coincidencias de URI y agrega un patrón de URI que coincida con los URI que colocaste 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);
    
  4. 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 correspondiente a la operación de copiado 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 -> {
    
                // query and return the contact for the requested name. Here you would decode
                // the incoming URI, query the data model based on the last name, and return 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:
    
            // query and return the contact for the requested name. Here you would decode
            // the incoming URI, query the data model based on the last name, and return the result
            // as a Cursor.
    
        ...
    
    }
    
  5. Configura el método getType() a fin de mostrar un tipo de MIME adecuado 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 transmisiones de datos

Puedes copiar y pegar grandes cantidades de datos binarios y de texto como transmisiones. Los datos pueden tener formatos como los 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 transmisiones de datos proporciona acceso a sus datos con un objeto descriptor de archivos como AssetFileDescriptor en lugar de un objeto Cursor. La aplicación de pegado lee la transmisión de datos mediante ese descriptor de archivos.

Para configurar tu aplicación a fin de copiar una transmisión de datos con un proveedor, sigue estos pasos:

  1. 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 Cómo codificar un identificador en el URI y, luego, mantén una tabla en tu proveedor que incluya identificadores y el nombre de transmisión 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 el proveedor para que apunte a una transmisión diferente cada vez que copies la transmisión en el portapapeles a través del URI.
  2. Proporciona un tipo de MIME para cada tipo de transmisión de datos que planees ofrecer. Las aplicaciones de pegado necesitan esta información para determinar si pueden pegar los datos en el portapapeles.
  3. Implementa uno de los métodos ContentProvider que muestra un descriptor de archivos para una transmisión. Si codificas identificadores en el URI de contenido, usa este método para determinar qué transmisión abrir.
  4. Para copiar la transmisión de datos en el portapapeles, crea el URI de contenido y colócalo en el portapapeles.

Para pegar una transmisión de datos, una aplicación obtiene el clip del portapapeles, obtiene el URI y lo utiliza en una llamada a un método del descriptor de archivos ContentResolver que abre la transmisión. El método ContentResolver llama al método ContentProvider correspondiente y le pasa el URI de contenido. Tu proveedor muestra el descriptor de archivos al método ContentResolver. La aplicación de pegado tiene la responsabilidad de leer los datos de la transmisión.

En la siguiente lista, se muestran los métodos más importantes del descriptor de archivos para un proveedor de contenido. Cada uno de ellos tiene un método ContentResolver correspondiente con la string "Descriptor" adjunta al nombre del método; por ejemplo, el ContentResolver análogo de openAssetFile() es openAssetFileDescriptor():

openTypedAssetFile()
Este método debería mostrar un descriptor de archivos de elementos, pero solo si el proveedor admite el tipo de MIME proporcionado. El emisor (la aplicación que realiza la acción de pegado) proporciona un patrón de tipo de MIME. El proveedor de contenido (de la aplicación que copió un URI en el portapapeles) muestra un controlador de archivos AssetFileDescriptor si puede proporcionar ese tipo de MIME, o muestra una excepción si no puede hacerlo.

Este método maneja las subsecciones de los archivos. Puedes usarlo para leer los activos que el proveedor de contenido copió en el portapapeles.

openAssetFile()
Este método representa un formato más general de openTypedAssetFile(). No filtra para obtener los tipos de MIME permitidos, pero puede leer las subsecciones de los archivos.
openFile()
Es un formato más general de openAssetFile(). No puede leer las subsecciones de los archivos.

De manera opcional, usa el método openPipeHelper() con tu método del descriptor de archivos. Esto permite que la aplicación de pegado lea los datos de la transmisión en un subproceso en segundo plano mediante el uso de un canal. Para usar este método, debes implementar la interfaz ContentProvider.PipeDataWriter. Se ofrece un ejemplo de esto en la aplicación de muestra NotePad, en el método openTypedAssetFile() de NotePadProvider.java.

Cómo diseñar una funcionalidad efectiva de copiar y pegar

Si quieres diseñar una funcionalidad efectiva de copiar y pegar para tu aplicación, recuerda estos puntos:

  • En todo momento, solo hay un clip en el portapapeles. Una nueva operación de copia por parte de cualquier aplicación del sistema reemplaza el clip anterior. Como el usuario puede salir de tu aplicación y hacer una copia antes de regresar, no puedes suponer que el portapapeles contiene el clip que el usuario copió anteriormente en tu aplicación.
  • El propósito previsto de varios objetos ClipData.Item por clip es permitir que se copien y peguen varias selecciones en vez de diferentes formatos de referencia para una sola selección. Por lo general, quieres que todos los objetos ClipData.Item de un clip tengan el mismo formato; es decir, todos deben tener texto simple, URI de contenido o Intent, pero no una combinación de ellos.
  • Cuando proporcionas datos, puedes ofrecer diferentes representaciones de MIME. Agrega los tipos de MIME que admitas a ClipDescription y, luego, implementa los tipos de MIME 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. Aunque haya un clip en el portapapeles y el usuario solicite la acción de pegado, no es necesario que tu aplicación la ejecute. Tú deberías realizar la acción de pegado si el tipo de MIME es compatible. Puedes elegir convertir los datos del portapapeles en texto mediante el uso de coerceToText(). Si tu aplicación admite más de uno de los tipos de MIME disponibles, puedes permitirle al usuario que elija cuál usar.