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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Системный буфер обмена 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 имеет тип text — то есть, если 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 и возвращает его. Представление совпадает с представлением, возвращаемым методом 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
        }
    }

    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 :

    • Для текста

      Котлин

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

      Котлин

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

      Котлин

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

    Котлин

    // 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 и выше.

Разместите 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)
    }
}

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

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

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

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

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

    Котлин

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

    Java

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

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

    Котлин

    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-типа:

    Котлин

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

    Котлин

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

    Котлин

    // 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 не указывает на допустимый поставщик контента.

    Котлин

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

    Котлин

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

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

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

Котлин

// 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) и выше система обычно отображает всплывающее сообщение (toast) при вызове функцией 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)

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

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

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"

    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 в буфер обмена.

    Котлин

    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, скопированным в буфер обмена.

    Котлин

    // 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 в зависимости от способа его написания, но отображается только шаблон для операции копирования из буфера обмена.

    Котлин

    // 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-тип для скопированных данных:

    Котлин

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