Android NFC开发-实践篇

Android NFC开发-理论篇中,我们了解了在Android中开发NFC的一些理论知识,这篇我们继续应用我们上一篇学到的知识,实现对NDEF格式标签和MifareClassic格式标签的读写操作。

基本操作

配置AndroidMenifest.xml:

<!--API level 9只包含有限的tag支持,包括:
    .通过ACTION_TAG_DISCOVERED来发布Tag信息
    .只有通过EXTRA_NDEF_MESSAGES扩展来访问NDEF消息
    .其他的tag属性和I/O操作都不支持
    所以你可能想要用API level 10来实现对tag的广泛的读写支持。-->
    <uses-sdk android:minSdkVersion="10"/>
    <!--NFC权限-->
    <uses-permission android:name="android.permission.NFC" />
    <!-- 要求当前设备必须要有NFC芯片 -->
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

获取设备默认的NfcAdapter对象,判断该设备是否支持NFC功能,若支持,判断此功能是否打开,并且创建一个PendingIntent对象,用于当NFC标签被检测到时,启动我们处理NFC标签的Activity

@Override
    protected void onStart() {
        super.onStart();
        mNfcAdapter= NfcAdapter.getDefaultAdapter(this);//设备的NfcAdapter对象
        if(mNfcAdapter==null){//判断设备是否支持NFC功能
            Toast.makeText(this,"设备不支持NFC功能!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        if (!mNfcAdapter.isEnabled()){//判断设备NFC功能是否打开
            Toast.makeText(this,"请到系统设置中打开NFC功能!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        mPendingIntent=PendingIntent.getActivity(this,0,new Intent(this,getClass()),0);//创建PendingIntent对象,当检测到一个Tag标签就会执行此Intent
    }

在OnNewIntent()方法中,获取到Tag对象

@Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        mTag=intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//获取到Tag标签对象
        String[] techList=mTag.getTechList();
        System.out.println("标签支持的tachnology类型:");
        for (String tech:techList){
            System.out.println(tech);
        }
    }

为了更好的处理NFC标签,我们需要在Activity获取焦点时(onResume),在主线程中启动前台发布系统,并且在Activity失去焦点时,关闭前台发布系统

//页面获取焦点
@Override
    protected void onResume() {
        super.onResume();
        if (mNfcAdapter!=null){          mNfcAdapter.enableForegroundDispatch(this,mPendingIntent,null,null);//打开前台发布系统,使页面优于其它nfc处理.当检测到一个Tag标签就会执行mPendingItent
        }
    }

//页面失去焦点
@Override
    protected void onPause() {
        super.onPause();
        if(mNfcAdapter!=null){
            mNfcAdapter.disableForegroundDispatch(this);//关闭前台发布系统
        }
    }

以上所有操作,都是对一个NFC标签的基本操作,我们封装在一个BaseNfcActivity中,对不同格式标签读写的Activity都继承BaseNfcActivity。

NDEF格式标签读写

我们可以通过Tag对象的getTechList()获取到标签的技术类型,只有支持NDEF格式的标签才可以进行NDEF格式标签的读写操作。

读写NDEF格式标签主要涉及到两个类:

  • NdefMessage:描述NDEF格式的信息,实际上我们写入NFC标签的就是NdefMessage对象。
  • NdefRecord:描述NDEF信息的一个信息段,一个NdefMessage可能包含一个或者多个NdefRecord。

获取Ndef对象

Ndef ndef=Ndef.get(mTag);//获取ndef对象

创建NdefRecord,Android为我们提供了创建NdefRecord的方法,是我们可以轻松创建一个NdefRecord对象

  • NdefRecord.createApplicationRecord(String packageName)
  • NdefRecord.createUri(Uri uri)
  • NdefRecord.createUri(String uriString)
  • NdefRecord.createTextRecord(String languageCode, String text)

遗憾的是NdefRecord.createTextRecord(String languageCode, String text)最小兼容sdk版本是21,对于需要兼容更小版本的应用来说就需要我们自己来实现这个方法。

不管什么格式的数据本质上都是由一些字节组成的。对于NDEF文本格式来说,这些数据的第1个字节描述了数据的状态,然后若干个字节描述文本的语言编码,最后剩余字节表示文本数据。这些数据格式由NFC Forum的相关规范定义,可以通过 http://members.nfc-forum.org/specs/spec_dashboard 下载相关的规范。

NDEF的文本数据规范:

偏移量长度(bytes)描述
01状态字节,见下表(状态字节编码格式)
1nISO/IANA语言编码。例如”en-US”,”fr-CA”。编码格式是US-ASCII,长度(n)由状态字节的后6位指定。
n+1m文本数据。编码格式是UTF-8或UTF-16。编码格式由状态字节的前3位指定。

状态字节编码格式:

字节位(0是最低位,7是最高位)含义
70:文本编码为UTF-8,1:文本编码为UTF-16
6必须设为0
5..0语言编码的长度(占用的字节个数)

创建文本NdefRecord

/**
     * 创建NDEF文本数据
     * @param text
     * @return
     */
    public static NdefRecord createTextRecord(String text) {
        byte[] langBytes = Locale.CHINA.getLanguage().getBytes(Charset.forName("US-ASCII"));
        Charset utfEncoding = Charset.forName("UTF-8");
        //将文本转换为UTF-8格式
        byte[] textBytes = text.getBytes(utfEncoding);
        //设置状态字节编码最高位数为0
        int utfBit = 0;
        //定义状态字节
        char status = (char) (utfBit + langBytes.length);
        byte[] data = new byte[1 + langBytes.length + textBytes.length];
        //设置第一个状态字节,先将状态码转换成字节
        data[0] = (byte) status;
        //设置语言编码,使用数组拷贝方法,从0开始拷贝到data中,拷贝到data的1到langBytes.length的位置
        System.arraycopy(langBytes, 0, data, 1, langBytes.length);
        //设置文本字节,使用数组拷贝方法,从0开始拷贝到data中,拷贝到data的1 + langBytes.length
        //到textBytes.length的位置
        System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);
        //通过字节传入NdefRecord对象
        //NdefRecord.RTD_TEXT:传入类型 读写
        NdefRecord ndefRecord = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,
                NdefRecord.RTD_TEXT, new byte[0], data);
        return ndefRecord;
    }

创建NdefMessage,并且写入Ndef标签

//往Ndef标签中写数据
    private void writeNdef(){
        if (mTag==null){
            Toast.makeText(this,"不能识别的标签类型!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        Ndef ndef=Ndef.get(mTag);//获取ndef对象
        if (!ndef.isWritable()){
            Toast.makeText(this,"该标签不能写入数据!",Toast.LENGTH_SHORT);
            return;
        }
        NdefRecord ndefRecord=createTextRecord(writeEdt.getText().toString());//创建一个NdefRecord对象
        NdefMessage ndefMessage=new NdefMessage(new NdefRecord[]{ndefRecord});//根据NdefRecord数组,创建一个NdefMessage对象
        int size=ndefMessage.getByteArrayLength();
        if (ndef.getMaxSize()<size){
            Toast.makeText(this,"标签容量不足!",Toast.LENGTH_SHORT);
            return;
        }
        try {
            ndef.connect();//连接
            ndef.writeNdefMessage(ndefMessage);//写数据
            Toast.makeText(this,"数据写入成功!",Toast.LENGTH_SHORT);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (FormatException e) {
            e.printStackTrace();
        }finally {
            try {
                ndef.close();//关闭连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

读Ndef文本数据

//读取Ndef标签中数据
    private void readNdef(){
        if (mTag==null){
            Toast.makeText(this,"不能识别的标签类型!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        Ndef ndef=Ndef.get(mTag);//获取ndef对象
        try {
            ndef.connect();//连接
            NdefMessage ndefMessage=ndef.getNdefMessage();//获取NdefMessage对象
            if (ndefMessage!=null)               readEdt.setText(parseTextRecord(ndefMessage.getRecords()[0]));
            Toast.makeText(this,"数据读取成功!",Toast.LENGTH_SHORT);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (FormatException e) {
            e.printStackTrace();
        }finally {
            try {
                ndef.close();//关闭链接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 解析NDEF文本数据,从第三个字节开始,后面的文本数据
     * @param ndefRecord
     * @return
     */
    public static String parseTextRecord(NdefRecord ndefRecord) {
        /**
         * 判断数据是否为NDEF格式
         */
        //判断TNF
        if (ndefRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {
            return null;
        }
        //判断可变的长度的类型
        if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
            return null;
        }
        try {
            //获得字节数组,然后进行分析
            byte[] payload = ndefRecord.getPayload();
            //下面开始NDEF文本数据第一个字节,状态字节
            //判断文本是基于UTF-8还是UTF-16的,取第一个字节"位与"上16进制的80,16进制的80也就是最高位是1,
            //其他位都是0,所以进行"位与"运算后就会保留最高位
            String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";
            //3f最高两位是0,第六位是1,所以进行"位与"运算后获得第六位
            int languageCodeLength = payload[0] & 0x3f;
            //下面开始NDEF文本数据第二个字节,语言编码
            //获得语言编码
            String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
            //下面开始NDEF文本数据后面的字节,解析出文本
            String textRecord = new String(payload, languageCodeLength + 1,
                    payload.length - languageCodeLength - 1, textEncoding);
            return textRecord;
        } catch (Exception e) {
            throw new IllegalArgumentException();
        }
    }

MifareClassic格式标签读写

MifareClassic格式标签数据结构
这里写图片描述

这里写图片描述

第一扇区的第一块一般用于制造商占用块

0-15个扇区:一个扇区对应4个块,所以总共有64个块,序号分别为0-63,第一个扇区对应:0-3块,第二个扇区对应:4-7块…

每个扇区的最后一个块用来存放密码或控制位,其余为数据块,一个块占用16个字节,keyA占用6字节,控制位占用4字节,keyB占用6字节。

MifareClassic标签读写常用api:

  • get():根据Tag对象来获得MifareClassic对象;
  • Connect():允许对MifareClassic标签进行IO操作;
  • getType():获得MifareClassic标签的具体类型:TYPE_CLASSIC,TYPE_PLUA,TYPE_PRO,TYPE_UNKNOWN;
  • getSectorCount():获得标签总共有的扇区数量;
  • getBlockCount():获得标签总共有的的块数量;
  • getSize():获得标签的容量:SIZE_1K,SIZE_2K,SIZE_4K,SIZE_MINI
  • authenticateSectorWithKeyA(int SectorIndex,byte[] Key):验证当前扇区的KeyA密码,返回值为ture或false。 常用KeyA:默认出厂密码:KEY_DEFAULT,各种用途的供货商必须配合该技术的MAD:KEY_MIFARE_APPLICATION_DIRECTORY
    被格式化成NDEF格式的密码:KEY_NFC_FORUM
  • getBlockCountInSector(int):获得当前扇区的所包含块的数量;
  • sectorToBlock(int):当前扇区的第1块的块号;
  • writeBlock(int,data):将数据data写入当前块;注意:data必须刚好是16Byte,末尾不能用0填充,应该用空格
  • readBlock(int):读取当前块的数据。
  • close():禁止对标签的IO操作,释放资源。

写MifareClassic格式标签数据

//写块
    private void writeBlock(){
        if (mTag==null){
            Toast.makeText(this,"无法识别的标签!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        if (!haveMifareClissic){
            Toast.makeText(this,"不支持MifareClassic",Toast.LENGTH_SHORT);
           finish();
            return;
        }
        MifareClassic mfc=MifareClassic.get(mTag);
        try {
            mfc.connect();//打开连接
            boolean auth;
            int sector=Integer.parseInt(sectorNum.getText().toString().trim());//写入的扇区
            int block=Integer.parseInt(blockNum.getText().toString().trim());//写入的块区
            auth=mfc.authenticateSectorWithKeyA(sector,MifareClassic.KEY_DEFAULT);//keyA验证扇区
            if (auth){
                mfc.writeBlock(block,"0123456789012345".getBytes());//写入数据
                Toast.makeText(this,"写入成功!",Toast.LENGTH_SHORT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                mfc.close();//关闭连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

读MifareClassic格式标签数据

//读取块
    private void readBlock(){
        if (mTag==null){
            Toast.makeText(this,"无法识别的标签!",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        if (!haveMifareClissic){
            Toast.makeText(this,"不支持MifareClassic",Toast.LENGTH_SHORT);
            finish();
            return;
        }
        MifareClassic mfc=MifareClassic.get(mTag);
        try {
            mfc.connect();//打开连接
            boolean auth;
            int sector=Integer.parseInt(sectorNum.getText().toString().trim());//写入的扇区
            int block=Integer.parseInt(blockNum.getText().toString().trim());//写入的块区
            auth=mfc.authenticateSectorWithKeyA(sector,MifareClassic.KEY_DEFAULT);//keyA验证扇区
            if (auth){
                readData.setText(bytesToHexString(mfc.readBlock(block)));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                mfc.close();//关闭连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    //字符序列转换为16进制字符串
    private String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("0x");
        if (src == null || src.length <= 0) {
            return null;
        }
        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);
            System.out.println(buffer);
            stringBuilder.append(buffer);
        }
        return stringBuilder.toString();
    }

Demo

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值