文章介绍NDEF和MifareClassic两种格式数据读写标签的方法。 在进行Nfc的读写之前,了解卡片的存储结构是非常重要的(非常重要,尤其对理解MifareClassic数据格式)。
卡片常用的数据格式:
MifareClassic数据格式就是NfcA。
IsoDep:各种交通卡。如:北京市政交通卡。
NfcB:二代身份证。
NfcF:Felica用的是。
NfcV:德州仪器的VicinityCard卡用的是。
Ndef:安卓主推的传输数据格式。
一、Nfc卡片存储结构
M1卡:容量1K。16个扇区(sector),每个扇区4个块(block),每个块(block) 16个byte数据
每个扇区的前三个块,可用于存贮数据。
第0扇区的第一个块除外,它用于存放厂商数据,已经固化,不可更改。
每个扇区的第四个块为控制块,用于控制该扇形区的访问权限。它的前6个字节为KeyA,后6个字节为KeyB,中间的4个字节为存取控制。KeyA和KeyB记录了两种加密方式,存取控制里记录了该扇区的读写权限支持KeyA验证还是KeyB验证。
扇形区 | 块 | 块中的数据 |
---|---|---|
扇区 0(sectorIndex 0) | 块 0(blockIndex 0) | 厂家数据不可修改 |
扇区 0(sectorIndex 0) | 块 1(blockIndex 1) | 0........................15 (byte) |
扇区 0(sectorIndex 0) | 块 2(blockIndex 2) | 0........................15 (byte) |
扇区 0(sectorIndex 0) | 块 3(blockIndex 3) | 密码A 存取控制 密码B |
: | : | : |
: | : | : |
扇区 15(sectorIndex 15) | 块 60(blockIndex 60) | 0........................15 (byte) |
扇区 15(sectorIndex 15) | 块 61(blockIndex 61) | 0........................15 (byte) |
扇区 15(sectorIndex 15) | 块 62(blockIndex 62) | 0........................15 (byte) |
扇区 15(sectorIndex 15) | 块 63(blockIndex 63) | 密码A 存取控制 密码B |
二、NFC数据过滤器
NFC有三种过滤器:ACTION_NDEF_DISCOVERED,ACTION_TECH_DISCOVERED,ACTION_TAG_DISCOVERED。
1、过滤器介绍
ACTION_NDEF_DISCOVERED
当卡片中包含NDEF格式的数据,将使用该模式启动Activity。
ACTION_TECH_DISCOVERED
当卡片中包含非NDEF格式的数据(如:MifareClassic)将使用该模式启动Activity。
ACTION_TAG_DISCOVERED
过滤规则是最不严格的,只要符合NFC规范中的任一种则都会响应。
优先级顺序,上面三种规则依次递减
- 如果卡片是Ndef格式的数据
如果activity注册了ACTION_NDEF_DISCOVERED,会优先响应ACTION_NDEF_DISCOVERED。
如果activity没有注册ACTION_NDEF_DISCOVERED,会优先响应ACTION_TECH_DISCOVERED。
如果activity没有注册ACTION_NDEF_DISCOVERED和ACTION_TECH_DISCOVERED,会响应ACTION_TAG_DISCOVERED。 - 如果卡片是非Ndef格式的数据
即使activity注册了ACTION_NDEF_DISCOVERED,也不会响应ACTION_NDEF_DISCOVERED。
如果activity注册了ACTION_TECH_DISCOVERED,会优先响应ACTION_TECH_DISCOVERED。
如果activity没有注册ACTION_TECH_DISCOVERED,会响应ACTION_TAG_DISCOVERED。
ACTION_TAG_DISCOVERED的优先级是最低,activity无法获得更详细的一些信息,所以不会一开始就选择TAG_DISCOVERED来响应。
2、过滤器注册
静态注册
在AndroidMainfest.xml中声明。在需要处理NFC读卡的Activity标签里声明过滤器。当在桌面刷卡的时候,手机系统会弹窗出现可以处理该Intent的APP。
<activity
android:name=".activity.InspectionsActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:label="@string/inspection"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden|adjustPan">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<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>
筛选NFC卡片的类型。
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter"/>
在res>xml目录下添加nfc_tech_filter.xml
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.NfcA</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcB</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcF</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.Ndef</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.NdefFormatable</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
<tech-list>
<tech>android.nfc.tech.MifareUltralight</tech>
</tech-list>
</resources>
动态注册
在代码里声明过滤器,在桌面刷卡的时候,不会弹出可响应APP的弹窗。
Intent intent = new Intent(activity, activity.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
//intentFilter过滤----ndef
IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
try {
//文本类型
ndefFilter.addDataType("text/plain");
} catch (IntentFilter.MalformedMimeTypeException e) {
e.printStackTrace();
}
//intentFilter过滤----非ndef
IntentFilter techFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
//intentFilter过滤器列表
mIntentFilter = new IntentFilter[]{ndefFilter, techFilter};
//匹配的数据格式列表
mTechList = new String[][]{
{MifareClassic.class.getName()},
{NfcA.class.getName()},
{Ndef.class.getName()},
{NdefFormatable.class.getName()}};
三、NFC的工具类
1、初始化
mNfcAdapter = NfcAdapter.getDefaultAdapter(activity);
if (mNfcAdapter == null) {
ToastUtils.showShort("设备不支持NFC功能!");
} else {
if (!mNfcAdapter.isEnabled()) {
showSettingDailog();
} else {
Logger.e(TAG, "NFC功能已打开!");
init();
}
}
2、启动
/**
* Nfc监听intent
*/
public void enableForegroundDispatch() {
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
mNfcAdapter.enableForegroundDispatch(activity, mPendingIntent, mIntentFilter, mTechList);
}
}
2、关闭
/**
* 取消监听Nfc
*/
public void disableForegroundDispatch() {
if (mNfcAdapter != null && mNfcAdapter.isEnabled()) {
mNfcAdapter.disableForegroundDispatch(activity);
}
}
3、读取NDEF格式
/**
* 读取Ndef的数据
*
* @return
*/
private String readNdef(Intent intent) {
String info = "";
Parcelable[] rawMsgs = intent.getParcelableArrayExtra(
NfcAdapter.EXTRA_NDEF_MESSAGES);
NdefMessage msgs[] = null;
if (rawMsgs != null) {
msgs = new NdefMessage[rawMsgs.length];
for (int i = 0; i < rawMsgs.length; i++) {
msgs[i] = (NdefMessage) rawMsgs[i];
}
NdefRecord record = msgs[0].getRecords()[0];
if (record != null) {
byte[] payload = record.getPayload();
//下面代码分析payload:状态字节+ISO语言编码(ASCLL)+文本数据(UTF_8/UTF_16)
//其中payload[0]放置状态字节:如果bit7为0,文本数据以UTF_8格式编码,如果为1则以UTF_16编码
//bit6是保留位,默认为0
/*
* payload[0] contains the "Status Byte Encodings" field, per the
* NFC Forum "Text Record Type Definition" section 3.2.1.
*
* bit7 is the Text Encoding Field.
*
* if (Bit_7 == 0): The text is encoded in UTF-8 if (Bit_7 == 1):
* The text is encoded in UTF16
*
* Bit_6 is reserved for future use and must be set to zero.
*
* Bits 5 to 0 are the length of the IANA language code.
*/
String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8"
: "UTF-16";
//处理bit5-0。bit5-0表示语言编码长度(字节数)
int languageCodeLength = payload[0] & 0x3f;
//获取语言编码(从payload的第2个字节读取languageCodeLength个字节作为语言编码)
try {
String languageCode = new String(payload, 1, languageCodeLength,
"US-ASCII");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//解析出实际的文本数据
try {
info = new String(payload, languageCodeLength + 1,
payload.length - languageCodeLength - 1, textEncoding);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
return info;
}
5、写NDEF格式
/**
* 往nfc写入数据
*/
public void writeNFCToTag(String text, Intent intent) throws IOException, FormatException {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
//生成语言编码的字节数组,中文编码
byte[] langBytes = Locale.CHINA.getLanguage().getBytes(
Charset.forName("US-ASCII"));
//将要写入的文本以UTF_8格式进行编码
Charset utfEncoding = Charset.forName("UTF-8");
//由于已经确定文本的格式编码为UTF_8,所以直接将payload的第1个字节的第7位设为0
byte[] textBytes = text.getBytes(utfEncoding);
int utfBit = 0;
//定义和初始化状态字节
char status = (char) (utfBit + langBytes.length);
//创建存储payload的字节数组
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);
NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{record});
//转换成字节获得大小
int size = ndefMessage.toByteArray().length;
//2.判断NFC标签的数据类型(通过Ndef.get方法)
Ndef ndef = Ndef.get(tag);
//判断是否为NDEF标签
if (ndef != null) {
ndef.connect();
//判断是否支持可写
if (!ndef.isWritable()) {
return;
}
//判断标签的容量是否够用
if (ndef.getMaxSize() < size) {
return;
}
//3.写入数据
ndef.writeNdefMessage(ndefMessage);
Logger.e(TAG, "写入NDEF成功");
} else {
//当我们买回来的NFC标签是没有格式化的,或者没有分区的执行此步
//Ndef格式类
NdefFormatable format = NdefFormatable.get(tag);
//判断是否获得了NdefFormatable对象,有一些标签是只读的或者不允许格式化的
if (format != null) {
//连接
format.connect();
//格式化并将信息写入标签
format.format(ndefMessage);
Logger.e(TAG, "格式化并写入NDEF成功");
} else {
Logger.e(TAG, "格式化并写入NDEF失败");
}
}
}
6、读取MifareClassic格式
private String readMifareClassic(Intent intent) {
String info = "";
List<byte[]> list = new ArrayList<>();
boolean auth = false;
//读取TAG
MifareClassic mfc = MifareClassic.get(getNFCTag(intent));
try {
mfc.connect();
if (mfc.isConnected()) {
int sectorCount = mfc.getSectorCount();//获取TAG中包含的扇区数
for (int j = 0; j < sectorCount; j++) {
auth = mfc.authenticateSectorWithKeyA(j,
MifareClassic.KEY_NFC_FORUM);
int bCount;
int bIndex;
if (auth) {
// 读取扇区中的块
bCount = mfc.getBlockCountInSector(j);
bIndex = mfc.sectorToBlock(j);
for (int i = 0; i < bCount; i++) {
byte[] data = mfc.readBlock(bIndex);
if (i < bCount - 1) {
if (!bytesToHexString(data).equals("0x00000000000000000000000000000000")) {
list.add(data);
}
}
bIndex++;
}
}
}
if (list.size() > 0) {
byte[] aa = new byte[list.size() * list.get(0).length];
for (int i = 0; i < list.size(); i++) {
byte[] bytes = list.get(i);
System.arraycopy(bytes, 0, aa, bytes.length * i, bytes.length);
}
info = new String(trim(aa), Charset.forName("utf-8"));
}
}
} catch (Exception e) {
Logger.e(TAG, e);
} finally {
try {
mfc.close();
} catch (IOException e) {
Logger.e(TAG, e);
}
}
return info;
}
7、写MifareClassic格式
public void writeMifareClassic(String data, Intent intent) throws IOException {
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
try {
mfc.connect();
int sectorCount = mfc.getSectorCount();//获取TAG中包含的扇区数
for (int j = 0; j < sectorCount; j++) {
if (mfc.authenticateSectorWithKeyA(j,
MifareClassic.KEY_NFC_FORUM)) {
// 读取扇区中的块
int bCount = mfc.getBlockCountInSector(j);
int bIndex = mfc.sectorToBlock(j);
for (int i = 0; i < bCount - 1; i++) {
if (!bytesToHexString(mfc.readBlock(bIndex + i)).equals("0x00000000000000000000000000000000")) {
mfc.writeBlock(bIndex + i, new byte[16]);
}
}
}
}
byte[] bytes = new byte[16];
System.arraycopy(data.getBytes(), 0, bytes, 0, data.getBytes().length);
if (mfc.authenticateSectorWithKeyA(1,
MifareClassic.KEY_NFC_FORUM)) {
mfc.writeBlock(4, bytes);
}
Logger.e(TAG, "写入MifareClassic成功");
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
8、清空数据
public boolean clear(Intent intent) throws IOException {
boolean result = false;
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
MifareClassic mfc = MifareClassic.get(tag);
try {
mfc.connect();
int sectorCount = mfc.getSectorCount();//获取TAG中包含的扇区数
for (int j = 0; j < sectorCount; j++) {
if (mfc.authenticateSectorWithKeyA(j,
MifareClassic.KEY_NFC_FORUM)) {
// 读取扇区中的块
int bCount = mfc.getBlockCountInSector(j);
int bIndex = mfc.sectorToBlock(j);
for (int i = 0; i < bCount - 1; i++) {
if (!bytesToHexString(mfc.readBlock(bIndex + i)).equals("0x00000000000000000000000000000000")) {
mfc.writeBlock(bIndex + i, new byte[16]);
}
}
}
}
result = true;
Logger.e(TAG, "清空成功");
} finally {
try {
mfc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
四、NFC调用
1、重写onNewIntent()方法
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
String id = nfcUtils.readNFCId(nfcUtils.getNFCTag(intent));
Logger.e(TAG, "nfcID:" + id);
String message = nfcUtils.readMessage(intent);
Logger.e(TAG, "nfcMessage:" + message);
}
2、启动和关闭
@Override
protected void onResume() {
super.onResume();
Logger.e(TAG, "onResume");
nfcUtils.enableForegroundDispatch();
}
@Override
protected void onPause() {
super.onPause();
Logger.e(TAG, "onPause");
nfcUtils.disableForegroundDispatch();
}