複製及貼上

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

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

使用者預期將內容複製到剪貼簿時的意見回饋,因此除了支援複製及貼上的架構外,在 Android 13 (API 級別 33) 以上版本中複製內容時,Android 還會向使用者顯示預設 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() 不是空值,則會傳回文字。
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()
}

將機密內容加入剪貼簿

如果應用程式允許使用者將機密內容 (例如密碼或信用卡資訊) 複製到剪貼簿,您必須先在 ClipData 中為 ClipDescription 新增標記,才能呼叫 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. 使用 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,以及如何使用它取得及貼上資料。

複製資料串流

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

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

資料串流的內容供應器提供 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 類型,應用程式的內容供應器將傳回 AssetFileDescriptor 檔案控制代碼,如果無法提供,則會擲回例外狀況。

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

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

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

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

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

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