Android NFC开发-实践篇
https://blog..net/_GYG/article/details/72899417
在Android NFC开发-理论篇中,我们了解了在Android中开发NFC的一些理论知识,这篇我们继续应用我们上一篇学到的知识,实现对NDEF格式标签和MifareClassic格式标签的读写操作。
基本操作
配置AndroidMenifest.xml:
获取设备默认的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)
描述
0
1
状态字节,见下表(状态字节编码格式)
1
n
ISO/IANA语言编码。例如”en-US”,”fr-CA”。编码格式是US-ASCII,长度(n)由状态字节的后6位指定。
n+1
m
文本数据。编码格式是UTF-8或UTF-16。编码格式由状态字节的前3位指定。
状态字节编码格式:
字节位(0是最低位,7是最高位)
含义
7
0:文本编码为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()
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();
}