2013年4月24日 星期三

[Android] 透過adb來檢查開機狀態

在開發的過程中adb可以達到很多事情,
比如狀態查詢、開機紀錄等。
這邊記錄一個小技巧,可以用以檢視實體機器是否已經開機成功,
這在當要做一些自動化測試的偵測時會很有效。
首先確認在Linux或者Windows底下已裝有adb工具,
用傳輸線連接電腦和裝置後,以下面方法來操作:
adb devices // 如果順利連線應可以看到有列出裝置
adb reboot // 將裝置重開
adb shell dumpsys power|grep -i mBootCompleted
最後一行代表將資訊裡的mBootCompleted的該行抓出,
如果目前還在開機中的話,這個值會是fail,
如果已經開到螢幕解鎖前的畫面時,這個值就會是true了。

2013年4月15日 星期一

[Android] Android ApiDemos sample

只是簡單的小技巧,不過還是記起來分享一下,
省得以後忘記XD。
當開發Android app的時候,
我們可以到Developer上找到ADT Bundle,
當中包含了Eclipse和Android SDK(目前應該是Android 4.2 JellyBean-API Level 17)。

但如果你想要看sample的話,
會發現你只能掛上Support13Demos, Support4Demos,
以及SupportAppNavigation這三個Demo。
最主體的那個API Demos會在模擬器開啟來的時候找的到,
問題是你會找不到Source Code = =|||||||
怎麼掛Demo?

File->new->project
->Android/Android Sample Project
->Next->Next就可以選擇三個之一了。
選擇完以後,直接build會有error。
在專案上按右鍵->new->Folder,
取個名字比如說libs。
libs上按右鍵->import->General->File System->Next
Browse->在你載下來Bundle的資料夾裡->sdk\extras\->
看你是掛哪個Demo就去看相對缺哪個lib,選擇那個資料夾。
掛載完之後,不要忘記在檔案上按右鍵->Build Path->
Add to Build Path。
完成! 接下來你應該可以在Referenced Libraries裡看到它。

那麼你又問了:怎麼掛最主要的API Demos?
請將這個連結的檔案載下來:
解壓縮後,在Eclipse裡:
File->new->Project...
Android/Android Project from Existing Code
Browse...
直接選解壓縮出來的samples資料夾,
全部加進來按Finish就完成囉!

2013年4月11日 星期四

[Android] Android Developer Note Text and Input 3

Spelling Checker Framework

(見Android Developer Spelling Checker)

序言:

Android平台提供了拼字檢查的framework,
讓你可以實作並在你的app中存取拼字檢查。
framework是Text Service APIs的其中之一,
由Android平台所提供。

要使用app裡的framework,
你要做一個特別型態的Android service,
用以產生一個拼字檢查器的session物件。
根據你所提供的文字,
session物件會回傳由拼字檢查器產生的拼字建議。

Spell Checker Lifecycle:

下面的流程圖展示了拼字檢查器的服務的生命週期:

要初始拼字檢查的時候,
你的app開始它的拼字檢查服務的實作。
在你的app的clients,比如說activities或者個別的UI元素,
從服務來request拼字檢查器的session,
接著使用session來取得文字的建議。
當client終止它的操作時,它關上它的拼字檢查器的session。
如果必要的話,你的app可以將拼字檢查器的service在任何時間關掉。

Implementing a Spell Checker Service:

要在你的app裡使用拼字檢查器的framework,
將一個拼字檢查器service的組件,
包括了session物件的定義。
你也可以將做為控制設定的選擇性的activity加到你的app。
你必須要加XML metadata檔案以描述拼字檢查服務,
且將適當的元素加到manifest檔上。

Spell checker classes:

使用下列的classes來定義了服務以及session物件:
一個SpellCheckerService的subclass
    SpellCheckerService實作了Service class,
以及拼字檢查器的framework介面。在你的subclass裡,
你必須要實作下面的method:
    createSession()
        一個factory method,
回傳一個SpellCheckerService.Session物件
到要做拼字檢查的client。
    可以看Spell Checker Service的sample app來學習更多這個class的實作。

SpellCheckerService.Session的實作
    拼字檢查器服務提供給clients的物件,
用以讓他們將文字傳到拼字檢查器,並取得建議。
在class裡面,你必須實作下面的methods:

    onCreate()
        被系統呼叫,用以回應createSession()
在這個method,你可以初始化SpellCheckerService.Session物件,
基於現有的locale以及其他。
    onGetSentenceSuggestionsMultiple()
        作確切的拼字檢查。
這個method回傳一個SentenceSuggestionsInfo的陣列,
包括sentences傳到的建議。

    你可以選擇性的實作onCancel(),以處理requests來取消拼字檢查;
onGetSuggestions(),以處理一個單字建議的request;
或是onGetSuggestionsMultiple()
以處理批次的單字建議的requests。

    可以參照Spell Checker Client來學習更多實作這個class的方法。

注意: 你必須將所有方面的拼字檢查實作成非同步且thread-safe。
一個拼字檢查器也許會同時被不同的code上所跑的不同的threads來呼叫。
SpellCheckerService以及SpellCheckerService.Session會自動處理好這件事情。

Spell checker manifest and metadata:

在code以外,你需要去提供適當的manifest檔,
還有一個metadata檔給拼字檢查器。
manifest檔定義了app,service,以及activity給控制設定,
就像在下面片段所展示的那樣。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.samplespellcheckerservice" >
    <application
        android:label="@string/app_name" >
        <service
            android:label="@string/app_name"
            android:name=".SampleSpellCheckerService"
            android:permission="android.permission.BIND_TEXT_SERVICE" >
            <intent-filter >
                <action android:name="android.service.textservice.SpellCheckerService" />
            </intent-filter>

            <meta-data
                android:name="android.view.textservice.scs"
                android:resource="@xml/spellchecker" />
        </service>

        <activity
            android:label="@string/sample_settings"
            android:name="SpellCheckerSettingsActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
        </activity>
    </application>
</manifest>

注意要使用service的組件,必須要request BIND_TEXT_SERVICE的permission ,
來確保只有系統結合到service。
service的定義同時也指定了spellchecker.xml的metadata檔,
在下一個section來描述。

spellchecker.xml metadata檔案包含了下面的XML:

<spell-checker xmlns:android="http://schemas.android.com/apk/res/android"
        android:label="@string/spellchecker_name"
        android:settingsActivity="com.example.SpellCheckerSettingsActivity">
    <subtype
            android:label="@string/subtype_generic"
            android:subtypeLocale="en”
    />
    <subtype
            android:label="@string/subtype_generic"
            android:subtypeLocale="fr”
    />
</spell-checker>

metadata指定了activity,讓拼字檢查使用控制設定。
它也定義了拼字檢查的子型態,
在這個狀況下,子型態定義了拼字檢查器可以處理的locales。

Accessing the Spell Checker Service from a Client:

app使用TextView的views,自動從拼字檢查得益,
因為TextView會自動使用拼字檢查器。下面的螢幕截圖展示了這個:


無論如何,你也許也想要在其他狀況裡,
去直接和拼字檢查服務來互動。
下面的流程圖展示了和拼字檢查器服務互動的控制流。

Spell Checker Client的sample app,
顯示了如何去和拼字檢查服務來互動。
在Android Open Source Project裡的LatinIME的輸入法編輯器,
也包含了一個拼字檢查的範例。

2013年4月9日 星期二

[Android] Android Developer Note Text and Input 2

Creating an Input Method (見Android developer Creating an IME)

序言:

一個IME(input method editor, 輸入法編輯器)是一個使用者控制介面,
可以讓使用者輸入文字。
Android提供了一個可延伸的輸入法框架,
允許apps來提供使用者不同類型的輸入法,
像是螢幕上的鍵盤,或者甚至是用聲音來輸入。
一旦安裝以後,使用者就可以在系統設定選擇他們想要使用的IME,
並且在整個系統的操作來使用它;
一次只有一個IME會被啟動。

要將IME加到Android系統,你得做一個Android app,
裏頭包含了一個繼承了InputMethodService的class。
除此以外,你通常會做一個"settings"的activity,
用來傳遞選項到IME service。
你也可以定義出一個設定的UI,
用以顯示部分的系統設定。

這段文章包含了下面內容:
1. IME的生命週期
2. 在app的manifest裡定義IME組件。
3. IME API。
4. 設計一個IME UI。
5. 從IME發送文字到app。
6. 使用IME subtypes運作。

如果你從沒做過IMEs,你應該先讀這篇導引文:
Onscreen Input Methods
還有,包在SDK裡面的軟鍵盤的sample app,
可以讓你拿來修改用以建立起屬於你自己的IME。

The IME Lifecycle:

下面的圖表描述了一個IME的生命週期:



下面的區塊描述了如何去實作符合這個生命週期的UI,
以及和IME相關聯的code。

Declaring IME Components in the Manifest:

在Android系統裡,
一個IME是一個Android app,
當中含有一個特別的IME服務。
app的manifest檔裡必須要聲明這個服務,
去request必須的許可權限,
提供一個符合action action.view.InputMethod的intent filter,
並且提供了定義了IME的特性的metadata。
除此以外,要提供允許使用者修改IME的設定介面的話,
你可以定義一個"settings"的activity,讓它可以從系統設定被啟動。

下面的片段聲明了IME服務的部分。
它去請求了BIND_INPUT_METHOD的權限許可,
以讓服務可以將IME連結到系統,
設定一個符合action android.view.InputMethod的intent filter,
並定義IME的metadata:

<!-- Declares the input method service -->
    <service android:name="FastInputIME"
        android:label="@string/fast_input_label"
        android:permission="android.permission.BIND_INPUT_METHOD">
        <intent-filter>
            <action android:name="android.view.InputMethod" />
        </intent-filter>
        <meta-data android:name="android.view.im" android:resource="@xml/method" />
    </service>

下一段聲明了IME的settings activity。
它有一個篩ACTION_MAIN的intent filter,
指示了這個activity是主要的IME app的進入點:

    <!-- Optional: an activity for controlling the IME settings -->
    <activity android:name="FastInputIMESettings" 
        android:label="@string/fast_input_settings">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
        </intent-filter>
    </activity>

你也可以從IME的UI,提供直接對IME設定存取。

The Input Method API:

IMEs所用的classes,
可以在android.inputmethodservice
android.view.inputmethod的packages裡找到。
KeyEvent的class對於處理鍵盤字元來說很重要。

一個IME的核心部分就是service component,
它是繼承InputMethodService的class。
在實作普通的服務的生命週期之外,
這個class有callbacks可以提供給你的IME的UI,
處理使用者輸入,並且將文字發到現在有focus的field上。
預設來說,InputMethodService的class提供了大多數的實作,
用以管理IME狀態、可見性,
以及和現在的輸入區域做溝通。

下面的classes也很重要:

BaseInputConnection
定義了從一個InputMethod回到接收輸入的app之間溝通的頻道。
你可以使用它來讀游標附近的文字,
將文字提交到文字方塊,
以及傳送raw key events到app。
app必須繼承這個class,
而非去實作底層的interface InputConnection

KeyboardView
一個View的延伸,render了鍵盤,
並且對於使用者輸入的events做出回應。
鍵盤的layout被一個的Keyboard實例所指定,
你可以將其定義在一個XML檔裡面。

Designing the Input Method UI:

一個IME有兩個主要的視覺元素:
input view以及candidates view。
你只需要將相關於你所設計的input method的元素實作好就行了。

Input view:
input view是使用者以按鍵形式、手寫,或手勢輸入的地方的UI。
當IME第一次被顯示時,系統會呼叫onCreateInputView()的callback。
在你的method的實作裡面,
做出你想要IME window所顯示的layout,
並且回傳到系統。
這個片段是一個實作onCreateInputView() method的範例:

    @Override 
    public View onCreateInputView() { 
        MyKeyboardView inputView = 
            (MyKeyboardView) getLayoutInflater().inflate( R.layout.input, null);
    
        inputView.setOnKeyboardActionListener(this); inputView.setKeyboard(mLatinKeyboard); 
        
        return mInputView; 
    } 

在這個範例裡,
MyKeyboardView是一個自訂的KeyboardView的實作的實例,
它render了一個Keyboard
如果你在建立的是一個傳統的QWERTY鍵盤,
可以參見軟鍵盤的sample app,作為一個如何繼承KeyboardView class的範例。

Candidates view:(候選視窗)

candidates view是IME顯示可能的字詞校正或建議給使用者選取的UI位置。
在IME的生命週期裡,當系統準備顯示candidate view時,
它會呼叫onCreateCandidatesView()
在你的method實作裡,回傳一個顯示字詞建議的layout,
或者回傳null,如果你不想顯示任何東西。
(null的回應是預設值,
所以如果你沒有提供建議的話,
你就不用去實作這個)

UI design considerations:

這個段落描述了一些特定的UI designs考慮給IMEs。

Handling multiple screen sizes
你的IME的UI必須要能夠在不同螢幕大小上縮放,
且它也要能處理兩個轉向。
在非全頻輸入法模式中,
留下足夠的空間給text field及相關的內容,
使得IME不會用有超過一半的螢幕空間。
全螢幕的IME就不會有這個議題了。

Handling different input types
Android text fields允許你設定一個特定的input type,
比如說自由格式的文字、數字、URLs、email地址,
以及搜尋字串。當你實作一個新的IME時,
你需要去偵測每個field裡的input type,
並且提供適合的Interface給它。
但是,你不用將你的IME調整好,
來檢查使用者是否輸入了合法的input type;
那是擁有text field的app的責任。

舉例來說,
這邊是Android平台提供給文字及電話號碼輸入的
Latin IME的螢幕截圖:
 

當一個input field接收到focus,且你的IME開始時,
系統會呼叫onStartInputView()
傳進一個EditorInfo物件,
當中包含了text field的input type以及其他屬性的詳細內容。
在這個物件裡,inputType field包含了text field的input type。

inputType field是一個int,
它包含了不同input type設定的bit pattern。
要測試text field的input type,
使用常數TYPE_MASK_CLASS來做遮罩(mask),像這樣:

inputType & InputType.TYPE_MASK_CLASS 

input type的bit pattern可以是下面幾個值之一,包括了:

TYPE_CLASS_NUMBER
輸入數字的text field。
如前面顯示的螢幕截圖那樣,
Latin IME顯示了一個數字pad給這個type的fields。
TYPE_CLASS_DATETIME
一個text field,用以輸入日期和時間。
TYPE_CLASS_PHONE
一個text field,用以輸入電話號碼。
TYPE_CLASS_TEXT
一個text field,用以輸入所有受支援的字元。

這些常數在給InputType的參考文件裡有更多的描述。
inputType field可以包含其他bits,指示一個text field的變數型態,比方說像:

TYPE_TEXT_VARIATION_PASSWORD
一個TYPE_CLASS_TEXT的變數,作為輸入密碼用。
輸入的method會顯示dingbat而非實際的文字。
TYPE_TEXT_VARIATION_URI
一個TYPE_CLASS_TEXT的變數,作為輸入網址URL,
及其他Uniform Resource Identifiers(URIs)。
TYPE_TEXT_FLAG_AUTO_COMPLETE
一個TYPE_CLASS_TEXT的變數,
作為輸入app從字典,搜尋,或其他功能所"自動完成"的文字。

記住,
當測試這些變數時,
要使用適當的常數來對inputType作遮罩。
可用的遮罩常數在inputType的參考文件裡有列出來。

注意:
在你自己的IME裡,當你將它送到一個密碼field時,
確保你正確地處理文字。
不論是在輸入的view或者是在候選的view裡,
都要將在你的UI的密碼隱藏起來。
同時也要記得,你不應該將密碼存在裝置上。
想要了解更多的話,可以看Designing for Security的導覽。

Sending Text to the Application:

當使用者使用你的IME來輸入文字時,
你可以將文字透過傳送個別的key events,
或者藉由編輯在app的text field的游標附近的文字,
來傳送文字。
在任何一個狀況下,
你都是使用一個InputConnection的實例來傳送文字。
要取得這個實例的話,
呼叫InputMethodService.getCurrentInputConnection()

Editing the text around the cursor:

當你在處理編輯已在text field存在的文字時,
一些在BaseInputConnection裡面的更有用的methods如下:

getTextBeforeCursor()
回傳一個CharSequence
當中包含了所request的字母的數量。
(當前游標的位置的所有字母)
getTextAfterCursor()
回傳一個CharSequence
當中包含了所request的字母的數量。
(當前游標的位置的所有字母)
deleteSurroundingText()
刪除在現有的游標位置前後,
指定數字的字母數量。
commitText()
提交一個CharSequence到text field,並且設定一個新的游標位置。

舉例來說,
下面的片段展示了如何取代文字"Fell"到左邊,
並用"Hello!":

    InputConnection ic = getCurrentInputConnection();
    
    ic.deleteSurroundingText(4, 0);
    
    ic.commitText("Hello", 1);
    
    ic.commitText("!", 1);

Composing text before committing
如果你的IME做了文字預測,
或者需要多重的步驟來作出一個字型(glyph)或者字(word),
你可以將過程秀在text field,
直到使用者提交了這個字,
接著你可以將部分的造詞替代成完整的文字。

你也許可能對文字作特別處理,
透過加上一個"span"上去,
當你將其傳到InputConnection#setComposingText()。

下面的片段展示了如何在text field裡秀出過程。

    InputConnection ic = getCurrentInputConnection();

    ic.setComposingText("Composi", 1);
...

    ic.setComposingText("Composin", 1);
...

    ic.commitText("Composing ", 1);

下面的螢幕截圖顯示了這是如何顯示給使用者的:
  

Intercepting hardware key events:

儘管輸入的method視窗沒有外顯的焦點,
它先取得了硬體的key events,
且可以選擇去消耗他們或送到app。
舉例來說,
在造詞時,
你也許會想要消耗方向鍵用以在UI作瀏覽給候選的選擇。
你也許也想要追蹤back key,
來解掉從輸入method的視窗跳出來的東西。

要解譯硬體keys,複寫onKeyDown()onKeyUp()這兩個函式。
從Soft Keyboard的sample app可以看範例。

當你有不想自己去處理的keys時,
記得要去call自己的super()

Creating an IME Subtype:

子型態允許IME表達自己可以支援多種輸入模式和語言。
一個子型態可以表示:
1. 一個地區語言選項(locale)比如說像en_US或fr_FR
2. 一個輸入模式,像是聲音、鍵盤,及手寫。
3. 其他的輸入模式、格式,或者IME的性質,
比如像是10鍵或qwerty鍵盤的layouts。
基本上,這個mode可以是任何文字,
像是"keyboard", "voice"等等。

一個子型態也可以表露這些的組合。

子型態資訊被用在IME切換對話框,
這個對話框可以被用在notification bar和IME設定。
資訊也允許了framework去將特定的子型態直接帶起。
當你建立起一個IME,就用子型態的功能,
因為它幫助使用者來辨識及在不同IME語言和模式間切換。

使用<subtype>元素,
將子型態定義在輸入method的XML resource檔案的其中之一。
下面的片段定義了一個有兩個子型態的IME:
一個鍵盤子型態(locale是US English),
另一個是鍵盤子型態法語的language locale:

<input-method xmlns:android="http://schemas.android.com/apk/res/android"
        android:settingsActivity="com.example.softkeyboard.Settings"
        android:icon="@drawable/ime_icon"
    <subtype android:name="@string/display_name_english_keyboard_ime"
            android:icon="@drawable/subtype_icon_english_keyboard_ime"
            android:imeSubtypeLanguage="en_US"
            android:imeSubtypeMode="keyboard"
            android:imeSubtypeExtraValue="somePrivateOption=true"
    />
    <subtype android:name="@string/display_name_french_keyboard_ime"
            android:icon="@drawable/subtype_icon_french_keyboard_ime"
            android:imeSubtypeLanguage="fr_FR"
            android:imeSubtypeMode="keyboard"
            android:imeSubtypeExtraValue="foobar=30,someInternalOption=false"
    />
    <subtype android:name="@string/display_name_german_keyboard_ime"
            ...
    />
/>

要確保你的子型態有正確地在UI裡被標籤上的話,
使用%s來取得一個子型態標籤,
其和子型態的locale標籤是一樣的。
這在下面兩個片段被展示。
第一個片段秀出了一部分的輸入method的XML file:

<subtype
        android:label="@string/label_subtype_generic"
        android:imeSubtypeLocale="en_US"
        android:icon="@drawable/icon_en_us"
        android:imeSubtypeMode="keyboard" />

下一個片段是IME的strings.xml檔的一部分。
這個被輸入method UI定義來設定子型態的標籤,
所使用的字串resource是被定義如:

<string name="label_subtype_generic">%s</string>

這會將子型態的顯示名稱設成"English (United States)"在任何英文語言區域,
或者在其他的區域會設到適當的地方。

Choosing IME subtypes from the notification bar:

Android系統安排了所有的子型態,給所有的IMEs表露。
IME子型態被以其所屬的IME的模式來看待。
在通知列(notification bar)裡,
一個使用者可以選擇現在所設定的IME的可用的子型態,
如同下面所秀出來的截圖:



Choosing IME subtypes from System Settings:

使用者可以控制子型態如何被使用,
藉由在系統設定裡的"語言及輸入裝置"設定窗來做調整。
在軟鍵盤的sample裡,
InputMethodSettingsFragment.java的檔案
包含了一個實作IME設定裡子型態的啟動器。

請參考在Android SDK的sample裡會有更多資訊。

General IME Considerations:

這裡有幾個當你在實作IME的時候,所要考慮的其他東西:
1. 提供一個方法給使用者直接從IME的UI來設定選項。
2. 因為裝置上可能灌了多個IME,
應提供使用者從輸入method UI直接來切換到另一個IME的方法。
3. 快速喚起IME的UI。預讀或在要求時才讀大資源,
可以讓使用者當按到一個text field時可以盡可能快地看到IME。
Cache資源以及views作為連串的輸入法調用(invocations)。
4. 反過來說,在輸入法視窗隱藏後,
你應該要快點將占了大量記憶體的空間盡速釋放。
這樣app可以有足夠的記憶體來運作。
考慮使用一個延遲的訊息來釋放資源,
如果IME是處於隱藏的狀態數秒時。
5. 確保使用者可以輸入盡可能多的字母,
使用相關聯的語言或地區。
記得使用者可能使用標點符號在密碼或使用者名稱,
所以你的IME必須要提供不同的字母來讓使用者輸入密碼,
並得到與裝置存取的能力。

2013年4月2日 星期二

[Android] Android Developer Note Text and Input 1

Text and Input

使用文字服務來加上方便的功能到你的app,
像是複製/貼上,以及拼字檢查等。
你也可以開發你自己的文字服務來提供客製的IMEs(輸入法)、
字典,以及拼字檢查器,作為可以發布給使用者的apps。

Copy and Paste:

Android提供了一個強大的基於剪貼簿的複製/貼上的framework。
它同時提供了簡單和複雜兩種資料型態,
包括了文字strings、複雜的資料結構、
文字及二元串流資料,甚至是app assets。
簡單的文字資料是直接存在剪貼簿裡的,
而複雜的資料是以貼上的app用content provider來解析,
作為一個參照(reference)來儲存的。
複製和貼上可以在一個app或多個app之間來做操作,
只要那些apps有實作這個framework。

因為一部分的framework使用了content providers,
這個標題會假定您已經對於Android Content Provider API有所熟悉了。
(我就是要跳著看怎樣不行歐= =++++)
相關的資料是在Content Providers裡所描述的。

The Clipboard Framework

當你使用了剪貼簿的framework,你將資料放進了一個clip物件,
然後將這個clip物件放到系統廣域(system-wide)的剪貼簿。
clip物件可以選用下列三種格式之一:
Text:
    一個文字string。你將string直接放進clip物件裡,
然後放到剪貼簿上。要貼上string時,從剪貼簿取得clip物件,
然後將string複製到你的app的儲存的地方。

URI:
    一個Uri物件代表任何格式的URI,這是主要作為
從content provider作為複製複雜的資料的用途的。
要複製資料,將一個Uri物件放到clip物件,
然後將clip物件放到剪貼簿上。要貼上資料,
先取得clip物件,拿到Uri物件,將其解析成資料source,
像是content provider,並且從source複製資料進你的app儲存空間。

Intent:
    一個Intent。這支援了複製app捷徑。
要複製資料的話,新增一個Intent,將其放到clip物件,
然後將clip物件放到剪貼簿上。
要貼上資料的話,拿到clip物件後,
將Intent物件複製進app的記憶區域。

剪貼簿一個時間只保存一個clip物件。
當一個app將clip物件放到剪貼簿上時,
前一個clip物件會消失。

如果你要允許使用者在你的app裡貼上資料的話,
你不需要去處理所有種類的資料。
你可以在你給使用者選項做貼上前,
檢驗剪貼簿上的資料。
除了有特定的資料格式以外,clip物件也包含metadata,
它會告訴你是什麼MIME種類,或者哪些種類可用。
這個metadata幫你決定你的app,
是否可以用剪貼簿資料來做有用的事情。
舉例來說,如果你有一個app主要是處理文字的話,
你也許想要忽略那種含有URI或Intent的clip物件。

你也許也想要讓使用者貼上文字,
且忽略掉原本剪貼版上的資料格式。
要這麼做的話,你可以強迫剪貼簿資料轉成文字表達,
然後再貼上文字。
這個部分在下面的Coercing the clipboard to text這個段落會說明。

Clipboard Classes:

這個段落描述了被剪貼簿的framework所使用的classes。

ClipboardManager
在Android系統裡,
系統剪貼簿是由global ClipboardManager class所表示的。
你不須直接實例化這個class;
取而代之的做法是,
藉由調用getSystemService(CLIPBOARD_SERVICE)來得到reference。

ClipData, ClipData.Item, and ClipDescription
要將資料加到剪貼簿,
製作一個含有資料描述和資料本身的ClipData物件。
剪貼簿一次只保留一個ClipData
一個ClipData包含一個ClipDescription物件,
以及一或多個ClipData.Item物件。

一個ClipDescription物件包含了clip的metadata。
尤其是,它含有一個給clip的資料可用的MIME類別的陣列。
當你將clip放到剪貼簿上時,這個陣列就可以做為貼到app上所用。
可以透過檢驗它來確認是否它們可以處理任何可用的MIME種類。

一個ClipData.Item物件含有text,URI,或者Intent data:
Text:
    一個CharSequence

URI:
    一個Uri。這通常含有一個content provider URI,
儘管任何URI都是被允許的。提供資料來放URI到剪貼簿上的app。
想要將資料貼上的apps從剪貼簿拿到URI,
並且使用它來存取content provider(或者其他的資料source),
並且取得資料。

Intent:
    一個Intent。這個資料型態允許你去複製一個app的捷徑到剪貼簿上。
使用者接著就可以貼上捷徑到他們的apps,作為稍後的用途。

你可以將超過一個的ClipData.Item物件到一個clip上。
這允許使用者來剪貼多重選擇區塊,以作為一個clip。
舉例來說,如果你有一個列表的widget允許使用者一次選取多個item,
你可以將所有items一次複製到剪貼簿上。
要這麼做的話,製作一個分開的ClipData.Item給每一項列表的item,
接著將ClipData.Item物件給ClipData物件。

ClipData convenience methods
ClipData class提供了方便的靜態methods,
給使用單一個ClipData.Item物件,
及一個簡單的ClipDescription物件,
以製作ClipData物件。

newPlainText(label, text)
    回傳一個ClipData物件,當中單一個物件包含了一個文字string。
ClipDescription物件的label是被設為label
ClipDescription裡單一的MIME種類是MIMETYPE_TEXT_PLAIN
使用newPlainText()來從文字string來製作clip。

newUri(resolver, label, URI)
回傳一個ClipData物件
如果URI是一個content URI的話,
(Uri.getScheme()回傳content:)
method使用在resolver裡提供的ContentResolver物件,
來從content provider取得可用的MIME種類,
並且將它們存到ClipDescription裡。
對於不是content: URI的URI,
這個method設定MIME種類到MIMETYPE_TEXT_URILIST

使用newUri()來從URI製作一個clip,尤其是content: URI。

newIntent(label, intent)
回傳一個ClipData物件,
其單一的ClipData.Item 物件包含一個Intent
ClipDescription物件的標籤被設為label
MIME種類被設為MIMETYPE_TEXT_INTENT

使用newIntent()來從一個Intent物件來製作一個clip。

Coercing the clipboard data to text
即便你的app只處理文字,
你可以透過使用ClipData.Item.coerceToText()的method,
轉換從剪貼簿所複製的非文字的資料。

這個method將ClipData.Item裡的資料轉換成文字,
並且回傳一個CharSequence
ClipData.Item.coerceToText()的回傳值是基於ClipData.Item的資料格式:

Text
    如果ClipData.Item是文字的話(getText()不為null),
coerceToText()回傳其文字。

URI:
    如果ClipData.Item是URI的話(getUri()不為null),
coerceToText()嘗試使用它來當作一個content URI:
    1. 如果URI是一個content URI,
且provider可以回傳一個文字串流(text stream),
coerceToText()回傳一個文字串流。
    2. 如果URI是一個content URI,
但provider並不提供文字串流,
coerceToText()回傳一個URI的表示式(representation)。
此表示式和由Uri.toString()所回傳的是相同的。
    3. 如果URI不是一個content URI,
coerceToText()回傳一個URI的表示式。
此表示式和由Uri.toString()所回傳的是相同的。

Intent:
    如果ClipData.Item是Intent的話(並非為null),
coerceToText()將它轉換成一個Intent URI並回傳之。
這個表示和被Intent.toUri(URI_INTENT_SCHEME)回傳的是一樣的。

剪貼簿的framework可以由下圖來做總結。
要複製資料的時候,
一個app將ClipData物件放上 ClipboardManager的全域剪貼簿。
ClipData包含了一或多個ClipData.Item物件,
以及一個ClipDescription
要貼上資料的時候,
一個app拿到ClipData
ClipDescription取得它的MIME種類,
並且從ClipData.Item
或者從ClipData.Item所指向的content provider取得資料。
A block diagram of the copy and paste framework

Copying to the Clipboard:

就如前面所描述的,要將資料複製到剪貼簿的話,
你要處理global的ClipboardManager物件,
做出一個ClipData物件,加上一個ClipDescription
以及一或多個ClipData.Item物件上去,
最後加上被完成的ClipData物件到ClipboardManager物件。
這在下面的程序中會詳盡的描述:

1. 如果你是用content URI來複製資料的話,設好一個content provider。
sample使用Note Pad sample app,
是一個對於使用content provider來複製貼上的範例。
NotePadProvider的class實作了content provider。
NotePad的class定義了provider與其他app之間的契約(contract),
包含了支援的MIME種類。

2. 拿到系統的剪貼簿:


...
// if the user selects copy
case R.id.menu_copy:
// Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager)
        getSystemService(Context.CLIPBOARD_SERVICE);


3. 將資料複製到一個新的ClipData物件:
對純文字而言:

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

對URI而言:
這個片段藉由對record ID做編碼放到content URI上,
建構了一個URI給provider。
這個技術在Encoding an identifier on the URI段落裡有更詳盡的敘述。

// Creates a Uri based on 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 that you use 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給app,
接著將其放進clip物件裡。

// 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. 將新的clip物件放上剪貼簿:

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


Pasting from the Clipboard:

如前面所描述的,我們藉由從global的剪貼簿物件,
從剪貼簿取得資料,拿到clip物件,看看它的資料,
並且可能的話將其從clip物件複製到自己的儲存空間。
這個段落詳盡敘述了怎麼樣針對三種剪貼簿資料的格式來處理。

Pasting plain text
想要貼上純文字,首先拿到global的剪貼簿,
接著拿到clip物件,
並且使用getText()將文字複製到自己的儲存空間,
如下面的程序所述:
1. 使用getSystemService(CLIPBOARD_SERVICE)
來取得global的ClipboardManager物件。
並且也聲明一個全域變數來包含這個被貼上的文字:

ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
String pasteData = "";

2. 接著決定是否要在現在的Activity裡打開或關掉"貼上"的選項。
你必須驗證剪貼簿有一個clip讓你來藉此處理資料類別。

// Gets the ID of the "paste" menu item
MenuItem mPasteItem = menu.findItem(R.id.menu_paste);
// If the clipboard doesn't contain data, disable the paste menu item.
// If it does contain data, decide if you can handle the data.
if (!(clipboard.hasPrimaryClip())) {

    mPasteItem.setEnabled(false);

    } else if (!(clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN))) {

        // This disables the paste menu item, since the clipboard has data but it is not plain text
        mPasteItem.setEnabled(false);
    } else {

        // This enables the paste menu item, since the clipboard contains plain text.
        mPasteItem.setEnabled(true);
    }
}

3. 從剪貼簿複製資料。這點只有在"貼上"的目錄選項有啟用時,
在程式裡才做得到,所以你可以預設剪貼簿含有純文字。
你不知道它是不是包含一個文字string,
或者一個指向純文字的URI。
下面的片段測試了這點,但它只展示了處理純文字的code。

// 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;
// The clipboard does not 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 is not
        // presented here.
        pasteData = resolveUri(Uri);
        return;
    } else {

    // Something is wrong. The MIME type was plain text, but the clipboard does not contain either
    // text or a Uri. Report an error.
    Log.e("Clipboard contains an invalid data type");
    return;
    }
}

Pasting data from a content URI
如果ClipData.Item物件包含了一個content URI,
且你已經決定了你可以處理其中一個MIME的種類,
做一個ContentResolver然後呼叫適合的content provider程序來取得資料。
下面的程序描述了,
如何從基於剪貼簿上的content URI的content provider取得資料。
它檢查了app可以使用的MIME類型是否能由provider取得。
1. 聲明一個全域變數來包含MIME種類。

// 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. 取得全域的剪貼簿。並且也取得一個content provider,
藉此可以用來存取content provider。

// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Gets a content resolver instance
ContentResolver cr = getContentResolver();

3. 從剪貼簿取得主要的clip,並且將其中的內容抓成URI:

// Gets the clipboard data from the clipboard
ClipData clip = clipboard.getPrimaryClip();
if (clip != null) {

    // Gets the first item from the clipboard data
    ClipData.Item item = clip.getItemAt(0);

    // Tries to get the item's contents as a URI
    Uri pasteUri = item.getUri();

4. 透過getType(Uri)測試看看URI是否為content URI。
這個method回傳null,如果Uri並沒有指到一個合法的(valid)content provider:

    // If the clipboard contains a URI reference
    if (pasteUri != null) {

        // Is this a content URI?
        String uriMimeType = cr.getType(pasteUri);

5. 測試看看content provider是否支援目前的app了解的MIME類型。
如果支援,就呼叫ContentResolver.query()來取得資料。
回傳值是一個Cursor:

        // If the return value is not 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 will vary according to the
                    // format of the data model.
                    }
                }

                // close the Cursor
                pasteCursor.close();
             }
         }
     }
}

Pasting an Intent
要貼上一個Intent,首先取得全域的剪貼簿。
檢查ClipData.Item物件來看看它是否包含一個Intent。
接著呼叫getIntent()來複製Intent到你的儲存空間。
下面的片段演示了作法:

// Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// Checks to see if the clip item contains an Intent, by testing to see if 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 your application was expecting an Intent to be
    // on the clipboard
}

Using Content Providers to Copy Complex Data:

content providers支援了複製複雜的資料,
像是資料庫紀錄或檔案系統。
要複製資料,你可以放個content URI到剪貼簿上。
貼到apps上然後從剪貼簿取得URI,
並且使用它來取得資料庫資料或者檔案串流descriptors。

因為貼上的app只有你的資料的content URI,
它需要知道要取得哪塊資料。
你可以藉由將identifier編碼給URI上的資料,來提供這個資訊。
或者,你可以提供一個特定的URI來回傳你想複製的資料。
使用哪種技巧,取決於你的資料的組織。

下面的區塊描述了如何設好URIs,
如何提供複雜的資料,以及如何提供檔案串流。
這些描述假設你對於大體的content provider設計的principles熟悉。

Encoding an identifier on the URI
用URI來複製資料到剪貼簿的一個好用的技巧,
是將資料的identifier紀錄到URI自己身上。
你的content provider接著可以從URI取得identifier,
並且使用它來取得資料。
貼上的app不會知道identifier存在;所有它所做的,
只是從剪貼簿拿到你的"參照"(URI及identifier),
給它你的content provider,並且取回資料。

你通常會將identifier透過串到URI的尾端,
來編碼到一個content URI上。
舉例來說,
假如你定義了你的provider URI如下面的字串:

"content://com.example.contacts"

如果你想要將一個名字編碼到這個URI的話,
使用下面的做法:

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);

如果你已經使用了一個content provider,
你可能想要加上一個新的URI path來指示作為複製的URI。
舉例來說,假設你已經有下面的URI paths:

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

你可以加上另一個path作為特定用來複製URIs:

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

接著你就能藉由pattern-matching偵測一個"複製"URI,
並且使用特定作為複製及貼上的code來處理了。

如果你已經在使用content provider、內部資料庫,
或者內部的table來管理你的資料,
你通常會用編碼技巧。
在這些狀況下,你有多塊資料要複製,
且想當然一個獨特的indentifier給每一塊。
要回應一個從貼上的app傳來的query,
你可以藉由資料的identifier來查找並回傳資料。

如果你沒有多塊的資料,那麼你大概不需要編碼一個identifier。
你可以簡單的使用一個對你的provider來說獨特的URI就行了。
要回應一個query的話,你的provider會回傳它現在所含的資料。

藉由ID取得單一一個record的方法,
Note Pad sample app裡被使用,
用來從notes list裡開啟一個note。
sample透過SQL資料庫,使用了_id的field。
但是你可以用任何你想要的數字或字母的identifier。

Copying data structures
我們將content provider設置好,做為將複製貼上複雜資料的準備,
寫成一個ContentProvider的subclass。
你也應該將你放上剪貼簿的URI做編碼,
如此一來,它會指向你所想提供的那個record。
除此以外,你必須要考慮目前存在的app狀態:
1. 如果已經有一個content provider,
你可以加到它的功能。你可能只需要去修改它的query() method,
來處理從apps而來要貼上資料的URIs。
你大概會要修改method來處理"複製"URI的樣式。
2. 如果你的app維護一個內部的資料庫,
你也許要將資料庫移進一個content provider來方便從它作複製。
3. 如果你現在並不是在使用資料庫,你可以實作一個content provider,
唯一用途就是作為提供資料給從剪貼簿來貼上東西的app。

在content provider裡,你會要複寫最少下面的methods:
query()
做貼上工作的app,
會假定他們可以使用你放上剪貼簿的URI,
來利用這個method取得你的資料。
要支援複製的話,你必須要有這個method,
來偵測含有特別的"複製"路徑的URIs。
你的app接著就可以做出一個"複製"URI來放上剪貼簿,
當中包含了複製的路徑以及一個指標,用以指向你所要複製的紀錄。

getType()
這個method應該要將一或多個你想要複製的資料的MIME類型做回傳。
method newUri()會呼叫getType()
為了要將MIME類型放進新的ClipData物件。

注意你不需要去有任何其他的content provider methods,
比如像是insert()update()
一個在做貼上的app,只需要去取得你支援的MIME類型,
並且從你的provider複製資料即可。
如果你已經有這些methods了,他們將不會干涉複製的操作。

下面的片段演示了如何將你的app設定來複製複雜的資料:

1. 在app全域變數的部分,聲明一個base URI字串,
以及一個路徑用以辨認你用來複製資料的URI字串。
同時也聲明一個MIME類型給被複製的資料:

// 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. 在使用者複製資料的Activity哩,
設好code來複製資料到剪貼簿。
作為對複製請求的回應,將URI放上剪貼簿:

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);

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

3. 在content provider的全域範圍內,
做出一個URI matcher,
並且將一個符合你放上剪貼簿的URI pattern的URI加上去:

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()的method設定好。這個method可以處理不同的URI樣式,
取決於你如何去寫code,但只有在剪貼簿上做複製的樣式會被顯示出來:

// 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:

        // query and return the contact for the requested name. Here you would decode
        // the incoming URI, query the data model based on the last name, and return the result
        // as a Cursor.

    ...
}

5. 將getType() method設定好,
用以回傳對於複製的資料而言恰當的MIME種類:

// Sets up your provider's getType() method.
public String getType(Uri uri) {

    ...

    switch (sUriMatcher.match(uri)) {

    case GET_SINGLE_CONTACT:

            return (MIME_TYPE_CONTACT);

Pasting data from a content URI描述了如何從剪貼簿拿到一個content URI,
並且用其來取得和貼上資料。

Copying data streams
你可以複製貼上大量的文字和二進位資料作為串流。
資料可以有如下的格式:
1. 儲存在實際裝置的檔案。
2. 從sockets拿到的串流。
3. 大量的存在provider裡的相關的資料庫系統裡的資料。

一個給資料串流使用的content provider,
提供了使用file descriptor物件,
像是AssetFileDescriptor instead of 一個Cursor物件。
貼上的app透過資料串流讀取資料串流。

要設定好你的app來從provider進行資料複製,
遵循下面的步驟:
1. 設好content URI給你要放上剪貼簿的資料串流。
這麼做的幾個選項包括如下所述:
   a. 將identifier編碼給資料串流放上URI,
就如在段落Encoding an identifier on the URI所描述的,
接著維護你含有identifiers及相對應的串流名稱的provider裡的table。
   b. 將串流名稱直接在URI上編碼。
   c. 使用一個獨特的URI,此URI總是回傳現在provider的串流。
如果你使用了這個選項,你必須記得去更新你的provider,
以指向不同的串流。
(只要你是透過URI從剪貼簿上的串流來複製的時候)
2. 提供一個MIME種類,給任何你想提供的資料種類。
貼上的app需要這個資訊,來決定他們能不能在剪貼簿上貼東西。
3. 實作其中一個ContentProvider methods來回傳一個file descriptor給串流。
如果你將content URI上的identifiers編碼,使用這個method來決定要開哪個串流。
4. 要複製資料串流到剪貼簿的話,建構content URI,
並且將其放上剪貼簿。

要貼上資料串流,一個app從剪貼簿拿到clip,
接著取得URI,
並且將其在呼叫ContentResolver file descriptor method後使用。
ContentResolver method呼叫了對應ContentProvider的 method。
貼上的app接著就有責任去從串流讀出資料。

下面的列表,
秀出一個content provider最重要的file descriptor methods列表。
每一個都有其相對應的ContentResolver method,
且有著字串"Descriptor"加在method名字後面;
舉例來說,ContentResolveropenAssetFile()的analog,
openAssetFileDescriptor():

openTypedAssetFile()
這個method應該要回傳一個asset file descriptor,
但只有在提供的MIME種類有被provider支援的時候才會這麼做。
呼叫者(做貼上動作的app)提供了一個MIME種類的樣式。
content provider(有一個複製的URI到剪貼簿上的content provider)
會回傳一個AssetFileDescriptor檔案handle
(如果它可以提供那個MIME種類),或者拋出exception如果不行的話。

openAssetFile()
這個method是一個openTypedAssetFile()更普遍的的格式。
它並不會篩選允許的MIME種類,
但它可以讀檔案的subsections。


這是一個openTypedAssetFile()更普遍的格式。
它不能讀檔案的subsection。

你可以選擇性的使用openPipeHelper()這個method,
利用你的file descriptor method。這會允許貼上的app,
在背景的thread來讀串流資料,藉由使用pipw。
要使用這個method,
你需要實作ContentProvider.PipeDataWriter的介面。

一個做法的範例被放在Note Pad samploe app裡面,
openTypedAssetFile()NotePadProvider.java 的method。

Designing Effective Copy/Paste Functionality
要為你的app設計有效率的複製和貼上的功能,
記得以下幾點:
1. 在任何時候,在剪貼簿上只有一個clip。
系統內新的app複製操作都會把前面的clip複寫。
既然使用者可能會導航(navigate)到別的地方去,
你不能假定剪貼簿有你前面使用過複製所留的資料。
2. 一個clip有多個ClipData.Item物件的意圖,
是要支援多重選擇的複製及貼上,
這樣會比每次選一段來的好。
你通常想要所有的ClipData.Item物件在相同一個cilp裡,
擁有同樣的格式,也就是說,
他們應該全都是純文字,content URI,
或者Intent,但不是不同種類的組合。
3. 當你提供資料時,你可以提供不同的MIME表示法。
將你所支援的MIME種類加到ClipDescription
然後將MIME種類實作在你的content provider。
4. 當你從剪貼簿取得資料,你的app就該負責檢查可用的MIME種類,
並且決定使用哪一個(如果有的話)。
即便是有一個clip在剪貼簿上,且使用者請求貼上,
你的app並不是非要做貼上不可。
你應該只在MIME種類能相容時才做貼上。
你可以選擇去逼迫剪貼簿上的資料轉為純文字,
使用coerceToText()
如果你的app支援不只一個可用的MIME類型,
你可以允許使用者來選擇使用哪個。