复制和粘贴

Android 提供了一个功能强大的基于剪贴板的框架来进行复制和粘贴。它支持简单和复杂的数据类型,包括文本字符串、复杂数据结构、文本和二进制流数据以及应用程序资源。简单文本数据直接存储在剪贴板中,而复杂数据则存储为一个引用,粘贴应用程序使用内容提供程序解析该引用。复制和粘贴在应用程序内以及在实现该框架的应用程序之间均有效。

由于框架的一部分使用了内容提供程序,因此本文档假定您对 Android 内容提供程序 API 有所了解,该 API 在内容提供程序中进行了描述。

用户希望在将内容复制到剪贴板时获得反馈,因此除了支持复制和粘贴的框架之外,Android 在 Android 13(API 级别 33)及更高版本中向用户显示默认 UI。由于此功能,存在重复通知的风险。您可以在避免重复通知部分了解有关此极端情况的更多信息。

An animation showing Android 13 clipboard notification
图 1. 在 Android 13 及更高版本中,内容进入剪贴板时显示的 UI。

在 Android 12L(API 级别 32)及更低版本中,手动向用户提供复制反馈。请参阅本文档中的相关建议

剪贴板框架

当您使用剪贴板框架时,将数据放入剪贴对象,然后将剪贴对象放入系统范围的剪贴板。剪贴对象可以采用以下三种形式之一

文本
文本字符串。将字符串直接放入剪贴对象,然后将其放入剪贴板。要粘贴字符串,请从剪贴板获取剪贴对象并将字符串复制到应用程序的存储中。
URI
一个表示任何形式 URI 的Uri对象。这主要用于从内容提供程序复制复杂数据。要复制数据,请将Uri对象放入剪贴对象,然后将剪贴对象放入剪贴板。要粘贴数据,请获取剪贴对象,获取Uri对象,将其解析为数据源(例如内容提供程序),然后将数据从源复制到应用程序的存储中。
意图
一个Intent。这支持复制应用程序快捷方式。要复制数据,请创建一个Intent,将其放入剪贴对象,然后将剪贴对象放入剪贴板。要粘贴数据,请获取剪贴对象,然后将Intent对象复制到应用程序的内存区域。

剪贴板一次只能保存一个剪贴对象。当应用程序将剪贴对象放入剪贴板时,上一个剪贴对象将消失。

如果您想让用户将数据粘贴到您的应用程序中,则不必处理所有类型的数据。在向用户提供粘贴选项之前,您可以检查剪贴板上的数据。除了具有一定的数据形式外,剪贴对象还包含元数据,告诉您哪些 MIME 类型可用。此元数据可帮助您确定您的应用程序是否可以对剪贴板数据执行有用的操作。例如,如果您有一个主要处理文本的应用程序,则可能希望忽略包含 URI 或意图的剪贴对象。

您可能还想让用户无论剪贴板上的数据形式如何都粘贴文本。为此,请将剪贴板数据强制转换为文本表示形式,然后粘贴此文本。这在将剪贴板强制转换为文本部分进行了描述。

剪贴板类

本节介绍剪贴板框架使用的类。

ClipboardManager

Android 系统剪贴板由全局ClipboardManager类表示。不要直接实例化此类。而是通过调用getSystemService(CLIPBOARD_SERVICE)来获取对它的引用。

ClipData、ClipData.Item 和 ClipDescription

要向剪贴板添加数据,请创建一个包含数据描述和数据本身的ClipData对象。剪贴板一次保存一个ClipData。一个ClipData包含一个ClipDescription对象和一个或多个ClipData.Item对象。

一个ClipDescription对象包含有关剪贴的元数据。特别是,它包含剪贴数据可用 MIME 类型的数组。此外,在 Android 12(API 级别 31)及更高版本中,元数据还包含有关对象是否包含样式化文本以及有关对象中文本的类型的信息。当您将剪贴放入剪贴板时,粘贴应用程序可以使用此信息,它们可以检查自己是否可以处理剪贴数据。

一个ClipData.Item对象包含文本、URI 或意图数据

文本
一个CharSequence
URI
一个Uri。这通常包含内容提供程序 URI,尽管允许使用任何 URI。提供数据并将其放入剪贴板的应用程序。想要粘贴数据的应用程序从剪贴板获取 URI 并使用它来访问内容提供程序或其他数据源并检索数据。
意图
一个Intent。此数据类型允许您将应用程序快捷方式复制到剪贴板。然后,用户可以将其粘贴到他们的应用程序中以供以后使用。

您可以向剪贴添加多个ClipData.Item对象。这允许用户将多个选择作为单个剪贴进行复制和粘贴。例如,如果您有一个列表 widget 允许用户一次选择多个项目,则可以将所有项目一次复制到剪贴板。为此,请为每个列表项创建一个单独的ClipData.Item,然后将ClipData.Item对象添加到ClipData对象。

ClipData 便利方法

ClipData类提供静态便利方法,用于使用单个ClipData.Item对象和一个简单的ClipDescription对象创建ClipData对象

newPlainText(label, text)
返回一个ClipData对象,其单个ClipData.Item对象包含一个文本字符串。ClipDescription对象的标签设置为labelClipDescription中的唯一 MIME 类型是MIMETYPE_TEXT_PLAIN

使用newPlainText()从文本字符串创建剪贴。

newUri(resolver, label, URI)
返回一个 ClipData 对象,其唯一的 ClipData.Item 对象包含一个 URI。 ClipDescription 对象的标签设置为 label。如果 URI 是内容 URI(即,如果 Uri.getScheme() 返回 content:),则该方法使用 ContentResolver 对象(在 resolver 中提供)从内容提供程序检索可用的 MIME 类型。然后将其存储在 ClipDescription 中。对于不是 content: URI 的 URI,该方法将 MIME 类型设置为 MIMETYPE_TEXT_URILIST

使用 newUri() 从 URI(特别是 content: URI)创建剪贴板内容。

newIntent(label, intent)
返回一个 ClipData 对象,其唯一的 ClipData.Item 对象包含一个 IntentClipDescription 对象的标签设置为 label。MIME 类型设置为 MIMETYPE_TEXT_INTENT

使用 newIntent()Intent 对象创建剪贴板内容。

将剪贴板数据强制转换为文本

即使您的应用程序只处理文本,您也可以使用 ClipData.Item.coerceToText() 方法转换剪贴板上的非文本数据进行复制。

此方法将 ClipData.Item 中的数据转换为文本并返回一个 CharSequenceClipData.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.ItemIntent(即,如果 getIntent() 不为 null),则 coerceToText() 会将其转换为 Intent URI 并返回。该表示形式与 Intent.toUri(URI_INTENT_SCHEME) 返回的表示形式相同。

剪贴板框架总结如**图 2** 所示。要复制数据,应用程序会将 ClipData 对象放置在 ClipboardManager 全局剪贴板上。 ClipData 包含一个或多个 ClipData.Item 对象和一个 ClipDescription 对象。要粘贴数据,应用程序获取 ClipData,从 ClipDescription 获取其 MIME 类型,并从 ClipData.ItemClipData.Item 引用的内容提供程序获取数据。

An image showing a block diagram of the copy and paste framework
图 2. Android 剪贴板框架。

复制到剪贴板

要将数据复制到剪贴板,请获取全局 ClipboardManager 对象的句柄,创建一个 ClipData 对象,并向其中添加 ClipDescription 和一个或多个 ClipData.Item 对象。然后,将完成的 ClipData 对象添加到 ClipboardManager 对象中。这在以下过程中有更详细的说明。

  1. 如果您使用内容 URI 复制数据,请设置内容提供程序。
  2. 获取系统剪贴板

    Kotlin

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

    Java

    ...
    // If the user selects copy.
    case R.id.menu_copy:
    
    // Gets a handle to the clipboard service.
    ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
  3. 将数据复制到新的 ClipData 对象

    • 对于文本

      Kotlin

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

      Java

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

      此代码片段通过将记录 ID 编码到提供程序的内容 URI 上来构造 URI。此技术在URI 上编码标识符部分中有更详细的介绍。

      Kotlin

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

      Java

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

      此代码片段为应用程序构建了一个 Intent,然后将其放入剪贴板对象中。

      Kotlin

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

      Java

      // Creates the Intent.
      Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      ClipData clip = ClipData.newIntent("Intent", appIntent);
  4. 将新的剪贴板对象放置到剪贴板上

    Kotlin

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

    Java

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

复制到剪贴板时提供反馈

用户期望在应用程序将内容复制到剪贴板时获得视觉反馈。在 Android 13 及更高版本中,系统会自动为用户执行此操作,但在早期版本中必须手动实现。

从 Android 13 开始,系统会在将内容添加到剪贴板时显示标准的视觉确认。新的确认执行以下操作

  • 确认内容已成功复制。
  • 提供已复制内容的预览。

An animation showing Android 13 clipboard notification
图 3. Android 13 及更高版本中,内容进入剪贴板时显示的 UI。

在 Android 12L(API 级别 32)及更低版本中,用户可能不确定他们是否成功复制了内容或复制了什么内容。此功能标准化了应用程序复制后显示的各种通知,并为用户提供了更多对剪贴板的控制。

避免重复通知

在 Android 12L(API 级别 32)及更低版本中,我们建议在用户成功复制时通过发出视觉的应用内反馈来提醒他们,例如使用 ToastSnackbar 等小部件,在复制后显示。

为了避免重复显示信息,我们强烈建议删除在 Android 13 及更高版本中应用内复制后显示的吐司或 snackbar。

Post snackbar after an in-app copy.
图 4. 如果您在 Android 13 中显示复制确认 snackbar,用户会看到重复的消息。
Post toast after an in-app copy.
图 5. 如果您在 Android 13 中显示复制确认吐司,用户会看到重复的消息。

以下是如何实现此功能的示例

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

将敏感内容添加到剪贴板

如果您的应用程序允许用户将敏感内容(例如密码或信用卡信息)复制到剪贴板,则必须在调用 ClipboardManager.setPrimaryClip() 之前向 ClipData 中的 ClipDescription 添加一个标志。添加此标志可以防止敏感内容出现在 Android 13 及更高版本中复制内容的视觉确认中。

Copied text preview without flagging sensitive content
图 6. 未添加敏感内容标志的复制文本预览。
Copied text preview flagging sensitive content.
图 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. 使用 getSystemService(CLIPBOARD_SERVICE) 获取全局 ClipboardManager 对象。此外,声明一个全局变量来保存粘贴的文本。

    Kotlin

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

    Java

    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    String pasteData = "";
  2. 确定您是否需要在当前活动中启用或禁用“粘贴”选项。验证剪贴板是否包含剪贴板内容,以及您是否可以处理剪贴板内容表示的数据类型。

    Kotlin

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

    Java

    // Gets the ID of the "paste" menu item.
    MenuItem pasteItem = menu.findItem(R.id.menu_paste);
    
    // If the clipboard doesn't contain data, disable the paste menu item.
    // If it does contain data, decide whether you can handle the data.
    if (!(clipboard.hasPrimaryClip())) {
    
        pasteItem.setEnabled(false);
    
    } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {
    
        // Disables the paste menu item, since the clipboard has data but
        // it isn't plain text.
        pasteItem.setEnabled(false);
    } else {
    
        // Enables the paste menu item, since the clipboard contains plain text.
        pasteItem.setEnabled(true);
    }
  3. 从剪贴板复制数据。代码中的此点仅在启用“粘贴”菜单项时才能访问,因此您可以假设剪贴板包含纯文本。您还不知道它是否包含文本字符串或指向纯文本的 URI。以下代码片段对此进行了测试,但它只显示了处理纯文本的代码。

    Kotlin

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

    Java

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

从内容 URI 粘贴数据

如果 ClipData.Item 对象包含内容 URI,并且您确定可以处理其 MIME 类型之一,请创建一个 ContentResolver 并调用相应的内容提供程序方法来检索数据。

以下过程描述了如何根据剪贴板上的内容 URI 从内容提供程序获取数据。它会检查提供程序是否提供了应用程序可以使用的 MIME 类型。

  1. 声明一个全局变量来保存 MIME 类型。

    Kotlin

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

    Java

    // Declares a MIME type constant to match against the MIME types offered by
    // the provider.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
  2. 获取全局剪贴板。还可以获取内容解析器,以便您可以访问内容提供程序。

    Kotlin

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

    Java

    // Gets a handle to the Clipboard Manager.
    ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
    
    // Gets a content resolver instance.
    ContentResolver cr = getContentResolver();
  3. 从剪贴板获取主剪贴板内容,并将其内容作为 URI 获取。

    Kotlin

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

    Java

    // Gets the clipboard data from the clipboard.
    ClipData clip = clipboard.getPrimaryClip();
    
    if (clip != null) {
    
        // Gets the first item from the clipboard data.
        ClipData.Item item = clip.getItemAt(0);
    
        // Tries to get the item's contents as a URI.
        Uri pasteUri = item.getUri();
  4. 通过调用 getType(Uri) 测试 URI 是否为内容 URI。如果 Uri 不指向有效的内容提供程序,则此方法返回 null。

    Kotlin

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

    Java

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
  5. 测试内容提供程序是否支持应用程序理解的 MIME 类型。如果支持,则调用ContentResolver.query()获取数据。返回值为Cursor

    Kotlin

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

    Java

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

粘贴 Intent

要粘贴 Intent,首先获取全局剪贴板。检查ClipData.Item对象以查看其是否包含Intent。然后调用getIntent()将 Intent 复制到您自己的存储中。以下代码片段演示了这一点

Kotlin

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

Java

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

您的应用访问剪贴板数据时显示的系统通知

在 Android 12(API 级别 31)及更高版本上,当您的应用调用getPrimaryClip()时,系统通常会显示一条吐司消息。消息中的文本包含以下格式

APP pasted from your clipboard

当您的应用执行以下操作之一时,系统不会显示吐司消息

  • 访问您自己应用中的ClipData
  • 重复访问特定应用中的ClipData。只有当您的应用第一次从该应用访问数据时,吐司才会出现。
  • 检索剪贴对象的相关元数据,例如通过调用getPrimaryClipDescription()而不是getPrimaryClip()

使用内容提供程序复制复杂数据

内容提供程序支持复制复杂数据,例如数据库记录或文件流。要复制数据,请将内容 URI 放入剪贴板。然后,粘贴应用程序从剪贴板获取此 URI 并使用它来检索数据库数据或文件流描述符。

由于粘贴应用程序仅拥有您数据的 URI,因此它需要知道要检索哪个数据片段。您可以通过在 URI 本身编码数据标识符来提供此信息,或者您可以提供一个返回要复制数据的唯一 URI。您选择哪种技术取决于数据的组织方式。

以下部分介绍如何设置 URI、提供复杂数据和提供文件流。描述假设您熟悉内容提供程序设计的一般原则。

在 URI 上编码标识符

使用 URI 将数据复制到剪贴板的一个有用技术是在 URI 本身编码数据的标识符。然后,您的内容提供程序可以从 URI 获取标识符并使用它来检索数据。粘贴应用程序无需知道标识符的存在。它只需要从剪贴板获取您的“引用”(URI 加标识符),将其提供给您的内容提供程序,然后获取数据即可。

您通常通过将其连接到 URI 的末尾来在内容 URI 上编码标识符。例如,假设您将提供程序 URI 定义为以下字符串

"content://com.example.contacts"

如果要将名称编码到此 URI 上,请使用以下代码片段

Kotlin

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

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

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

Java

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

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

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

如果您已经在使用内容提供程序,则可能希望添加一个新的 URI 路径来指示 URI 用于复制。例如,假设您已经拥有以下 URI 路径

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

您可以添加另一个用于复制 URI 的路径

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

然后,您可以通过模式匹配检测“复制”URI 并使用特定于复制和粘贴的代码进行处理。

如果您已经在使用内容提供程序、内部数据库或内部表来组织数据,则通常使用编码技术。在这些情况下,您有多个要复制的数据片段,并且每个片段可能都有一个唯一的标识符。响应粘贴应用程序的查询,您可以按其标识符查找数据并将其返回。

如果您没有多个数据片段,则可能不需要编码标识符。您可以使用对您的提供程序唯一的 URI。响应查询,您的提供程序将返回其当前包含的数据。

复制数据结构

为复制和粘贴复杂数据设置内容提供程序,作为ContentProvider组件的子类。编码您放入剪贴板的 URI,使其指向您要提供的准确记录。此外,请考虑应用程序的现有状态

  • 如果您已经拥有内容提供程序,则可以将其功能添加到其中。您可能只需要修改其query()方法以处理来自想要粘贴数据的应用程序的 URI。您可能希望修改该方法以处理“复制”URI 模式。
  • 如果您的应用程序维护内部数据库,则可能希望将此数据库移动到内容提供程序以方便从中复制。
  • 如果您没有使用数据库,则可以实现一个简单的内容提供程序,其唯一目的是向从剪贴板粘贴的应用程序提供数据。

在内容提供程序中,至少覆盖以下方法

query()
粘贴应用程序假设它们可以使用此方法和您放入剪贴板的 URI 获取您的数据。为了支持复制,请让此方法检测包含特殊“复制”路径的 URI。然后,您的应用程序可以创建一个“复制”URI 并将其放入剪贴板,其中包含复制路径和指向您要复制的准确记录的指针。
getType()
此方法必须返回您打算复制的数据的 MIME 类型。方法newUri()调用getType()将 MIME 类型放入新的ClipData对象中。

复杂数据的 MIME 类型在内容提供程序中进行了描述。

您不需要任何其他内容提供程序方法,例如insert()update()。粘贴应用程序只需要获取您支持的 MIME 类型并从您的提供程序复制数据。如果您已经拥有这些方法,它们不会干扰复制操作。

以下代码片段演示了如何设置应用程序以复制复杂数据

  1. 在应用程序的全局常量中,声明一个基本 URI 字符串和一个标识您用于复制数据的 URI 字符串的路径。还要为复制的数据声明 MIME 类型。

    Kotlin

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

    Java

    // Declares the base URI string.
    private static final String CONTACTS = "content://com.example.contacts";
    
    // Declares a path string for URIs that you use to copy data.
    private static final String COPY_PATH = "/copy";
    
    // Declares a MIME type for the copied data.
    public static final String MIME_TYPE_CONTACT = "vnd.android.cursor.item/vnd.example.contact";
  2. 在用户从中复制数据的活动中,设置代码以将数据复制到剪贴板。响应复制请求,将 URI 放入剪贴板。

    Kotlin

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

    Java

    public class MyCopyActivity extends Activity {
        ...
    // The user has selected a name and is requesting a copy.
    case R.id.menu_copy:
    
        // Appends the last name to the base URI.
        // The name is stored in "lastName".
        uriString = CONTACTS + COPY_PATH + "/" + lastName;
    
        // Parses the string into a URI.
        Uri copyUri = Uri.parse(uriString);
    
        // Gets a handle to the clipboard service.
        ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
    
        ClipData clip = ClipData.newUri(getContentResolver(), "URI", copyUri);
    
        // Sets the clipboard's primary clip.
        clipboard.setPrimaryClip(clip);
  3. 在内容提供程序的全局范围内,创建一个 URI 匹配器并添加一个与您放入剪贴板的 URI 匹配的 URI 模式。

    Kotlin

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

    Java

    public class MyCopyProvider extends ContentProvider {
        ...
    // A Uri Match object that simplifies matching content URIs to patterns.
    private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
    // An integer to use in switching based on the incoming URI pattern.
    private static final int GET_SINGLE_CONTACT = 0;
    ...
    // Adds a matcher for the content URI. It matches
    // "content://com.example.contacts/copy/*"
    sUriMatcher.addURI(CONTACTS, "names/*", GET_SINGLE_CONTACT);
  4. 设置query()方法。此方法可以处理不同的 URI 模式,具体取决于您的编码方式,但仅显示剪贴板复制操作的模式。

    Kotlin

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

    Java

    // Sets up your provider's query() method.
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
        String sortOrder) {
        ...
        // Switch based on the incoming content URI.
        switch (sUriMatcher.match(uri)) {
    
        case GET_SINGLE_CONTACT:
    
            // Queries and returns the contact for the requested name. Decodes the
            // incoming URI, queries the data model based on the last name, and
            // returns the result as a Cursor.
        ...
    }
  5. 设置getType()方法以返回复制数据的适当 MIME 类型

    Kotlin

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

    Java

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

从内容 URI 粘贴数据部分介绍了如何从剪贴板获取内容 URI 并使用它获取和粘贴数据。

复制数据流

您可以将大量文本和二进制数据作为流复制和粘贴。数据可以具有以下形式

  • 存储在实际设备上的文件
  • 来自套接字的流
  • 存储在提供程序底层数据库系统中的大量数据

用于数据流的内容提供程序使用文件描述符对象(例如AssetFileDescriptor)而不是Cursor对象来访问其数据。粘贴应用程序使用此文件描述符读取数据流。

要设置您的应用程序以使用提供程序复制数据流,请按照以下步骤操作

  1. 为要放入剪贴板的数据流设置内容 URI。执行此操作的选项包括以下内容
    • 在 URI 上编码数据流的标识符,如在 URI 上编码标识符部分所述,然后在您的提供程序中维护一个包含标识符和相应流名称的表。
    • 直接在 URI 上编码流名称。
    • 使用始终返回提供程序中当前流的唯一 URI。如果您使用此选项,请记住在每次使用 URI 将流复制到剪贴板时更新您的提供程序以指向不同的流。
  2. 为计划提供的每种数据流类型提供 MIME 类型。粘贴应用程序需要此信息来确定它们是否可以粘贴剪贴板上的数据。
  3. 实现返回流文件描述符的ContentProvider方法之一。如果您在内容 URI 上编码标识符,请使用此方法确定要打开哪个流。
  4. 要将数据流复制到剪贴板,请构造内容 URI 并将其放入剪贴板。

要粘贴数据流,应用程序从剪贴板获取剪贴,获取 URI,并将其用于对ContentResolver文件描述符方法的调用,该方法打开流。ContentResolver方法调用相应的ContentProvider方法,并将内容 URI 传递给它。您的提供程序将文件描述符返回给ContentResolver方法。然后,粘贴应用程序有责任从流中读取数据。

以下列表显示了内容提供程序最重要的文件描述符方法。每个方法都有一个相应的ContentResolver方法,并在方法名称后面附加了“Descriptor”字符串。例如,openAssetFile()ContentResolver模拟方法为openAssetFileDescriptor()

openTypedAssetFile()

此方法返回资产文件描述符,但前提是提供程序支持提供的 MIME 类型。调用方(执行粘贴操作的应用程序)提供 MIME 类型模式。复制 URI 到剪贴板的应用程序的内容提供程序返回AssetFileDescriptor文件句柄(如果它可以提供该 MIME 类型),如果它不能提供则抛出异常。

此方法处理文件的子部分。您可以使用它来读取内容提供程序已复制到剪贴板的资产。

openAssetFile()
此方法是openTypedAssetFile()的更通用形式。它不筛选允许的 MIME 类型,但可以读取文件的子部分。
openFile()
这是openAssetFile()的更通用形式。它无法读取文件的子部分。

您可以选择使用openPipeHelper()方法和您的文件描述符方法。这允许粘贴应用程序使用管道在后台线程中读取流数据。要使用此方法,请实现ContentProvider.PipeDataWriter接口。

设计有效的复制和粘贴功能

要为您的应用程序设计有效的复制和粘贴功能,请记住以下几点

  • 在任何时间,剪贴板中只存在一个剪贴内容。系统中任何应用程序的新复制操作都会覆盖之前的剪贴内容。由于用户可能会离开您的应用程序并在返回之前进行复制,因此您无法假设剪贴板包含用户之前在您的应用程序中复制的剪贴内容。
  • 每个剪贴内容包含多个ClipData.Item对象的预期用途是支持多个选择的复制粘贴,而不是对单个选择的不同形式的引用。通常,您希望一个剪贴内容中的所有ClipData.Item对象都具有相同的形式。也就是说,它们必须全部是简单文本、内容 URI 或Intent,而不能混合。
  • 当您提供数据时,您可以提供不同的 MIME 表示形式。将您支持的 MIME 类型添加到ClipDescription中,然后在您的内容提供程序中实现这些 MIME 类型。
  • 当您从剪贴板获取数据时,您的应用程序负责检查可用的 MIME 类型,然后决定使用哪一个(如果有)。即使剪贴板上有剪贴内容并且用户请求粘贴,您的应用程序也不必执行粘贴操作。如果 MIME 类型兼容,则执行粘贴操作。您可以使用coerceToText()将剪贴板上的数据强制转换为文本。如果您的应用程序支持多个可用的 MIME 类型,您可以让用户选择使用哪一个。