摘要
网上关于Android NFC 操作的文章没有一篇合适自己,没有从根本解决问题,故整理一翻,理清思路。NFC是Near Field Communication缩写,即近距离无线通讯技术。可以在移动设备、消费类电子产品、PC 和智能控件工具间进行近距离无线通信。比如读取图书标签、公交卡等。
NFC Tag解析顺序(可以不在Activity配置)
nfc类型有三种NDEF,TECH,TAG,优先级依次降低,其对应的Intent过滤规则分别是:
NDEF:
1
2
3
4
5<intent-filter> <action android:name = "android.nfc.action.NDEF_DISCOVERED" /> <data android:mimeType = "text/plain" /> </intent-filter>
TECH :
1
2
3
4
5<intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED" /> </intent-filter> <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/filter_nfc" />
TAG:
1
2
3
4
5<intent-filter> <action android:name="android.nfc.action.TAG_DISCOVERED"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter>
假如有三个Activity分别设置上述三种类型,NFC感应时会依次寻找对应的Activity。
补充说明:Intent Filter就是 用来注册 Activity 、 Service 和 Broadcast Receiver 具有能在某种数据上执行一个动作的能力。
使用 Intent Filter ,应用程序组件告诉 Android ,它们能为其它程序的组件的动作请求提供服务,包括同一个程序的组
件、本地的或第三方的应用程序。
支持的Tag分类
Android为使用的android.nfc.tech包情况提供了通用的支持,android.nfc.tech包如下表中所描述的 。您可以使用的getTechList()方法来确定标签的技术支持,并建立 与之一由android.nfc.tech类相应TagTechnology对象。
Class | Description | 对应识别的卡种 |
---|---|---|
TagTechnology | 所有标签技术类必须实现的接口。 | |
NfcA | 提供NFC-A(ISO 14443-3A)的性能和I / O操作的访问。 | m1卡 |
NfcB | 提供NFC-B (ISO 14443-3B)的性能和I / O操作的访问。 | |
NfcF | 提供 NFC-F (JIS 6319-4)的性能和I / O操作的访问。 | |
NfcV | 提供 NFC-V (ISO 15693)的性能和I / O操作的访问。 | 15693卡 |
IsoDep | 提供 ISO-DEP (ISO 14443-4)的性能和I / O操作的访问。 | CPU卡 |
Ndef | 提供NFC标签已被格式化为NDEF的数据和操作的访问。 | |
NdefFormatable | 提供可能被格式化为NDEF的 formattable的标签。 | |
MifareClassic | 如果此Android设备支持MIFARE,提供访问的MIFARE Classic性能和I / O操作。 | m1卡 |
MifareUltralight | 如果此Android设备支持MIFARE,提供访问的MIFARE 超轻性能和I / O操作。 |
14443协议和15693协议的区别: 14443是近场耦合,15693是远copy场耦合,14443具有加密功能,15693具有穿透性好,抗干扰性高! 14443按加密的方式分为14443a/b,14443a一般百多用于私用,例如:会员消费卡这类的;14443b因加密特性较好,用于公用多,例如:身份证。 15693常用于高频读距要求知高的场合,例如:货物身份识别、图书标签等
下面讲下不同卡种的概念,捋清思路:
名称 | 类别 | 描述 | 应用 |
M1卡 | 非接触式IC卡 | 有密码,可读可写,价格贵,感应距离短,芯片种类有S50,S70(芯片,是指菲利浦下属子公司恩智浦出品的芯片缩写) | 一卡通,公交等系统 |
CPU卡 | 有接触式也有非接触式的 | 相当于微型计算机,具有用户空间大、读取速度快、支持一卡多用等特点 | 可适用于金融、保险、交警、政府行业等多个领域 |
15693卡 |
| ISO15693是针对射频识别应用的一个国际标准,该标准定义了工作在13.56Mhz下智能标签和读写器的空气接口及数据通信规范,符合此标准的标签最远识读距离达到2米。 | 开放式门禁、开放式会议签到、贵重物品管理、数字化景区门票管理、数字化图书馆图书管理、医药管理、资产管理、产品防伪、物流及供应链等多种领域进行大量应用 |
M1-UltraLight卡 | 非接触式IC卡 | 无密码,又称为MF0,从UltraLight(超轻的)这个名字就可以看出来,它是一个低成本、小容量的卡片。低成本,是指它是目前市场中价格最低的遵守ISO14443A协议的芯片之一;小容量,是指其存储容量只有512bit(Mifare S50有8192bit)。,量分成16个块,每块包含4个字节。Mifare UltraLight的读写操作和 Mifare S50是完全兼容的。 | 门禁、考勤、会议签到、身份识别等 |
NFC使用步骤
1.权限
1
2
3
4<uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" />
2.在manifest节点下为Activity添加Intent Filter(可不配置)
3.Activity
过程:初始化 -> 设置系统调度 -> 系统调用onNewIntent(Intent intent) -> Tag读写流程 -> 最后取消系统调度
每次检测到Tag时,Activity的生命周期回调onPause()->onNewIntent()-> onResume()
Tag读写流程:
- 建立连接
- 读写
- 关闭连接
1.初始化
1
2
3mNfcAdapter = NfcAdapter.getDefaultAdapter(this); pi = PendingIntent.getActivity(this, 0, new Intent(this, getClass()) .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
2.设置系统调度
1
2
3
4
5@Override protected void onResume() { super.onResume(); mNfcAdapter.enableForegroundDispatch(this, pi, null, null); //启动 }
3.系统调用onNewIntent(Intent intent)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleNfcIntent(intent); } /** * 处理nfc标签 * * @param intent */ private void handleNfcIntent(Intent intent) { String action = intent.getAction(); //根据不同的Action进行获取不同的标签 if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {//默认 Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); processTag(tag); } }
此过程能拿到卡片的uid,方法调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); //卡片uid String uid=bytes2HexStr(tag.getId()); public static String bytes2HexStr(byte[] src) { StringBuilder builder = new StringBuilder(); if (src == null || src.length <= 0) { return ""; } char[] buffer = new char[2]; for (int i = 0; i < src.length; i++) { buffer[0] = Character.forDigit((src[i] >>> 4) & 0x0F, 16); buffer[1] = Character.forDigit(src[i] & 0x0F, 16); builder.append(buffer); } return builder.toString().toUpperCase(); }
卡片uid分析:
UID: 全球唯一标识符,每张卡都不一样,8个字节,读写器可以用UID号来操作指定的卡。
以ISO15693卡片为例,假如uid的16进制数据()为:
cabce04b000104e0(逆序分析UID[7]..UID[0])
cabce04b00 01 04 e0
e0:UID[7]为E0是固定的
04:UID[6]为卡制造商编码(如NXP公司为04,TI公司为07,上海贝岭为23)
01:UID[5]为产品类别代码,比如ICODE SL2 ICS20是01H,Tag-it HF-I Plus Chip为80H,Tag-it HF-I Plus Inlay为00H。
cabce04b00:UID[4]..UID[0]才是真正卡号,是制造商内部分配的号码。
打印被读取卡片支持的Tag列表:
1
2
3for (String tech : tag.getTechList()) { LogUtils.i("------------" + tech); }
4.Tag读写流程
1
2
3
4
5
6
7
8
9
10
11protected void processTag( Tag tag) { IsoDep isodep= IsoDep.get(tag); if (isodep!= null){ isodep.connect(); // 建立连接 byte[] data = new byte[20]; byte[] response = isodep.transceive(data); // 传送消息 isoDep.close(); // 关闭连接 } }
- 根据实际要读取的卡片支持的Tag,取出对应的class实例,然后进行连接读写操作,如下常见Tag实例。
-
m1卡: MifareClassic mfc = MifareClassic.get(tag);
-
iso15693卡:NfcV tech = NfcV.get(tag);
-
- 发送过去的字节数据,根据实际协议文档进行组装;
- 返回出来的字节数据,根据实际协议文档进行解析
5.取消系统调度
1
2
3
4
5
6@Override protected void onPause() { if (mNfcAdapter != null) mNfcAdapter.disableForegroundDispatch(this); // 取消调度 }
Tag读写工具类
iso15693卡:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179package com.haiheng.device.devicedriver.nfc; import android.nfc.tech.NfcV; import com.haiheng.core.util.ByteUtils; import java.io.IOException; /** * NfcV(ISO 15693)读写操作 * 用法 * NfcV mNfcV = NfcV.get(tag); * mNfcV.connect(); * <p> * NfcVUtils mNfcVutil = new NfcVUtils(mNfcV); * 取得UID * mNfcVutil.getUID(); * 读取block在1位置的内容 * mNfcVutil.readOneBlock(1); * 从位置7开始读2个block的内容 * mNfcVutil.readBlocks(7, 2); * 取得block的个数 * mNfcVutil.getBlockNumber(); * 取得1个block的长度 * mNfcVutil.getOneBlockSize(); * 往位置1的block写内容 * mNfcVutil.writeBlock(1, new byte[]{0, 0, 0, 0}) * * @author Kelly * @version 1.0.0 * @filename NfcVUtils.java * @time 2018/10/30 10:29 * @copyright(C) 2018 song */ public class NfcVUtils { private NfcV mNfcV; /** * UID数组行式 */ private byte[] ID; private String UID; private String DSFID; private String AFI; /** * block的个数 */ private int blockNumber; /** * 一个block长度 */ private int oneBlockSize; /** * 信息 */ private byte[] infoRmation; /** * * 初始化 * * @param mNfcV NfcV对象 * * @throws IOException * */ public NfcVUtils(NfcV mNfcV) throws IOException { this.mNfcV = mNfcV; ID = this.mNfcV.getTag().getId(); byte[] uid = new byte[ID.length]; int j = 0; for (int i = ID.length - 1; i >= 0; i--) { uid[j] = ID[i]; j++; } this.UID = ByteUtils.byteArrToHexString(uid); getInfoRmation(); } public String getUID() { return UID; } /** * * 取得标签信息 * */ private byte[] getInfoRmation() throws IOException { byte[] cmd = new byte[10]; cmd[0] = (byte) 0x22; // flag cmd[1] = (byte) 0x2B; // command System.arraycopy(ID, 0, cmd, 2, ID.length); // UID infoRmation = mNfcV.transceive(cmd); blockNumber = infoRmation[12]; oneBlockSize = infoRmation[13]; AFI = ByteUtils.byteArrToHexString(new byte[]{infoRmation[11]}); DSFID = ByteUtils.byteArrToHexString(new byte[]{infoRmation[10]}); return infoRmation; } public String getDSFID() { return DSFID; } public String getAFI() { return AFI; } public int getBlockNumber() { return blockNumber + 1; } public int getOneBlockSize() { return oneBlockSize + 1; } /** * * 读取一个位置在position的block * * @param position 要读取的block位置 * * @return 返回内容字符串 * * @throws IOException * */ public String readOneBlock(int position) throws IOException { byte cmd[] = new byte[11]; cmd[0] = (byte) 0x22; cmd[1] = (byte) 0x20; System.arraycopy(ID, 0, cmd, 2, ID.length); // UID cmd[10] = (byte) position; byte res[] = mNfcV.transceive(cmd); if (res[0] == 0x00) { byte block[] = new byte[res.length - 1]; System.arraycopy(res, 1, block, 0, res.length - 1); return ByteUtils.byteArrToHexString(block); } return null; } /** * * 读取从begin开始end个block * * begin + count 不能超过blockNumber * * @param begin block开始位置 * * @param count 读取block数量 * * @return 返回内容字符串 * * @throws IOException * */ public String readBlocks(int begin, int count) throws IOException { if ((begin + count) > blockNumber) { count = blockNumber - begin; } StringBuffer data = new StringBuffer(); for (int i = begin; i < count + begin; i++) { data.append(readOneBlock(i)); } return data.toString(); } /** * * 将数据写入到block, * * @param position 要写内容的block位置 * * @param data 要写的内容,必须长度为blockOneSize * * @return false为写入失败,true为写入成功 * * @throws IOException * */ public boolean writeBlock(int position, byte[] data) throws IOException { byte cmd[] = new byte[15]; cmd[0] = (byte) 0x22; cmd[1] = (byte) 0x21; System.arraycopy(ID, 0, cmd, 2, ID.length); // UID //block cmd[10] = (byte) position; //value System.arraycopy(data, 0, cmd, 11, data.length); byte[] rsp = mNfcV.transceive(cmd); if (rsp[0] == 0x00) return true; return false; } }
m1卡:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272package com.haiheng.hand.device.nfc; import android.nfc.Tag; import android.nfc.tech.MifareClassic; import com.haiheng.core.util.ByteUtils; import com.haiheng.core.util.log.LogWriter; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * m1卡一般16个扇区64块,第一个扇区等闲不能操作。每个扇区4块,每块16字节。 * 扇区从0数起,第2扇区第一块索引就是8,每个扇区前3块存数,据最后一块一般存密码。 * * * @author Kelly * @version 1.0.0 * @filename M1CardUtils.java * @time 2019/3/15 14:02 * @copyright(C) 2019 song */ public class M1CardUtils { /** * 读指定扇区的指定块 * * @param mifareClassic * @param sectorIndex 扇区索引 0~15(16个扇区) * @param blockIndex 块索引 0~3 * @param keyA 密钥,可为空,没有使用默认的密钥 * @return */ public static byte[] readBlock(MifareClassic mifareClassic, int sectorIndex, int blockIndex,byte[] keyA) throws IOException { try { String metaInfo = ""; mifareClassic.connect(); int type = mifareClassic.getType();//获取TAG的类型 int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数 String typeS = getMifareClassicType(type); metaInfo += "卡片类型:" + typeS + "n共" + sectorCount + "个扇区n共" + mifareClassic.getBlockCount() + "个块n存储空间: " + mifareClassic.getSize() + "Bn"; LogWriter.i(metaInfo); byte[] data = null; String hexString = null; if (m1AuthByKey(mifareClassic, sectorIndex,keyA) || m1Auth(mifareClassic, sectorIndex)){ int bCount; int bIndex; bCount = mifareClassic.getBlockCountInSector(sectorIndex);//获得当前扇区的所包含块的数量; bIndex = mifareClassic.sectorToBlock(sectorIndex);//当前扇区的第1块的块号 for (int i = 0; i < bCount; i++) { data = mifareClassic.readBlock(bIndex); hexString = ByteUtils.byteArrToHexString(data); LogWriter.w(sectorIndex + "扇区" + bIndex + "块:" + hexString); if (blockIndex == i) { break; } bIndex++; } }else { LogWriter.w("密码校验失败,扇区:" + sectorIndex); } return data; } catch (Exception e) { throw new IOException(e); } finally { try { if (mifareClassic != null) { mifareClassic.close(); } } catch (IOException e) { throw new IOException(e); } } } /** * 读指定扇区的所有块 * * @param mifareClassic * @param sectorIndex 扇区索引 0~15(16个扇区) * @param keyA 密钥,可为空,没有使用默认的密钥 * @return */ public static List<byte[]> readBlock(MifareClassic mifareClassic, int sectorIndex, byte[] keyA) throws IOException { List<byte[]> dataList = new ArrayList<byte[]>(); try { String metaInfo = ""; mifareClassic.connect(); int type = mifareClassic.getType();//获取TAG的类型 int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数 String typeS = getMifareClassicType(type); metaInfo += "卡片类型:" + typeS + "n共" + sectorCount + "个扇区n共" + mifareClassic.getBlockCount() + "个块n存储空间: " + mifareClassic.getSize() + "Bn"; LogWriter.i(metaInfo); if (m1AuthByKey(mifareClassic, sectorIndex,keyA) || m1Auth(mifareClassic, sectorIndex)){ int bCount; int bIndex; bCount = mifareClassic.getBlockCountInSector(sectorIndex);//获得当前扇区的所包含块的数量; bIndex = mifareClassic.sectorToBlock(sectorIndex);//当前扇区的第1块的块号 for (int i = 0; i < bCount; i++) { byte[] data = mifareClassic.readBlock(bIndex); String hexString = ByteUtils.byteArrToHexString(data); LogWriter.w(sectorIndex + "扇区" + bIndex + "块:" + hexString); dataList.add(data); bIndex++; } }else { LogWriter.w("密码校验失败,扇区:" + sectorIndex); } return dataList; } catch (Exception e) { throw new IOException(e); } finally { try { if (mifareClassic != null) { mifareClassic.close(); } } catch (IOException e) { throw new IOException(e); } } } /** * 读所有扇区的所有块 * * @param mifareClassic * @param keyA 密钥,可为空,没有使用默认的密钥 * @return */ public static Map<Integer,List<byte[]>> readBlock(MifareClassic mifareClassic,byte[] keyA) throws IOException { try { Map<Integer,List<byte[]>> result = new HashMap<Integer,List<byte[]>>(); String metaInfo = ""; mifareClassic.connect(); int type = mifareClassic.getType();//获取TAG的类型 int sectorCount = mifareClassic.getSectorCount();//获取TAG中包含的扇区数,一般m1卡扇区数为16个 String typeS = getMifareClassicType(type); metaInfo += "卡片类型:" + typeS + "n共" + sectorCount + "个扇区n共" + mifareClassic.getBlockCount() + "个块n存储空间: " + mifareClassic.getSize() + "Bn"; LogWriter.i(metaInfo); for (int j = 0; j < sectorCount; j++) { if (m1AuthByKey(mifareClassic, j,keyA) || m1Auth(mifareClassic, j)){ List<byte[]> dataList = new ArrayList<byte[]>(); int bCount; int bIndex; bCount = mifareClassic.getBlockCountInSector(j);//获得当前扇区的所包含块的数量; bIndex = mifareClassic.sectorToBlock(j);//当前扇区的第1块的块号 for (int i = 0; i < bCount; i++) { byte[] data = mifareClassic.readBlock(bIndex); String hexString = ByteUtils.byteArrToHexString(data); LogWriter.w(j + "扇区" + bIndex + "块:" + hexString); dataList.add(data); bIndex++; } result.put(j,dataList); }else { LogWriter.w("密码校验失败,扇区:" + j); } } return result; } catch (Exception e) { throw new IOException(e); } finally { try { if (mifareClassic != null) { mifareClassic.close(); } } catch (IOException e) { throw new IOException(e); } } } /** * 获取m1卡类型 * @param type * @return */ private static String getMifareClassicType(int type) { String str = null; switch (type) { case MifareClassic.TYPE_CLASSIC: str = "TYPE_CLASSIC"; break; case MifareClassic.TYPE_PLUS: str = "TYPE_PLUS"; break; case MifareClassic.TYPE_PRO: str = "TYPE_PRO"; break; case MifareClassic.TYPE_UNKNOWN: str = "TYPE_UNKNOWN"; break; } return str; } /** * 往指定扇区的指定块写数据 * @param tag * @param sectorIndex 扇区索引 0~15(16个扇区) * @param blockIndex 块索引 0~63 * @param blockByte 写入数据必须是16字节 * @return * @throws IOException */ public static boolean writeBlock(Tag tag, int sectorIndex,int blockIndex,byte[] blockByte) throws IOException { MifareClassic mifareClassic = MifareClassic.get(tag); try { mifareClassic.connect(); if (m1Auth(mifareClassic, sectorIndex)) { mifareClassic.writeBlock(blockIndex, blockByte); } else { return false; } } catch (IOException e) { throw new IOException(e); } finally { try { mifareClassic.close(); } catch (IOException e) { throw new IOException(e); } } return true; } /** * 使用默认密码校验 * * @param mifareClassic * @param position * @return * @throws IOException */ public static boolean m1Auth(MifareClassic mifareClassic, int position) throws IOException { if (mifareClassic.authenticateSectorWithKeyA(position, MifareClassic.KEY_DEFAULT)) { return true; } else if (mifareClassic.authenticateSectorWithKeyB(position, MifareClassic.KEY_DEFAULT)) { return true; } return false; } /** * 使用指定密码校验 * * @param mifareClassic * @param position * @param keyA * @return * @throws IOException */ public static boolean m1AuthByKey(MifareClassic mifareClassic, int position,byte[] keyA) throws IOException { if (keyA != null && keyA.length == 6){ if(mifareClassic.authenticateSectorWithKeyA(position, keyA)) { return true; } } return false; } }
特别说明:关于Tag读写只是进行简单工具类封装,没有进行面向组件封装。
配置示例源码:https://github.com/kellysong/android-blog-demo/tree/master/nfc-demo
最后
以上就是精明短靴最近收集整理的关于Android NFC使用详解的全部内容,更多相关Android内容请搜索靠谱客的其他文章。
发表评论 取消回复