[NFC] 读羊城通卡片信息
学习开源项目NFCard,最新版源码以及几年前代码相比较,发现之前的版本可以支持读羊城通,而现在不再支持读羊城通卡信息,那定一个小目标。通过NFC读取羊城通卡片信息之余额和交易记录。
实现的效果如图:
目录
1.建立工程,编写NFC相关代码;
2.根据开源项目中的指令,读取余额;
3.根据开源项目中的指令,读取交易记录;
4.根据卡片原始信息解析数据;
一、编写NFC相关代码
import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
private NfcAdapter mNfcAdapter;
private PendingIntent mPendingIntent;
private Intent mIntent;
private final int READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK;
private NfcAdapter.ReaderCallback mReaderCallback = new NfcAdapter.ReaderCallback() {
@Override
public void onTagDiscovered(Tag tag) {
System.out.println(tag.toString());
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
protected void onStart() {
super.onStart();
initNfc();
}
@Override
protected void onResume() {
super.onResume();
registerNfc();
}
@Override
protected void onPause() {
super.onPause();
unRegisterNfc();
}
private void initNfc() {
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
mIntent = new Intent(NfcAdapter.ACTION_TECH_DISCOVERED);
mIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
mPendingIntent = PendingIntent.getActivity(this, 0, mIntent, 0);
}
private void registerNfc() {
Bundle bundle = new Bundle();
mNfcAdapter.enableReaderMode(this, mReaderCallback, READER_FLAGS, bundle);
}
private void unRegisterNfc() {
mNfcAdapter.disableReaderMode(this);
}
}
运行上述代码的效果:(羊城通紧贴在手机NFC感应处)
I/System.out: TAG: Tech [android.nfc.tech.IsoDep, android.nfc.tech.NfcA]
看源码解释一下:
注册NFC调用了NfcAdapter的enableReaderMode方法,先看看源码:
/**
* Limit the NFC controller to reader mode while this Activity is in the foreground.
*
* <p>In this mode the NFC controller will only act as an NFC tag reader/writer,
* thus disabling any peer-to-peer (Android Beam) and card-emulation modes of
* the NFC adapter on this device.
*
* <p>Use {@link #FLAG_READER_SKIP_NDEF_CHECK} to prevent the platform from
* performing any NDEF checks in reader mode. Note that this will prevent the
* {@link Ndef} tag technology from being enumerated on the tag, and that
* NDEF-based tag dispatch will not be functional.
*
* <p>For interacting with tags that are emulated on another Android device
* using Android's host-based card-emulation, the recommended flags are
* {@link #FLAG_READER_NFC_A} and {@link #FLAG_READER_SKIP_NDEF_CHECK}.
*
* @param activity the Activity that requests the adapter to be in reader mode
* @param callback the callback to be called when a tag is discovered
* @param flags Flags indicating poll technologies and other optional parameters
* @param extras Additional extras for configuring reader mode.
*/
public void enableReaderMode(Activity activity, ReaderCallback callback, int flags,
Bundle extras) {
mNfcActivityManager.enableReaderMode(activity, callback, flags, extras);
}
三行代码三大段注释,nice。enableReaderMode()抠脚翻译如下:
限制NFC的模式为读卡器模式
在这种模式中,就仅仅是可以用NFC读写带有NFC芯片的标签(卡片、贴纸等),不允许点对点模式和卡模拟模式。
可以通过FLAG_READER_SKIP_NDEF_CHECK这个标志过滤NDEF标签,这个就是标志就是第三个参数啦,NDEF(NFC Data Exchange Format,NFC数据交换格式)是Android SDK API主要支持NFC论坛标准。
如果是准备与另一台Android卡模拟设备交互,那么建议设置的标志就是FLAG_READER_NFC_A和FLAG_READER_SKIP_NDEF_CHECK
参数callback:发现符合的标签就回调到callback
参数extras : 对读卡器模式进行一些配置(先晾它一会,目前只是传了一个空bundle进去)
参数flags:标志
上述参数中有一个flags,我们顺便也看看有哪些flag以及flag的作用是什么,看源码然后抠脚解释下:
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag enables polling for Nfc-A technology.
*/
public static final int FLAG_READER_NFC_A = 0x1;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag enables polling for Nfc-B technology.
*/
public static final int FLAG_READER_NFC_B = 0x2;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag enables polling for Nfc-F technology.
*/
public static final int FLAG_READER_NFC_F = 0x4;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag enables polling for Nfc-V (ISO15693) technology.
*/
public static final int FLAG_READER_NFC_V = 0x8;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag enables polling for NfcBarcode technology.
*/
public static final int FLAG_READER_NFC_BARCODE = 0x10;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag allows the caller to prevent the
* platform from performing an NDEF check on the tags it
* finds.
*/
public static final int FLAG_READER_SKIP_NDEF_CHECK = 0x80;
/**
* Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}.
* <p>
* Setting this flag allows the caller to prevent the
* platform from playing sounds when it discovers a tag.
*/
public static final int FLAG_READER_NO_PLATFORM_SOUNDS = 0x100;
flag | value | meaning |
---|---|---|
FLAG_READER_NFC_A | 0x1 | 支持NFCA数据格式 |
FLAG_READER_NFC_B | 0x2 | 支持NFCB数据格式 |
FLAG_READER_NFC_F | 0x4 | 支持NFCF数据格式 |
FLAG_READER_NFC_V | 0x8 | 支持NFCV数据格式 |
FLAG_READER_NFC_BARCODE | 0x10 | 支持NFCBARCODE数据格式 |
FLAG_READER_SKIP_NDEF_CHECK | 0x80 | 过滤NDEF数据格式 |
FLAG_READER_NO_PLATFORM_SOUNDS | 0x100 | 关闭发现TAG时的声音 |
看完上述,估计有点蒙,好像也有点跑偏,赶紧回到注册NFC的这个方法中,我们在onResume中调用了enableReaderMode,此方法在卡片(此处指羊城通)贴到手机NFC感应处时会回调到ReaderCallback中,所以我们在onTagDiscovered这个回调中即可与卡片进行交互。
二、根据指令读取羊城通余额
首先我们可以去交通信息中心下载一份《城市公共交通IC卡技术规范》卡片的部分,认真去阅读(一头扎进去估计难看懂),我们知道选择目录的指令为:00A40400+lc+文件名+00;读取余额的指令为:805C000204(指令为7816报文格式)
其次我们可以去阅读NFCard这个开源项目,从源码中知道,选择的文件名为:"PAY.TICL",lc为:08
到此,我们整理下所需指令:
command | meaning |
---|---|
00A40400085041592E5449434C00 | 选择PAY.TICL目录(P的十六进制ASCII码为50) |
805C000204 | 读取余额 |
上述拼接指令过程中,需要把字符换成对应的十六进制ASCII码,好在Google的Sample给我们提供了这些转换方法,恩,又可以抄一波(具体Sample路径:"sdk根目录"\samples\android-"version"\connectivity\CardReader..\LoyaltyCardReader.java):
/**
* Utility class to convert a byte array to a hexadecimal string.
*
* @param bytes Bytes to convert
* @return String, containing hexadecimal representation.
*/
public static String ByteArrayToHexString(byte[] bytes) {
final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] hexChars = new char[bytes.length * 2];
int v;
for ( int j = 0; j < bytes.length; j++ ) {
v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
/**
* Utility class to convert a hexadecimal string to a byte string.
*
* <p>Behavior with input strings containing non-hexadecimal characters is undefined.
*
* @param s String containing hexadecimal characters to convert
* @return Byte array generated from input
*/
public static byte[] HexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
有了卡片,有了卡片指令,我们就开始通过NFC进行交互
回到我们NFC的回调方法中,我们可以从回调方法onTagDiscovered(Tag tag)拿到一个TAG,这个TAG从输出的日志看,有IsoDep,通过IsoDep类的transceive方法即可发送指令数据到卡片并且返回响应数据:
@Override
public void onTagDiscovered(Tag tag) {
System.out.println(tag.toString());
if (tag.toString().contains(IsoDep.class.getName())) {
IsoDep isoDep = IsoDep.get(tag);
if (isoDep != null) {
try {
isoDep.connect();//连接
//选择目录
System.out.print("指令报文:" + "00A40400085041592E5449434C00");
byte[] resp_dir = isoDep.transceive(Commands.HexStringToByteArray("00A40400085041592E5449434C00"));
System.out.println(" 响应报文:" + Commands.ByteArrayToHexString(resp_dir));
//读取余额
System.out.print("指令报文:" + "805C000204");
byte[] resp_balance = isoDep.transceive(Commands.HexStringToByteArray("805C000204"));
System.out.println(" 响应报文:" + Commands.ByteArrayToHexString(resp_balance));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
具体数据为:
I/System.out: 指令报文:00A40400085041592E5449434C00 响应报文:6F3484085041592E5449434CA5289F0801029F0C21FFFFFFFFFFFFFFFF000000000000000000000000000000002016122400000186A09000
I/System.out: 指令报文:805C000204 响应报文:00000E479000
根据7816报文格式,响应报文格式为DATA+SW1+SW2,SW1和SW2为状态字,分别占一个字节,由此DATA=00000E47,SW1=90,SW2=00。00000E47转十进制则是3655,这个时候我们用QQ来读一下羊城通对比一下余额是否正确,QQ读出余额如下图:
QQ读出羊城通余额我们使用的transceive方法,将原始数据发送至卡片标签,并且得到响应,如果中途移开卡片,则会抛出TagLostException(也是继承IOException),如果中途读写失败或者取消,则抛出IOException。
/**
* Send raw ISO-DEP data to the tag and receive the response.
*
* <p>Applications must only send the INF payload, and not the start of frame and
* end of frame indicators. Applications do not need to fragment the payload, it
* will be automatically fragmented and defragmented by {@link #transceive} if
* it exceeds FSD/FSC limits.
*
* <p>Use {@link #getMaxTransceiveLength} to retrieve the maximum number of bytes
* that can be sent with {@link #transceive}.
*
* <p>This is an I/O operation and will block until complete. It must
* not be called from the main application thread. A blocked call will be canceled with
* {@link IOException} if {@link #close} is called from another thread.
*
* <p class="note">Requires the {@link android.Manifest.permission#NFC} permission.
*
* @param data command bytes to send, must not be null
* @return response bytes received, will not be null
* @throws TagLostException if the tag leaves the field
* @throws IOException if there is an I/O failure, or this operation is canceled
*/
public byte[] transceive(byte[] data) throws IOException {
return transceive(data, true);
}
小结:
我们通过开源项目NFCard、Google的Sample之CardReader、交通信息中心的《城市公共交通IC卡技术规范》文档,成功读出了羊城通余额。
这里需要提醒的是,最新版的NFCard源码读不出我手中的羊城通,显示为未知卡片,反而找到2013年的版本才读出来,本人手中的卡有效期也是2013年-2018年。这样估计是各版本的卡有所不同。
接下来就是读取卡片的卡号、有效期、交易记录这些信息,并且解析数据,显示在界面上。