複製及貼上

Android 平台透過強大的剪貼簿架構,提供複製及貼上功能。支援簡單和複雜的資料類型,包括文字字串、複雜資料結構、文字和二進位串流資料,以及應用程式資產。簡易文字資料會直接儲存在剪貼簿中,而複雜的資料則會儲存為參照,由執行貼上操作的應用程式利用內容供應器進行解析。無論是在應用程式內,還是在實作該架構的應用程式之間,複製及貼上皆能正常運作。

由於該架構的一部分使用內容供應器,因此本文假設您對 Android 內容供應器 API 有一定程度的瞭解。詳情請參閱「內容供應器」。

使用者預期在將內容複製到剪貼簿時獲得回饋,因此除了支援複製及貼上功能的架構外,Android 13 (API 級別 33) 以上版本也會在使用者複製內容時顯示預設 UI。由於這項功能,可能會收到重複的通知。如要進一步瞭解這個極端案例,請參閱「避免重複通知」一節。

顯示 Android 13 剪貼簿通知的動畫
圖 1. 在 Android 13 以上版本中,當內容進入剪貼簿時會顯示的 UI。

在 Android 12L (API 級別 32) 以下版本中複製內容時,手動向使用者提供意見回饋。請參閱本文件的相關建議

剪貼簿架構

使用剪貼簿架構時,請將資料放入剪輯物件,然後將剪輯物件放入系統全域剪貼簿。剪輯物件可採用下列任一格式:

Text
文字字串。直接將字串放在剪輯物件中,再將物件放入剪貼簿。如要貼上字串,請從剪貼簿取得剪輯物件,然後將字串複製到應用程式儲存空間。
URI
Uri 物件,代表任何形式的 URI。這主要用於複製內容供應器提供的複雜資料。如要複製資料,請將 Uri 物件放入剪輯物件中,然後將剪輯物件放入剪貼簿。如要貼上資料,請取得剪輯物件、取得 Uri 物件,並將其解析為資料來源 (例如內容供應器),然後將來源中的資料複製到應用程式儲存空間。
意圖
Intent。可支援複製應用程式捷徑。如要複製資料,請建立 Intent,將其放入剪輯物件中,然後將剪輯物件放入剪貼簿。如要貼上資料,請取得剪輯物件,然後將 Intent 物件複製到應用程式的記憶體區域。

剪貼簿一次只能保留一個剪輯物件。當應用程式將剪輯物件放入剪貼簿時,先前的剪輯物件會消失。

如要讓使用者將資料貼入應用程式,則不必處理所有類型的資料。您可以先檢查剪貼簿中的資料,再將資料提供貼上選項。 除了特定資料形式以外,剪輯物件中還包含中繼資料,讓您知道有哪些 MIME 類型可用。此中繼資料可協助您判斷應用程式能否利用剪貼簿資料執行某些操作。舉例來說,如果您的應用程式主要處理文字,建議您忽略含有 URI 或意圖的剪輯物件。

您也可以允許使用者貼上文字,無論剪貼簿中的資料形式為何。方法是將剪貼簿資料強制轉換成文字,然後貼上這段文字。相關說明請參閱「將剪貼簿轉換成文字」一節。

剪貼簿類別

本節說明剪貼簿架構使用的類別。

ClipboardManager

Android 系統剪貼簿是以全域 ClipboardManager 類別表示。請勿直接將這個類別例項化。請改為叫用 getSystemService(CLIPBOARD_SERVICE) 來取得該項目的參照。

ClipData、ClipData.Item 與 ClipDescription

如要將資料新增至剪貼簿,請建立包含資料和資料說明的 ClipData 物件。剪貼簿一次保留一個 ClipDataClipData 包含 ClipDescription 物件和一或多個 ClipData.Item 物件。

ClipDescription 物件中包含關於剪輯的中繼資料。具體來說,其中包含片段資料可用的 MIME 類型陣列。此外,在 Android 12 (API 級別 31) 以上版本中,中繼資料會包含相關資訊,說明物件是否包含風格化文字,以及物件中的文字類型。將剪輯複製到剪貼簿時,系統會向執行貼上操作的應用程式提供這項資訊,檢查應用程式是否能處理剪輯資料。

ClipData.Item 物件包含文字、URI 或意圖資料:

Text
CharSequence
URI
Uri。通常包含內容供應器 URI,但可使用任何 URI。提供資料的應用程式會將 URI 複製到剪貼簿。想要貼上資料的應用程式會從剪貼簿取得 URI,並用來存取內容供應器或其他資料來源,並擷取資料。
意圖
Intent。這個資料類型可讓您將應用程式捷徑複製到剪貼簿。之後,使用者即可將捷徑貼到自己的應用程式中,方便日後使用。

您可以在一個剪輯中加入多個 ClipData.Item 物件。如此一來,使用者就能複製多個選項,並貼到單一短片中。舉例來說,如果您的清單小工具可讓使用者一次選取多個項目,您可以一次將所有項目複製到剪貼簿。方法是為每個清單項目分別建立 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:,這個方法會使用 resolver 提供的 ContentResolver 物件,從內容供應器擷取可用的 MIME 類型。然後將金鑰儲存在 ClipDescription 中。如果 URI 不是 content: 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 中的資料形式:

Text
如果 ClipData.Item 是文字 (也就是如果 getText() 不是空值),coerceToText() 會傳回文字。
URI
如果 ClipData.Item 是 URI (也就是如果 getUri() 不是空值),coerceToText() 會嘗試將其視為內容 URI。
  • 如果 URI 是內容 URI,且供應器可以傳回文字串流,coerceToText() 就會傳回文字串流。
  • 如果 URI 是內容 URI,但供應器未提供文字串流,coerceToText() 會傳回 URI 的表示形式。此表示與 Uri.toString() 傳回的表示形式相同。
  • 如果 URI 不是內容 URI,coerceToText() 會傳回 URI 的表示形式。此表示與 Uri.toString() 傳回的表示形式相同。
意圖
如果 ClipData.ItemIntent (也就是 getIntent() 不是空值),coerceToText() 會將其轉換為意圖 URI 並傳回。此表示形式與 Intent.toUri(URI_INTENT_SCHEME) 傳回的表示形式相同。

圖 2 概述了剪貼簿架構。為了複製資料,應用程式會將 ClipData 物件放入 ClipboardManager 全域剪貼簿中。ClipData 包含一或多個 ClipData.Item 物件和一個 ClipDescription 物件。為了貼上資料,應用程式會取得 ClipData,從 ClipDescription 取得其 MIME 類型,並從 ClipData.ItemClipData.Item 參照的內容供應器中取得相關資料。

這張圖片顯示複製及貼上架構的區塊圖
圖 2.Android 剪貼簿架構。

複製到剪貼簿

如要將資料複製到剪貼簿,請取得全域 ClipboardManager 物件的控制代碼、建立 ClipData 物件,並在其中新增 ClipDescription 和一或多個 ClipData.Item 物件。接著,將完成的 ClipData 物件新增至 ClipboardManager 物件。請參閱以下程序的詳細說明:

  1. 如要使用內容 URI 複製資料,請設定內容供應器。
  2. 取得系統剪貼簿:

    Kotlin

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

    Java

    ...
    // If the user selects copy.
    case R.id.menu_copy:
    
    // Gets a handle to the clipboard service.
    ClipboardManager clipboard = (ClipboardManager)
            getSystemService(Context.CLIPBOARD_SERVICE);
    
  3. 將資料複製到新的 ClipData 物件:

    • 文字

      Kotlin

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

      Java

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

      這段程式碼會將記錄 ID 編碼至供應器的內容 URI,藉此建構 URI。如要進一步瞭解這項技巧,請參閱「在 URI 中將 ID 編碼」一節。

      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,然後將其放入剪輯物件中:

      Kotlin

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

      Java

      // Creates the Intent.
      Intent appIntent = new Intent(this, com.example.demo.myapplication.class);
      ...
      // Creates a clip object with the Intent in it. Its label is "Intent"
      // and its data is the Intent object created previously.
      ClipData clip = ClipData.newIntent("Intent", appIntent);
      
  4. 將新的剪輯物件放在剪貼簿中:

    Kotlin

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

    Java

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

在複製到剪貼簿時提供回饋

使用者會預期在應用程式將內容複製到剪貼簿時獲得視覺回饋。系統會為 Android 13 以上版本的使用者自動執行這項功能,但在先前版本中則須手動實作。

從 Android 13 開始,系統會在內容新增至剪貼簿時,以視覺形式顯示標準的確認內容。新的確認功能會執行以下作業:

  • 確認已成功複製內容。
  • 提供複製內容的預覽畫面。

顯示 Android 13 剪貼簿通知的動畫
圖 3. 在 Android 13 以上版本中,當內容進入剪貼簿時會顯示的 UI。

在 Android 12L (API 級別 32) 以下版本中,使用者可能會不確定是否成功複製內容或複製的內容。這項功能可標準化應用程式複製後顯示的各種通知,讓使用者能進一步控管剪貼簿。

避免重複通知

在 Android 12L (API 級別 32) 以下版本中,建議您在複製後使用 ToastSnackbar 等小工具發出視覺、應用程式內意見回饋,以便在成功複製時通知使用者。

為了避免重複顯示資訊,針對 Android 13 以上版本,強烈建議您移除應用程式內複製後顯示的浮動式訊息或 Snackbar。

在應用程式內複製後發布 Snackbar。
圖 4. 如果您在 Android 13 中顯示複製確認 Snackbar,使用者就會看到重複的訊息。
在應用程式內複製後發布浮動式訊息。
圖 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 以上版本中,複製內容的確認畫面中不會顯示敏感內容。

敏感內容未加上標記時的複製文字預覽。
圖 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. 使用 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 未指向有效的內容供應器,這個方法會傳回空值。

    Kotlin

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

    Java

        // If the clipboard contains a URI reference...
        if (pasteUri != null) {
    
            // ...is this a content URI?
            String uriMimeType = cr.getType(pasteUri);
    
  5. 測試內容供應器是否支援應用程式可理解的 MIME 類型。如果支援,請呼叫 ContentResolver.query() 來取得資料。傳回值為 Cursor

    Kotlin

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

    Java

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

貼上意圖

如要貼上意圖,請先取得全域剪貼簿。檢查 ClipData.Item 物件,確認其中是否包含 Intent。然後呼叫 getIntent(),將意圖複製到您自己的儲存空間。請參考下列程式碼片段:

Kotlin

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

Java

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

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

if (pasteIntent != null) {

    // Handle the Intent.

} else {

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

應用程式存取剪貼簿資料時顯示的系統通知

在 Android 12 (API 級別 31) 以上版本中,應用程式呼叫 getPrimaryClip() 時,系統通常會顯示浮動式訊息。訊息中的文字包含下列格式:

APP pasted from your clipboard

應用程式進行下列其中一項操作時,系統不會顯示浮動式訊息:

  • 透過自己的應用程式存取 ClipData
  • 從特定應用程式重複存取 ClipData。只有當您的應用程式首次從該應用程式存取資料時,才會顯示浮動式訊息。
  • 擷取剪輯物件的中繼資料,例如透過呼叫 getPrimaryClipDescription() 而非 getPrimaryClip()

使用內容供應器複製複雜資料

內容供應器支援複製複雜資料,例如資料庫記錄或檔案串流。如要複製資料,請在剪貼簿中加入內容 URI。接著,執行貼上操作的應用程式會從剪貼簿取得這個 URI,並用來擷取資料庫資料或檔案串流描述元。

由於執行貼上操作的應用程式只有資料的內容 URI,因此需要知道要擷取哪些資料。如要提供這項資訊,您可以將 URI 上的資料 ID 進行編碼,或是提供唯一 URI 來傳回您要複製的資料。要採用何種技術,取決於資料的結構。

以下各節說明如何設定 URI、提供複雜資料,以及檔案串流。這些說明假設您熟悉內容供應器設計的一般原則。

在 URI 中將 ID 編碼

使用 URI 將資料複製到剪貼簿的實用技巧,是為 URI 上的資料 ID 編碼。接著,內容供應器就能從 URI 取得 ID 並用於擷取資料。執行貼上操作的應用程式不需要知道 ID 是否存在。您只需從剪貼簿取得「參照」(URI 和 ID),將其提供給內容供應器並傳回資料即可。

您通常會將 ID 串連至 URI 結尾,藉此將 ID 編碼至內容 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,並使用專門用於複製及貼上的程式碼處理。

如果您已經使用內容供應器、內部資料庫或內部資料表整理資料,則通常會使用編碼技術。在這些情況下,您要複製的多項資料,而且可能每個片段都有專屬 ID。如要回應貼上應用程式的查詢,您可以依 ID 查詢資料並傳回。

如果沒有多件資料,可能就不需要對 ID 進行編碼。您可以使用供應器專屬的 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,並使用該 URI 取得及貼上資料。

複製資料串流

您可以採用串流形式複製及貼上大量文字和二進位資料。資料的形式包括:

  • 儲存在實際裝置上的檔案
  • 來自通訊端的串流
  • 儲存在供應器基礎資料庫系統中的大量資料

資料串流的內容供應器會透過 AssetFileDescriptor 等檔案描述元物件 (而非 Cursor 物件) 存取其資料。執行貼上操作的應用程式會使用這個檔案描述元讀取資料串流。

如要設定應用程式以透過供應器複製資料串流,請按照下列步驟操作:

  1. 為您要放入剪貼簿的資料串流設定內容 URI。做法包括:
    • 按照「在 URI 中將 ID 編碼」一節所述,將資料串流的 ID 編碼至 URI,然後在供應器中維護含有 ID 和對應串流名稱的資料表。
    • 直接在 URI 上編碼串流名稱。
    • 使用始終會從供應器傳回目前串流的專屬 URI。如果使用這個選項,每次使用 URI 將串流複製到剪貼簿時,請記得更新供應器,使其指向其他串流。
  2. 針對您打算提供的每種資料串流提供 MIME 類型。執行貼上操作的應用程式需要使用這項資訊,才能判斷是否可將資料貼到剪貼簿。
  3. 實作其中一種 ContentProvider 方法,可傳回串流的檔案描述元。如果您在內容 URI 中將 ID 編碼,請使用這個方法來決定要開啟的串流。
  4. 如要將資料串流複製到剪貼簿,請建構內容 URI 並放入剪貼簿。

如要貼上資料串流,應用程式會從剪貼簿取得剪輯、取得 URI,然後在呼叫開啟串流的 ContentResolver 檔案描述元時使用該 URI。ContentResolver 方法會呼叫對應的 ContentProvider 方法,並傳遞內容 URI。您的供應器會將檔案描述元傳回 ContentResolver 方法。接著,執行貼上操作的應用程式會負責從串流中讀取資料。

以下清單列出內容供應器最重要的檔案描述元方法。每個方法都有對應的 ContentResolver 方法,且方法名稱附加了「Descriptor」字串。例如,openAssetFile()ContentResolver 類比為 openAssetFileDescriptor()

openTypedAssetFile()

這個方法會傳回素材資源檔案描述元,但前提是供應器支援提供的 MIME 類型。呼叫端 (執行貼上操作的應用程式) 會提供 MIME 類型模式。如果應用程式的內容供應器可以提供該 MIME 類型,則將 URI 複製到剪貼簿的內容供應器會傳回 AssetFileDescriptor 檔案控制代碼;如果無法提供,便會擲回例外狀況。

這個方法可以用來處理檔案的子區段。您可以用這個方法來讀取內容供應器複製到剪貼簿的素材資源。

openAssetFile()
此方法是 openTypedAssetFile() 的通用形式。系統不會篩選允許的 MIME 類型,但可以讀取檔案的子區段。
openFile()
這是 openAssetFile() 的一般形式。無法讀取檔案的子區段。

您可以選擇搭配檔案描述元方法使用 openPipeHelper() 方法。這麼做可讓執行貼上操作的應用程式使用管道讀取背景執行緒中的串流資料。如要使用這個方法,請實作 ContentProvider.PipeDataWriter 介面。

設計有效的複製及貼上功能

如要為您的應用程式設計有效的複製及貼上功能,請注意以下幾點:

  • 確保剪貼簿中始終只有一個剪輯。系統中任何應用程式的新複製作業會覆寫上一個片段。由於使用者可能在傳回前離開應用程式,而是複製內容,因此您無法假設剪貼簿包含使用者先前在「您的應用程式」中複製的剪輯。
  • 我們希望為每個剪輯使用多個 ClipData.Item 物件,以便支援複製及貼上多個選取項目,而非單一選取項目的不同參照形式。通常您會希望剪輯中的所有 ClipData.Item 物件的格式都相同。也就是說,這些變數都必須是簡單的文字、內容 URI 或 Intent,且不得混合使用。
  • 提供資料時,您可以提供不同的 MIME 表示法。將支援的 MIME 類型新增至 ClipDescription,然後在內容供應器中實作 MIME 類型。
  • 從剪貼簿取得資料時,您的應用程式會負責檢查可用的 MIME 類型,然後決定要使用的類型 (如果有的話)。即使剪貼簿中有剪輯且有使用者要求貼上,您的應用程式也不需要執行貼上作業。在 MIME 類型相容的情況下執行貼上作業。您可以使用 coerceToText() 將剪貼簿中的資料強制轉換為文字。如果您的應用程式支援多種可用的 MIME 類型,您可以讓使用者自行選擇要採用哪一種 MIME。