2013年3月21日 星期四

[Android] Android Developer Note Connectivity 2

NFC (見Android Developer Near Field Communication)

近場通訊(NFC, Near Field Communication)是一組短距離無線傳輸的技術,
基本上需要4公分或更近的距離來開一個連線。
NFC允許你分享負擔小的資料(小量的資料),透過一個NFC的標籤(tag),
以及一個支援Android的裝置,或者在兩台Android裝置之間傳輸。

Tags可以很多樣化。簡單的tags可以讀寫一些東西,
有時候附加著一次性的程式區塊(one-time-programmable areas)令卡片變成唯讀。
更多複雜的tags可以提供數學運算,且可以有加密的硬體來進行區段的授權。
最複雜的tags包含操作環境並允許複雜的互動和程式碼在tag上跑。
存在tag裡面的資料也可以被寫成多種格式,
但很多的Android framework APIs是基於一個NFC Forum的標準,
被叫作NDEF(NFC Data Exchange Format)。

下面將介紹:
1. NFC Basics
這份文件描述Android如何去處理發現的NFC tags,
以及如何去通知app說資料是和其相關的。
這篇文件也談到了在你的app裡,怎麼去處理NDEF的資料,
而且給出了關於在Android支援基礎NFC功能的framwork APIs的全觀。
2. Advanced NFC
這份文件談到了Android所支援,可以讓我們使用多樣化tag的技術的APIs。
當你並不是在處理NDEF資料,或者你處理的是NDEF資料但Android不能完全理解時,
你需要手動以bytes的單位去進行讀寫tag的動作(用你自己協定的堆疊結構)。
在這些狀況下,Android提供了支援偵測特定tag科技,
且可以以你自己協定的堆疊結構來開始和tag做溝通。




序言
這篇討論如何以NDEF的格式的訊息來傳送接收NFC資料且描述相關支援的API。
兩種主要的狀況:
1. 從NFC tag讀出NDEF資料。
2. 發送(Beaming) NDEF資料從一台裝置到另一台有Android Beam的裝置。

從NFC tag讀出NDEF資料是由tag調度系統(dispatch system),它分析並探索NFC tags,
正確地分類資料並開啟對該分類資料有興趣的app。
一個想要去處理被掃描的NFC tag的app可以聲明一個intent filter並要求處理資料。

Android Beam功能允許一個裝置推播(push)NDEF訊息到另一台裝置,
藉由物理上碰觸兩個裝置。這個互動會提供更簡單的方式,
藉以傳輸資料(比起像藍芽)。因為使用NFC不需要手動的裝置探索或做配對。
當兩台裝置來到一定範圍內時連線會自動開始。
Android Beam功能可以透過一套NFC APIs來使用,
所以任何app可以在裝置之間傳輸資訊。舉例來說:
聯絡人、瀏覽器、和YouTube的app可以用這個技術來分享。

The Tag Dispatch System
Android裝置通常會在螢幕解鎖的時候找尋著NFC tags,
除非NFC在設定目錄中被停用。當一個Android裝置找到一個NFC tag,
最被期待的行為是以最適合的activity來處理intent而不用詢問使用者要用哪個app。
因為裝置掃描NFC tag的範圍非常小,比較有可能是讓使用者手動選擇一個activity,
這樣的話就可能強迫他們將裝置移離tag從而令連線中斷。
你應該將activity設計成只處理你關心的NFC tags,以令Activity Chooser不須出現。

Android系統為了達到這點,提供了一個特別的tag dispatch系統用以分析掃描的NFC tags,
解析(parse)他們,且嘗試去定位那些會對掃出來的資料有興趣的app。
它是這麼做的:
1. Parse出NFC tag且找出MIME type或URI來確定tag裡的資料。
2. 將MIME type或URI,和資料一起裝進一個intent。
這兩個步驟,在接下來How NFC tags are mapped to MIME types and URIs會描述。
3. 用這個intent來開activity。在接下來的How NFC Tags are Dispatched to Applications會描述。

How NFC tags are mapped to MIME types and URIs
在開始程式之前我們得先了解不同種類的NFC tags,以及tag dispatch system怎麼parse它們,
及偵測到一個NDEF message時怎麼處理。NFC tags會來自很多樣的技術,
也會有很多種data寫法。Android有對NDEF標準最多的support(標準定義在NFC Forum)。

NDEF data被包在一個message裡 (NdefMessage),這個訊息包含一或多個紀錄(NdefRecord)
每一個NDEF record必須在你定義的種類下完整定義。
Android也支援別種不含NDEF資料的tags。
你可以藉由使用android.nfc.tech package裡的classes來處理它們。
建議上還是使用NDEF可以得到最大的support。

當找到NDEF格式的資料時,裝置可以分解出訊息且嘗試找出其MIME type或鑑別URI。
為了要做到這點,系統會讀第一個NdefMessageNdefRecord紀錄,
來決定如何去解圖整個NDEF messages。(一個NDEF訊息可以有多個NDEF紀錄)
在一個被完整定出來的NDEF訊息裡,第一個 NdefRecord 紀錄包含以下的fields:

3-bit TNF(Type Name Format)
指出如何解譯變數長度種類的field。合法值列在下面的表裡。
Variable length type
描述紀錄的種類。如果使用,用這個field來指定Record Type Definition(RTD)。
合法的RTD值列在下面第二張表裡。
Variable length ID
一個獨特的紀錄的識別碼(identifier)。這個field不常被用,
但如果你需要獨特地識別一個tag,你可以為它開一個ID。
Variable length payload
實際所需讀寫的資料量。一個NDEF訊息可以包含多個NDEF紀錄,
所以不要假設總資料量會列在NDEF訊息的第一個NDEF紀錄裡。

tag dispatch系統使用TNF和type files來嘗試去將MIME type或URI和NDEF訊息作對應,
如果成功的話,它會將資訊包近一個 ACTION_NDEF_DISCOVERED的intent,並隨附實際的資料。
但還是存在有那種tag dispatch系統無法由第一個NDEF紀錄決定的時候。
這種狀況發生在兩者無法對應,或者當NFC tag一開始就沒有含NDEF資料的時候。
這樣的狀況下,一個有著關於此tag技術的資訊的Tag的物件,以及要送的資料,
將會被包進一個ACTION_TECH_DISCOVERED的intent裡作為代替。

第一個表格描述了tag dispatch系統如何將TNF和type fields對應到MIME types或URIs上。
它也有描述了TNFs不能對應到任一MIME type或URI的狀況。
在這些狀況下,tag dispatch系統會退回到ACTION_TECH_DISCOVERED
舉例來說,如果tag dispatch系統遇到了一個TNF_ABSOLUTE_URI type的紀錄,
它會將該紀錄的變數長度的type field映射(maps)到一個URI上。
tag dispatch系統將URI及其他tag的資訊一起包在data field的ACTION_NDEF_DISCOVERED的intent裡,
比如像是payload本體。另外一方面如果它遇到了一個type為TNF_UNKNOWN的紀錄,
它會改為將tag的技術(technologies)包進intent。
表一. 支援的TNFs及對應
Type Name Format (TNF)Mapping
TNF_ABSOLUTE_URIURI based on the type field.
TNF_EMPTYFalls back to ACTION_TECH_DISCOVERED.
TNF_EXTERNAL_TYPEURI based on the URN in the type field. The URN is encoded into the NDEF type field in a shortened form: <domain_name>:<service_name>. Android maps this to a URI in the form: vnd.android.nfc://ext/<domain_name>:<service_name>.
TNF_MIME_MEDIAMIME type based on the type field.
TNF_UNCHANGEDInvalid in the first record, so falls back to ACTION_TECH_DISCOVERED.
TNF_UNKNOWNFalls back to ACTION_TECH_DISCOVERED.
TNF_WELL_KNOWNMIME type or URI depending on the Record Type Definition (RTD), which you set in the type field. See Table 2. for more information on available RTDs and their mappings.
表二. 對TNF_WELL_KNOWN所支援的RTDs及其對應
Record Type Definition (RTD)Mapping
RTD_ALTERNATIVE_CARRIERFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_CARRIERFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_REQUESTFalls back to ACTION_TECH_DISCOVERED.
RTD_HANDOVER_SELECTFalls back to ACTION_TECH_DISCOVERED.
RTD_SMART_POSTERURI based on parsing the payload.
RTD_TEXTMIME type of text/plain.
RTD_URIURI based on payload.

How NFC Tags are Dispatched to Applications
當tag dispatch系統完成了前面將NFC tag打包加上資訊封進intent以後,
它將intent傳給一個有興趣的app讓它來篩選。
如果多於一個的app可以處理這個intent,Activity Chooser會顯示出來給使用者選。
tag dispatch系統定義了三種intents,下面以最高優先到最低往下列出。

這個intent是在當掃出NDEF資料且形態是認得的type的tag時,開一個Activity。
這是最高優先的intent,且tag dispatch系統會嘗試在用別的intent前,
使用這個intent來開Activity,只要有可能的狀況下。
如果沒有activities註冊去處理 ACTION_NDEF_DISCOVERED的intent,
tag dispatch系統會嘗試用這個intent開一個app。
如果tag被掃出NDEF資料但沒辦法對應到MIME type或URI,
這個intent也會即刻開始(且不會先開始ACTION_NDEF_DISCOVERED);
或者,如果這個tag沒有NDEF資料但是一個已知的tag技術,也是一樣即刻開始。
如果沒有任何activities能處理前述兩者的intent,這個intent會被啟動。

基本的tag dispatch系統運作如下:
1. 嘗試去用被tag dispatch系統parse NFC tag時產生出來的intent來開一個Activity。
2. 如果沒有任何activities能篩該intent,試著去用較低的優先級的intent來開一個Activity,
直到一個app篩到了該intent或者直到tag dispatch系統試了所有可能的intents。
3. 還是沒有可以篩任何intent的app的話,就什麼都不做。

只要在可能的狀況下,盡量用NDEF訊息和ACTION_NDEF_DISCOVERED的intent,
因為它在這三個裡面是最為確定的。這個intent比起其他兩個intents來說,
允許你去在一個更適合的時間開始你的app,可以給使用者較佳的體驗。

Requesting NFC Access in the Android Manifest
在你能存取裝置的NFC硬體且正確處理NFC的intents之前,
在你的AndroidManifest.xml檔裡,
聲明這些項目:
1. NFC的<uses-permission> 元素(用來授權存取NFC硬體)
<uses-permission android:name="android.permission.NFC" />
2. 你的app最小可支援的SDK版本。
API level   9只會支援受限的tag dispatch(透過 ACTION_TAG_DISCOVERED),
且只會給予NDEF訊息透過EXTRA_NDEF_MESSAGES的extra來存取。
沒有其他的tag屬性或I/O操作是被允許的。
API level 10包含了全面的讀寫支援以及前台的NDEF推播(pushing),
API level 14提供了一個更簡單的方法來推播NDEF訊息給其他裝置,
使用Android Beam和額外的方便性的methods來做出NDEF紀錄。
<uses-sdk android:minSdkVersion="10"/>
3. uses-feature元件來令你的app在Google Play上只會讓有NFC硬體功能的裝置看到。
<uses-feature android:name="android.hardware.nfc" android:required="true" />
但如果你的app有NFC功能,但並不是沒有它就整個無用的話,
你可以略去使用uses-feature元件的部分並在執行期檢查NFC可用性。
(透過檢查getDefaultAdapter()是否為null)

Filtering for NFC Intents
想讓app能處理被掃到的NFC tag,你的app可以去篩三者之一或以上的intent。
但通常最好用的還是ACTION_NDEF_DISCOVERED的intent了,
然後ACTION_TECH_DISCOVERED的intent會在前者沒有被任何app篩到,
或者payload本身並不是NDEF。最後一個 ACTION_TAG_DISCOVERED有太一般的分類來篩選。
一般而言大部分的app會在最後一個之前篩前兩個,
所以你的app指篩最後一個的話,執行可能性就會很低,
只有在都沒人要的時候才會拿的到執行機會。

因為NFC tag部屬(deployments)的很多且很多時候不在你的控制下,
這就是為什麼當必要時你可以返回到其他兩個intents的原因。
當你能控管tag types和寫入的data時,建議使用NDEF來作為tag的格式。
下面的段落將會描述如何去篩每一種的intent。

ACTION_NDEF_DISCOVERED
要篩這種的intents的話要聲稱你要篩什麼樣的type的資料,
可以篩MIME或URI。
比如篩text/plain的MIME type:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:mimeType="text/plain" />
</intent-filter>
或者篩一個http://developer.android.com/index.html格式的URI:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <category android:name="android.intent.category.DEFAULT"/>
   <data android:scheme="http"
              android:host="developer.android.com"
              android:pathPrefix="/index.html" />
</intent-filter>
ACTION_TECH_DISCOVERED
要篩這類的intents,則需要做一個XML的資源檔,
當中在tech-list的set裡明訂了那些technology你的activity有支援。
如果一個tech-list的set是tag本身所支援的一個technologies的子集
(可以透過呼叫 getTechList()來取得資訊),你的activity被視作相符(match)。
比如說如果tag被掃出支援 MifareClassic, NdefFormatable, and NfcA這三個,
那麼你的tech-list列表裡必須要有這三個其中一項才行。
下面是定義的範例,你可以自己移掉不需要的。
將這個檔案存在<project-root>/res/xml資料夾(但你可以做任何命名都OK。)
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>
你可以也指定多個tech-list sets。每一個set都會被視為獨立的,
且如果任何單一個set是從getTechList()抓回來的子集的話,
你的activity會被視為相符(match)。這提供了AND和OR的相符配對機制。
下面的範例在尋找符合能同時支援NfcA和Ndef或者同時支援NfcB和Ndef的配對。
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
</resources>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>
</resources>
AndroidManifest.xml檔案裡,
<activity>裡的 <meta-data>元件來指定你剛做好的資源檔,如下面的範例:
<activity>
...<intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>
<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
    android:resource="@xml/nfc_tech_filter" />
...</activity>

ACTION_TAG_DISCOVERED
使用以下的filter:
<intent-filter>
    <action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>

Obtaining information from intents
若一個activity因為NFC intent而開始,你可以從intent拿到掃到的NFC tag資訊。
Intents可能含有下面的extras,這取決於被掃的tag。
1. EXTRA_TAG(必要): 一個Tag物件(代表被掃的tag)
2. EXTRA_NDEF_MESSAGES(選擇性): 一個從tag parse出來的NDEF訊息的陣列,
這個extra在intent上是強制性的(mandatory)。
3. {@link android.nfc.NfcAdapter#EXTRA_ID(選擇性): tag的low-level ID。

為了取得這些extras,檢查你的activity是否是被NFC intents其中之一給啟動,
以保證一個tag被掃到,然後從intent中取出extras。
下面的範例檢查了ACTION_NDEF_DISCOVERED intent並從intent extra中拿到NEDF訊息:
public void onResume() {
    super.onResume();
    ...
    if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
        if (rawMsgs != null) {
            msgs = new NdefMessage[rawMsgs.length];
            for (int i = 0; i < rawMsgs.length; i++) {
                msgs[i] = (NdefMessage) rawMsgs[i];
            }
        }
    }
    //process the msgs array
}
另外,你可以從intent中取得一個Tag物件,
它會包含了payload且允許你去列出(enumerate)tag的technologies。
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
Creating Common Types of NDEF Records
這節描述如何去作出常見的NDEF紀錄的type,來幫助你在寫入NFC tags,
或者使用Android Beam傳輸資料。
4.0版(API level 14)開始提供了 createUri()method可用來幫你自動作出URI紀錄。
4.1版(API level 16)開始提供了 createExternal()createMime()來幫你製作MIME,
和外部type的NDEF紀錄。
使用這些methods幫助你避免手動製作NDEF紀錄可能犯的錯XD

所有這些紀錄的範例都應該要放在NDEF訊息裡的第一個NDEF紀錄裡。
(當在寫入tag或在beaming時)
TNF_ABSOLUTE_URI (建議上請使用RTD_URI,因為比較有效率)
你可以這麼來使用:
NdefRecord uriRecord = new NdefRecord(
    NdefRecord.TNF_ABSOLUTE_URI ,
    "http://developer.android.com/index.html".getBytes(Charset.forName("US-ASCII")),
    new byte[0], new byte[0]);
在intent filter要作的大概像這樣:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:scheme="http"
        android:host="developer.android.com"
        android:pathPrefix="/index.html" />
</intent-filter>
TNF_MIME_MEDIA
以下面的方法來製作:
使用 createMime()method:
手動作出 NdefRecord :
NdefRecord mimeRecord = new NdefRecord(
    NdefRecord.TNF_MIME_MEDIA ,
    "application/vnd.com.example.android.beam".getBytes(Charset.forName("US-ASCII")),
    new byte[0], "Beam me up, Android!".getBytes(Charset.forName("US-ASCII")));
intent filter的部分會像這樣:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="application/vnd.com.example.android.beam" />
</intent-filter>
TNF_WELL_KNOWN with RTD_TEXT
以下面的方法來製作:
public NdefRecord createTextRecord(String payload, Locale locale, boolean encodeInUtf8) {
    byte[] langBytes = locale.getLanguage().getBytes(Charset.forName("US-ASCII"));
    Charset utfEncoding = encodeInUtf8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16");
    byte[] textBytes = payload.getBytes(utfEncoding);
    int utfBit = encodeInUtf8 ? 0 : (1 << 7);
    char status = (char) (utfBit + langBytes.length);
    byte[] data = new byte[1 + langBytes.length + textBytes.length];
    data[0] = (byte) status;
    System.arraycopy(langBytes, 0, data, 1, langBytes.length);
    System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
    NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,
    NdefRecord.RTD_TEXT, new byte[0], data);
    return record;
}
intent filter的部分會像這樣:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
</intent-filter>
TNF_WELL_KNOWN with RTD_URI
 createUri(String)method:
NdefRecord rtdUriRecord1 = NdefRecord.createUri("http://example.com");
或者是createUri(Uri)也可以:
Uri uri = new Uri("http://example.com");
NdefRecord rtdUriRecord2 = NdefRecord.createUri(uri);
再手動建立 NdefRecord:
byte[] uriField = "example.com".getBytes(Charset.forName("US-ASCII"));
byte[] payload = new byte[uriField.length + 1];              //add 1 for the URI Prefix
byte payload[0] = 0x01;                                      //prefixes http://www. to the URI
System.arraycopy(uriField, 0, payload, 1, uriField.length);  //appends URI to payload
NdefRecord rtdUriRecord = new NdefRecord(
    NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, new byte[0], payload);
intent filter會像這樣:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:scheme="http"
        android:host="example.com"
        android:pathPrefix="" />
</intent-filter>
TNF_EXTERNAL_TYPE
使用 createExternal()method
byte[] payload; //assign to your data
String domain = "com.example"; //usually your app's package name
String type = "externalType";
NdefRecord extRecord = NdefRecord.createExternal(domain, type, payload);
手動建立NdefRecord :
byte[] payload;
...
NdefRecord extRecord = new NdefRecord(
    NdefRecord.TNF_EXTERNAL_TYPE, "com.example:externalType", new byte[0], payload);
intent filter會像這樣:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:scheme="vnd.android.nfc"
        android:host="ext"
        android:pathPrefix="/com.example:externalType"/>
</intent-filter>
使用TNF_EXTERNAL_TYPE會比較能得到對Android及非Android的裝置的較佳支援。

(註:Note部分是在講canonical和URN的東西,就不翻譯了。)

Android Application Records(AAR)
在4.0版開始引進,AAR提供了更強的功能給NFC tag啟動app用。
AAR將app的pakage的名字包進NDEF紀錄裡,
你可以加一個AAR到任何NDEF紀錄,(在NDEF訊息裡)
因為Android會在整個NDEF訊息裡搜索AARs。
如果找到一個AAR就會根據包進去的pakage來開app(早這麼做就好了不是嗎XDDD)
如果app沒有在該裝置上安裝好,Google Play會啟動去下載app。

AARs很有用(如果你想避免別的app去篩到同樣的intent,
且潛性地去處理你已經包好地特定tags)。
AARs只在application level被支援,因為package名字地約束,
且不在Activity level用intent filtering。
如果你想在Activity的層級來處理的話就該使用intent filters。

如果一個tag包含一個AAR,那tag dispatch系統會如下述方式調度:
1. 試著按照平常那樣使用intent filter開始一個Activity。
如果同時符合AAR和intent,就開始該Activity。
2. 如果Activity篩過了intent(match),但不符合AAR,
如果多個Activities可以處理這個intent,或者沒有Activity可以處理,
就開被AAR指定的app。
3. 如果沒有app可以用AAR開始,就去Google Play去下載AAR。(根據AAR給出的app名字)

注意:你可以複寫AARs和intent dispatch系統,利用foreground dispatch系統,
它可以允許一個前景的activity來擁有優先權,當一個NFC tag被找到的時候。
使用這個method,activity必然會在前景來覆蓋掉AARs和intent dispatch系統。

如果還是想篩沒有AAR的tags的話,
可以將intent filters聲明成跟平常一樣。
如果你的app對其他沒有AAR的tags有興趣的話,這會很有用。
記得AAR只有4.0以上版本支援,所以大多數狀況下,
你可能會偏向於用兩者組合並且可以讓app支援最多tags。

Android提供用來製作AAR的API是createApplicationRecord()
你所需作的就是將AAR包進NdefMessage裡。
別用在第一個紀錄裡,除非AAR是唯一的紀錄;
這是因為Android系統會檢查NdefMessage裡的第一個紀錄來決定tag的MIME type或URI,
用來製作intent給app來篩。
下面是製作AAR的範例:
NdefMessage msg = new NdefMessage(
        new NdefRecord[] {
            ...,
            NdefRecord.createApplicationRecord("com.example.android.beam")}
Beaming NDEF Messages to Other Devices
Android Beam允許在兩台Android裝置間進行簡單的點對點資料交換。
想要beam資料給別的裝置的app必須要在前景且接收的裝置不能在上鎖狀態。
當兩者夠靠近時,要beam別人的裝置會出現"Touch to Beam"UI,
使用者接著可以選擇要不要傳送訊息給接收的裝置。

Note: 前景的NDEF推播在API level 10可以使用,也提供了相似的功能,
它相關的API已經被deprecated,但對較舊的裝置仍然支援。

要打開Android Beam給app,可以使用下面兩者之一:
接受來設定NdefMessage訊息來beam。自動beam訊息出去,
當兩個裝置夠進的時候。
接受一個包含著一個createNdefMessage()的callback,
當一個裝置在一個距離來beam資料。
callback只有在必要時會讓你產生NDEF訊息。

一個activity只能一次推播一則NDEF訊息,
(如果兩個都被設定的話)
要使用Android Beam的話,下面是大概的導引:
1. 要beam資料的activity必須在前景。兩個裝置都要螢幕解鎖
2. 你必須封裝資料然後用NdefMessage物件來beam。
3. 收到beamed資料的NFC裝置必須支援com.android.npp NDEF push protocol,
或NFC Forum's SNEP(Simple NDEF Exchange Protocol)。
前者需要API level 9(Android 2.3)~API level 13(Android 3.2),
而後者對於裝置需求為API level 14(Android 4.0)或以上。

注意:Android Beam在前景時會將標準的intent dispatch系統給關掉,
但你的activity有開啟前景的dispatching的話,還是可以來掃match intent filters的tags。

要啟用Android Beam:
1. 建立一個你要來推播到其他裝置的NdefMessage,當中要包含NdefRecords
2. 使用 NdefMessage來呼叫 setNdefPushMessage()
並傳入NfcAdapter.CreateNdefMessageCallback物件在你的activity的onCreate()method。
這些methods需要至少一個你想以Android Beam開啟的activity,
伴隨一個選擇性的列表給其他activities來啟動。
大體上你只要用 setNdefPushMessage()如果你的Activity總是只需推送同樣的NDEF訊息,
當兩台裝置在範圍內溝通。
當你的app在處理現在的app內容且想要推送NDEF訊息,
取決於使用者現在在你的app裡正做什麼時,

下面的sample範例,
展示了如何使用一個簡單的activity來呼叫NfcAdapter.CreateNdefMessageCallback
在activity的onCreate() method裡面。
這個範例也有methods來幫助你產生一個MIME紀錄。
package com.example.android.beam;
import android.app.Activity;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.NfcAdapter.CreateNdefMessageCallback;
import android.nfc.NfcEvent;
import android.os.Bundle;
import android.os.Parcelable;
import android.widget.TextView;
import android.widget.Toast;
import java.nio.charset.Charset;

public class Beam extends Activity implements CreateNdefMessageCallback {
    NfcAdapter mNfcAdapter;
    TextView textView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TextView textView = (TextView) findViewById(R.id.textView);
        // Check for available NFC Adapter
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            Toast.makeText(this, "NFC is not available", Toast.LENGTH_LONG).show();
            finish();
            return;
        }
        // Register callback
        mNfcAdapter.setNdefPushMessageCallback(this, this);
    }

    @Override
    public NdefMessage createNdefMessage(NfcEvent event) {
        String text = ("Beam me up, Android!\n\n" +
                "Beam Time: " + System.currentTimeMillis());
        NdefMessage msg = new NdefMessage(
                new NdefRecord[] { createMime(
                        "application/vnd.com.example.android.beam", text.getBytes())
         /**
          * The Android Application Record (AAR) is commented out. When a device
          * receives a push with an AAR in it, the application specified in the AAR
          * is guaranteed to run. The AAR overrides the tag dispatch system.
          * You can add it back in to guarantee that this
          * activity starts when receiving a beamed message. For now, this code
          * uses the tag dispatch system.
          */
          //,NdefRecord.createApplicationRecord("com.example.android.beam")
        });
        return msg;
    }

    @Override
    public void onResume() {
        super.onResume();
        // Check to see that the Activity started due to an Android Beam
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
            processIntent(getIntent());
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        // onResume gets called after this to handle the intent
        setIntent(intent);
    }

    /**
     * Parses the NDEF Message from the intent and prints to the TextView
     */
    void processIntent(Intent intent) {
        textView = (TextView) findViewById(R.id.textView);
        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
                NfcAdapter.EXTRA_NDEF_MESSAGES);
        // only one message sent during the beam
        NdefMessage msg = (NdefMessage) rawMsgs[0];
        // record 0 contains the MIME type, record 1 is the AAR, if present
        textView.setText(new String(msg.getRecords()[0].getPayload()));
    }
}
注意這個code將一個AAR給註解掉了,不過你可以移除。
如果你想開啟AAR,那麼app會指定在AAR裡永遠接收Android Beam的訊息。
如果app並沒安裝,Google Play會自動開始下載app。
所以下面的intent filter並不是對Android 4.0以上裝備技術上必要(如果已經使用AAR):

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:mimeType="application/vnd.com.example.android.beam"/>
</intent-filter>
用這個intent filter,com.example.android.beam app現在當掃到一個NFC tag,
或者收到一個附帶有一個AAR的type com.example.android.beam時,
Android Beam可以被開啟。
或者當一個NDEF格式的訊息,
包含了一個 type application/vnd.com.example.android.beam的MIME紀錄時。

儘管AARs保證app會被開始或下載,intent filters還是較為推薦,
因為可以讓你選擇你的app而不是每次都被AAR綁架XD。

AAR並不去擔保Activity level的部分,而且因為有些Android裝置並不支援AARs,
你應該也把identifying的資訊放在第一個NDEF的紀錄並保進第一個NDEF訊息,
且一併進行篩(filter)的動作。


26 則留言:

  1. 你好,我看了您這邊很仔細的教學,
    想請問 AAR 的部分只能指定 package name 嗎?
    這樣豈不是等於每次都會重新把這個 app 叫起來?
    不能只指定某個 class name 嗎?
    (實際使用過指定 class name後,會變成開啓Google play = 沒找到符合項目@@)

    回覆刪除
    回覆
    1. 您好,
      其實可以將AAR的目的當成是設計者告訴Android:
      "我就是要用這個app就對了!"
      在這個前提下,基本上的確每次都會呼叫特定的app。
      (但叫出來一次以後之後可能留在背景,所以未必每次都是"重新"呼叫)
      也是在這個想法下,指定class name感覺就沒有太大的意義囉。

      另外還會衍伸出一個問題:
      如果您知道的話,
      在Java編譯一個檔案時,
      如果檔案內有2個以上的class的話,
      比如編譯Test.java,裡面有class:
      testDog和testCat。
      編譯產生出來的結果印象中會是:Test$1.class和Test$2.class
      (具體可以自己試試看XD)
      總之除了main會有特定的名稱出現以外,
      其他的一個檔案中的classes通通都會被編號化。
      在這種狀況下,反觀指定package中的class name,
      我會覺得相當不保險。

      退一萬步說,您總不可能只下載一個class就能運作app吧?

      刪除
  2. 請問為什麼 傳送之後 被傳送方只會出現
    play.google.com/store/apps/details?id=(後面是package名字)
    求解 感謝~

    回覆刪除
    回覆
    1. 您好,可以請您具體一點描述您的問題嗎?
      用了什麼方式來傳送NFC,然後傳送了什麼?
      如果是只出現文字,可能表示你傳送的型態是以純文字來判定,
      而非MIME的application。
      再不然還有可能是沒有裝google play,
      但這種可能性大概只會在開發中的測試機上才有,
      而且這樣取而代之也應該會連瀏覽器的play商店才對。

      請再檢查看看吧~

      刪除
  3. 你好^^
    想請問你關於 authenticateSectorWithKeyA 的用法
    謝謝你~

    回覆刪除
    回覆
    1. 您好。
      http://developer.android.com/reference/android/nfc/tech/MifareClassic.html#authenticateSectorWithKeyA(int, byte[])
      如果是用法的話,請您參照如上所附的developer連結。
      實例的部分,可以參見引路蜂的教學:
      http://www.imobilebbs.com/wordpress/archives/2822
      希望對您有幫助~

      刪除
  4. NFC permission required: Neither user 10379 nor current process has android.permission.NFC.

    一開始我認為是 AndroidManifest.xml


    檢查很久,實在找不到錯誤在哪邊。
    請問一下這問題是?

    回覆刪除
    回覆
    1. 請問您有在您的manifest裡面加入這行嗎?

      可能要確認一下在需要使用到NFC的權限處的manifest有加入該項歐~

      刪除
  5. 作者已經移除這則留言。

    回覆刪除
  6. 我有加
    uses-permission android:name="android.permission.NFC"
    uses-feature android:name="android.hardware.nfc" android:required="true"

    我比較擔心是因為我是 Unity 使用 .jar 出現的異常
    但錯誤碼是怎麼看都只是忘記加權限的問題而....所以我很疑惑

    我想貼上 AndroidManifest.xml 但是剛剛試了幾次 空白的沒顯示 ?

    回覆刪除
    回覆
    1. 補充 :
      錯誤 mNfcAdapter.setNdefPushMessageCallback(this, this.activity);
      我想是因為我前面的參數有問題。
      想請問一下,第一個參數該怎麼使用呢?
      真的蠻抱歉的 關於這方面小弟是初學者,不太懂
      this 和 CreateNdefMessageCallback 意思是指?

      刪除
    2. 沒事了 我已經解決!

      刪除
    3. 有解決就好: )
      看了一下,第一個參數不該是this的樣子,
      應該是你寫好格式的一個callback。
      (NfcAdapter.CreateNdefMessageCallback callback)

      刪除
  7. 你好,想請問用AAR的方式開啟某個APP,這之間會有傳送什麼東西嗎?
    我想要"只有以AAR的方式"開啟這個APP時,才會執行某些特定動作,不知行不行得通?

    回覆刪除
    回覆
    1. 在做createNdefMessage的時候,
      您可以看到範例中有加入除了createMime以外的text。
      如果接收的時候您有正確parse,
      我想應該是可以透過這些額外的東西,
      去判斷達到您想要做的事情的。
      (Ex. 傳送特定字串才去call設定好的function)

      刪除
  8. 您好,

    拙學有一個NFC的疑惑請輩指引,NFC手機和NFC晶片卡之間,
    從晶片卡讀取資料,處理加入其他資料(如時間),再寫入晶片卡,
    這一連串動作,是否可以在一次感應下就完成?
    因為目前學生作出來是感應時,只有讀取,不會寫入,再感應才會寫入,
    也就是讀取和寫入要分兩次感應,能不能一次感應就完成讀取,處理,寫入的動作呢?
    查過許多參考書和網路資料,有的說可以,但都沒有實例,全部都是讀寫分開,
    到底可不可以一次感應就完成呢?

    拙學 Stephen tzushow@gmail.com

    回覆刪除
    回覆
    1. 您客氣了,
      關於這個問題請參見
      http://tapintonfc.blogspot.tw/2012/07/nfc-workshop-series-how-to-read-nfc-tag.html
      以及
      http://tapintonfc.blogspot.tw/2012/07/the-above-footage-from-our-nfc-workshop.html
      我本身沒有試過,
      但我猜想應該是可以的,
      因為看function前端均經過如下的傳入:
      protected void onNewIntent(Intent intent) {
      super.onNewIntent(intent);
      if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {

      所以完全可以先透過
      Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
      取得原有的message,
      再透過if(writableTag(detectedTag))這段來進行寫入,
      只不過前者是抓後面的EXTRA_NDEF_MESSAGES來讀取,
      後者是取EXTRA_TAG用以寫入罷了。
      可以根據這樣的思路來嘗試看看,
      再告訴我這樣的猜測正不正確XD

      由於工作忙碌,
      這個部落格疏於回復,
      雖然隔有點久,
      希望上面的想法仍對您有所幫助。

      刪除
  9. 你好,目前初入NFC的世界,想請問這篇文章主是要執行寫入與讀取嗎?

    回覆刪除
    回覆
    1. 您好:
      是的,這篇主要是在談NFC tag的讀寫,
      以及後面的一些延伸的討論和權限設定等等。

      刪除
    2. 你好,可否請問一下,目前我在做的是:切換一些功能開關然後寫進NFC裡面,例如:刷NFC就會自動開WIFI、關GPS、開藍牙這類的,請問我這種資料的寫入也能參考這篇嗎?

      刪除
    3. 你好,我想請問一下,因目前我在做刷NFC會自動切換功能,例如:GPS關閉、藍牙關閉、WIFI開啟,這樣子的,那我能參考這篇把它寫進NFC嗎?

      刪除
    4. 你可以試著用前面提過的這兩篇來實作讀寫:
      http://tapintonfc.blogspot.tw/2012/07/nfc-workshop-series-how-to-read-nfc-tag.html
      以及
      http://tapintonfc.blogspot.tw/2012/07/the-above-footage-from-our-nfc-workshop.html
      至於說後端你接收到tag後做的功能,
      應該是去看你自己讀取後要怎麼做,
      跟NFC本身就沒有關係了。

      刪除
    5. 您好,請教:"把功能寫進NFC後,當NFC接收到就變更對應設定的狀態"
      這句話是否與您的"應該是去看你自己讀取後要怎麼做,跟NFC本身就沒有關係了。"
      意思一樣呢?

      不好意思再請教個問題,GPS與行動網路的直接切換控制,網路上都說無法設定,我也查不到可執行的觸發事件,請問目前仍然無法克服嗎?

      感謝您

      刪除
    6. 奇怪..留言會被吃掉?!

      您好,請教:"把功能寫進NFC後,當NFC接收到就變更對應設定的狀態"
      這句話是否與您的"應該是去看你自己讀取後要怎麼做,跟NFC本身就沒有關係了。"
      意思一樣呢?

      不好意思再請教個問題,GPS與行動網路的直接切換控制,網路上都說無法設定,我也查不到可執行的觸發事件,請問目前仍然無法克服嗎?

      感謝您

      刪除
    7. 意思差不多,因為那是後面的操作了。
      Android更新到M以後會有更嚴格的限制,
      對於相對應的權限,
      除了要宣告在Manifest裡面以外,
      你的程式碼同樣也要有做要求取得權限的動作。
      這個部分應該網路上蠻多的,可以去搜尋看看android request permission的範例。
      GPS開關的部分,如果牽扯到了security的部分,權限操作較麻煩,可以自行搜尋看看android m GPS permission看看有沒有解法,我個人沒有試過這類,能不能用不好下斷言。

      刪除