在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-166必须设为05..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_MINIauthenticateSectorWithKeyA(int SectorIndex,byte[] Key):验证当前扇区的KeyA密码,返回值为ture或false。 常用KeyA:默认出厂密码:KEY_DEFAULT,各种用途的供货商必须配合该技术的MAD:KEY_MIFARE_APPLICATION_DIRECTORY 被格式化成NDEF格式的密码:KEY_NFC_FORUMgetBlockCountInSector(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