近場通信(Near Field Communication,NFC)是一種短距離(最多20cm)、高頻率的無線通信技術。它是一個標準,通過把智能卡和閱讀器接口集成到單個設備中,擴展了無線電頻率識別(RFID)標準。該標準最初是為了在手機上使用,因此吸引了很多對非接觸式數據傳輸感興趣的供應商(例如信用卡銷售商)。該標準使得NFC有以下3種具體應用方式:
智能卡仿真
設備是非接觸式智能卡(因此其他閱讀器可以讀取)。
讀卡器模式
設備可以讀取RFID標籤。
P2P模式
兩個設備可以來回溝通並交換數據。
在Android 2.3(API level 9)中,Google引入了NFC功能中的讀卡器模式。從Android 2.3.3(API level 10)開始,還可以將數據寫到NFC標籤並通過P2P模式交換。
NFC標籤中包含的是使用NFC數據交換格式編碼的數據,其消息格式遵循的協議是:NFC Forum Type 2 Specification。每條NDEF消息包含一條或多條NDEF記錄。關於NFC的官方技術說明書可以在http://www.nfc-forum.org/獲取。為了開發和測試NFC應用,強烈建議獲取NFC兼容的設備(例如Nexus S,在http://www.google.com/phone/detail/nexus-s)和NFC兼容的標籤。
為了在應用中使用NFC功能,需要在清單文件中聲明以下許可權限:
<uses-permission android:name="android.permission.NFC" />
為了把應用限制為使用NFC的設備,需要在manifest文件中添加以下代碼:
<uses-feature android:name="android.hardware.nfc" />
讀標籤
當掃瞄RFID/NFC標籤時,讀卡器模式會接收通知。在Android 2.3(API level 9)中,實現這一點的唯一方式是創建一個Activity,它監聽android.nfc.action.TAG_DISCOVERED intent,當讀取一個標籤時會廣播該intent。Android 2.3.3(API level 10)提供了更全面的方式來接收該通知,其遵循如圖17-2所示的過程。
圖17-2:Android 2.3.3(API level 10)中的NFC標記流
在Android 2.3.3(API level 10)以及更新的版本中,當發現NFC標籤時,在Intent中會放置一個標籤對像(Parcelable)作為EXTRA_TAG。然後,系統開始跟從邏輯流,確定要發送Intent的最佳Activity。在設計上,它優先把標籤分發給正確的活動,而不需要向用戶彈出活動選擇器對話框(即在透明模式下),以避免在標籤和設備之間的連接被不必要的用戶交互干擾。首先要檢查的是在前端是否存在Activity,其調用了enableForegroundDispatch方法。如果該Activity存在,intent就會傳遞給該Activity並結束;如果不存在,系統會檢查標籤數據的第一條NdefMessage。如果NdefRecord是URI、Smart Poster或MIME數據,系統會檢查註冊了ACTION_NDEF_DISCOVEREDintent並包含這種類型數據的Activity(android.nfc.action.NDEF_DISCOVERED)。如果存在該Activity,該匹配的Activity(匹配越接近越好)會接收intent。如果不存在匹配的Activity,系統會查找註冊了ACTION_TECH_DISCOVERED的活動,並且匹配特定的標籤技術集(再次強調,匹配越接近越好)。如果存在匹配的活動,intent會傳遞給該活動。然而,如果在前面的檢查中都沒有找到匹配的活動,intent會最終作為ACTION_TAG_DISCOVERED傳遞,這和Android 2.3(API level 9)處理標籤的方式類似。
為了把前端的Activity設置為第一個接收標籤的Activity,必須檢索NFC設備適配器,並調用Activity的context引用的enableForegroundDispatch方法。實際的NFC設備適配器是由類NfcAdapter表示的。為了檢索該設備的適配器,啟動Android 2.3(API level 9)中的getDefaultAdapter方法或Android 2.3.3(API level 10)中的getDefaultAdapter(context)方法:
NfcAdapter adapter = NfcAdapter.getDefaultAdapter; // --- for API 10 only // NfcAdapter adapter = NfcAdapter.getDefaultAdapter(context); if(adapter != null) { // true if enabled, false if not boolean enabled = adapter.isEnabled; }
一旦檢索到NFC設備適配器,構建PendingIntent對象,並把它傳遞給enableForegroundDispatch方法。該方法必須從主線程中調用,而且Activity必須正在前端運行(調用了onResume方法):
PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(this, getClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0); NfcAdapter.getDefaultAdapter(this).enableForegroundDispatch(this, intent, null, null);
當Activity離開前端(調用了onPause方法)後,必須調用disableForeground-Dispatch方法:
@Override protected void onPause { super.onPause; if(NfcAdapter.getDefaultAdapter(this) != null) NfcAdapter.getDefaultAdapter(this).disableForegroundDispatch(this); } }
如果一個Activity註冊了ACTION_NDEF_DISCOVERED,那麼這個Activity必須把android.nfc.action.NDEF_DISCOVERED作為intent-filter,並在manifest文件中將其指定為專用的數據過濾器:
<activity android:name=".NFC233"> <!-- listen for android.nfc.action.NDEF_DISCOVERED --> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED"/> <data android:mimeType="text/*" /> </intent-filter> </activity>
這也適合TECH_DISCOVERED的情況(以下示例也包含描述特定技術的元數據資源,它保存在NFC標籤中,如NDEF內容):
<activity android:name=".NFC233"> <intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED" /> </intent-filter> <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfcfilter" /> </activity> <?xml version="1.0" encoding="utf-8"?> <!-- capture anything using NfcF or with NDEF payloads--> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <tech-list> <tech>android.nfc.tech.NfcF</tech> </tech-list> <tech-list> <tech>android.nfc.tech.NfcA</tech> <tech>android.nfc.tech.MifareClassic</tech> <tech>android.nfc.tech.Ndef</tech> </tech-list> </resources>
註冊ACTION_TAG_DISCOVERED intent的示例manifest文件的內容如下所示:
<!-- this will show up as a dialog when the nfc tag is scanned --> <activity android:name=".NFC" android:theme="@android:style/Theme.Dialog"> <intent-filter> <action android:name="android.nfc.action.TAG_DISCOVERED"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity>
當讀取一個標籤時,系統會把有效負荷作為關聯數據廣播intent。在Android 2.3.3(API level 10)中,Tag對象也作為EXTRA_TAG包含進來。該Tag對像提供檢索特定的TagTechnology的方式,並能夠執行高級操作(如I/O)。注意,該類傳遞和返回數組時沒有使用clone方式,所以注意不要修改它們:
Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
在Android 2.3(API level 9)及更新的版本中,該標籤的ID作為字節數組封裝在intent中,其key是「android.nfc.extra.ID」(NfcAdapter.EXTRA_ID):
byte byte_id = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
該數據是作為Parcelable對像(NdefMessage)數組打包的,其key是「android.nfc.extra.NDEF_MESSAGES」(NfcAdapter.EXTRA_NDEF_MESSAGES):
Parcelable msgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); NdefMessage nmsgs = new NdefMessage[msgs.length]; for(int i=0;i<msgs.length;i++) { nmsgs[i] = (NdefMessage) msgs[i]; }
每個NdefMessage對像內部都有一個NdefRecord數組。該記錄總是包含3比特的TNF(類型名稱格式,type name format)、記錄類型、唯一ID以及有效負荷。關於這方面的具體信息,可查看NdefRecord文檔(http://developer.android.com/reference/android/nfc/NdefRecord.html)。現在,已經存在一些已知的類型,這裡將探討4種最常見的:TEXT、URI、SMART_POSTER和ABSOLUTE_URI:
// enum of types we are interested in private static enum NFCType { UNKNOWN, TEXT, URI, SMART_POSTER, ABSOLUTE_URI } private NFCType getTagType(final NdefMessage msg) { if(msg == null) return null; // we are only grabbing the first recognizable item for (NdefRecord record : msg.getRecords) { if(record.getTnf == NdefRecord.TNF_WELL_KNOWN) { if(Arrays.equals(record.getType, NdefRecord.RTD_TEXT)) { return NFCType.TEXT; } if(Arrays.equals(record.getType, NdefRecord.RTD_URI)) { return NFCType.URI; } if(Arrays.equals(record.getType, NdefRecord.RTD_SMART_POSTER)) { return NFCType.SMART_POSTER; } } else if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) { return NFCType.ABSOLUTE_URI; } } return null; }
NdefRecord.RTD_TEXT類型的有效載荷,其第一個字節會定義狀態,這種類型還會對有效載荷中的文本進行編碼:
/* * the First Byte of the payload contains the "Status Byte Encodings" field, * per the NFC Forum "Text Record Type Definition" section 3.2.1. * * Bit_7 is the Text Encoding Field. * * if Bit_7 == 0 the the text is encoded in UTF-8 * * else if Bit_7 == 1 then the text is encoded in UTF16 * Bit_6 is currently always 0 (reserved for future use) * Bits 5 to 0 are the length of the IANA language code. */ private String getText(final byte payload) { if(payload == null) return null; try { String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16"; int languageCodeLength = payload[0] & 0077; return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding); } catch (Exception e) { e.printStackTrace; } return null; }
標準URI類型(NdefRecord.RTD_URI)的有效載荷,其第一個字節定義URI的前綴:
/** * NFC Forum "URI Record Type Definition" * * Conversion of prefix based on section 3.2.2 of the NFC Forum URI Record * Type Definition document. */ private String convertUriPrefix(final byte prefix) { if(prefix == (byte) 0x00) return ""; else if(prefix == (byte) 0x01) return "http://www."; else if(prefix == (byte) 0x02) return "https://www."; else if(prefix == (byte) 0x03) return "http://"; else if(prefix == (byte) 0x04) return "https://"; else if(prefix == (byte) 0x05) return "tel:"; else if(prefix == (byte) 0x06) return "mailto:"; else if(prefix == (byte) 0x07) return "ftp://anonymous:anonymous@"; else if(prefix == (byte) 0x08) return "ftp://ftp."; else if(prefix == (byte) 0x09) return "ftps://"; else if(prefix == (byte) 0x0A) return "sftp://"; else if(prefix == (byte) 0x0B) return "smb://"; else if(prefix == (byte) 0x0C) return "nfs://"; else if(prefix == (byte) 0x0D) return "ftp://"; else if(prefix == (byte) 0x0E) return "dav://"; else if(prefix == (byte) 0x0F) return "news:"; else if(prefix == (byte) 0x10) return "telnet://"; else if(prefix == (byte) 0x11) return "imap:"; else if(prefix == (byte) 0x12) return "rtsp://"; else if(prefix == (byte) 0x13) return "urn:"; else if(prefix == (byte) 0x14) return "pop:"; else if(prefix == (byte) 0x15) return "sip:"; else if(prefix == (byte) 0x16) return "sips:"; else if(prefix == (byte) 0x17) return "tftp:"; else if(prefix == (byte) 0x18) return "btspp://"; else if(prefix == (byte) 0x19) return "btl2cap://"; else if(prefix == (byte) 0x1A) return "btgoep://"; else if(prefix == (byte) 0x1B) return "tcpobex://"; else if(prefix == (byte) 0x1C) return "irdaobex://"; else if(prefix == (byte) 0x1D) return "file://"; else if(prefix == (byte) 0x1E) return "urn:epc:id:"; else if(prefix == (byte) 0x1F) return "urn:epc:tag:"; else if(prefix == (byte) 0x20) return "urn:epc:pat:"; else if(prefix == (byte) 0x21) return "urn:epc:raw:"; else if(prefix == (byte) 0x22) return "urn:epc:"; else if(prefix == (byte) 0x23) return "urn:nfc:"; return null; }
在絕對URI(NdefRecord.TNF_ABSOLUTE_URI)類型中,整個有效負荷是以UTF-8編碼並組成URI:
if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) { String uri = new String(record.getPayload, Charset.forName("UTF-8"); }
特殊的Smart Poster(NdefRecord.RTD_SMART_POSTER)類型包含多條文本子記錄或URI(或絕對URI)數據:
private void getTagData(final NdefMessage msg) { if(Arrays.equals(record.getType, NdefRecord.RTD_SMART_POSTER)) { try { // break out the subrecords NdefMessage subrecords = new NdefMessage(record.getPayload); // get the subrecords String fulldata = getSubRecordData(subrecords); System.out.println("SmartPoster: "+fulldata); } catch (Exception e) { e.printStackTrace; } } } // method to get subrecord data private String getSubRecordData(final NdefRecord records) { if(records == null || records.length < 1) return null; String data = ""; for(NdefRecord record : records) { if(record.getTnf == NdefRecord.TNF_WELL_KNOWN) { if(Arrays.equals(record.getType, NdefRecord.RTD_TEXT)) { data += getText(record.getPayload) + "\n"; } if(Arrays.equals(record.getType, NdefRecord.RTD_URI)) { data += getURI(record.getPayload) + "\n"; } else { data += "OTHER KNOWN DATA\n"; } } else if(record.getTnf == NdefRecord.TNF_ABSOLUTE_URI) { data += getAbsoluteURI(record.getPayload) + "\n"; } else data += "OTHER UNKNOWN DATA\n"; } return data; }
寫入Tag對像
Android 2.3.3(API level 10)提供了把數據寫入Tag對象的功能。為了實現此功能,必須使用Tag對像獲取該標籤內合適的TagTechnology。NFC標籤基於很多獨立的技術,提供了很多功能。TagTechnology實現了基於這些技術的各種功能。因此,需要使用NDEF技術來檢索和修改標籤中的NdefRecords和NdefMessages:
// get the tag from the Intent Tag mytag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); // get the Ndef (TagTechnology) from the tag Ndef ndefref = Ndef.get(mytag);
注意,當使用TagTechnology執行I/O操作時需要注意如下幾點:
·在使用其他I/O操作前必須調用connect方法。
·I/O操作可能是阻塞式的,而且在應用的main線程中永遠都不應該調用它。
·一次只能連接一個TagTechnology。後續的connect調用會返回IOException。
·在通過TagTechnology完成I/O操作後,必須調用close方法,該方法會通過IOException撤銷所有在其他線程(包括connect方法)中執行的阻塞式的I/O操作。
因此,要把數據寫到tag中,會在獨立於main線程的線程中調用connect方法。一旦完成該操作,會檢查isConnected方法,來驗證連接是否已經建立。如果連接已經建立,則會調用包含構造的NdefMessage(至少包含一條記錄)的writeNdefMessage方法。寫入數據之後,接下來會調用close方法來清理進程。
使用NDEF TagTechnology引用,把文本記錄寫入到tag中的完整代碼如下:
// pass in the Ndef TagTechnology reference and the text we wish to encode private void writeTag(final Ndef ndefref, final String text) { if(ndefref == null || text == null || !ndefref.isWritable) { return; } (new Thread { public void run { try { Message.obtain(mgsToaster, 0, "Tag writing attempt started").sendToTarget; int count = 0; if(!ndefref.isConnected) { ndefref.connect; } while(!ndefref.isConnected) { if(count > 6000) { throw new Exception("Unable to connect to tag"); } count++; sleep(10); } ndefref.writeNdefMessage(msg); Message.obtain(mgsToaster, 0, "Tag write successful!").sendToTarget; } catch (Exception t) { t.printStackTrace; Message.obtain(mgsToaster, 0, "Tag writing failed! - "+t.getMessage).sendToTarget; } finally { // ignore close failure... try { ndefref.close; } catch (IOException e) { } } } }).start; } // create a new NdefRecord private NdefRecord newTextRecord(String text) { byte langBytes = Locale.ENGLISH. getLanguage. getBytes(Charset.forName("US-ASCII")); byte textBytes = text.getBytes(Charset.forName("UTF-8")); char status = (char) (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); return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data); }
P2P模式和Beam
在Android 2.3.3(API level 10)中,當一個設備被設置為通過NFC把數據傳輸到另一個NFC數據接收設備上時,會啟用P2P模式。發送數據的設備可能也會接收到數據接收設備所發送的數據,因此會出現對等(P2P)通信。在API 10中,這是通過前置推送方式來完成的,但是該方法在後期的API發佈(API 14+,Android 4.0+)中被廢棄了,取而代之的是一個新的推送API,稱為Beam。在這一節中,我們將描述這兩個方法。
API 10-13
在API 10中,NfcAdapter類的enableForegroundNdePush方法會完成P2P NFC消息交換。因此,當Activity處於活動狀態(在前台運行)時,會向另一台支持com.android.npp NDEF推送協議的設備發送一條NdefMessage消息。enableForegroundNdefPush方法必須在主線程中、在通信開始之前調用(正如其onResume方法),當Activity在後台運行(在onPause方法)時,應該取消該方法。
@Override public void onResume { super.onResume; NdefRecord rec = new NdefRecord[1]; rec[0] = newTextRecord("NFC Foreground Push Message"); NdefMessage msg = new NdefMessage(rec); NfcAdapter.getDefaultAdapter(this).enableForegroundNdefPush(this, msg); } // create a new NdefRecord private NdefRecord newTextRecord(String text) { byte langBytes = Locale.ENGLISH. getLanguage. getBytes(Charset.forName("US-ASCII")); byte textBytes = text.getBytes(Charset.forName("UTF-8")); char status = (char) (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); return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data); }
當enableForegroundNdefPush方法處在活躍狀態時,標準的tag分發方式會被禁用。只有前端的活動可能會通過enableForegroundDispatch方法接收到標籤發現(tag-discovered)請求。通過這種方式,可以確保其他活動和服務不會攔截正在掃瞄的NFC標籤,從而保證正在運行的活動接收到數據。
當Activity不再在前端(調用完onPause方法後)時,注意必須調用disableFore-groundNdefPush方法:
@Override protected void onPause { super.onPause; if(NfcAdapter.getDefaultAdapter(this) != null) { NfcAdapter.getDefaultAdapter(this).disableForegroundNdefPush(this); } }
Beam:API 14+
要使用Android Beam,其設備必須取消鎖定屏幕,初始化該beam的設備必須正在運行相應的Activity。
在API 14以及更新版本中,Android Beam功能是通過NfcAdapter的兩個方法來實現的:setNdefPushMessage和setNdefPushMessageCallback。setNdefPushMessage方法接收參數NdefMessage,並且會立即發送一條消息,而setNdefPushMessageCallback是異步執行的,並提供了接口NfcAdapter.CreateNdefMessageCallback。當一台設備在另一台設備的NFC通信範圍內,會調用該接口的CreateNdefMessage方法。如果調用兩個pushMessage方法,會先執行setNdefPushMessageCallback方法。
// here we use the callback to push a message via the NfcAdapter NfcAdapter nfcadapter = NfcAdapter.getDefaultAdapter(this); // here a callback is generated CreateNdefMessageCallback nfccallback = new CreateNdefMessageCallback { @Override public NdefMessage createNdefMessage(NfcEvent event) { String text = "Beaming via callback"; byte mimeBytes = "application/com.oreilly.demo.android.pa.sensordemo". getBytes(Charset.forName("US-ASCII"));NdefRecord mimeRecord = new Ndef Record(NdefRecord.TNF_MIME_MEDIA, mimeBytes, new byte[0], text.getBytes); NdefMessage msg = new NdefMessage(new NdefRecord {mimeRecord}); return msg; } }; nfcadapter.setNdefPushMessageCallback(nfccallback, this); // here we just push a message directly String directtext = "Beaming Directly"; byte directMimeBytes = "application/com.oreilly.demo.android.pa.sensordemo". getBytes(Charset.forName("US-ASCII"));NdefRecord directMimeRecord = new NdefRecord (NdefRecord.TNF_MIME_MEDIA, directMimeBytes, new byte[0], directtext.getBytes); NdefMessage directmsg = new NdefMessage(new NdefRecord {directMimeRecord}); nfcadapter.setNdefPushMessage(directmsg, this);
在前面的例子中,沒有使用Android Application Record(AAR),因此在manifest文件中,關於該活動的定義會包含如下intent過濾器:
<activity android:name=".NFC40"> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED"/> <category android:name="android.intent.category.DEFAULT"/> <data android:mimeType="application/com.oreilly.demo.android.pa.sensordemo"/> </intent-filter> </activity>
如果你想在應用層處理NFC操作,強烈建議你使用AAR,而不是intent過濾器(由於包名約束,AAR在活動級別不可用),因此其他應用不能干預特定的NFC操作的處理。
// generate the NdefMessage with an AAR NdefMessage msg = new NdefMessage(new NdefRecord {mimeRecord, NdefRecord.createApplicationRecord("com.oreilly.demo.android.pa.sensordemo")}); nfcadapter.setNdefPushMessage(msg, this);