总结一下Android中NFC开发相关的知识点。
NFC的几个概念
-
接触式IC卡 例如手机SIM卡、金融IC卡。
-
非接触式IC卡 又称射频卡,将无线射频识别技术和IC卡结合起来,免接触。
-
RFID 无线射频识别,一种无线通讯技术。
基本原理:阅读器将电信号转换为无线电信号(电磁波的一个频带)发给标签,标签使用接收到的无线电波能量供电,然后将存储在自身数据以无线电信号的形式应答给阅读器,以读取到标签中的数据。
-
NFC(Near Field Communication) 短距离无线通讯技术,基于RFID,一般在10cm之内使用13.56MHz频率通讯
Android如何使用NFC
权限声明
在AndroidManifest.xml中如下申请NFC权限:
<uses-permission android:name="android.permission.NFC" />
添加下面一行,保证在GooglePlay商店此应用只显示给带NFC硬件的设备
<uses-feature android:name="android.hardware.nfc" android:required="true" />
在代码中动态判断设备是否支持NFC:
if(NfcAdapter.getDefaultAdapter() == null){
Log.v("monkey","设备不支持NFC.");
}
NFC的三种模式
- Reader/writer mode 通过Android设备对NFC标签进行读写数据操作。
- P2P mode NFC设备之间交换数据,AndroidBeam使用这种模式。
- Card emulation mode NFC设备模拟成NFC标签。
Reader/write mode
Android通过标签分发系统分析发现的NFC标签,封装成对应的Intent发给Android上层进行对应的处理。
Android中关于NFC有三种类型的Action(优先级由高到低):
ACTION_NDEF_DISCOVERED
ACTION_TECH_DISCOVERED
ACTION_TAG_DISCOVERED
这里的优先级可以通过标签分发系统的机制来解释:
- 如果一个包含NDEF格式数据的NFC标签被发现,Android优先发送带
ACTION_NDEF_DISCOVERED
action的Intent。 - 如果没有Activity处理
ACTION_NDEF_DISCOVERED
类型的intent 或者 NFC标签不包含NDEF格式的数据(使用了其他已知技术)或者 NDEF格式的标签不能被标签分发系统映射成正确的Intent,则会发送优先级较低的ACTION_TECH_DISCOVERED
intent来尝试启动Activity(不会发送ACTION_NDEF_DISCOVERED
)。 - 如果仍然没有Activity响应上述两个Action,则系统会发送优先级最低的
ACTION_TAG_DISCOVERED
的Intent。
图为标签分发系统:
NFC数据格式
NFC的数据格式可以分为NDEF和非NDEF两种。NDEF是NFC Forum定义的一种标准格式,在Android中得到最大范围的支持,是Android最推荐使用的格式。
-
NDEF数据在AndroidSDK中被封装成一个包含多个NdefRecord的NdefMessage,android.nfc.tech.Ndef类封装了各种便捷的读写数据的操作。
前面说过当Android设备发现NDEF格式的标签之后会发出
ACTION_NDEF_DISCOVERED
类型的Intent。通常在intent中还会带上更加具体的mime type & URI等数据,以便筛选更加具体的Activity来进行处理(这个是Android推荐使用这个格式的原因之一)。下面就来说说,NDEF具体的数据格式以及Android如何分析NDEF数据来生成对应的Intent。NDEF数据格式:
- TNF 3bits的字段,标识了如何解析第二个字段type。包含的值见表一:
表一,TNF字段以及映射:
TNF 映射的Intent TNF_ABSOLUTE_URI 基于type的URI TNF_EMPTY 无映射,降级到 ACTION_TECH_DISCOVERED
TNF_EXTERNAL_TYPE 基于type的URI. type形式为 : 。映射后的URI形式为: vnd.android.nfc://ext/<domain>:<service>
.TNF_MIME_MEDIA 基于type的MIME类型 TNF_UNCHANGED 无效,降级为 ACTION_TECH_DISCOVERED
TNF_UNKNOWN 降级为 ACTION_TECH_DISCOVERED
TNF_WELL_KNOWN 基于type(RTD)的MIME type or URI ,需要再根据type的值进行映射(见表二) 表二,RTDs for TNF_WELL_KNOWN以及映射:
RTD 映射的Intent RTD_ALTERNATIVE_CARRIER 降级为 ACTION_TECH_DISCOVERED
RTD_HANDOVER_CARRIER 降级为 ACTION_TECH_DISCOVERED
RTD_HANDOVER_REQUEST 降级为 ACTION_TECH_DISCOVERED
RTD_HANDOVER_SELECT 降级为 ACTION_TECH_DISCOVERED
RTD_SMART_POSTER 基于解析payload的URI RTD_TEXT MIME类型 text/p RTD_URI 基于payloadd的URI - type,描述Record的类型,如果TNF为TNF_WELL_KNOWN,则这个字段指定RTD,见上表二。
- ID,字段的唯一标识,一般不常用,可以用来标识一张标签。
- payload,有效荷载,保存真实的数据。
创建各种类型的NDEF数据&响应对应NDEF 数据的IntentFilter实例
- 创建TNF_ABSOLUTE_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>
2. 创建TNF_MIME_MEDIA类型的数据:
NdefRecord mimeRecord = NdefRecord.createMime("application/vnd.com.example.android.beam",
"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>
3. 创建TNF_EXTERNAL_TYPE类型的数据:
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);
对应的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>
4. 创建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>
读写NDEF格式的NFC数据:
1. Intent中包含三个相关的数据:
EXTRA_TAG 扫描到的标签对象
EXTRA_NDEF_MESSAGES 从标签中获取到的NDEF数据对象,只有ACTION_NDEF_DISCOVERED类型的 Intent包含
EXTRA_ID 标签的ID(可选)
2 .解析NDEF数据的代码一般为:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
...
if (intent != null && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
Parcelable[] rawMessages =
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMessages != null) {
NdefMessage[] messages = new NdefMessage[rawMessages.length];
for (int i = 0; i < rawMessages.length; i++) {
messages[i] = (NdefMessage) rawMessages[i];
}
// Process the messages array.
...
}
}
}
写NDEF数据的代码一般为:
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
NdefRecord ndefRecord = NdefRecord.createExternal("domain", "service", "content".getBytes());
NdefMessage ndefMessage = new NdefMessage(new NdefRecord[] {ndefRecord});
Ndef ndef = Ndef.get(tag); //获取Ndef tech的对象
if (ndef != null) { //非NDEF数据
try {
ndef.connect();
ndef.writeNdefMessage(ndefMessage);
} catch (IOException e) {
e.printStackTrace();
} catch (FormatException e) {
e.printStackTrace();
}
} else { //可格式化为NDEF数据
NdefFormatable ndefFormatable = NdefFormatable.get(tag);
if (ndefFormatable != null) {
try {
ndefFormatable.connect();
ndefFormatable.format(ndefMessage);
} catch (IOException e) {
e.printStackTrace();
} catch (FormatException e) {
e.printStackTrace();
}
}
}
}
-
非NDEF数据可以配合android.nfc.tech包下的对应的类来进行使用。
每个NFC标签可能支持不同种类的technologies(不同格式数据的读写),可以通过如下代码获取标签支持的techonologies:
mTag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG);
String[] teches = mTag.getTechList();
Android支持的常见Technology如下表:
Class | Description |
---|---|
TagTechnology |
The interface that all tag technology classes must implement. |
NfcA |
Provides access to NFC-A (ISO 14443-3A) properties and I/O operations. |
NfcB |
Provides access to NFC-B (ISO 14443-3B) properties and I/O operations. |
NfcF |
Provides access to NFC-F (JIS 6319-4) properties and I/O operations. |
NfcV |
Provides access to NFC-V (ISO 15693) properties and I/O operations. |
IsoDep |
Provides access to ISO-DEP (ISO 14443-4) properties and I/O operations. |
Ndef |
Provides access to NDEF data and operations on NFC tags that have been formatted as NDEF. |
NdefFormatable |
Provides a format operations for tags that may be NDEF formattable. |
Android设备可选支持的Technology:
Class | Description |
---|---|
MifareClassic |
Provides access to MIFARE Classic properties and I/O operations, if this Android device supports MIFARE. |
MifareUltralight |
Provides access to MIFARE Ultralight properties and I/O operations, if this Android device supports MIFARE. |
前面讲解过,标签在某些情况下会降级为ACTION_TECH_DISCOVERED
类型的Intent,对应的xml文件为:
<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>
其中**android:resource="@xml/nfc_tech_filter"**指定NFC technology的过滤文件,放在res/xml
目录下任意命名。其中指定的technologies必须是所识别的标签支持的technologies的子集,才能匹配到。例如:
<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>
或者分开多个**
<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>
各种类型的非NDEF数据格式的读写
查看对应的规格书按照协议来使用,在developer.android.com中可以简单地查看一种数据格式的说明:如 MIFAREClassic
P2P mode
即AndroidBeam功能,使两台Android设备进行快速的数据交换。发送数据的设备打开发送数据的应用(相当于一张标签),接受数据的设备解锁靠近发送数据的设备之后,发送数据的设备UI显示“Touch to Beam",点击即可将数据发送给接收端(接收端设备接受到数据通过标签分发系统决定打开的应用进行处理)。
相关的API有两个:
NfcAdapter.setNdefPushMessage() :发送端设置要发送的NDEF数据。两台设备靠近之后点击”Touch to Beam"发送此数据。
NfcAdapter.setNdefPushMessageCallback():此方法接受一个NfcAdapter.CreateNdefMessageCallback类型的回调接口,此接口在两台设备靠近时回调进行NDEF数据的创建,然后点击“Touch to Beam"发送数据。
两个接口的唯一不同在于:接口1一开始便确定了要发送的数据;接口2在两个设备靠近时才创建要发送的NDEF数据。
代码示例(Demo中onCreate中为发送端逻辑,onResume中为接收端处理逻辑):
public class MainActivity extends AppCompatActivity {
private NfcAdapter mNfcAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
Toast.makeText(this, "not support nfc", Toast.LENGTH_SHORT).show();
finish();
return;
}
final NdefRecord ndefRecord = NdefRecord.createMime("application/vnd.com.example.android.beam", "hello android".getBytes());
mNfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
@Override
public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
Log.v("seewo", "createNdefMessage");
return new NdefMessage(ndefRecord);
}
}, this);
// mNfcAdapter.setNdefPushMessage(new NdefMessage(ndefRecord), this); //接口一的实现
}
@Override
protected void onResume() {
super.onResume();
//接收端的处理逻辑
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
processIntent(getIntent());
}
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
}
void processIntent(Intent intent) {
Parcelable[] rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage msg = (NdefMessage) rawMessages[0];
String info = new String(msg.getRecords()[0].getPayload());
Toast.makeText(this, info, Toast.LENGTH_SHORT).show();
}
}
Card emulation mode
Android4.4之前通过在Android设备中内置一个芯片来实现。通信路径如下图:
Android4.4之后可以通过纯软件模拟NFC卡。通信路径如下图:
在Android上层程序只需实现一个服务,并且给服务绑定一个ID,便可以模拟成NFC卡,根据读卡器发出的ID响应数据。
public class MyHostApduService extends HostApduService {
@Override
public byte[] processCommandApdu(byte[] apdu, Bundle extras) {
...
}
@Override
public void onDeactivated(int reason) {
...
}
}
<service android:name=".MyHostApduService" android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
</intent-filter>
<meta-data android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/servicedesc"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/aiddescription"
android:category="other">
<aid-filter android:name="F0010203040506"/>
<aid-filter android:name="F0394148148100"/>
</aid-group>
</host-apdu-service>
相关资料
1.NDEF数据解析实例:http😕/note.youdao.com/share/?id=8b45d342e34d2bce0fade2218bafd79c&type=note#/
2.AndnroidNFC官网介绍:https😕/developer.android.com/guide/topics/connectivity/nfc/index.html
3.NFC相关的应用:NXPTagWriter、NXPTagInfo、MIFARE经典工具