前言
近场通信(NFC)是一种NXP半导体公司提出来的被动技术,它可以在支持NFC的设备上使用“tag”。它是一种有效距离非常小的无线技术,这个距离大概是4cm,但是也可以到10cm,这取决于设备的无线电波和tag的大小。
不像蓝牙的Beacon,NFC tag不需要电源。这使它在半永久的地方使用非常合理,而且可以作为一个媒介来使任务自动化,或者把信息分发到一系列的位置。
NFC之NDEF
信息以比特数据的形式保存在NDEF(NFC数据交换格式)信息里。每个NDEF消息包含至少一个NDEF记录,该记录包含以下字段:
- 3 bit类型名格式(TNF)
- 变量长度类型
- 变量长度ID(可选)
- 变量长度有效负载
TNF字段包含的值是给Android系统用来判断如何处理剩下的NDEF消息中的信息的。剩下的数据通常包含在一个物理的tag内部。但是,使用类似Android Beam的技术,一个设备本身可以可以承担物理tag的角色。
请注意,不是所有的NFC tag都可以在Android设备内部的NFC读取器不完全兼容。根据NFC论坛的定义,有以下类型的NFC tag:
- Type 1 : 基于ISO/IEC 14443A,可读可写,能设置为只读,而且只有96B的空间,但是可以扩展到2KB。
- Type 2 : 基于ISO/IEC 14443A,可读可写,能设置为只读,而且由48B的空间,但是可以扩展到2KB。
- Type 3 : 基于(JIS) X 6319-4,预设值成可读和可写或者只读,而且内存最多可以到1MB。
- Type 4 :与ISO/IEC 14443兼容,预设值成可读和可写或者只读,而且内存最多可以到32KB。
- MIFARE Classic : 与ISO/IEC 14443兼容,可读可写,能设置成只读,而且有1KB或者4KB可用空间。
Android中使用NFC
这些是最通用的tag类型,但是有些正在流通的NFC tag不符合NFC论坛的标准。这些tag不能确保与所有的NFC硬件兼容。根据设备制造商的不同,你可以发现一些Android设备可以读那些tag,但是其他的却不行。MIFARE传统是这种tag的一个例子,有些Android设备读取或者写不了它。知道这一点很重要的。因为它可能让一些用户感觉很困惑,尤其当他们换了设备突然发现一些tag在新设备上不工作了。
在应用中使用NFC要求有NFC权限。若要把这个权限加到应用中,可以打开manifest XML文件,然后加入以下的代码:
<uses-permission android:name="android.permission.NFC"/>
另外,你也可以把<users-feature />
元素加到manifest中,这样Google Play Store可以对应用进行过滤,以致没有NFC的设备没法下载它。这是可选的,但是它可以使你不让用户失望。把下列代码加到应用manifest XML中可以开启Google Play Store的过滤功能:
<uses-feature android:name="android.hardware.nfc" android:required="true" />
当你的设备扫描tag的时候,它会读取存在于TNF中的数据,并判断tag的MIME类型或URL。内部的tag分发系统用来判断tag是否是兼容的、空的,或者它应该是在一个特定的应用里打开。决定使用哪个应用打开依赖于创建的Intent,然后与适合的Activity进行匹配。
如果你的应用要响应Intent,则需要过滤一个或者多个下面的Intent:
- ACTION_NDEF_DISCOVERED
- ACTION_TECH_DISCOVERED
- ACTION_TAG_DISCOVERED
ACTION_NDEF_DISCOVERED
如果要过滤这个Intent,可以基于MIME类型或者在URI上实现。下面是一个对MIME类型的text/plain的进行过滤:
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain" />
</intent-filter>
过滤URI的方法类似,但是要把<data>
元素的属性android:mimetype
改成android:scheme
,而且还需要添加一些属性。下面是一个过滤http://www.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="www.android.com"
android:pathPrefix="/index.html" />
</intent-filter>
ACTION_TECH_DISCOVERED
当过滤这个Intent的时候,需要创建一个包含所有你想监控的技术类型的资源XML文件。这可确保当扫描到一个tag的时候,你的应用只在tag包含期望的技术的时候才打开。这个文件应该存在于工程的/res/xml
目录下。示例如下:
<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>
为了引用到你的XML技术列表,需要加一个<meta-data>
的标签到应用manifest XML
中,这回包含你的资源列表的路径。下面是一个使用需要引用到ACTION_TECH_DISCOVERED
的<intent-filter>
和<meta-data>
的例子:
<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" />
请注意,<meta-data>
元素中使用的资源路径由一个属性值为@xml/nfc_tech_filter
。这个值指向工程中的文件/res/xml/nfc_tech_filter.xml
。
ACTION_TAG_DISCOVERED
ACTION_TAG_DISCOVERED是最后一个Intent,也可能是最容易实现的了。因为你不需要用到过滤的tag包含的类型技术或者信息的时候,可以使用如下<intent-filter>
:
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>
读/写信息到NFC tag的时候,要求你定义自己的协议栈。下面的代码演示了如何使用相当普遍的MIFARE Ultralight tag
:
public class MifareUltralightTagTester {
private static final String TAG =
MifareUltralightTagTester.class.getSimpleName();
// 写到tag中:
public void writeTag(Tag tag, String tagText) {
MifareUltralight ultralight = MifareUltralight.get(tag);
try {
ultralight.connect();
ultralight.writePage(4, "abcd".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(5, "efgh".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(6, "ijkl".getBytes(Charset.forName("US-ASCII")));
ultralight.writePage(7, "mnop".getBytes(Charset.forName("US-ASCII")));
} catch (IOException e) {
Log.e(TAG, "IOException while closing MifareUltralight", e);
} finally {
if (ultralight != null) {
try {
ultralight.close();
} catch (IOException e) {
Log.e(TAG, "IOException while closing MifareUltralight", e);
}
}
}
}
// 读取tag信息:
public String readTag(Tag tag) {
MifareUltralight mifare = MifareUltralight.get(tag);
try {
mifare.connect();
byte[] payload = mifare.readPages(4);
return new String(payload, Charset.forName("US-ASCII"));
} catch (IOException e) {
Log.e(TAG, "IOException while writing MifareUltralight message", e);
} finally {
if (mifare != null) {
try {
mifare.close();
}
catch (IOException e) {
Log.e(TAG, "Error closing tag", e);
}
}
}
return null;
}
}
你可能想知道这段代码是如何工作的,比如,当附近由一个tag的时候,你已经定义了一个Intent去触发。如果没有这个问题的解决方案,每次你把tag放在手机旁边,它不会写而会不断地读这个tag。这是Foreground Dispatch System
的由来。
Foreground Dispatch System
允许你破解一个Intent而阻止它去正常情况下要去的地方。它要求在应用的onCreate()
方法中加一个PendingIntent
,以及在onPause()
里使用disableForegroundDispatch()
以及在onResume()
函数里调用enableForegroundDispatch()
。最后,必须要创建一个方法来处理来自扫描NFC tag的数据。
下面演示了如何使用Foreground Dispatch System
:
@Override
protected void onCreate(Bundle savedInstanceState) {
// 函数代码在这里
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, new Intent(this, getClass())
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
// 添加一个IntentFilter来告之拦截的是什么。
IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
// 这会捕获所有的MIME
ndef.addDataType("*/*");
} catch (MalformedMimeTypeException e) {
throw new RuntimeException("fail", e);
}
intentFiltersArray = new IntentFilter[] { };
// techListsArray是用来创建你讲支持的技术列表
// 这是开启前台分发时使用的。
techListsArray = new String[][] { new String[] { NfcF.class.getName() } };
}
@Override
public void onPause() {
super.onPause();
// 释放以恢复默认的扫描行为
myAdapter.disableForegroundDispatch(this);
}
@Override
public void onResume() {
super.onResume();
// 开启以拦截默认的扫描行为
myAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray,
techListsArray);
}
public void onNewIntent(Intent intent) {
Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
// 这里的逻辑用来处理tagFromIntent
}
附录
- Android Development Patterns-Best Practices for Professional Developers
- [美]Phil Dutson