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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ClipData, ClipData.Item и ClipDescription

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Котлин

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

    Ява

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

    • Для текста

      Котлин

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

      Ява

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

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

      Котлин

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

      Ява

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

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

      Котлин

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

      Ява

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

    Котлин

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

    Ява

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

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

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

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

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

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

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

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

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

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

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

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

fun textCopyThenPost(textCopied:String) {
    val clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
    // When setting the clipboard text.
    clipboardManager.setPrimaryClip(ClipData.newPlainText   ("", textCopied))
    // Only show a toast for Android 12 and lower.
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
        Toast.makeText(context, Copied, Toast.LENGTH_SHORT).show()
}

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

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

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

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

// If your app is compiled with the API level 33 SDK or higher.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true)
    }
}

// If your app is compiled with a lower SDK.
clipData.apply {
    description.extras = PersistableBundle().apply {
        putBoolean("android.content.extra.IS_SENSITIVE", true)
    }
}

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

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

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

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

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

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

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

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

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

            // If the return value isn't null, the Uri is a content Uri.
            uriMimeType?.takeIf {
    
                // Does the content provider offer a MIME type that the current
                // application can use?
                it == MIME_TYPE_CONTACT
            }?.apply {
    
                // Get the data from the content provider.
                cr.query(pasteUri, null, null, null, null)?.use { pasteCursor ->
    
                    // If the Cursor contains data, move to the first record.
                    if (pasteCursor.moveToFirst()) {
    
                        // Get the data from the Cursor here.
                        // The code varies according to the format of the data model.
                    }
    
                    // Kotlin `use` automatically closes the Cursor.
                }
            }
        }
    }
    

    Ява

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

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

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

Котлин

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

Ява

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

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

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

APP pasted from your clipboard

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

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

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

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

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

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

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

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

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

"content://com.example.contacts"

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

Котлин

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

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

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

Ява

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

    Котлин

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

    Ява

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

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

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

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

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

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

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

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

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

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

openTypedAssetFile()

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

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

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

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

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

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

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