Android Nfc读写标签

  文章介绍NDEFMifareClassic两种格式数据读写标签的方法。 在进行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
数据块.png

二、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规范中的任一种则都会响应。

优先级顺序,上面三种规则依次递减

  1. 如果卡片是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。
  2. 如果卡片是非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();
    }

代码太多了,懒的粘了

第一次写文章欢迎大家指教

附上GitHub地址:

https://github.com/yaogoodgoodde/SimpleNfc.git

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容