Android NFC使用详解

摘要 

网上关于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对象。 

ClassDescription对应识别的卡种
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卡


非接触IC卡

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实例。
    1. m1卡: MifareClassic mfc = MifareClassic.get(tag);

    2. 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() + "B\n";
            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() + "B\n";
            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() + "B\n";
            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

转载:https://blog.csdn.net/u011082160/article/details/89146192

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值