概述
摘要
网上关于Android NFC 操作的文章没有一篇合适自己,没有从根本解决问题,故整理一翻,理清思路。NFC是Near Field Communication缩写,即近距离无线通讯技术。可以在移动设备、消费类电子产品、PC 和智能控件工具间进行近距离无线通信。比如读取图书标签、公交卡等。
NFC Tag解析顺序(可以不在Activity配置)
nfc类型有三种NDEF,TECH,TAG,优先级依次降低,其对应的Intent过滤规则分别是:
NDEF:
<intent-filter>
<action android:name = "android.nfc.action.NDEF_DISCOVERED" />
<data android:mimeType = "text/plain" />
</intent-filter>
TECH :
<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:
<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.权限
<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.初始化
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
pi = PendingIntent.getActivity(this, 0, new Intent(this, getClass())
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
2.设置系统调度
@Override
protected void onResume() {
super.onResume();
mNfcAdapter.enableForegroundDispatch(this, pi, null, null); //启动
}
3.系统调用onNewIntent(Intent intent)
@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,方法调用:
Tag 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列表:
for (String tech : tag.getTechList()) {
LogUtils.i("------------" + tech);
}
4.Tag读写流程
protected 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.取消系统调度
@Override
protected void onPause() {
if (mNfcAdapter != null)
mNfcAdapter.disableForegroundDispatch(this); // 取消调度
}
Tag读写工具类
iso15693卡:
package 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卡:
package 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 NFC使用详解所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复