複製及貼上

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

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

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

動畫:顯示 Android 13 剪貼簿通知
圖 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 物件,當中包含資料和資料說明。剪貼簿一次只保留一個 ClipDataClipData 包含 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.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 中的資料形式:

文字
如果 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。
圖 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() 前,先在 ClipDataClipDescription 中加上標記。加上此標記後,在 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,並使用專門用於複製及貼上的程式碼來處理該 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 類型模式。如果 (將 URI 複製到剪貼簿的應用程式) 內容供應器可以提供 MIME 類型,會傳回一個 AssetFileDescriptor 檔案控制代碼;如果無法提供,則會擲回例外狀況。

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

openAssetFile()
這種方法是 openTypedAssetFile() 更為通用的形式。這種方法不會過濾允許的 MIME 類型,但可以讀取檔案的子區段。
openFile()
這是 openAssetFile() 更為通用的形式,無法讀取檔案的子區段。

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

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

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

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