Скопируйте и вставьте

Попробуйте способ создания
Jetpack Compose — рекомендуемый набор инструментов пользовательского интерфейса для Android. Узнайте, как использовать копирование и вставку в Compose.

Android предоставляет мощную платформу для копирования и вставки на основе буфера обмена. Он поддерживает простые и сложные типы данных, включая текстовые строки, сложные структуры данных, текстовые и двоичные потоковые данные, а также ресурсы приложений. Простые текстовые данные хранятся непосредственно в буфере обмена, а сложные данные хранятся в виде ссылки, которую приложение для вставки разрешает поставщику контента. Копирование и вставка работают как внутри приложения, так и между приложениями, реализующими эту платформу.

Поскольку часть платформы использует поставщиков контента, этот документ предполагает некоторое знакомство с API-интерфейсом поставщика контента Android, который описан в разделе Поставщики контента .

Пользователи ожидают обратной связи при копировании контента в буфер обмена, поэтому в дополнение к платформе, обеспечивающей копирование и вставку, Android показывает пользователям пользовательский интерфейс по умолчанию при копировании в Android 13 (уровень API 33) и более поздних версиях. Из-за этой функции существует риск дублирования уведомлений. Подробнее об этом крайнем случае можно узнать в разделе « Избегайте дублирования уведомлений» .

Анимация, показывающая уведомление буфера обмена Android 13.
Рис. 1. Пользовательский интерфейс, отображаемый при попадании содержимого в буфер обмена в Android 13 и более поздних версиях.

Вручную предоставляйте пользователям обратную связь при копировании в Android 12L (уровень API 32) и более ранних версиях. См. рекомендации по этому поводу в этом документе.

Структура буфера обмена

При использовании платформы буфера обмена поместите данные в объект клипа, а затем поместите объект клипа в общесистемный буфер обмена. Объект клипа может принимать одну из трех форм:

Текст
Текстовая строка. Поместите строку непосредственно в объект клипа, который затем поместите в буфер обмена. Чтобы вставить строку, извлеките объект клипа из буфера обмена и скопируйте строку в хранилище вашего приложения.
URI
Объект Uri , представляющий любую форму URI. Это в первую очередь предназначено для копирования сложных данных от поставщика контента. Чтобы скопировать данные, поместите объект Uri в объект клипа и поместите объект клипа в буфер обмена. Чтобы вставить данные, получите объект клипа, получите объект Uri , разрешите его источнику данных, например поставщику контента, и скопируйте данные из источника в хранилище вашего приложения.
Намерение
Intent . Это поддерживает копирование ярлыков приложений. Чтобы скопировать данные, создайте Intent , поместите его в объект клипа и поместите объект клипа в буфер обмена. Чтобы вставить данные, получите объект клипа, а затем скопируйте объект Intent в область памяти вашего приложения.

Буфер обмена одновременно содержит только один объект клипа. Когда приложение помещает объект клипа в буфер обмена, предыдущий объект клипа исчезает.

Если вы хотите, чтобы пользователи могли вставлять данные в ваше приложение, вам не обязательно обрабатывать все типы данных. Вы можете просмотреть данные в буфере обмена, прежде чем предоставить пользователям возможность вставить их. Помимо определенной формы данных, объект клипа также содержит метаданные, которые сообщают вам, какие типы MIME доступны. Эти метаданные помогут вам решить, может ли ваше приложение сделать что-нибудь полезное с данными буфера обмена. Например, если у вас есть приложение, которое в основном обрабатывает текст, вы можете игнорировать объекты клипов, содержащие URI или намерение.

Вы также можете разрешить пользователям вставлять текст независимо от формы данных в буфере обмена. Для этого принудительно преобразуйте данные буфера обмена в текстовое представление, а затем вставьте этот текст. Это описано в разделе «Приведение буфера обмена к тексту» .

Классы буфера обмена

В этом разделе описываются классы, используемые платформой буфера обмена.

Менеджер буфера обмена

Буфер обмена системы Android представлен глобальным классом ClipboardManager . Не создавайте экземпляр этого класса напрямую. Вместо этого получите ссылку на него, вызвав getSystemService(CLIPBOARD_SERVICE) .

ClipData, ClipData.Item и ClipDescription

Чтобы добавить данные в буфер обмена, создайте объект ClipData , содержащий описание данных и сами данные. Буфер обмена одновременно хранит по одному ClipData . ClipData содержит объект ClipDescription и один или несколько объектов ClipData.Item .

Объект ClipDescription содержит метаданные о клипе. В частности, он содержит массив доступных типов MIME для данных клипа. Кроме того, в Android 12 (уровень API 31) и выше метаданные включают информацию о том, содержит ли объект стилизованный текст , и о типе текста в объекте . Когда вы помещаете клип в буфер обмена, эта информация становится доступной для вставляющих приложений, которые могут проверить, могут ли они обработать данные клипа.

Объект ClipData.Item содержит текст, URI или данные намерения:

Текст
CharSequence .
URI
Uri . Обычно он содержит URI поставщика контента, хотя допускается любой URI. Приложение, предоставляющее данные, помещает URI в буфер обмена. Приложения, которые хотят вставить данные, получают URI из буфера обмена и используют его для доступа к поставщику контента или другому источнику данных и получения данных.
Намерение
Intent . Этот тип данных позволяет скопировать ярлык приложения в буфер обмена. Затем пользователи могут вставить ярлык в свои приложения для дальнейшего использования.

В клип можно добавить несколько объектов ClipData.Item . Это позволяет пользователям копировать и вставлять несколько фрагментов как один клип. Например, если у вас есть виджет списка, который позволяет пользователю выбирать более одного элемента одновременно, вы можете скопировать все элементы в буфер обмена одновременно. Для этого создайте отдельный ClipData.Item для каждого элемента списка, а затем добавьте объекты ClipData.Item к объекту ClipData .

Удобные методы ClipData

Класс ClipData предоставляет статические удобные методы для создания объекта ClipData с одним объектом ClipData.Item и простым объектом ClipDescription :

newPlainText(label, text)
Возвращает объект ClipData , единственный объект которого ClipData.Item содержит текстовую строку. Для метки объекта ClipDescription установлено значение label . Единственный тип MIME в ClipDescriptionMIMETYPE_TEXT_PLAIN .

Используйте newPlainText() , чтобы создать клип из текстовой строки.

newUri(resolver, label, URI)
Возвращает объект ClipData , единственный объект которого ClipData.Item содержит URI. Для метки объекта ClipDescription установлено значение label . Если URI является URI контента, то есть если Uri.getScheme() возвращает content: — метод использует объект ContentResolver , предоставленный в resolver для получения доступных типов MIME от поставщика контента. Затем он сохраняет их в ClipDescription . Для URI, который не является URI content: метод устанавливает тип MIME в MIMETYPE_TEXT_URILIST .

Используйте newUri() для создания клипа из URI, в частности, из URI content: .

newIntent(label, intent)
Возвращает объект ClipData , единственный объект которого ClipData.Item содержит Intent . Для метки объекта ClipDescription установлено значение label . Тип MIME установлен на MIMETYPE_TEXT_INTENT .

Используйте newIntent() , чтобы создать клип из объекта Intent .

Преобразование данных буфера обмена в текст

Даже если ваше приложение обрабатывает только текст, вы можете скопировать нетекстовые данные из буфера обмена, преобразовав их с помощью метода ClipData.Item.coerceToText() .

Этот метод преобразует данные в ClipData.Item в текст и возвращает CharSequence . Значение, возвращаемое ClipData.Item.coerceToText() основано на форме данных в ClipData.Item :

Текст
Если ClipData.Item является текстовым (т. е. если getText() не имеет значения null), coerceToText() возвращает текст.
URI
Если ClipData.Item является URI, то есть если getUri() не имеет значения null, coerceToText() пытается использовать его в качестве URI контента.
  • Если URI является URI контента и поставщик может вернуть текстовый поток, coerceToText() возвращает текстовый поток.
  • Если URI является URI контента, но поставщик не предлагает текстовый поток, coerceToText() возвращает представление URI. Представление такое же, как и возвращаемое Uri.toString() .
  • Если URI не является URI контента, coerceToText() возвращает представление URI. Представление такое же, как и возвращаемое Uri.toString() .
Намерение
Если ClipData.Item является Intent , то есть если getIntent() не имеет значения null, coerceToText() преобразует его в URI намерения и возвращает его. Представление такое же, как и возвращаемое Intent.toUri(URI_INTENT_SCHEME) .

Схема буфера обмена представлена ​​на рисунке 2. Чтобы скопировать данные, приложение помещает объект ClipData в глобальный буфер обмена ClipboardManager . ClipData содержит один или несколько объектов ClipData.Item и один объект ClipDescription . Чтобы вставить данные, приложение получает ClipData , получает свой MIME-тип из ClipDescription и получает данные из ClipData.Item или от поставщика контента, на который ссылается ClipData.Item .

Изображение, показывающее блок-схему платформы копирования и вставки.
Рисунок 2. Структура буфера обмена Android.

Скопировать в буфер обмена

Чтобы скопировать данные в буфер обмена, получите дескриптор глобального объекта ClipboardManager , создайте объект ClipData и добавьте к нему ClipDescription и один или несколько объектов ClipData.Item . Затем добавьте готовый объект ClipData в объект ClipboardManager . Это описано далее в следующей процедуре:

  1. Если вы копируете данные с использованием URI контента, настройте поставщика контента.
  2. Получите системный буфер обмена:

    Котлин

    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
        }
    }

    Ява

    ...
    // 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. Скопируйте данные в новый объект ClipData :

    • Для текста

      Котлин

      // Creates a new text clip to put on the clipboard.
      val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")

      Ява

      // Creates a new text clip to put on the clipboard.
      ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
    • Для URI

      Этот фрагмент кода создает URI, кодируя идентификатор записи в URI контента для поставщика. Этот метод более подробно описан в разделе «Кодирование идентификатора в URI» .

      Котлин

      // 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)

      Ява

      // 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);
    • Для намерения

      Этот фрагмент создает Intent для приложения, а затем помещает его в объект клипа:

      Котлин

      // 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)

      Ява

      // 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. Поместите новый объект клипа в буфер обмена:

    Котлин

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip)

    Ява

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip);

Оставляйте отзывы при копировании в буфер обмена.

Пользователи ожидают визуальной обратной связи, когда приложение копирует контент в буфер обмена. Это делается автоматически для пользователей Android 13 и более поздних версий, но в предыдущих версиях это необходимо реализовать вручную.

Начиная с Android 13, система отображает стандартное визуальное подтверждение при добавлении контента в буфер обмена. Новое подтверждение выполняет следующие действия:

  • Подтверждает, что содержимое было успешно скопировано.
  • Обеспечивает предварительный просмотр скопированного содержимого.

Анимация, показывающая уведомление буфера обмена Android 13.
Рис. 3. Пользовательский интерфейс, отображаемый при попадании содержимого в буфер обмена в Android 13 и более поздних версиях.

В Android 12L (уровень API 32) и ниже пользователи могут быть не уверены, успешно ли они скопировали контент или то, что они скопировали. Эта функция стандартизирует различные уведомления, отображаемые приложениями после копирования, и предлагает пользователям больше контроля над буфером обмена.

Избегайте дублирования уведомлений

В Android 12L (уровень API 32) и более ранних версиях мы рекомендуем предупреждать пользователей об успешном копировании, предоставляя визуальную обратную связь в приложении и используя виджет, такой как Toast или Snackbar , после копирования.

Чтобы избежать дублирования отображения информации, мы настоятельно рекомендуем удалить всплывающие уведомления или закусочные, отображаемые после копирования в приложении для Android 13 и более поздних версий.

Публикуйте закусочную после копии в приложении.
Рисунок 4. Если вы показываете панель подтверждения копирования в Android 13, пользователь видит повторяющиеся сообщения.
Опубликуйте тост после копии в приложении.
Рисунок 5. Если вы показываете всплывающее сообщение о подтверждении копирования в Android 13, пользователь видит повторяющиеся сообщения.

Вот пример того, как это реализовать:

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()
}

Добавьте конфиденциальный контент в буфер обмена

Если ваше приложение позволяет пользователям копировать конфиденциальный контент в буфер обмена, например пароли или информацию о кредитной карте, вы должны добавить флаг ClipDescription в ClipData перед вызовом ClipboardManager.setPrimaryClip() . Добавление этого флага предотвращает появление конфиденциального контента при визуальном подтверждении скопированного контента в Android 13 и более поздних версиях.

Предварительный просмотр скопированного текста без пометки конфиденциального контента.
Рисунок 6. Предварительный просмотр скопированного текста без флага конфиденциального содержимого.
Предварительный просмотр скопированного текста помечает конфиденциальный контент.
Рисунок 7. Предварительный просмотр скопированного текста с флагом конфиденциального содержимого.

Чтобы пометить конфиденциальный контент, добавьте дополнительное логическое значение к ClipDescription . Это должны делать все приложения, независимо от целевого уровня API.

// 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)
    }
}

Вставить из буфера обмена

Как описано ранее, вставьте данные из буфера обмена, получив глобальный объект буфера обмена, получив объект клипа, просмотрев его данные и, если возможно, скопировав данные из объекта клипа в собственное хранилище. В этом разделе подробно объясняется, как вставить три формы данных буфера обмена.

Вставить обычный текст

Чтобы вставить простой текст, получите глобальный буфер обмена и убедитесь, что он может возвращать простой текст. Затем получите объект клипа и скопируйте его текст в свое хранилище с помощью getText() , как описано в следующей процедуре:

  1. Получите глобальный объект ClipboardManager с помощью getSystemService(CLIPBOARD_SERVICE) . Кроме того, объявите глобальную переменную, содержащую вставленный текст:

    Котлин

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

    Ява

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
  2. Определите, нужно ли вам включить или отключить опцию «вставить» в текущем действии. Убедитесь, что буфер обмена содержит клип и что вы можете обрабатывать тип данных, представленный клипом:

    Котлин

    // 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
        }
    }

    Ява

    // 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);
    }
  3. Скопируйте данные из буфера обмена. Этот момент кода доступен только в том случае, если пункт меню «Вставить» включен, поэтому можно предположить, что буфер обмена содержит обычный текст. Вы еще не знаете, содержит ли он текстовую строку или URI, указывающий на обычный текст. Следующий фрагмент кода проверяет это, но показывает только код для обработки обычного текста:

    Котлин

    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
                }
            }
        }
    }

    Ява

    // 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;
        }
    }

Вставка данных из URI контента

Если объект ClipData.Item содержит URI контента и вы определяете, что можете обрабатывать один из его типов MIME, создайте ContentResolver и вызовите соответствующий метод поставщика контента для получения данных.

Следующая процедура описывает, как получить данные от поставщика контента на основе URI контента в буфере обмена. Он проверяет, доступен ли у поставщика тип MIME, который может использовать приложение.

  1. Объявите глобальную переменную, содержащую тип MIME:

    Котлин

    // 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"

    Ява

    // 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. Получите глобальный буфер обмена. Также приобретите преобразователь контента, чтобы иметь доступ к поставщику контента:

    Котлин

    // Gets a handle to the Clipboard Manager.
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    // Gets a content resolver instance.
    val cr = contentResolver

    Ява

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
  3. Получите основной клип из буфера обмена и получите его содержимое в виде URI:

    Котлин

    // 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

    Ява

    // 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. Проверьте, является ли URI URI контента, вызвав getType(Uri) . Этот метод возвращает значение null, если Uri не указывает на допустимого поставщика контента.

    Котлин

        // If the clipboard contains a URI reference...
        pasteUri?.let {
    
            // ...is this a content URI?
            val uriMimeType: String? = cr.getType(it)

    Ява

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
  5. Проверьте, поддерживает ли поставщик контента тип MIME, понятный приложению. Если это так, вызовите ContentResolver.query() , чтобы получить данные. Возвращаемое значение — Cursor .

    Котлин

            // 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.
                }
            }
        }
    }

    Ява

            // 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();
                 }
             }
         }
    }

Вставить намерение

Чтобы вставить намерение, сначала получите глобальный буфер обмена. Проверьте объект ClipData.Item , чтобы узнать, содержит ли он Intent . Затем вызовите getIntent() , чтобы скопировать намерение в свое хранилище. Следующий фрагмент демонстрирует это:

Котлин

// 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.
}

Ява

// 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.
}

Системное уведомление, отображаемое, когда ваше приложение обращается к данным буфера обмена

В Android 12 (уровень API 31) и выше система обычно показывает всплывающее сообщение, когда ваше приложение вызывает getPrimaryClip() . Текст внутри сообщения имеет следующий формат:

APP pasted from your clipboard

Система не отображает всплывающее сообщение, когда ваше приложение выполняет одно из следующих действий:

  • Доступ к ClipData из вашего собственного приложения.
  • Повторно обращается к ClipData из определенного приложения. Всплывающее сообщение появляется только тогда, когда ваше приложение впервые обращается к данным из этого приложения.
  • Извлекает метаданные для объекта клипа, например, вызывая getPrimaryClipDescription() вместо getPrimaryClip() .

Используйте поставщиков контента для копирования сложных данных

Поставщики контента поддерживают копирование сложных данных, таких как записи базы данных или потоки файлов. Чтобы скопировать данные, поместите URI контента в буфер обмена. Приложения для вставки затем получают этот URI из буфера обмена и используют его для получения данных базы данных или дескрипторов файловых потоков.

Поскольку приложение для вставки имеет только URI контента для ваших данных, ему необходимо знать, какую часть данных нужно получить. Вы можете предоставить эту информацию, закодировав идентификатор данных в самом URI, или вы можете предоставить уникальный URI, который возвращает данные, которые вы хотите скопировать. Какой метод вы выберете, зависит от организации ваших данных.

В следующих разделах описывается, как настроить URI, предоставить сложные данные и потоки файлов. В описаниях предполагается, что вы знакомы с общими принципами проектирования поставщиков контента.

Закодируйте идентификатор в URI

Полезным методом копирования данных в буфер обмена с помощью URI является кодирование идентификатора данных в самом URI. Затем ваш поставщик контента может получить идентификатор из URI и использовать его для получения данных. Приложению вставки не обязательно знать о существовании идентификатора. Ему просто нужно получить вашу «ссылку» — URI плюс идентификатор — из буфера обмена, передать ей поставщика контента и получить обратно данные.

Обычно вы кодируете идентификатор в URI контента, объединяя его с концом URI. Например, предположим, что вы определяете URI своего поставщика в виде следующей строки:

"content://com.example.contacts"

Если вы хотите закодировать имя в этом URI, используйте следующий фрагмент кода:

Котлин

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)

Ява

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);

Если вы уже используете поставщика контента, возможно, вам захочется добавить новый путь URI, указывающий, что URI предназначен для копирования. Например, предположим, что у вас уже есть следующие пути URI:

"content://com.example.contacts/people"
"content://com.example.contacts/people/detail"
"content://com.example.contacts/people/images"

Вы можете добавить еще один путь для копирования URI:

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

Затем вы можете обнаружить «копирующий» URI путем сопоставления с образцом и обработать его с помощью кода, предназначенного для копирования и вставки.

Обычно вы используете технику кодирования, если уже используете поставщика контента, внутреннюю базу данных или внутреннюю таблицу для организации своих данных. В этих случаях у вас есть несколько фрагментов данных, которые вы хотите скопировать, и, предположительно, уникальный идентификатор для каждого фрагмента. В ответ на запрос приложения для вставки вы можете найти данные по их идентификатору и вернуть их.

Если у вас нет нескольких фрагментов данных, вам, вероятно, не нужно кодировать идентификатор. Вы можете использовать URI, уникальный для вашего провайдера. В ответ на запрос ваш провайдер возвращает данные, которые он содержит в данный момент.

Копирование структур данных

Настройте поставщика контента для копирования и вставки сложных данных в качестве подкласса компонента ContentProvider . Закодируйте URI, который вы поместили в буфер обмена, чтобы он указывал на именно ту запись, которую вы хотите предоставить. Кроме того, рассмотрите существующее состояние вашего приложения:

  • Если у вас уже есть поставщик контента, вы можете расширить его функциональность. Возможно, вам потребуется всего лишь изменить его метод query() для обработки URI, поступающих от приложений, которые хотят вставить данные. Вероятно, вы захотите изменить метод для обработки шаблона URI «копии».
  • Если ваше приложение поддерживает внутреннюю базу данных, вы можете переместить эту базу данных в поставщика контента, чтобы облегчить копирование из нее.
  • Если вы не используете базу данных, вы можете реализовать простой поставщик контента, единственной целью которого является предоставление данных приложениям, которые вставляются из буфера обмена.

В поставщике контента переопределите как минимум следующие методы:

query()
Приложения для вставки предполагают, что они могут получить ваши данные, используя этот метод с URI, который вы поместили в буфер обмена. Для поддержки копирования этот метод должен обнаруживать URI, содержащие специальный путь «копирования». Затем ваше приложение может создать URI «копии» для помещения в буфер обмена, содержащий путь копирования и указатель на точную запись, которую вы хотите скопировать.
getType()
Этот метод должен возвращать типы MIME для данных, которые вы собираетесь скопировать. Метод newUri() вызывает getType() , чтобы поместить типы MIME в новый объект ClipData .

Типы MIME для сложных данных описаны в разделе Поставщики контента .

Вам не нужны какие-либо другие методы поставщика контента, такие как insert() или update() . Приложению для вставки необходимо только получить поддерживаемые типы MIME и скопировать данные от вашего провайдера. Если у вас уже есть эти методы, они не будут мешать операциям копирования.

Следующие фрагменты демонстрируют, как настроить приложение для копирования сложных данных:

  1. В глобальных константах вашего приложения объявите базовую строку URI и путь, идентифицирующий строки URI, которые вы используете для копирования данных. Также объявите тип MIME для скопированных данных.

    Котлин

    // 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"

    Ява

    // 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. В действии, из которого пользователи копируют данные, настройте код для копирования данных в буфер обмена. В ответ на запрос копирования поместите URI в буфер обмена.

    Котлин

    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)
        }
    }

    Ява

    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);
  3. В глобальной области вашего поставщика контента создайте средство сопоставления URI и добавьте шаблон URI, соответствующий URI, помещенным в буфер обмена.

    Котлин

    // 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() {
        ...
    }

    Ява

    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. Настройте метод query() . Этот метод может обрабатывать различные шаблоны URI, в зависимости от того, как вы его кодируете, но отображается только шаблон для операции копирования в буфер обмена.

    Котлин

    // 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.
            }
        }
        ...
    }

    Ява

    // 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.
        ...
    }
  5. Настройте метод getType() для возврата соответствующего типа MIME для скопированных данных:

    Котлин

    // Sets up your provider's getType() method.
    override fun getType(uri: Uri): String? {
        ...
        return when(sUriMatcher.match(uri)) {
            GET_SINGLE_CONTACT -> MIME_TYPE_CONTACT
            ...
        }
    }

    Ява

    // Sets up your provider's getType() method.
    public String getType(Uri uri) {
        ...
        switch (sUriMatcher.match(uri)) {
        case GET_SINGLE_CONTACT:
            return (MIME_TYPE_CONTACT);
        ...
        }
    }

В разделе «Вставка данных из URI контента» описывается, как получить URI контента из буфера обмена и использовать его для получения и вставки данных.

Копирование потоков данных

Вы можете копировать и вставлять большие объемы текстовых и двоичных данных в виде потоков. Данные могут иметь следующую форму:

  • Файлы, хранящиеся на реальном устройстве
  • Потоки из сокетов
  • Большие объемы данных хранятся в базовой системе базы данных поставщика.

Поставщик контента для потоков данных предоставляет доступ к своим данным с помощью объекта дескриптора файла, такого как AssetFileDescriptor , вместо объекта Cursor . Приложение вставки считывает поток данных, используя этот файловый дескриптор.

Чтобы настроить приложение для копирования потока данных с поставщиком, выполните следующие действия:

  1. Настройте URI контента для потока данных, который вы помещаете в буфер обмена. Варианты для этого включают следующее:
    • Закодируйте идентификатор потока данных в URI, как описано в разделе «Кодирование идентификатора в URI» , а затем создайте у своего поставщика таблицу, содержащую идентификаторы и соответствующее имя потока.
    • Закодируйте имя потока непосредственно в URI.
    • Используйте уникальный URI, который всегда возвращает текущий поток от поставщика. Если вы используете эту опцию, не забудьте обновить своего провайдера, чтобы он указывал на другой поток каждый раз, когда вы копируете поток в буфер обмена с помощью URI.
  2. Укажите тип MIME для каждого типа потока данных, который вы планируете предлагать. Приложениям вставки нужна эта информация, чтобы определить, могут ли они вставить данные в буфер обмена.
  3. Реализуйте один из методов ContentProvider , который возвращает дескриптор файла для потока. Если вы кодируете идентификаторы в URI контента, используйте этот метод, чтобы определить, какой поток открывать.
  4. Чтобы скопировать поток данных в буфер обмена, создайте URI контента и поместите его в буфер обмена.

Чтобы вставить поток данных, приложение получает клип из буфера обмена, получает URI и использует его при вызове метода дескриптора файла ContentResolver , который открывает поток. Метод ContentResolver вызывает соответствующий метод ContentProvider , передавая ему URI контента. Ваш поставщик возвращает дескриптор файла методу ContentResolver . Затем приложение вставки отвечает за чтение данных из потока.

В следующем списке показаны наиболее важные методы файловых дескрипторов для поставщика контента. Каждому из них соответствует метод ContentResolver со строкой «Descriptor», добавленной к имени метода. Например, аналогом openAssetFile() ContentResolver является openAssetFileDescriptor() .

openTypedAssetFile()

Этот метод возвращает дескриптор файла ресурса, но только если предоставленный тип MIME поддерживается поставщиком. Вызывающий объект — приложение, выполняющее вставку, — предоставляет шаблон типа MIME. Поставщик контента приложения, которое копирует URI в буфер обмена, возвращает дескриптор файла AssetFileDescriptor , если он может предоставить этот тип MIME, и выдает исключение, если не может.

Этот метод обрабатывает подразделы файлов. Вы можете использовать его для чтения ресурсов, которые поставщик контента скопировал в буфер обмена.

openAssetFile()
Этот метод является более общей формой openTypedAssetFile() . Он не фильтрует разрешенные типы MIME, но может читать подразделы файлов.
openFile()
Это более общая форма openAssetFile() . Он не может читать подразделы файлов.

При желании вы можете использовать метод openPipeHelper() с методом дескриптора файла. Это позволяет приложению вставки считывать данные потока в фоновом потоке с помощью канала. Чтобы использовать этот метод, реализуйте интерфейс ContentProvider.PipeDataWriter .

Разработайте эффективные функции копирования и вставки

Чтобы разработать эффективную функцию копирования и вставки для вашего приложения, помните следующие моменты:

  • В любой момент времени в буфере обмена находится только один клип. Новая операция копирования, выполненная любым приложением в системе, перезаписывает предыдущий клип. Поскольку пользователь может выйти из вашего приложения и скопировать его, прежде чем вернуться, вы не можете предполагать, что буфер обмена содержит клип, который пользователь ранее скопировал в ваше приложение.
  • Предполагаемая цель использования нескольких объектов ClipData.Item для каждого клипа — поддержка копирования и вставки нескольких выделенных фрагментов, а не различных форм ссылки на один выделенный фрагмент. Обычно требуется, чтобы все объекты ClipData.Item в клипе имели одинаковую форму. То есть все они должны быть простым текстом, URI контента или Intent и не быть смешанными.
  • Предоставляя данные, вы можете предлагать различные представления MIME. Добавьте типы MIME, которые вы поддерживаете, в ClipDescription , а затем реализуйте типы MIME в своем поставщике контента.
  • Когда вы получаете данные из буфера обмена, ваше приложение отвечает за проверку доступных типов MIME, а затем решает, какой из них использовать, если таковой имеется. Даже если в буфере обмена есть клип и пользователь запрашивает вставку, вашему приложению не обязательно выполнять вставку. Вставьте, если тип MIME совместим. Вы можете преобразовать данные в буфере обмена в текст, используя coerceToText() . Если ваше приложение поддерживает более одного из доступных типов MIME, вы можете позволить пользователю выбрать, какой из них использовать.
,
Попробуйте способ создания
Jetpack Compose — рекомендуемый набор инструментов пользовательского интерфейса для Android. Узнайте, как использовать копирование и вставку в Compose.

Android предоставляет мощную платформу для копирования и вставки на основе буфера обмена. Он поддерживает простые и сложные типы данных, включая текстовые строки, сложные структуры данных, текстовые и двоичные потоковые данные, а также ресурсы приложений. Простые текстовые данные хранятся непосредственно в буфере обмена, а сложные данные хранятся в виде ссылки, которую приложение для вставки разрешает поставщику контента. Копирование и вставка работают как внутри приложения, так и между приложениями, реализующими эту платформу.

Поскольку часть платформы использует поставщиков контента, этот документ предполагает некоторое знакомство с API-интерфейсом поставщика контента Android, который описан в разделе Поставщики контента .

Пользователи ожидают обратной связи при копировании контента в буфер обмена, поэтому в дополнение к платформе, обеспечивающей копирование и вставку, Android показывает пользователям пользовательский интерфейс по умолчанию при копировании в Android 13 (уровень API 33) и более поздних версиях. Из-за этой функции существует риск дублирования уведомлений. Подробнее об этом крайнем случае можно узнать в разделе « Избегайте дублирования уведомлений» .

Анимация, показывающая уведомление буфера обмена Android 13.
Рис. 1. Пользовательский интерфейс, отображаемый при попадании содержимого в буфер обмена в Android 13 и более поздних версиях.

Вручную предоставляйте пользователям обратную связь при копировании в Android 12L (уровень API 32) и более ранних версиях. См. рекомендации по этому поводу в этом документе.

Структура буфера обмена

При использовании платформы буфера обмена поместите данные в объект клипа, а затем поместите объект клипа в общесистемный буфер обмена. Объект клипа может принимать одну из трех форм:

Текст
Текстовая строка. Поместите строку непосредственно в объект клипа, который затем поместите в буфер обмена. Чтобы вставить строку, извлеките объект клипа из буфера обмена и скопируйте строку в хранилище вашего приложения.
URI
Объект Uri , представляющий любую форму URI. Это в первую очередь предназначено для копирования сложных данных от поставщика контента. Чтобы скопировать данные, поместите объект Uri в объект клипа и поместите объект клипа в буфер обмена. Чтобы вставить данные, получите объект клипа, получите объект Uri , разрешите его источнику данных, например поставщику контента, и скопируйте данные из источника в хранилище вашего приложения.
Намерение
Intent . Это поддерживает копирование ярлыков приложений. Чтобы скопировать данные, создайте Intent , поместите его в объект клипа и поместите объект клипа в буфер обмена. Чтобы вставить данные, получите объект клипа, а затем скопируйте объект Intent в область памяти вашего приложения.

Буфер обмена одновременно содержит только один объект клипа. Когда приложение помещает объект клипа в буфер обмена, предыдущий объект клипа исчезает.

Если вы хотите, чтобы пользователи могли вставлять данные в ваше приложение, вам не обязательно обрабатывать все типы данных. Вы можете просмотреть данные в буфере обмена, прежде чем предоставить пользователям возможность вставить их. Помимо определенной формы данных, объект клипа также содержит метаданные, которые сообщают вам, какие типы MIME доступны. Эти метаданные помогут вам решить, может ли ваше приложение сделать что-нибудь полезное с данными буфера обмена. Например, если у вас есть приложение, которое в основном обрабатывает текст, вы можете игнорировать объекты клипов, содержащие URI или намерение.

Вы также можете разрешить пользователям вставлять текст независимо от формы данных в буфере обмена. Для этого принудительно преобразуйте данные буфера обмена в текстовое представление, а затем вставьте этот текст. Это описано в разделе «Приведение буфера обмена к тексту» .

Классы буфера обмена

В этом разделе описываются классы, используемые платформой буфера обмена.

Менеджер буфера обмена

Буфер обмена системы Android представлен глобальным классом ClipboardManager . Не создавайте экземпляр этого класса напрямую. Вместо этого получите ссылку на него, вызвав getSystemService(CLIPBOARD_SERVICE) .

ClipData, ClipData.Item и ClipDescription

Чтобы добавить данные в буфер обмена, создайте объект ClipData , содержащий описание данных и сами данные. Буфер обмена одновременно хранит по одному ClipData . ClipData содержит объект ClipDescription и один или несколько объектов ClipData.Item .

Объект ClipDescription содержит метаданные о клипе. В частности, он содержит массив доступных типов MIME для данных клипа. Кроме того, в Android 12 (уровень API 31) и выше метаданные включают информацию о том, содержит ли объект стилизованный текст , и о типе текста в объекте . Когда вы помещаете клип в буфер обмена, эта информация становится доступной для вставляющих приложений, которые могут проверить, могут ли они обработать данные клипа.

Объект ClipData.Item содержит текст, URI или данные намерения:

Текст
CharSequence .
URI
Uri . Обычно он содержит URI поставщика контента, хотя допускается любой URI. Приложение, предоставляющее данные, помещает URI в буфер обмена. Приложения, которые хотят вставить данные, получают URI из буфера обмена и используют его для доступа к поставщику контента или другому источнику данных и получения данных.
Намерение
Intent . Этот тип данных позволяет скопировать ярлык приложения в буфер обмена. Затем пользователи могут вставить ярлык в свои приложения для дальнейшего использования.

В клип можно добавить несколько объектов ClipData.Item . Это позволяет пользователям копировать и вставлять несколько фрагментов как один клип. Например, если у вас есть виджет списка, который позволяет пользователю выбирать более одного элемента одновременно, вы можете скопировать все элементы в буфер обмена одновременно. Для этого создайте отдельный ClipData.Item для каждого элемента списка, а затем добавьте объекты ClipData.Item к объекту ClipData .

Удобные методы ClipData

Класс ClipData предоставляет статические удобные методы для создания объекта ClipData с одним объектом ClipData.Item и простым объектом ClipDescription :

newPlainText(label, text)
Возвращает объект ClipData , единственный объект которого ClipData.Item содержит текстовую строку. Для метки объекта ClipDescription установлено значение label . Единственный тип MIME в ClipDescriptionMIMETYPE_TEXT_PLAIN .

Используйте newPlainText() , чтобы создать клип из текстовой строки.

newUri(resolver, label, URI)
Возвращает объект ClipData , единственный объект которого ClipData.Item содержит URI. Метка объекта ClipDescription установлен на label . Если URI - это контент URI - то есть, если Uri.getScheme() возвращает content: - Метод использует объект ContentResolver , предоставленный в resolver для извлечения доступных типов MIME от поставщика контента. Затем он хранит их в ClipDescription . Для URI, который не является content: URI, метод устанавливает тип MIME на MIMETYPE_TEXT_URILIST .

Используйте newUri() , чтобы создать клип из URI, особенно content: URI.

newIntent(label, intent)
Возвращает объект ClipData , один объект ClipData.Item содержит Intent . Метка объекта ClipDescription установлен на label . Тип MIME установлен на MIMETYPE_TEXT_INTENT .

Используйте newIntent() , чтобы создать клип из объекта Intent .

Приступить к данным буфера обмена на текст

Даже если ваше приложение обрабатывает только текст, вы можете скопировать не текстовые данные из буфера обмена, преобразуя его с помощью метода ClipData.Item.coerceToText() .

Этот метод преобразует данные в ClipData.Item в текст и возвращает CharSequence . Значение, которое ClipData.Item.coerceToText() возвращает, основано на форме данных в ClipData.Item :

Текст
Если ClipData.Item - это текст, то есть, если getText() не null, - coercetotext () возвращает текст.
Ури
Если ClipData.Item - это URI, то есть если getUri() не null - coerceToText() пытается использовать его в качестве контента URI.
  • Если URI - это URI контента, а поставщик может вернуть текстовый поток, coerceToText() возвращает текстовый поток.
  • Если URI - это URI контента, но поставщик не предлагает текстовый поток, coerceToText() возвращает представление URI. Представление такое же, как и возвращено Uri.toString() .
  • Если URI не является контентом URI, coerceToText() возвращает представление URI. Представление такое же, как и возвращено Uri.toString() .
Намерение
Если ClipData.Item является Intent , то есть, если getIntent() не null - coerceToText() преобразует его в URI намерения и возвращает его. Представление такое же, как и возвращение Intent.toUri(URI_INTENT_SCHEME) .

Структура буфера обмена суммирована на рисунке 2. Чтобы скопировать данные, приложение помещает объект ClipData в глобальный буфер обмена ClipboardManager . ClipData содержит один или несколько объектов ClipData.Item и один объект ClipDescription . Чтобы вставить данные, приложение получает ClipData , получает свой тип MIME от ClipDescription и получает данные из ClipData.Item или от поставщика контента, на который ссылается ClipData.Item .

Изображение, показывающее блок -схему каркаса копирования и вставки
Рисунок 2. Андоидная платформа обмена обмена.

Скопировать в буфер обмена

Чтобы скопировать данные в буфер обмена, получите ручку в Global ClipboardManager , создайте объект ClipData и добавьте к ClipData.Item ClipDescription Затем добавьте готовый объект ClipData в объект ClipboardManager . Это описано далее в следующей процедуре:

  1. Если вы копируете данные, используя URI контента, настройте поставщика контента.
  2. Получите системный буфер обмена:

    Котлин

    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
        }
    }

    Ява

    ...
    // 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. Скопируйте данные в новый объект ClipData :

    • Для текста

      Котлин

      // Creates a new text clip to put on the clipboard.
      val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")

      Ява

      // Creates a new text clip to put on the clipboard.
      ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
    • Для URI

      Этот фрагмент строит URI, кодируя идентификатор записи на URI контента для поставщика. Этот метод более подробно рассказывается о кодировании идентификатора в разделе URI .

      Котлин

      // 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)

      Ява

      // 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);
    • Для намерения

      Этот фрагмент создает Intent для приложения, а затем помещает его в объект клипа:

      Котлин

      // 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)

      Ява

      // 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. Поместите новый объект клипа в буфер обмена:

    Котлин

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip)

    Ява

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip);

Дайте обратную связь при копировании в буфер обмена

Пользователи ожидают визуальной обратной связи, когда приложение копирует контент в буфер обмена. Это делается автоматически для пользователей в Android 13 и выше, но оно должно быть вручную реализовано в предыдущих версиях.

Начиная с Android 13, система отображает стандартное визуальное подтверждение, когда контент добавляется в буфер обмена. Новое подтверждение делает следующее:

  • Подтверждает, что содержание было успешно скопировано.
  • Предоставляет предварительный просмотр скопированного контента.

Анимация, показывающая уведомление Android 13 буфера обмена
Рисунок 3. Показанный пользовательский интерфейс, когда контент входит в буфер обмена в Android 13 и выше.

В Android 12L (API -уровне 32) и ниже пользователи могут быть не уверены, успешно ли они скопировали контент или что они скопировали. Эта функция стандартизирует различные уведомления, показанные приложениями после копирования, и предлагает пользователям больше контроля над буфером обмена.

Избегайте дублирующих уведомлений

В Android 12L (API-уровне 32) и ниже мы рекомендуем предупреждать пользователей, когда они успешно копируют, выпустив визуальную обратную связь в приложении, используя виджет, такой как Toast или Snackbar , после копирования.

Чтобы избежать дублирующих отображений информации, мы настоятельно рекомендуем удалять тосты или закуски, показанные после копии в приложении для Android 13 и выше.

POST SNACKBAR после копии в приложении.
Рисунок 4. Если вы показываете закусочную копию подтверждения в Android 13, пользователь видит повторяющиеся сообщения.
Опубликовать тост после копии в приложении.
Рисунок 5. Если вы показываете тост подтверждения копии в Android 13, пользователь видит дублирующиеся сообщения.

Вот пример того, как это реализовать:

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()
}

Добавить чувствительный контент в буфер обмена

Если ваше приложение позволяет пользователям копировать конфиденциальное контент в буфер обмена, например, пароли или информацию о кредитной карте, вы должны добавить флаг в ClipDescription в ClipData перед вызовом ClipboardManager.setPrimaryClip() . Добавление этого флага предотвращает появление чувствительного контента в визуальном подтверждении копированного контента в Android 13 и выше.

Скопированный предварительный просмотр текста без помечения конфиденциального контента
Рисунок 6. Скопированный предварительный просмотр текста без чувствительного содержания флага.
Скопированный текст предварительный просмотр, помечающий конфиденциальное содержание.
Рисунок 7. Скопированный предварительный просмотр текста с чувствительным содержанием флага.

Чтобы пометить чувствительный контент, добавьте логическое дополнительное в ClipDescription . Все приложения должны делать это, независимо от целевого уровня API.

// 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)
    }
}

Вставить из буфера обмена

Как описано ранее, вставьте данные из буфера обмена, получая объект глобального буфера обмена, получая объект Clip, просмотр его данных и, если возможно, копирование данных из объекта Clip в собственное хранилище. В этом разделе подробно объясняется, как вставить три формы данных буфера обмена.

Вставьте простой текст

Чтобы вставить простой текст, получите глобальный буфер обмена и убедитесь, что он может вернуть простой текст. Затем получите объект CLIP и скопируйте его текст в собственное хранилище, используя getText() , как описано в следующей процедуре:

  1. Получите Global ClipboardManager Object, используя getSystemService(CLIPBOARD_SERVICE) . Кроме того, объявьте глобальную переменную для содержания вставленного текста:

    Котлин

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

    Ява

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
  2. Определите, нужно ли вам включить или отключить опцию «вставка» в текущей деятельности. Убедитесь, что буфер обмена содержит клип и что вы можете обрабатывать тип данных, представленных клипсом:

    Котлин

    // 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
        }
    }

    Ява

    // 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);
    }
  3. Скопируйте данные из буфера обмена. Эта точка в коде доступна только в том случае, если включен элемент меню «Вставки», поэтому вы можете предположить, что буфер обмена содержит простой текст. Вы еще не знаете, содержит ли он текстовую строку или URI, который указывает на простой текст. Следующий фрагмент кода проверяет это, но он показывает только код для обработки простого текста:

    Котлин

    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
                }
            }
        }
    }

    Ява

    // 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;
        }
    }

Вставьте данные из URI контента

Если объект ClipData.Item содержит URI контента, и вы определяете, что вы можете обрабатывать один из его типов MIME, создайте ContentResolver и вызовите соответствующий метод поставщика контента для извлечения данных.

В следующей процедуре описывается, как получить данные от поставщика контента на основе URI контента в буфере обмена. Он проверяет, доступен ли тип MIME, который может использовать приложение, у поставщика.

  1. Объявить глобальную переменную для содержания типа MIME:

    Котлин

    // 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"

    Ява

    // 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. Получите глобальный буфер обмена. Также получите разрешающий контент, чтобы вы могли получить доступ к поставщику контента:

    Котлин

    // Gets a handle to the Clipboard Manager.
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    // Gets a content resolver instance.
    val cr = contentResolver

    Ява

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
  3. Получите основной клип от буфера обмена и получите его содержимое в качестве URI:

    Котлин

    // 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

    Ява

    // 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. Проверьте, является ли URI контентом URI, позвонив getType(Uri) . Этот метод возвращает NULL, если Uri не указывает на действительный поставщик контента.

    Котлин

        // If the clipboard contains a URI reference...
        pasteUri?.let {
    
            // ...is this a content URI?
            val uriMimeType: String? = cr.getType(it)

    Ява

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
  5. Проверьте, поддерживает ли поставщик контента тип MIME, который понимает приложение. Если это так, вызовите ContentResolver.query() , чтобы получить данные. Возвратное значение - Cursor .

    Котлин

            // 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.
                }
            }
        }
    }

    Ява

            // 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();
                 }
             }
         }
    }

Вставьте намерение

Чтобы вставить намерение, сначала получите глобальный буфер обмена. Изучите объект ClipData.Item , чтобы увидеть, содержит ли он Intent . Затем позвоните getIntent() , чтобы скопировать намерение на собственное хранилище. Следующий фрагмент демонстрирует это:

Котлин

// 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.
}

Ява

// 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.
}

Системное уведомление, показанное, когда ваше приложение обращается к данным буфера обмена

На Android 12 (API -уровне 31) и выше, система обычно показывает тост -сообщение, когда вызовы в вашем приложении getPrimaryClip() . Текст внутри сообщения содержит следующий формат:

APP pasted from your clipboard

Система не отображает тосты

  • Доступ к ClipData из вашего собственного приложения.
  • Повторно обращается к ClipData из конкретного приложения. Тост появляется только тогда, когда ваше приложение впервые обращается к данным из этого приложения.
  • Получает метаданные для объекта клипа, например, вызов getPrimaryClipDescription() вместо getPrimaryClip() .

Используйте поставщиков контента для копирования сложных данных

Поставщики контента поддерживают копирование сложных данных, таких как записи базы данных или потоки файлов. Чтобы скопировать данные, поместите URI контента в буфер обмена. Затем вставьте приложения, затем получите этот URI из буфера обмена и используйте его для извлечения данных базы данных или дескрипторов потока файлов.

Поскольку приложение для вставки имеет только контент URI для ваших данных, оно должно знать, какой кусок данных для извлечения. Вы можете предоставить эту информацию, кодируя идентификатор для данных на самом URI, или вы можете предоставить уникальный URI, который возвращает данные, которые вы хотите скопировать. Какой метод вы выберете, зависит от организации ваших данных.

В следующих разделах описывается, как настроить URI, предоставить сложные данные и предоставить потоки файлов. Описания предполагают, что вы знакомы с общими принципами дизайна поставщика контента.

Кодировать идентификатор на URI

Полезный метод копирования данных в буфер обмена с помощью URI - это кодировать идентификатор для данных на самом URI. Ваш поставщик контента может затем получить идентификатор из URI и использовать его для извлечения данных. Приложение вставки не должно знать, что идентификатор существует. Он просто должен получить вашу «ссылку» - URI плюс идентификатор - от буфера обмена, дайте ему поставщика контента и вернуть данные.

Обычно вы кодируете идентификатор в URI контента, объединив его до конца URI. Например, предположим, что вы определяете свой поставщик URI как следующую строку:

"content://com.example.contacts"

Если вы хотите кодировать имя в этот URI, используйте следующий фрагмент кода:

Котлин

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)

Ява

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);

Если вы уже используете поставщика контента, вы можете добавить новый путь URI, который указывает, что URI предназначен для копирования. Например, предположим, что у вас уже есть следующие пути URI:

"content://com.example.contacts/people"
"content://com.example.contacts/people/detail"
"content://com.example.contacts/people/images"

Вы можете добавить еще один путь, который для копирования URI:

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

Затем вы можете обнаружить URI «копировать» путем сопоставления шаблонов и обработать его с помощью кода, который является специфическим для копирования и вставки.

Вы обычно используете метод кодирования, если вы уже используете поставщика контента, внутреннюю базу данных или внутреннюю таблицу для организации ваших данных. В этих случаях у вас есть несколько деталей данных, которые вы хотите скопировать, и, по -видимому, уникальный идентификатор для каждого произведения. В ответ на запрос из приложения вставки вы можете найти данные по его идентификатору и вернуть его.

Если у вас нет нескольких деталей данных, то вам, вероятно, не нужно кодировать идентификатор. Вы можете использовать URI, который является уникальным для вашего поставщика. В ответ на запрос ваш провайдер возвращает данные, которые он в настоящее время содержит.

Копировать структуры данных

Установите поставщик контента для копирования и вставки сложных данных в качестве подкласса компонента ContentProvider . Кодируйте URI, который вы помещаете в буфер обмена, чтобы он указывал на точную запись, которую вы хотите предоставить. Кроме того, рассмотрим существующее состояние вашего заявления:

  • Если у вас уже есть поставщик контента, вы можете добавить его функциональность. Возможно, вам потребуется только изменять метод query() для обработки URI, поступающих из приложений, которые хотят вставить данные. Вы, вероятно, хотите изменить метод для обработки «копии» шаблона URI.
  • Если ваше приложение поддерживает внутреннюю базу данных, вы можете переместить эту базу данных в поставщика контента, чтобы облегчить копирование из нее.
  • Если вы не используете базу данных, вы можете реализовать простого поставщика контента, единственной целью которой является предложение данных для приложений, которые встают из буфера обмена.

В поставщике контента переопределите, по крайней мере, следующие методы:

query()
Вставки приложений предполагают, что они могут получить ваши данные, используя этот метод с помощью URI, который вы помещаете в буфер обмена. Чтобы поддержать копирование, попросите этот метод обнаружить URI, содержащую специальный путь «копии». Затем ваше приложение может создать URI «Копировать» для размещения буфера обмена, содержащий путь копии и указатель на точную запись, которую вы хотите скопировать.
getType()
Этот метод должен вернуть типы MIME для данных, которые вы собираетесь скопировать. Метод newUri() вызывает getType() чтобы поместить типы MIME в новый объект ClipData .

Типы MIME для сложных данных описаны у поставщиков контента .

Вам не нужно иметь какие -либо другие методы поставщика контента, такие как insert() или update() . Приложение для вставки должно получить только ваши поддерживаемые типы MIME и копировать данные от вашего поставщика. Если у вас уже есть эти методы, они не будут мешать операциям копирования.

Следующие фрагменты демонстрируют, как настроить приложение для копирования сложных данных:

  1. В глобальных константах для вашего приложения объявьте базовую строку URI и путь, который идентифицирует строки URI, которые вы используете для копирования данных. Также объявите тип MIME для скопированных данных.

    Котлин

    // 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"

    Ява

    // 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. В деятельности пользователи копируют данные, настройте код для копирования данных в буфер обмена. В ответ на запрос на копию поместите URI в буфер обмена.

    Котлин

    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)
        }
    }

    Ява

    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);
  3. В глобальном масштабе вашего поставщика контента создайте сопоставление URI и добавьте шаблон URI, который соответствует URI, который вы помещаете в буфер обмена.

    Котлин

    // 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() {
        ...
    }

    Ява

    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. Установите метод query() . Этот метод может обрабатывать различные шаблоны URI, в зависимости от того, как вы его кодируете, но показывает только шаблон для операции копирования буфера.

    Котлин

    // 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.
            }
        }
        ...
    }

    Ява

    // 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.
        ...
    }
  5. Настройка метода getType() для возврата соответствующего типа MIME для копирования данных:

    Котлин

    // Sets up your provider's getType() method.
    override fun getType(uri: Uri): String? {
        ...
        return when(sUriMatcher.match(uri)) {
            GET_SINGLE_CONTACT -> MIME_TYPE_CONTACT
            ...
        }
    }

    Ява

    // Sets up your provider's getType() method.
    public String getType(Uri uri) {
        ...
        switch (sUriMatcher.match(uri)) {
        case GET_SINGLE_CONTACT:
            return (MIME_TYPE_CONTACT);
        ...
        }
    }

Данные вставки из раздела URI контента описывают, как получить контент URI из буфера обмена и использовать его для получения и вставки данных.

Копировать потоки данных

Вы можете скопировать и вставить большие объемы текстовых и двоичных данных в качестве потоков. Данные могут иметь такие формы, как следующие:

  • Файлы, хранящиеся на фактическом устройстве
  • Потоки из розетков
  • Большие объемы данных, хранящихся в системе базы данных, провайдера

Поставщик контента для потоков данных предоставляет доступ к своим данным с помощью объекта дескриптора файла, такого как AssetFileDescriptor , вместо объекта Cursor . Приложение вставки считывает поток данных, используя этот дескриптор файла.

Чтобы настроить приложение для копирования потока данных с поставщиком, выполните следующие действия:

  1. Установите URI контента для потока данных, который вы помещаете в буфер обмена. Варианты для этого включают следующее:
    • Кодируйте идентификатор для потока данных на URI, как описано в кодировании идентификатора в разделе URI , а затем поддерживайте таблицу в вашем поставщике, который содержит идентификаторы и соответствующее имя потока.
    • Кодируйте имя потока непосредственно на URI.
    • Используйте уникальный URI, который всегда возвращает текущий поток от поставщика. Если вы используете эту опцию, не забудьте обновить своего провайдера, чтобы указать на другой поток, когда вы копируете поток в буфер обмена с помощью URI.
  2. Предоставьте тип MIME для каждого типа потока данных, который вы планируете предложить. Вставка приложений нуждается в этой информации, чтобы определить, могут ли они вставить данные в буфер обмена.
  3. Реализуйте один из методов ContentProvider , который возвращает дескриптор файла для потока. Если вы кодируете идентификаторы в URI содержимого, используйте этот метод, чтобы определить, какой поток открыть.
  4. Чтобы скопировать поток данных в буфер обмена, постройте URI содержимого и поместите его в буфер обмена.

Чтобы вставить поток данных, приложение получает зажим из буфера обмена, получает URI и использует его в вызове метода дескриптора файла ContentResolver , который открывает поток. Метод ContentResolver вызывает соответствующий метод ContentProvider , передавая его URI Content. Ваш провайдер возвращает дескриптор файла в метод ContentResolver . Приложение для вставки несет ответственность за чтение данных из потока.

В следующем списке показаны наиболее важные методы дескриптора файла для поставщика контента. Каждый из них имеет соответствующий метод ContentResolver с строкой «дескриптор», добавленный к имени метода. Например, ContentResolver -аналог openAssetFile() openAssetFileDescriptor() .

openTypedAssetFile()

Этот метод возвращает дескриптор файла актива, но только если поставщик поддерживается тип приготовления MIME. Вызывающий абонент - приложение, выполняющее вставку - обеспечивает шаблон типа MIME. Поставщик контента приложения, который копирует URI в буфер обмена, возвращает ручку файла AssetFileDescriptor , если он может предоставить этот тип MIME и бросает исключение, если он не может.

Этот метод обрабатывает подразделы файлов. Вы можете использовать его для чтения активов, которые поставщик контента скопировал в буфер обмена.

openAssetFile()
Этот метод является более общей формой openTypedAssetFile() . Он не фильтрует для разрешенных типов MIME, но может читать подразделы файлов.
openFile()
Это более общая форма openAssetFile() . Он не может читать подразделы файлов.

При желании вы можете использовать метод openPipeHelper() с помощью метода дескриптора файла. Это позволяет вставленному приложению считывать данные потока в фоновом потоке, используя трубу. Чтобы использовать этот метод, реализуйте интерфейс ContentProvider.PipeDataWriter .

Проектирование эффективной функциональности копирования и вставки

Чтобы разработать эффективную функциональность копирования и вставки для вашего приложения, запомните эти точки:

  • В любое время в буфер обмена есть только один клип. Новая операция копирования любым приложением в системе перезаписывает предыдущий клип. Поскольку пользователь может отойти от вашего приложения и скопировать перед возвратом, вы не можете предположить, что буфер обмена содержит клип, который пользователь ранее скопировал в вашем приложении.
  • Предполагаемой целью нескольких объектов ClipData.Item на клип является поддержка копирования и вставки нескольких выборов, а не различных форм ссылки на один выбор. Обычно вы хотите, чтобы все объекты ClipData.Item в клипе имели одну и ту же форму. То есть все они должны быть простым текстом, контентом URI или Intent , а не смешанными.
  • Когда вы предоставляете данные, вы можете предложить разные представления MIME. Добавьте типы MIME, которые вы поддерживаете в ClipDescription , а затем реализуйте типы MIME в вашем поставщике контента.
  • Когда вы получаете данные из буфера обмена, ваше приложение отвечает за проверку доступных типов MIME, а затем для решения, какой, если таковой имеется, использовать. Даже если в буфере обмена есть клип, и пользователь запрашивает вставку, ваше приложение не требуется для вставки. Сделайте вставку, если тип MIME совместим. Вы можете принудить данные в буфер обмена, чтобы текст, используя coerceToText() . Если ваше приложение поддерживает более одного из доступных типов MIME, вы можете позволить пользователю выбрать, какой из них использовать.
,
Попробуйте Compose Way
JetPack Compose - это рекомендуемый инструментарий пользовательского интерфейса для Android. Узнайте, как использовать копию и вставку в Compose.

Android обеспечивает мощную платформу на основе буфера обмена для копирования и вставки. Он поддерживает простые и сложные типы данных, включая текстовые строки, сложные структуры данных, текстовые и бинарные потоковые данные, а также активы приложений. Простые текстовые данные хранятся непосредственно в буфере обмена, в то время как сложные данные хранятся в качестве ссылки, которую приложение для вставки разрешается с поставщиком контента. Копирование и вставка работы как в приложении, так и между приложениями, которые реализуют структуру.

Поскольку часть структуры использует поставщиков контента, этот документ предполагает некоторое знакомство с API поставщика контента Android, который описан у поставщиков контента .

Пользователи ожидают обратной связи при копировании контента в буфер обмена, поэтому в дополнение к фреймворкам, которая питает копирование и вставку, Android показывает пользовательский интерфейс по умолчанию при копировании в Android 13 (API 33) и выше. Из -за этой функции существует риск дублирования уведомления. Вы можете узнать больше об этом крае .

Анимация, показывающая уведомление Android 13 буфера обмена
Рисунок 1. Показанный пользовательский интерфейс, когда контент входит в буфер обмена в Android 13 и выше.

Вручную предоставьте обратную связь для пользователей при копировании в Android 12L (API -уровне 32) и ниже. Смотрите рекомендации для этого в этом документе.

Структура буфера обмена

Когда вы используете платформу буфера обмена, поместите данные в объект клипа, а затем поместите объект Clip в общес. Объект клипа может занять одну из трех форм:

Текст
Текстовая строка. Поместите строку непосредственно в объект клипа, который вы затем помещаете в буфер обмена. Чтобы вставить строку, получите объект Clip из буфера обмена и скопируйте строку в хранилище вашего приложения.
Ури
Объект Uri , представляющий любую форму URI. Это в первую очередь для копирования сложных данных от поставщика контента. Чтобы скопировать данные, поместите объект Uri в объект клипа и поместите объект Clip в буфер обмена. Чтобы вставить данные, получить объект CLIP, получить объект Uri , разрешить их к источнику данных, такому как поставщик контента, и скопируйте данные из источника в хранилище вашего приложения.
Намерение
Intent . Это поддерживает копирование ярлыков приложений. Чтобы скопировать данные, создайте Intent , поместите их в объект клипа и поместите объект Clip в буфер обмена. Чтобы вставить данные, получите объект Clip, а затем скопируйте объект Intent в область памяти вашего приложения.

Буфер обмена содержит только один объект клипа за раз. Когда приложение помещает объект клипа в буфер обмена, предыдущий объект клипа исчезает.

Если вы хотите позволить пользователям вставить данные в ваше приложение, вам не нужно обрабатывать все типы данных. Вы можете изучить данные в буфер обмена, прежде чем предоставить пользователям возможность вставить их. Помимо наличия определенной формы данных, объект CLIP также содержит метаданные, которые рассказывают вам, какие типы MIME доступны. Эти метаданные помогают вам решить, может ли ваше приложение сделать что -то полезное с данными буфера обмена. Например, если у вас есть приложение, которое в основном обрабатывает текст, вы можете игнорировать объекты клипов, которые содержат URI или намерение.

Вы также можете позволить пользователям вставить текст независимо от формы данных в буфере обмена. Чтобы сделать это, заставьте данные буфера обмена в текстовое представление, а затем вставьте этот текст. Это описано в приставлении в разделе буфера обмена для текста .

Классы буфера обмена

В этом разделе описывается классы, используемые платформой буфера обмена.

Буферный зал

Система Android Система обмена представлена ​​глобальным классом ClipboardManager . Не создайте экземпляр этого класса напрямую. Вместо этого получите ссылку на это, вызывая getSystemService(CLIPBOARD_SERVICE) .

ClipData, ClipData.Item и ClipDescription

Чтобы добавить данные в буфер обмена, создайте объект ClipData , который содержит описание данных и самих данных. Буфер обмена содержит по одному ClipData за раз. ClipData содержит объект ClipDescription и один или несколько объектов ClipData.Item .

Объект ClipDescription содержит метаданные о клипе. В частности, он содержит множество доступных типов MIME для данных клипа. Кроме того, на Android 12 (API -уровне 31) и выше, метаданные включают информацию о том, содержит ли объект стилизованный текст и тип текста в объекте . Когда вы ставите зажим в буфер обмена, эта информация доступна для вставки приложений, которые могут изучить, могут ли они обрабатывать данные клипа.

Объект ClipData.Item содержит данные текста, URI или намерения:

Текст
CharSequence .
Ури
Uri . Обычно это содержит URI поставщика контента, хотя любой URI разрешен. Приложение, которое предоставляет данные, помещает URI в буфер обмена. Приложения, которые хотят вставить данные, получают URI из буфера обмена и использовать его для доступа к поставщику контента или другого источника данных и извлечения данных.
Намерение
Intent . Этот тип данных позволяет скопировать сочетание приложения в буфер обмена. Затем пользователи могут вставить ярлык в свои приложения для последующего использования.

Вы можете добавить более одного объекта ClipData.Item в клип. Это позволяет пользователям копировать и вставлять несколько выборов в виде одного клипа. Например, если у вас есть виджет списка, который позволяет пользователю выбирать более одного элемента за раз, вы можете скопировать все элементы в буфер обмена одновременно. Для этого создайте отдельный ClipData.Item для каждого элемента списка, а затем добавьте объекты ClipData.Item в объект ClipData .

Clipdata Удобные методы

Класс ClipData предоставляет статические методы удобства для создания объекта ClipData с одним объектом ClipData.Item и простой объектом ClipDescription :

newPlainText(label, text)
Возвращает объект ClipData , один объект ClipData.Item содержит текстовую строку. Метка объекта ClipDescription установлен на label . Одиночный тип MIME в ClipDescription - MIMETYPE_TEXT_PLAIN .

Используйте newPlainText() для создания клипа из текстовой строки.

newUri(resolver, label, URI)
Возвращает объект ClipData , чей единый объект ClipData.Item содержит URI. Метка объекта ClipDescription установлен на label . Если URI - это контент URI - то есть, если Uri.getScheme() возвращает content: - Метод использует объект ContentResolver , предоставленный в resolver для извлечения доступных типов MIME от поставщика контента. Затем он хранит их в ClipDescription . Для URI, который не является content: URI, метод устанавливает тип MIME на MIMETYPE_TEXT_URILIST .

Используйте newUri() , чтобы создать клип из URI, особенно content: URI.

newIntent(label, intent)
Возвращает объект ClipData , один объект ClipData.Item содержит Intent . Метка объекта ClipDescription установлен на label . Тип MIME установлен на MIMETYPE_TEXT_INTENT .

Используйте newIntent() , чтобы создать клип из объекта Intent .

Приступить к данным буфера обмена на текст

Даже если ваше приложение обрабатывает только текст, вы можете скопировать не текстовые данные из буфера обмена, преобразуя его с помощью метода ClipData.Item.coerceToText() .

Этот метод преобразует данные в ClipData.Item в текст и возвращает CharSequence . Значение, которое ClipData.Item.coerceToText() возвращает, основано на форме данных в ClipData.Item :

Текст
Если ClipData.Item - это текст, то есть, если getText() не null, - coercetotext () возвращает текст.
Ури
Если ClipData.Item - это URI, то есть если getUri() не null - coerceToText() пытается использовать его в качестве контента URI.
  • Если URI - это URI контента, а поставщик может вернуть текстовый поток, coerceToText() возвращает текстовый поток.
  • Если URI - это URI контента, но поставщик не предлагает текстовый поток, coerceToText() возвращает представление URI. Представление такое же, как и возвращено Uri.toString() .
  • Если URI не является контентом URI, coerceToText() возвращает представление URI. Представление такое же, как и возвращено Uri.toString() .
Намерение
Если ClipData.Item является Intent , то есть, если getIntent() не null - coerceToText() преобразует его в URI намерения и возвращает его. Представление такое же, как и возвращение Intent.toUri(URI_INTENT_SCHEME) .

Структура буфера обмена суммирована на рисунке 2. Чтобы скопировать данные, приложение помещает объект ClipData в глобальный буфер обмена ClipboardManager . ClipData содержит один или несколько объектов ClipData.Item и один объект ClipDescription . Чтобы вставить данные, приложение получает ClipData , получает свой тип MIME от ClipDescription и получает данные из ClipData.Item или от поставщика контента, на который ссылается ClipData.Item .

Изображение, показывающее блок -схему каркаса копирования и вставки
Рисунок 2. Андоидная платформа обмена обмена.

Скопировать в буфер обмена

Чтобы скопировать данные в буфер обмена, получите ручку в Global ClipboardManager , создайте объект ClipData и добавьте к ClipData.Item ClipDescription Затем добавьте готовый объект ClipData в объект ClipboardManager . Это описано далее в следующей процедуре:

  1. Если вы копируете данные, используя URI контента, настройте поставщика контента.
  2. Получите системный буфер обмена:

    Котлин

    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
        }
    }

    Ява

    ...
    // 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. Скопируйте данные в новый объект ClipData :

    • Для текста

      Котлин

      // Creates a new text clip to put on the clipboard.
      val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")

      Ява

      // Creates a new text clip to put on the clipboard.
      ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
    • Для URI

      Этот фрагмент строит URI, кодируя идентификатор записи на URI контента для поставщика. Этот метод более подробно рассказывается о кодировании идентификатора в разделе URI .

      Котлин

      // 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)

      Ява

      // 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);
    • Для намерения

      Этот фрагмент создает Intent для приложения, а затем помещает его в объект клипа:

      Котлин

      // 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)

      Ява

      // 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. Поместите новый объект клипа в буфер обмена:

    Котлин

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip)

    Ява

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip);

Дайте обратную связь при копировании в буфер обмена

Пользователи ожидают визуальной обратной связи, когда приложение копирует контент в буфер обмена. Это делается автоматически для пользователей в Android 13 и выше, но оно должно быть вручную реализовано в предыдущих версиях.

Начиная с Android 13, система отображает стандартное визуальное подтверждение, когда контент добавляется в буфер обмена. Новое подтверждение делает следующее:

  • Подтверждает, что содержание было успешно скопировано.
  • Предоставляет предварительный просмотр скопированного контента.

Анимация, показывающая уведомление Android 13 буфера обмена
Рисунок 3. Показанный пользовательский интерфейс, когда контент входит в буфер обмена в Android 13 и выше.

В Android 12L (API -уровне 32) и ниже пользователи могут быть не уверены, успешно ли они скопировали контент или что они скопировали. Эта функция стандартизирует различные уведомления, показанные приложениями после копирования, и предлагает пользователям больше контроля над буфером обмена.

Избегайте дублирующих уведомлений

В Android 12L (API-уровне 32) и ниже мы рекомендуем предупреждать пользователей, когда они успешно копируют, выпустив визуальную обратную связь в приложении, используя виджет, такой как Toast или Snackbar , после копирования.

Чтобы избежать дублирующих отображений информации, мы настоятельно рекомендуем удалять тосты или закуски, показанные после копии в приложении для Android 13 и выше.

POST SNACKBAR после копии в приложении.
Рисунок 4. Если вы показываете закусочную копию подтверждения в Android 13, пользователь видит повторяющиеся сообщения.
Опубликовать тост после копии в приложении.
Рисунок 5. Если вы показываете тост подтверждения копии в Android 13, пользователь видит дублирующиеся сообщения.

Вот пример того, как это реализовать:

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()
}

Добавить чувствительный контент в буфер обмена

Если ваше приложение позволяет пользователям копировать конфиденциальное контент в буфер обмена, например, пароли или информацию о кредитной карте, вы должны добавить флаг в ClipDescription в ClipData перед вызовом ClipboardManager.setPrimaryClip() . Добавление этого флага предотвращает появление чувствительного контента в визуальном подтверждении копированного контента в Android 13 и выше.

Скопированный предварительный просмотр текста без помечения конфиденциального контента
Рисунок 6. Скопированный предварительный просмотр текста без чувствительного содержания флага.
Скопированный текст предварительный просмотр, помечающий конфиденциальное содержание.
Рисунок 7. Скопированный предварительный просмотр текста с чувствительным содержанием флага.

Чтобы пометить чувствительный контент, добавьте логическое дополнительное в ClipDescription . Все приложения должны делать это, независимо от целевого уровня API.

// 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)
    }
}

Вставить из буфера обмена

Как описано ранее, вставьте данные из буфера обмена, получая объект глобального буфера обмена, получая объект Clip, просмотр его данных и, если возможно, копирование данных из объекта Clip в собственное хранилище. В этом разделе подробно объясняется, как вставить три формы данных буфера обмена.

Paste plain text

To paste plain text, get the global clipboard and verify that it can return plain text. Then get the clip object and copy its text to your own storage using getText() , as described in the following procedure:

  1. Get the global ClipboardManager object using getSystemService(CLIPBOARD_SERVICE) . Also, declare a global variable to contain the pasted text:

    Kotlin

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

    Ява

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
  2. Determine whether you need to enable or disable the "paste" option in the current activity. Verify that the clipboard contains a clip and that you can handle the type of data represented by the 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 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
        }
    }

    Ява

    // 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);
    }
  3. Copy the data from the clipboard. This point in the code is only reachable if the "paste" menu item is enabled, so you can assume that the clipboard contains plain text. You don't yet know if it contains a text string or a URI that points to plain text. The following code snippet tests this, but it only shows the code for handling plain text:

    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
                }
            }
        }
    }

    Ява

    // 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;
        }
    }

Paste data from a content URI

If the ClipData.Item object contains a content URI and you determine that you can handle one of its MIME types, create a ContentResolver and call the appropriate content provider method to retrieve the data.

The following procedure describes how to get data from a content provider based on a content URI on the clipboard. It checks whether a MIME type that the application can use is available from the provider.

  1. Declare a global variable to contain the MIME type:

    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"

    Ява

    // 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. Get the global clipboard. Also get a content resolver so you can access the content provider:

    Kotlin

    // Gets a handle to the Clipboard Manager.
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    // Gets a content resolver instance.
    val cr = contentResolver

    Ява

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
  3. Get the primary clip from the clipboard and get its contents as a 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

    Ява

    // 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. Test whether the URI is a content URI by calling getType(Uri) . This method returns null if Uri doesn't point to a valid content provider.

    Kotlin

        // If the clipboard contains a URI reference...
        pasteUri?.let {
    
            // ...is this a content URI?
            val uriMimeType: String? = cr.getType(it)

    Ява

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
  5. Test whether the content provider supports a MIME type that the application understands. If it does, call ContentResolver.query() to get the data. The return value is a Cursor .

    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.
                }
            }
        }
    }

    Ява

            // 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();
                 }
             }
         }
    }

Paste an Intent

To paste an intent, first get the global clipboard. Examine the ClipData.Item object to see whether it contains an Intent . Then call getIntent() to copy the intent to your own storage. The following snippet demonstrates this:

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.
}

Ява

// 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.
}

System notification shown when your app accesses clipboard data

On Android 12 (API level 31) and higher, the system usually shows a toast message when your app calls getPrimaryClip() . The text inside the message contains the following format:

APP pasted from your clipboard

The system doesn't show a toast message when your app does one of the following:

  • Accesses ClipData from your own app.
  • Repeatedly accesses ClipData from a specific app. The toast appears only when your app accesses the data from that app for the first time.
  • Retrieves metadata for the clip object, such as by calling getPrimaryClipDescription() instead of getPrimaryClip() .

Use content providers to copy complex data

Content providers support copying complex data such as database records or file streams. To copy the data, put a content URI on the clipboard. Pasting applications then get this URI from the clipboard and use it to retrieve database data or file stream descriptors.

Since the pasting application only has the content URI for your data, it needs to know which piece of data to retrieve. You can provide this information by encoding an identifier for the data on the URI itself, or you can provide a unique URI that returns the data you want to copy. Which technique you choose depends on the organization of your data.

The following sections describe how to set up URIs, provide complex data, and provide file streams. The descriptions assume you are familiar with the general principles of content provider design.

Encode an identifier on the URI

A useful technique for copying data to the clipboard with a URI is to encode an identifier for the data on the URI itself. Your content provider can then get the identifier from the URI and use it to retrieve the data. The pasting application doesn't have to know that the identifier exists. It just has to get your "reference"—the URI plus the identifier—from the clipboard, give it your content provider, and get back the data.

You usually encode an identifier onto a content URI by concatenating it to the end of the URI. For example, suppose you define your provider URI as the following string:

"content://com.example.contacts"

If you want to encode a name onto this URI, use the following code snippet:

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)

Ява

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);

If you are already using a content provider, you might want to add a new URI path that indicates the URI is for copying. For example, suppose you already have the following URI paths:

"content://com.example.contacts/people"
"content://com.example.contacts/people/detail"
"content://com.example.contacts/people/images"

You can add another path that for copying URIs:

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

You can then detect a "copy" URI by pattern-matching and handle it with code that is specific for copying and pasting.

You normally use the encoding technique if you're already using a content provider, internal database, or internal table to organize your data. In these cases, you have multiple pieces of data you want to copy, and presumably a unique identifier for each piece. In response to a query from the pasting application, you can look up the data by its identifier and return it.

If you don't have multiple pieces of data, then you probably don't need to encode an identifier. You can use a URI that is unique to your provider. In response to a query, your provider returns the data it currently contains.

Copy data structures

Set up a content provider for copying and pasting complex data as a subclass of the ContentProvider component. Encode the URI you put on the clipboard so that it points to the exact record you want to provide. In addition, consider the existing state of your application:

  • If you already have a content provider, you can add to its functionality. You might only need to modify its query() method to handle URIs coming from applications that want to paste data. You probably want to modify the method to handle a "copy" URI pattern.
  • If your application maintains an internal database, you might want to move this database into a content provider to facilitate copying from it.
  • If you aren't using a database, you can implement a simple content provider whose sole purpose is to offer data to applications that are pasting from the clipboard.

In the content provider, override at least the following methods:

query()
Pasting applications assume they can get your data by using this method with the URI you put on the clipboard. To support copying, have this method detect URIs containing a special "copy" path. Your application can then create a "copy" URI to put on the clipboard, containing the copy path and a pointer to the exact record you want to copy.
getType()
This method must return the MIME types for the data you intend to copy. The method newUri() calls getType() to put the MIME types into the new ClipData object.

MIME types for complex data are described in Content providers .

You don't need to have any of the other content provider methods, such as insert() or update() . A pasting application only needs to get your supported MIME types and copy data from your provider. If you already have these methods, they won't interfere with copy operations.

The following snippets demonstrate how to set up your application to copy complex data:

  1. In the global constants for your application, declare a base URI string and a path that identifies URI strings you are using to copy data. Also declare a MIME type for the copied data.

    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"

    Ява

    // 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. In the activity users copy data from, set up the code to copy data to the clipboard. In response to a copy request, put the URI on the clipboard.

    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)
        }
    }

    Ява

    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);
  3. In the global scope of your content provider, create a URI matcher and add a URI pattern that matches URIs you put on the clipboard.

    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() {
        ...
    }

    Ява

    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. Set up the query() method. This method can handle different URI patterns, depending on how you code it, but only the pattern for the clipboard copying operation shows.

    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.
            }
        }
        ...
    }

    Ява

    // 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.
        ...
    }
  5. Set up the getType() method to return an appropriate MIME type for copied data:

    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
            ...
        }
    }

    Ява

    // Sets up your provider's getType() method.
    public String getType(Uri uri) {
        ...
        switch (sUriMatcher.match(uri)) {
        case GET_SINGLE_CONTACT:
            return (MIME_TYPE_CONTACT);
        ...
        }
    }

The Paste data from a content URI section describes how to get a content URI from the clipboard and use it to get and paste data.

Copy data streams

You can copy and paste large amounts of text and binary data as streams. The data can have forms such as the following:

  • Files stored on the actual device
  • Streams from sockets
  • Large amounts of data stored in a provider's underlying database system

A content provider for data streams provides access to its data with a file descriptor object, such as AssetFileDescriptor , instead of a Cursor object. The pasting application reads the data stream using this file descriptor.

To set up your application to copy a data stream with a provider, follow these steps:

  1. Set up a content URI for the data stream you are putting on the clipboard. Options for doing this include the following:
    • Encode an identifier for the data stream onto the URI, as described in the Encode an identifier on the URI section, and then maintain a table in your provider that contains identifiers and the corresponding stream name.
    • Encode the stream name directly on the URI.
    • Use a unique URI that always returns the current stream from the provider. If you use this option, remember to update your provider to point to a different stream whenever you copy the stream to the clipboard using the URI.
  2. Provide a MIME type for each type of data stream you plan to offer. Pasting applications need this information to determine whether they can paste the data on the clipboard.
  3. Implement one of the ContentProvider methods that returns a file descriptor for a stream. If you encode identifiers on the content URI, use this method to determine which stream to open.
  4. To copy the data stream to the clipboard, construct the content URI and place it on the clipboard.

To paste a data stream, an application gets the clip from the clipboard, gets the URI, and uses it in a call to a ContentResolver file descriptor method that opens the stream. The ContentResolver method calls the corresponding ContentProvider method, passing it the content URI. Your provider returns the file descriptor to the ContentResolver method. The pasting application then has the responsibility to read the data from the stream.

The following list shows the most important file descriptor methods for a content provider. Each of these has a corresponding ContentResolver method with the string "Descriptor" appended to the method name. For example, the ContentResolver analog of openAssetFile() is openAssetFileDescriptor() .

openTypedAssetFile()

This method returns an asset file descriptor, but only if the provided MIME type is supported by the provider. The caller—the application doing the pasting—provides a MIME type pattern. The content provider of the application that copies a URI to the clipboard returns an AssetFileDescriptor file handle if it can provide that MIME type and throws an exception if it can't.

This method handles subsections of files. You can use it to read assets that the content provider has copied to the clipboard.

openAssetFile()
This method is a more general form of openTypedAssetFile() . It doesn't filter for allowed MIME types, but it can read subsections of files.
openFile()
This is a more general form of openAssetFile() . It can't read subsections of files.

You can optionally use the openPipeHelper() method with your file descriptor method. This lets the pasting application read the stream data in a background thread using a pipe. To use this method, implement the ContentProvider.PipeDataWriter interface.

Design effective copy and paste functionality

To design effective copy and paste functionality for your application, remember these points:

  • At any time, there is only one clip on the clipboard. A new copy operation by any application in the system overwrites the previous clip. Since the user might navigate away from your application and copy before returning, you can't assume the clipboard contains the clip that the user previously copied in your application.
  • The intended purpose of multiple ClipData.Item objects per clip is to support copying and pasting of multiple selections rather than different forms of reference to a single selection. You usually want all of the ClipData.Item objects in a clip to have the same form. That is, they must all be simple text, content URI, or Intent , and not mixed.
  • When you provide data, you can offer different MIME representations. Add the MIME types you support to the ClipDescription , and then implement the MIME types in your content provider.
  • When you get data from the clipboard, your application is responsible for checking the available MIME types and then deciding which one, if any, to use. Even if there is a clip on the clipboard and the user requests a paste, your application isn't required to do the paste. Do the paste if the MIME type is compatible. You might coerce the data on the clipboard to text using coerceToText() . If your application supports more than one of the available MIME types, you can let the user pick which one to use.
,
Try the Compose way
Jetpack Compose is the recommended UI toolkit for Android. Learn how to use copy and paste in Compose.

Android provides a powerful clipboard-based framework for copying and pasting. It supports simple and complex data types, including text strings, complex data structures, text and binary stream data, and application assets. Simple text data is stored directly in the clipboard, while complex data is stored as a reference that the pasting application resolves with a content provider. Copying and pasting works both within an application and between applications that implement the framework.

Since part of the framework uses content providers, this document assumes some familiarity with the Android Content Provider API, which is described in Content providers .

Users expect feedback when copying content to the clipboard, so in addition to the framework that powers copy and paste, Android shows a default UI to users when copying in Android 13 (API level 33) and higher. Due to this feature, there is a risk of duplicate notification. You can learn more about this edge case the in the Avoid duplicate notifications section.

An animation showing Android 13 clipboard notification
Figure 1. UI shown when content enters the clipboard in Android 13 and up.

Manually provide feedback to users when copying in Android 12L (API level 32) and lower. See recommendations for this in this document.

The clipboard framework

When you use the clipboard framework, put data into a clip object, and then put the clip object on the system-wide clipboard. The clip object can take one of three forms:

Текст
A text string. Put the string directly in the clip object, which you then put on the clipboard. To paste the string, get the clip object from the clipboard and copy the string into your application's storage.
URI
A Uri object representing any form of URI. This is primarily for copying complex data from a content provider. To copy data, put a Uri object into a clip object and put the clip object onto the clipboard. To paste the data, get the clip object, get the Uri object, resolve it to a data source, such as a content provider, and copy the data from the source into your application's storage.
Намерение
An Intent . This supports copying application shortcuts. To copy data, create an Intent , put it in a clip object, and put the clip object on the clipboard. To paste the data, get the clip object and then copy the Intent object into your application's memory area.

The clipboard holds only one clip object at a time. When an application puts a clip object on the clipboard, the previous clip object disappears.

If you want to let users paste data into your application, you don't have to handle all types of data. You can examine the data on the clipboard before you give users the option to paste it. Besides having a certain data form, the clip object also contains metadata that tells you what MIME types are available. This metadata helps you decide whether your application can do something useful with the clipboard data. For example, if you have an application that primarily handles text, you might want to ignore clip objects that contain a URI or intent.

You might also want to let users paste text regardless of the form of data on the clipboard. To do this, force the clipboard data into a text representation, and then paste this text. This is described in the Coerce the clipboard to text section.

Clipboard classes

This section describes the classes used by the clipboard framework.

ClipboardManager

The Android system clipboard is represented by the global ClipboardManager class. Don't instantiate this class directly. Instead, get a reference to it by invoking getSystemService(CLIPBOARD_SERVICE) .

ClipData, ClipData.Item, and ClipDescription

To add data to the clipboard, create a ClipData object that contains a description of the data and the data itself. The clipboard holds one ClipData at a time. A ClipData contains a ClipDescription object and one or more ClipData.Item objects.

A ClipDescription object contains metadata about the clip. In particular, it contains an array of available MIME types for the clip's data. Additionally, on Android 12 (API level 31) and higher, the metadata includes information about whether the object contains stylized text and about the type of text in the object . When you put a clip on the clipboard, this information is available to pasting applications, which can examine whether they can handle the clip data.

A ClipData.Item object contains the text, URI, or intent data:

Текст
A CharSequence .
URI
A Uri . This usually contains a content provider URI, although any URI is allowed. The application that provides the data puts the URI on the clipboard. Applications that want to paste the data get the URI from the clipboard and use it to access the content provider or other data source and retrieve the data.
Намерение
An Intent . This data type lets you copy an application shortcut to the clipboard. Users can then paste the shortcut into their applications for later use.

You can add more than one ClipData.Item object to a clip. This lets users copy and paste multiple selections as a single clip. For example, if you have a list widget that lets the user select more than one item at a time, you can copy all the items to the clipboard at once. To do this, create a separate ClipData.Item for each list item, and then add the ClipData.Item objects to the ClipData object.

ClipData convenience methods

The ClipData class provides static convenience methods for creating a ClipData object with a single ClipData.Item object and a simple ClipDescription object:

newPlainText(label, text)
Returns a ClipData object whose single ClipData.Item object contains a text string. The ClipDescription object's label is set to label . The single MIME type in ClipDescription is MIMETYPE_TEXT_PLAIN .

Use newPlainText() to create a clip from a text string.

newUri(resolver, label, URI)
Returns a ClipData object whose single ClipData.Item object contains a URI. The ClipDescription object's label is set to label . If the URI is a content URI—that is, if Uri.getScheme() returns content: —the method uses the ContentResolver object provided in resolver to retrieve the available MIME types from the content provider. It then stores them in ClipDescription . For a URI that isn't a content: URI, the method sets the MIME type to MIMETYPE_TEXT_URILIST .

Use newUri() to create a clip from a URI—particularly a content: URI.

newIntent(label, intent)
Returns a ClipData object whose single ClipData.Item object contains an Intent . The ClipDescription object's label is set to label . The MIME type is set to MIMETYPE_TEXT_INTENT .

Use newIntent() to create a clip from an Intent object.

Coerce the clipboard data to text

Even if your application only handles text, you can copy non-text data from the clipboard by converting it with the ClipData.Item.coerceToText() method.

This method converts the data in ClipData.Item to text and returns a CharSequence . The value that ClipData.Item.coerceToText() returns is based on the form of data in ClipData.Item :

Текст
If ClipData.Item is text—that is, if getText() isn't null—coerceToText() returns the text.
URI
If ClipData.Item is a URI—that is, if getUri() isn't null— coerceToText() tries to use it as a content URI.
  • If the URI is a content URI and the provider can return a text stream, coerceToText() returns a text stream.
  • If the URI is a content URI but the provider doesn't offer a text stream, coerceToText() returns a representation of the URI. The representation is the same as that returned by Uri.toString() .
  • If the URI isn't a content URI, coerceToText() returns a representation of the URI. The representation is the same as that returned by Uri.toString() .
Намерение
If ClipData.Item is an Intent —that is, if getIntent() isn't null— coerceToText() converts it to an Intent URI and returns it. The representation is the same as that returned by Intent.toUri(URI_INTENT_SCHEME) .

The clipboard framework is summarized in figure 2. To copy data, an application puts a ClipData object on the ClipboardManager global clipboard. The ClipData contains one or more ClipData.Item objects and one ClipDescription object. To paste data, an application gets the ClipData , gets its MIME type from the ClipDescription , and gets the data from the ClipData.Item or from the content provider referred to by ClipData.Item .

An image showing a block diagram of the copy and paste framework
Figure 2. The Android clipboard framework.

Copy to the clipboard

To copy data to the clipboard, get a handle to the global ClipboardManager object, create a ClipData object, and add a ClipDescription and one or more ClipData.Item objects to it. Then, add the finished ClipData object to the ClipboardManager object. This is described further in the following procedure:

  1. If you are copying data using a content URI, set up a content provider.
  2. Get the system clipboard:

    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
        }
    }

    Ява

    ...
    // 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. Copy the data to a new ClipData object:

    • For text

      Kotlin

      // Creates a new text clip to put on the clipboard.
      val clip: ClipData = ClipData.newPlainText("simple text", "Hello, World!")

      Ява

      // Creates a new text clip to put on the clipboard.
      ClipData clip = ClipData.newPlainText("simple text", "Hello, World!");
    • For a URI

      This snippet constructs a URI by encoding a record ID onto the content URI for the provider. This technique is covered in more detail in the Encoding an identifier on the URI section.

      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)

      Ява

      // 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);
    • For an intent

      This snippet constructs an Intent for an application and then puts it in the clip object:

      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)

      Ява

      // 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. Put the new clip object on the clipboard:

    Kotlin

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip)

    Ява

    // Set the clipboard's primary clip.
    clipboard.setPrimaryClip(clip);

Provide feedback when copying to the clipboard

Users expect visual feedback when an app copies content to the clipboard. This is done automatically for users in Android 13 and higher, but it must be manually implemented in prior versions.

Starting in Android 13, the system displays a standard visual confirmation when content is added to the clipboard. The new confirmation does the following:

  • Confirms the content was successfully copied.
  • Provides a preview of the copied content.

An animation showing Android 13 clipboard notification
Figure 3. UI shown when content enters the clipboard in Android 13 and up.

In Android 12L (API level 32) and lower, users might be unsure whether they successfully copied content or what they copied. This feature standardizes the various notifications shown by apps after copying and offers users more control over the clipboard.

Avoid duplicate notifications

In Android 12L (API level 32) and lower, we recommend alerting users when they successfully copy by issuing visual, in-app feedback, using a widget like a Toast or a Snackbar , after copying.

To avoid duplicate displays of information, we strongly recommend removing toasts or snackbars shown after an in-app copy for Android 13 and higher.

Post snackbar after an in-app copy.
Figure 4. If you show a copy confirmation snackbar in Android 13, the user sees duplicate messages.
Post toast after an in-app copy.
Figure 5. If you show a copy confirmation toast in Android 13, the user sees duplicate messages.

Here's an example of how to implement this:

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()
}

Add sensitive content to the clipboard

If your app lets users copy sensitive content to the clipboard, such as passwords or credit card information, you must add a flag to ClipDescription in ClipData before calling ClipboardManager.setPrimaryClip() . Adding this flag prevents sensitive content from appearing in the visual confirmation of copied content in Android 13 and higher.

Copied text preview without flagging sensitive content
Figure 6. Copied text preview without a sensitive content flag.
Copied text preview flagging sensitive content.
Figure 7. Copied text preview with a sensitive content flag.

To flag sensitive content, add a boolean extra to the ClipDescription . All apps must do this, regardless of the targeted API level.

// 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)
    }
}

Paste from the clipboard

As described previously, paste data from the clipboard by getting the global clipboard object, getting the clip object, looking at its data, and if possible copying the data from the clip object to your own storage. This section explains in detail how to paste the three forms of clipboard data.

Paste plain text

To paste plain text, get the global clipboard and verify that it can return plain text. Then get the clip object and copy its text to your own storage using getText() , as described in the following procedure:

  1. Get the global ClipboardManager object using getSystemService(CLIPBOARD_SERVICE) . Also, declare a global variable to contain the pasted text:

    Kotlin

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

    Ява

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
  2. Determine whether you need to enable or disable the "paste" option in the current activity. Verify that the clipboard contains a clip and that you can handle the type of data represented by the 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 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
        }
    }

    Ява

    // 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);
    }
  3. Copy the data from the clipboard. This point in the code is only reachable if the "paste" menu item is enabled, so you can assume that the clipboard contains plain text. You don't yet know if it contains a text string or a URI that points to plain text. The following code snippet tests this, but it only shows the code for handling plain text:

    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
                }
            }
        }
    }

    Ява

    // 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;
        }
    }

Paste data from a content URI

If the ClipData.Item object contains a content URI and you determine that you can handle one of its MIME types, create a ContentResolver and call the appropriate content provider method to retrieve the data.

The following procedure describes how to get data from a content provider based on a content URI on the clipboard. It checks whether a MIME type that the application can use is available from the provider.

  1. Declare a global variable to contain the MIME type:

    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"

    Ява

    // 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. Get the global clipboard. Also get a content resolver so you can access the content provider:

    Kotlin

    // Gets a handle to the Clipboard Manager.
    val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
    // Gets a content resolver instance.
    val cr = contentResolver

    Ява

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
  3. Get the primary clip from the clipboard and get its contents as a 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

    Ява

    // 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. Test whether the URI is a content URI by calling getType(Uri) . This method returns null if Uri doesn't point to a valid content provider.

    Kotlin

        // If the clipboard contains a URI reference...
        pasteUri?.let {
    
            // ...is this a content URI?
            val uriMimeType: String? = cr.getType(it)

    Ява

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
  5. Test whether the content provider supports a MIME type that the application understands. If it does, call ContentResolver.query() to get the data. The return value is a Cursor .

    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.
                }
            }
        }
    }

    Ява

            // 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();
                 }
             }
         }
    }

Paste an Intent

To paste an intent, first get the global clipboard. Examine the ClipData.Item object to see whether it contains an Intent . Then call getIntent() to copy the intent to your own storage. The following snippet demonstrates this:

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.
}

Ява

// 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.
}

System notification shown when your app accesses clipboard data

On Android 12 (API level 31) and higher, the system usually shows a toast message when your app calls getPrimaryClip() . The text inside the message contains the following format:

APP pasted from your clipboard

The system doesn't show a toast message when your app does one of the following:

  • Accesses ClipData from your own app.
  • Repeatedly accesses ClipData from a specific app. The toast appears only when your app accesses the data from that app for the first time.
  • Retrieves metadata for the clip object, such as by calling getPrimaryClipDescription() instead of getPrimaryClip() .

Use content providers to copy complex data

Content providers support copying complex data such as database records or file streams. To copy the data, put a content URI on the clipboard. Pasting applications then get this URI from the clipboard and use it to retrieve database data or file stream descriptors.

Since the pasting application only has the content URI for your data, it needs to know which piece of data to retrieve. You can provide this information by encoding an identifier for the data on the URI itself, or you can provide a unique URI that returns the data you want to copy. Which technique you choose depends on the organization of your data.

The following sections describe how to set up URIs, provide complex data, and provide file streams. The descriptions assume you are familiar with the general principles of content provider design.

Encode an identifier on the URI

A useful technique for copying data to the clipboard with a URI is to encode an identifier for the data on the URI itself. Your content provider can then get the identifier from the URI and use it to retrieve the data. The pasting application doesn't have to know that the identifier exists. It just has to get your "reference"—the URI plus the identifier—from the clipboard, give it your content provider, and get back the data.

You usually encode an identifier onto a content URI by concatenating it to the end of the URI. For example, suppose you define your provider URI as the following string:

"content://com.example.contacts"

If you want to encode a name onto this URI, use the following code snippet:

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)

Ява

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);

If you are already using a content provider, you might want to add a new URI path that indicates the URI is for copying. For example, suppose you already have the following URI paths:

"content://com.example.contacts/people"
"content://com.example.contacts/people/detail"
"content://com.example.contacts/people/images"

You can add another path that for copying URIs:

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

You can then detect a "copy" URI by pattern-matching and handle it with code that is specific for copying and pasting.

You normally use the encoding technique if you're already using a content provider, internal database, or internal table to organize your data. In these cases, you have multiple pieces of data you want to copy, and presumably a unique identifier for each piece. In response to a query from the pasting application, you can look up the data by its identifier and return it.

If you don't have multiple pieces of data, then you probably don't need to encode an identifier. You can use a URI that is unique to your provider. In response to a query, your provider returns the data it currently contains.

Copy data structures

Set up a content provider for copying and pasting complex data as a subclass of the ContentProvider component. Encode the URI you put on the clipboard so that it points to the exact record you want to provide. In addition, consider the existing state of your application:

  • If you already have a content provider, you can add to its functionality. You might only need to modify its query() method to handle URIs coming from applications that want to paste data. You probably want to modify the method to handle a "copy" URI pattern.
  • If your application maintains an internal database, you might want to move this database into a content provider to facilitate copying from it.
  • If you aren't using a database, you can implement a simple content provider whose sole purpose is to offer data to applications that are pasting from the clipboard.

In the content provider, override at least the following methods:

query()
Pasting applications assume they can get your data by using this method with the URI you put on the clipboard. To support copying, have this method detect URIs containing a special "copy" path. Your application can then create a "copy" URI to put on the clipboard, containing the copy path and a pointer to the exact record you want to copy.
getType()
This method must return the MIME types for the data you intend to copy. The method newUri() calls getType() to put the MIME types into the new ClipData object.

MIME types for complex data are described in Content providers .

You don't need to have any of the other content provider methods, such as insert() or update() . A pasting application only needs to get your supported MIME types and copy data from your provider. If you already have these methods, they won't interfere with copy operations.

The following snippets demonstrate how to set up your application to copy complex data:

  1. In the global constants for your application, declare a base URI string and a path that identifies URI strings you are using to copy data. Also declare a MIME type for the copied data.

    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"

    Ява

    // 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. In the activity users copy data from, set up the code to copy data to the clipboard. In response to a copy request, put the URI on the clipboard.

    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)
        }
    }

    Ява

    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);
  3. In the global scope of your content provider, create a URI matcher and add a URI pattern that matches URIs you put on the clipboard.

    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() {
        ...
    }

    Ява

    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. Set up the query() method. This method can handle different URI patterns, depending on how you code it, but only the pattern for the clipboard copying operation shows.

    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.
            }
        }
        ...
    }

    Ява

    // 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.
        ...
    }
  5. Set up the getType() method to return an appropriate MIME type for copied data:

    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
            ...
        }
    }

    Ява

    // Sets up your provider's getType() method.
    public String getType(Uri uri) {
        ...
        switch (sUriMatcher.match(uri)) {
        case GET_SINGLE_CONTACT:
            return (MIME_TYPE_CONTACT);
        ...
        }
    }

The Paste data from a content URI section describes how to get a content URI from the clipboard and use it to get and paste data.

Copy data streams

You can copy and paste large amounts of text and binary data as streams. The data can have forms such as the following:

  • Files stored on the actual device
  • Streams from sockets
  • Large amounts of data stored in a provider's underlying database system

A content provider for data streams provides access to its data with a file descriptor object, such as AssetFileDescriptor , instead of a Cursor object. The pasting application reads the data stream using this file descriptor.

To set up your application to copy a data stream with a provider, follow these steps:

  1. Set up a content URI for the data stream you are putting on the clipboard. Options for doing this include the following:
    • Encode an identifier for the data stream onto the URI, as described in the Encode an identifier on the URI section, and then maintain a table in your provider that contains identifiers and the corresponding stream name.
    • Encode the stream name directly on the URI.
    • Use a unique URI that always returns the current stream from the provider. If you use this option, remember to update your provider to point to a different stream whenever you copy the stream to the clipboard using the URI.
  2. Provide a MIME type for each type of data stream you plan to offer. Pasting applications need this information to determine whether they can paste the data on the clipboard.
  3. Implement one of the ContentProvider methods that returns a file descriptor for a stream. If you encode identifiers on the content URI, use this method to determine which stream to open.
  4. To copy the data stream to the clipboard, construct the content URI and place it on the clipboard.

To paste a data stream, an application gets the clip from the clipboard, gets the URI, and uses it in a call to a ContentResolver file descriptor method that opens the stream. The ContentResolver method calls the corresponding ContentProvider method, passing it the content URI. Your provider returns the file descriptor to the ContentResolver method. The pasting application then has the responsibility to read the data from the stream.

The following list shows the most important file descriptor methods for a content provider. Each of these has a corresponding ContentResolver method with the string "Descriptor" appended to the method name. For example, the ContentResolver analog of openAssetFile() is openAssetFileDescriptor() .

openTypedAssetFile()

This method returns an asset file descriptor, but only if the provided MIME type is supported by the provider. The caller—the application doing the pasting—provides a MIME type pattern. The content provider of the application that copies a URI to the clipboard returns an AssetFileDescriptor file handle if it can provide that MIME type and throws an exception if it can't.

This method handles subsections of files. You can use it to read assets that the content provider has copied to the clipboard.

openAssetFile()
This method is a more general form of openTypedAssetFile() . It doesn't filter for allowed MIME types, but it can read subsections of files.
openFile()
This is a more general form of openAssetFile() . It can't read subsections of files.

You can optionally use the openPipeHelper() method with your file descriptor method. This lets the pasting application read the stream data in a background thread using a pipe. To use this method, implement the ContentProvider.PipeDataWriter interface.

Design effective copy and paste functionality

To design effective copy and paste functionality for your application, remember these points:

  • At any time, there is only one clip on the clipboard. A new copy operation by any application in the system overwrites the previous clip. Since the user might navigate away from your application and copy before returning, you can't assume the clipboard contains the clip that the user previously copied in your application.
  • The intended purpose of multiple ClipData.Item objects per clip is to support copying and pasting of multiple selections rather than different forms of reference to a single selection. You usually want all of the ClipData.Item objects in a clip to have the same form. That is, they must all be simple text, content URI, or Intent , and not mixed.
  • When you provide data, you can offer different MIME representations. Add the MIME types you support to the ClipDescription , and then implement the MIME types in your content provider.
  • When you get data from the clipboard, your application is responsible for checking the available MIME types and then deciding which one, if any, to use. Even if there is a clip on the clipboard and the user requests a paste, your application isn't required to do the paste. Do the paste if the MIME type is compatible. You might coerce the data on the clipboard to text using coerceToText() . If your application supports more than one of the available MIME types, you can let the user pick which one to use.