基本概念以及读取过程
近场通信(NFC)是一个短范围无线技术集合,通常需要4厘米或更短的距离才能初始化连接。NFC允许在NFC标签和Android设备之间或两个Android设备之间共享小的数据的负载。
NFC标签具有复杂的分类。简单的NFC标签只提供读写语法,某些时候一次只能以只读的方式读取卡片的可编程区域。复杂一点的NFC标签提供了数学运算能力,而且有加密的硬件来认证对一个扇区的访问。最复杂的NFC标签包含了运算环境,允许在标签上执行复杂的交互代码。存储在标签中的数据也可以用各种格式来编写,但是大多数的Android框架API都使用基于NDEF(NFC Data Exchange Format)的标准。
本文介绍在Android系统中,通过你所能执行的基本任务。它解释了如何用NDEF消息格式来发送和接收NFC数据,并且介绍了支持这些功能的Android框架API。有关更高级的话题,包括对非NDEF格式数据的讨论,情况“高级 NFC”
NDEF数据和Android一起工作的场景主要有两个:
1. 从NFC标签中读取NDEF数据;
2. 把NDEF消息从一个设备发送给另一个设备。
从NFC标签中读取NDEF数据是用标签调度系统来处理的,它会分析被发现的NFC标签,对数据进行适当的分类,并启动对该类数据感兴趣的应用程序。想要处理被扫描到NFC标签的应用程序会声明一个Intent过滤器,并请求处理数据。
Android Beam™ 功能允许设备把一个NDEF消息推送到物理上相互监听的另一个设备上。这种交互提供了比其他无线技术(如蓝牙)更容易的发送数据的方法。因为NFC不需要手动的设备发现或配对要求。两个设备在接近到一定范围时会自动的连接。Android Beam通过一组NFC API来使用,以便应用程序能够在设备之间来传输信息。例如,通信录、浏览器以及YouTube等应用程序都使用Android Beam来跟其他设备共享通信录、网页和视频。
NFC标签调度系统
通常,除非是在设备的设置菜单中NFC被禁用,否则Android设备会在非锁屏的状态下搜索NFC。当Android设备发现NFC标签时,期望的行为是用最合适的Activity来处理该Intent,而不是询问用户使用什么应用程序。因为设备只能在很短的范围内扫描到NFC标签,强制的让用户手动的选择一个Activity,会导致设备离开NFC标签,从而中断该连接。你应该开发你自己的Activity来处理你所关心的NFC标签,从而阻止 选择器的操作。
为了帮助你达到这个目标,Android提供了特殊的标签调度系统,来分析扫描到的NFC标签,通过解析数据,在被扫描到的数据中尝试找到感兴趣的应用程序,具体做法如下:
1. 解析NFC标签并搞清楚标签中标识数据负载的MIME类型或URI;
2. 把MIME类型或URI以及数据负载封装到一个Intent中。
3. 基于Intent来启动Activity。
怎样把NFC标签映射到MIME类型和URI
开始编写NFC应用程序之前,重要的是要理解不同类型的NFC标签、标签调度系统是如何解析NFC标签的、以及在检测到NDEF消息时,标签调度系统所做的特定的工作等。NFC标签涉及到广泛的技术,并且有很多不同的方法向标签中写入数据。Android支持由NFC Forum所定义的NDEF标准。
NDEF数据被封装在一个消息(NdefMessage)中,该消息中包含了一条或多条记录(NdefRecord)。每个NDEF记录必须具有良好的你想要创建的记录类型的规范的格式。Android也支持其他的不包含NDEF数据类型的标签,你能够使用android.nfc.tech包中的类来工作。要使用其他类型标签来工作,涉及到编写自己的跟该标签通信的协议栈,因此我们建议你尽可能的使用NDEF,以便减少开发难度,并且最大化的支持Android设备。
注意:要下载完整的NDEF规范,请去“NFC论坛规范下载”网址来下载。
现在,你已经具备了一些NFC标签的背景知识,接下来要详细的介绍Android是如何处理NDEF格式的标签的。当Android设备扫描到包含NDEF格式数据的NFC标签时,它会解析该消息,并尝试搞清楚数据的MIME类型或URI标识。首先系统会读取消息(NdefMessage)中的第一条NdefRecord,来判断如何解释整个NDEF消息(一个NDEF消息能够有多条NDEF记录)。在格式良好的NDEF消息中,第一条NdefRecord包含以下字段信息:
3-bit TNF(类型名称格式)
指示如何解释可变长度类型字段,在下表1中介绍有效值。
可变长度类型
说明记录的类型,如果使用TNF_WELL_KNOWN,那么则使用这个字段来指定记录的类型定义(RTD)。在下表2中定义了有效的RTD值。
可变长度ID
唯一标识该记录。这个字段不经常使用,但是,如果需要唯一的标识一个标记,那么就可以为该字段创建一个ID。
可变长度负载
你想读/写的实际的数据负载。一个NDEF消息能够包含多个NDEF记录,因此不要以为在NDEF消息的第一条NDEF记录中包含了所有的负载。
标签调度系统使用TNF和类型字段来尝试把MIME类型或URI映射到NDEF消息中。如果成功,它会把信息跟实际的负载一起封装到ACTION_NEDF_DISCOVERED类型的Intent中。但是,会有标签调度系统不能根据第一条NDEF记录来判断数据类型的情况,这样就会有NDEF数据不能被映射到MIME类型或URI,或者是NFC标签没有包含NDEF开始数据的情况发生。在这种情况下,就会用一个标签技术信息相关的Tag对象和封装在ACTION_TECH_DISCOVERED类型Intent对象内部的负载来代替。
表1.介绍标签调度系统映射如何把TNF和类型字段映射到MIME型或URI上。同时也介绍了那种类型的TNF不能被映射到MIME类型或URI上。这种情况下,标签调度系统会退化到ACTION_TECH_DISCOVERED类型的Intent对象。
例如,如果标签调度系统遇到一个TNF_ABSOLUTE_URI类型的记录,它会把这个记录的可变长度类型字段映射到一个URI中。标签调度系统会把这个URI跟其他相关的标签的信息(如数据负载)一起封装到ACTION_NDEF_DISCOVERED的Intent对象中。在另一方面,如果遇到了TNF_UNKNOWN类型,它会创建一个封装了标签技术信息的Intent对象来代替。
表1.所支持的TNF和它们的映射
类型名称格式(TNF) | 映射 |
TNF_ABSOLUTE_URI | 基于类型字段的URI |
TNF_EMPTY | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
TNF_EXTERNAL_TYPE | 基于类型字段中URN的URI。URN是缩短的格式(<domain_name>:<service_name)被编码到NDEF类型中。Android会把这个URN映射成以下格式的URI:vnd.android.nfc://ext/<domain_name>:<service_name>。 |
TNF_MIME_MEDIA | 基于类型字段的MIME类型 |
TNF_UNCHANGED | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
TNF_UNKNOWN | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
TNF_WELL_KNOWN | 依赖你在类型字段中设置的记录类型定义(RTD)的MIME类型或URI, |
表2.TNF_WELL_KNOWN所支持的RTD和它们的映射
记录类型定义(RTD) | 映射 |
RTD_ALTERNATIVE_CARRIER | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
RTD_HANDOVER_CARRIER | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
RTD_HANDOVER_REQUEST | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
RTD_HANDOVER_SELECT | 退化到ACTION_TECH_DISCOVERED类型的Intent对象 |
RTD_SMART_POSTER | 基于负载解析的URI |
RTD_TEXT | text/plain类型的MIME |
RTD_URI | 基于有效负载的URI |
应用程序如何调度NFC标签
当标签调度系统完成对NFC标签和它的标识信息封装的Intent对象的创建时,它会把该Intent对象发送给感兴趣的应用程序。如果有多个应用程序能够处理该Intent对象,就会显示Activity选择器,让用户选择Activity。标签调度系统定义了三种Intent对象,以下按照由高到低的优先级列出这三种Intent对象:
1. ACTION_NDEF_DISCOVERED:这种Intent用于启动包含NDEF负载和已知类型的标签的Activity。这是最高优先级的Intent,并且标签调度系统在任何其他Intent之前,都会尽可能的尝试使用这种类型的Intent来启动Activity。
2. ACTION_TECH_DISCOVERED:如果没有注册处理ACTION_NDEF_DISCOVERED类型的Intent的Activity,那么标签调度系统会尝试使用这种类型的Intent来启动应用程序。如果被扫描到的标签包含了不能被映射到MIME类型或URI的NDEF数据,或者没有包含NDEF数据,但是是已知的标签技术,那么也会直接启动这种类型的Intent对象(而不是先启动ACTION_NDEF_DISCOVERED类型的Intent)
3. ACTION_TAB_DISCOVERED:如果没有处理ACTION_NDEF_DISCOVERED或ACTION_TECH_DISCOVERED类型Intent的Activity,就会启动这种类型的Intent。
标签调度系统的基本工作方法如下:
1. 用解析NFC标签时由标签调度系统创建的Intent对象(ACTION_NDEF_DISCOVERED或ACTION_TECH_DISCOVERED)来尝试启动Activity;
2. 如果没有对应的处理Intent的Activity,那么就会尝试使用下一个优先级的Intent(ACTION_TECH_DISCOVERED或ACTION_TAG_DISCOVERED)来启动Activity,直到有对应的应用程序来处理这个Intent,或者是直到标签调度系统尝试了所有可能的Intent。
3. 如果没有应用程序来处理任何类型的Intent,那么就不做任何事情。
图1.标签调度系统
在可能的情况下,都会使用NDEF消息和ACTION_NDEF_DISCOVERED类型的Intent来工作,因为它是这三种Intent中最标准的。这种Intent与其他两种Intent相比,它会允许你在更加合适的时机来启动你的应用程序,从而给用户带来更好的体验。
在Android的Manifest中申请NFC访问
在访问设备的NFC硬件和正确的处理NFC的Intent之前,要在AndroidManifest.xml文件中进行以下声明:
1. 在<uses-permission>元素中声明访问NFC硬件:
<uses-permission android:name="android.permission.NFC" />
2. 你的应用程序所支持的最小的SDK版本。API Level 9只通过ACTION_TAG_DISCOVERED来支持有限的标签调度,并且只能通过EXTRA_NDEF_MESSAGES来访问NDEF消息。没有其他的标签属性或I/O操作可用。API Level 10中包含了广泛的读写支持,从而更好的推动了NDEF的应用前景,并且API Leve 14用Android Beam和额外的方便的创建NDEF记录的方法,向外提供了更容易的把NDEF消息推送给其他设备的方法。
<uses-sdk android:minSdkVersion="10"/>
3. 使用uses-feature元素,在Google Play中,以便你的应用程序能够只针对有NFC硬件的设备来显示。
<uses-featureandroid:name="android.hardware.nfc" android:required="true"/>
如果你的应用程序使用了NFC功能,但是相关的功能又不是你的应用程序的关键功能,你可以忽略uses-feature元素,并且要在运行时通过调用getDefaultAdapter()方法来检查NFC是否有效。
过滤NFC的Intent
要在你想要处理被扫描到的NFC标签时启动你的应用程序,可以在你的应用程序的Android清单中针对一种、两种或全部三种类型的NFC的Intent来过滤。但是,通常想要在应用程序启动时控制最常用的ACTION_NDEF_DISCOVERED类型的Intent。在没有过滤ACTION_NDEF_DISCOVERED类型的Intent的应用程序,或数据负载不是NDEF时,才会从ACTION_NDEF_DISCOVERED类型的Intent回退到ACTION_TECH_DISCOVERED类型的Intent。通常ACTION_TAB_DISCOVERED是最一般化的过滤分类。很多应用程序都会在过滤ACTION_TAG_DISCOVERED之前,过滤ACTION_NDEF_DISCOVERED或ACTION_TECH_DISCOVERED,这样就会降低你的应用程序被启动的可能性。ACTION_TAG_DISCOVERED只是在没有应用程序处理ACTION_NDEF_DISCOVERED或ACTION_TECH_DISCOVERED类型的Intent的情况下,才使用的最后手段。
因为NFC标签的多样性,并且很多时候不在你的控制之下,因此在必要的时候你要回退到其他两种类型的Intent。在你能够控制标签的类型和写入的数据时,我们建议你使用NDEF格式。下文将介绍如何过滤每种类型的Intent对象。
ACTION_NDEF_DISCOVERED
要过滤ACTION_NDEF_DISCOVERED类型的Intent,就要在清单中跟你想要过滤的数据一起来声明该类型的Intent过滤器。以下是过滤text/plain类型的MIME的ACTION_NDEF_DISCOVERED类型过滤器的声明:
<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
如果你的Activity要过滤ACTION_TECH_DISCOVERED类型的Intent,你必须创建一个XML资源文件,该文件在tech-list集合中指定你的Activity所支持的技术。如果tech-list集合是标签所支持的技术的一个子集,那么你的Activity被认为是匹配的。通过调用getTechList()方法来获得标签所支持的技术集合。
例如,如果扫描到的标签支持MifareClassic、NdefFormatable和NfcA,那么为了跟它们匹配,tech-list集合就必须指定所有这三种技术,或者指定其中的两种或一种。
以下示例定义了所有的相关的技术。你可以根据需要删除其中一些设置。然后把这个文件保存到<project-root>/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>
你也能够指定多个tech-list集合,每个tech-list集合被认为是独立的,并且如果任何一个tech-list集合是由getTechList()返回的技术的子集,那么你的Activity就被认为是匹配的。下列示例能够跟支持NfcA和Ndef技术NFC标签或者跟支持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
使用下列Intent过滤器来过滤ACTION_TAG_DISCOVERED类型的Intent:
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>
从Intent中获取信息
如果因为NFC的Intent而启动一个Activity,那么你就能够从Intent中获取被扫描到的NFC标签的相关信息。根据被扫描到的标签,Intent对象能够以下额外的信息:
1. EXTRA_TAG(必须的):它是一个代表了被扫描到的标签的Tag对象;
2. EXTRA_NDEF_MESSAGES(可选):它是一个解析来自标签中的NDEF消息的数组。这个附加信息是强制在Intent对象上的;
3. {@link android.nfc.NfcAdapter#EXTRA_ID(可选):标签的低级ID。
要获取这些附加信息,就要确保你的Activity是被扫描到的NFC的Intent对象启动的,然后才能获得Intent之外的附加信息。下例检查ACTION_NDEF_DISCOVERED类型的Intent,并从Intent对象的附加信息中获取NDEF消息。
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对象,该对象包含了数据负载,并允许你列举标签的技术:
Tag tag= intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
创建通用的NDEF记录类型
本节介绍如何创建通用的NDEF记录类型,以便帮助你向NFC标签写入或用Android Beam发送数据。从Android4.0(API Level14)开始,可以用createUri()方法来帮助你自动的创建URI记录。从Android4.1(API Level 16)开始,可以用createExternal()和createMime()方法来帮助你创建MIME和外部类型的NDEF记录。使用这些辅助方法会尽可能的避免手动创建NDEF记录的错误。
本节还要介绍如何创建NDEF记录对应的Intent过滤器。所有的这些写入或发送到NFC标签的NDEF记录例子都应该是NDEF消息的第一条记录。
TNF_ABSOLUTE_URI
注意:我们推荐你使用RTD_URI类型,而不是TNF_ABSOLUTE_URI,因为它更高效。
用下列方法创建一个TNF_ABSOLUTE_URI类型的NDEF记录:
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过滤器如下:
<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
使用下列方法创建TNF_MIME_MEDIA类型的NDEF记录。
使用createMime()方法:
NdefRecord mimeRecord = NdefRecord.createMime("application/vnd.com.example.android.beam",
"Beam me up, Android".getBytes(Charset.forName("US-ASCII")));
手动的创建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过滤器如下:
<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和RTD_TEXT
用下列方法创建TNF_WELL_KNOWN类型的NDEF记录:
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过滤器如下:
<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_KNOW和RTD_URI
用下列方法创建TNF_WELL_KNOWN类型的NDEF记录。
使用createUri(String)方法:
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过滤器如下:
<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
使用下列方法创建TNF_EXTERNAL_TYPE类型的记录。
使用createExternal()方法:
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过滤器如下:
<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类型NFC部署,以便更好的支持Android设备和非Android设备。
注意:TNF_EXTERNAL_TYPE类型的URN包含以下格式:
urn:nfc:ext:example.com.externalType,但是,NFC论坛的RTD规范声明,URN的urn:nfc:ext:部分在NDEF记录中必须忽略。因此你需要提供的所有信息是用“:”号把域名(示例中的example.com)和类型(示例中的externalType)分离开。在调度TNF_EXTERNAL_TYPE类型的记录时,Android会把urn:nfc:ext:example.com:externalType的URN转换成vnd.android.nfc://ext/example.com:externalType的URI,它是在示例中声明的Intent过滤器。