Как копировать текст расшифровки

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

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

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

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

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

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

Фреймворк буфера обмена

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

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

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

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

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

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

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

ClipboardManager

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

ClipData, ClipData.Item и ClipDescription

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

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

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

Текст
A CharSequence.
URI
A 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, который не является 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. Получите доступ к системному буферу обмена:

    Kotlin

    when(menuItem.itemId) {
        ...
        R.id.menu_copy -> { // if the user selects copy
            // Gets a handle to the clipboard service.
            val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        }
    }

    Java

    ...
    // If the user selects copy.
    case R.id.menu_copy:
    
    // Gets a handle to the clipboard service.
    ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
  3. Скопируйте данные в новый объект ClipData:

    • Для текста

      Kotlin

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

      Java

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

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

      Kotlin

      // Creates a Uri using a base Uri and a record ID based on the contact's last
      // name. Declares the base URI string.
      const val CONTACTS = "content://com.example.contacts"
      
      // Declares a path string for URIs, used to copy data.
      const val COPY_PATH = "/copy"
      
      // Declares the Uri to paste to the clipboard.
      val copyUri: Uri = Uri.parse("$CONTACTS$COPY_PATH/$lastName")
      ...
      // Creates a new URI clip object. The system uses the anonymous
      // getContentResolver() object to get MIME types from provider. The clip object's
      // label is "URI", and its data is the Uri previously created.
      val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri)

      Java

      // Creates a Uri using a base Uri and a record ID based on the contact's last
      // name. Declares the base URI string.
      private static final String CONTACTS = "content://com.example.contacts";
      
      // Declares a path string for URIs, used to copy data.
      private static final String COPY_PATH = "/copy";
      
      // Declares the Uri to paste to the clipboard.
      Uri copyUri = Uri.parse(CONTACTS + COPY_PATH + "/" + lastName);
      ...
      // Creates a new URI clip object. The system uses the anonymous
      // getContentResolver() object to get MIME types from provider. The clip object's
      // label is "URI", and its data is the Uri previously created.
      ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
    • Для намерения

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

      Kotlin

      // Creates the Intent.
      val appIntent = Intent(this, com.example.demo.myapplication::class.java)
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      val clip: ClipData = ClipData.newIntent("Intent", appIntent)

      Java

      // Creates the Intent.
      Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      ClipData clip = ClipData.newIntent("Intent", appIntent);
  4. Поместите новый объект в буфер обмена:

    Kotlin

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

    Java

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

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

Если ваше приложение позволяет пользователям копировать в буфер обмена конфиденциальные данные, например пароли или информацию о кредитных картах, перед вызовом ClipboardManager.setPrimaryClip() необходимо добавить флаг в ClipDescription в ClipData. Это предотвратит появление конфиденциального контента в визуальном подтверждении скопированного контента в 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 и скопируйте его текст в свое хранилище с помощью getText(), как описано ниже.

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

    Kotlin

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

    Java

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

    Kotlin

    // Gets the ID of the "paste" menu item.
    val pasteItem: MenuItem = menu.findItem(R.id.menu_paste)
    
    // If the clipboard doesn't contain data, disable the paste menu item.
    // If it does contain data, decide whether you can handle the data.
    pasteItem.isEnabled = when {
        !clipboard.hasPrimaryClip() -> {
            false
        }
        !(clipboard.primaryClipDescription.hasMimeType(MIMETYPE_TEXT_PLAIN)) -> {
            // Disables the paste menu item, since the clipboard has data but it
            // isn't plain text.
            false
        }
        else -> {
            // Enables the paste menu item, since the clipboard contains plain text.
            true
        }
    }

    Java

    // Gets the ID of the "paste" menu item.
    MenuItem pasteItem = menu.findItem(R.id.menu_paste);
    
    // If the clipboard doesn't contain data, disable the paste menu item.
    // If it does contain data, decide whether you can handle the data.
    if (!(clipboard.hasPrimaryClip())) {
    
        pasteItem.setEnabled(false);
    
    } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    
        // Disables the paste menu item, since the clipboard has data but
        // it isn't plain text.
        pasteItem.setEnabled(false);
    } else {
    
        // Enables the paste menu item, since the clipboard contains plain text.
        pasteItem.setEnabled(true);
    }
  3. Скопируйте данные из буфера обмена. Эта точка в коде доступна, только если пункт меню "Вставить" включен, поэтому можно предположить, что буфер обмена содержит обычный текст. Вы пока не знаете, содержит ли оно текстовую строку или URI, указывающий на обычный текст. В следующем фрагменте кода показано, как это проверить, но только для обычного текста:

    Kotlin

    when (menuItem.itemId) {
        ...
        R.id.menu_paste -> {    // Responds to the user selecting "paste".
            // Examines the item on the clipboard. If getText() doesn't return null,
            // the clip item contains the text. Assumes that this application can only
            // handle one item at a time.
            val item = clipboard.primaryClip.getItemAt(0)
    
            // Gets the clipboard as text.
            pasteData = item.text
    
            return if (pasteData != null) {
                // If the string contains data, then the paste operation is done.
                true
            } else {
                // The clipboard doesn't contain text. If it contains a URI,
                // attempts to get data from it.
                val pasteUri: Uri? = item.uri
    
                if (pasteUri != null) {
                    // If the URI contains something, try to get text from it.
    
                    // Calls a routine to resolve the URI and get data from it.
                    // This routine isn't presented here.
                    pasteData = resolveUri(pasteUri)
                    true
                } else {
    
                    // Something is wrong. The MIME type was plain text, but the
                    // clipboard doesn't contain text or a Uri. Report an error.
                    Log.e(TAG,"Clipboard contains an invalid data type")
                    false
                }
            }
        }
    }

    Java

    // Responds to the user selecting "paste".
    case R.id.menu_paste:
    
    // Examines the item on the clipboard. If getText() does not return null,
    // the clip item contains the text. Assumes that this application can only
    // handle one item at a time.
     ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);
    
    // Gets the clipboard as text.
    pasteData = item.getText();
    
    // If the string contains data, then the paste operation is done.
    if (pasteData != null) {
        return true;
    
    // The clipboard doesn't contain text. If it contains a URI, attempts to get
    // data from it.
    } else {
        Uri pasteUri = item.getUri();
    
        // If the URI contains something, try to get text from it.
        if (pasteUri != null) {
    
            // Calls a routine to resolve the URI and get data from it.
            // This routine isn't presented here.
            pasteData = resolveUri(Uri);
            return true;
        } else {
    
            // Something is wrong. The MIME type is plain text, but the
            // clipboard doesn't contain text or a Uri. Report an error.
            Log.e(TAG, "Clipboard contains an invalid data type");
            return false;
        }
    }

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

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

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

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

    Kotlin

    // Declares a MIME type constant to match against the MIME types offered
    // by the provider.
    const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"

    Java

    // Declares a MIME type constant to match against the MIME types offered by
    // the provider.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
  2. Получить глобальный буфер обмена. Также получите преобразователь контента, чтобы получить доступ к поставщику контента:

    Kotlin

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

    Java

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

    Kotlin

    // Gets the clipboard data from the clipboard.
    val clip: ClipData? = clipboard.primaryClip
    
    clip?.run {
    
        // Gets the first item from the clipboard data.
        val item: ClipData.Item = getItemAt(0)
    
        // Tries to get the item's contents as a URI.
        val pasteUri: Uri? = item.uri

    Java

    // Gets the clipboard data from the clipboard.
    ClipData clip = clipboard.getPrimaryClip();
    
    if (clip != null) {
    
        // Gets the first item from the clipboard data.
        ClipData.Item item = clip.getItemAt(0);
    
        // Tries to get the item's contents as a URI.
        Uri pasteUri = item.getUri();
  4. Проверьте, является ли URI URI контента, вызвав getType(Uri). Этот метод возвращает значение null, если Uri не указывает на действительного поставщика контента.

    Kotlin

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

    Java

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

    Java

            // If the return value isn't null, the Uri is a content Uri.
            if (uriMimeType != null) {
    
                // Does the content provider offer a MIME type that the current
                // application can use?
                if (uriMimeType.equals(MIME_TYPE_CONTACT)) {
    
                    // Get the data from the content provider.
                    Cursor pasteCursor = cr.query(uri, null, null, null, null);
    
                    // If the Cursor contains data, move to the first record.
                    if (pasteCursor != null) {
                        if (pasteCursor.moveToFirst()) {
    
                        // Get the data from the Cursor here.
                        // The code varies according to the format of the data model.
                        }
                    }
    
                    // Close the Cursor.
                    pasteCursor.close();
                 }
             }
         }
    }

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

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

Kotlin

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

// Checks whether the clip item contains an Intent by testing whether
// getIntent() returns null.
val pasteIntent: Intent? = clipboard.primaryClip?.getItemAt(0)?.intent

if (pasteIntent != null) {

    // Handle the Intent.

} else {

    // Ignore the clipboard, or issue an error if
    // you expect an Intent to be on the clipboard.
}

Java

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

// Checks whether the clip item contains an Intent, by testing whether
// getIntent() returns null.
Intent pasteIntent = clipboard.getPrimaryClip().getItemAt(0).getIntent();

if (pasteIntent != null) {

    // Handle the Intent.

} else {

    // Ignore the clipboard, or issue an error if
    // you expect an Intent to be on the clipboard.
}

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

На устройствах с 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, используйте следующий фрагмент кода:

Kotlin

val uriString = "content://com.example.contacts/Smith"

// uriString now contains content://com.example.contacts/Smith.

// Generates a uri object from the string representation.
val copyUri = Uri.parse(uriString)

Java

String uriString = "content://com.example.contacts" + "/" + "Smith";

// uriString now contains content://com.example.contacts/Smith.

// Generates a uri object from the string representation.
Uri copyUri = Uri.parse(uriString);

Если вы уже используете поставщика контента, добавьте новый путь 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 "copy".
  • Если в вашем приложении есть внутренняя база данных, вы можете переместить ее в поставщик контента, чтобы упростить копирование из нее.
  • Если вы не используете базу данных, вы можете реализовать простой поставщик контента, единственной целью которого является предоставление данных приложениям, которые вставляют данные из буфера обмена.

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

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

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

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

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

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

    Kotlin

    // Declares the base URI string.
    private const val CONTACTS = "content://com.example.contacts"
    
    // Declares a path string for URIs that you use to copy data.
    private const val COPY_PATH = "/copy"
    
    // Declares a MIME type for the copied data.
    const val MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact"

    Java

    // Declares the base URI string.
    private static final String CONTACTS = "content://com.example.contacts";
    
    // Declares a path string for URIs that you use to copy data.
    private static final String COPY_PATH = "/copy";
    
    // Declares a MIME type for the copied data.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
  2. В действии, из которого пользователи копируют данные, настройте код для копирования данных в буфер обмена. В ответ на запрос копирования поместите URI в буфер обмена.

    Kotlin

    class MyCopyActivity : Activity() {
        ...
    when(item.itemId) {
        R.id.menu_copy -> { // The user has selected a name and is requesting a copy.
            // Appends the last name to the base URI.
            // The name is stored in "lastName".
            uriString = "$CONTACTS$COPY_PATH/$lastName"
    
            // Parses the string into a URI.
            val copyUri: Uri? = Uri.parse(uriString)
    
            // Gets a handle to the clipboard service.
            val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    
            val clip: ClipData = ClipData.newUri(contentResolver, "URI", copyUri)
    
            // Sets the clipboard's primary clip.
            clipboard.setPrimaryClip(clip)
        }
    }

    Java

    public class MyCopyActivity extends Activity {
        ...
    // The user has selected a name and is requesting a copy.
    case R.id.menu_copy:
    
        // Appends the last name to the base URI.
        // The name is stored in "lastName".
        uriString = CONTACTS + COPY_PATH + "/" + lastName;
    
        // Parses the string into a URI.
        Uri copyUri = Uri.parse(uriString);
    
        // Gets a handle to the clipboard service.
        ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
    
        ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
    
        // Sets the clipboard's primary clip.
        clipboard.setPrimaryClip(clip);
  3. В глобальной области видимости поставщика контента создайте сопоставитель URI и добавьте шаблон URI, который соответствует URI, помещаемым в буфер обмена.

    Kotlin

    // A Uri Match object that simplifies matching content URIs to patterns.
    private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    
        // Adds a matcher for the content URI. It matches.
        // "content://com.example.contacts/copy/*"
        addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT)
    }
    
    // An integer to use in switching based on the incoming URI pattern.
    private const val GET_SINGLE_CONTACT = 0
    ...
    class MyCopyProvider : ContentProvider() {
        ...
    }

    Java

    public class MyCopyProvider extends ContentProvider {
        ...
    // A Uri Match object that simplifies matching content URIs to patterns.
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
    // An integer to use in switching based on the incoming URI pattern.
    private static final int GET_SINGLE_CONTACT = 0;
    ...
    // Adds a matcher for the content URI. It matches
    // "content://com.example.contacts/copy/*"
    sUriMatcher.addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT);
  4. Настройте метод query(). Этот метод может обрабатывать разные шаблоны URI в зависимости от того, как вы его закодируете, но показывается только шаблон для операции копирования в буфер обмена.

    Kotlin

    // Sets up your provider's query() method.
    override fun query(
            uri: Uri,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        ...
        // When based on the incoming content URI:
        when(sUriMatcher.match(uri)) {
    
            GET_SINGLE_CONTACT -> {
    
                // Queries and returns the contact for the requested name. Decodes
                // the incoming URI, queries the data model based on the last name,
                // and returns the result as a Cursor.
            }
        }
        ...
    }

    Java

    // Sets up your provider's query() method.
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
        ...
        // Switch based on the incoming content URI.
        switch (sUriMatcher.match(uri)) {
    
        case GET_SINGLE_CONTACT:
    
            // Queries and returns the contact for the requested name. Decodes the
            // incoming URI, queries the data model based on the last name, and
            // returns the result as a Cursor.
        ...
    }
  5. Настройте метод getType(), чтобы возвращать подходящий MIME-тип для скопированных данных:

    Kotlin

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

    Java

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

В разделе Вставка данных из 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-тип совместим. Вы можете принудительно преобразовать данные в буфере обмена в текст с помощью coerceToText(). Если ваше приложение поддерживает несколько доступных MIME-типов, вы можете предоставить пользователю возможность выбрать один из них.