学习开源项目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; flagvaluemeaningFLAG_READER_NFC_A0x1支持NFCA数据格式FLAG_READER_NFC_B0x2支持NFCB数据格式FLAG_READER_NFC_F0x4支持NFCF数据格式FLAG_READER_NFC_V0x8支持NFCV数据格式FLAG_READER_NFC_BARCODE0x10支持NFCBARCODE数据格式FLAG_READER_SKIP_NDEF_CHECK0x80过滤NDEF数据格式FLAG_READER_NO_PLATFORM_SOUNDS0x100关闭发现TAG时的声音看完上述,估计有点蒙,好像也有点跑偏,赶紧回到注册NFC的这个方法中,我们在onResume中调用了enableReaderMode,此方法在卡片(此处指羊城通)贴到手机NFC感应处时会回调到ReaderCallback中,所以我们在onTagDiscovered这个回调中即可与卡片进行交互。
二、根据指令读取羊城通余额
首先我们可以去交通信息中心下载一份《城市公共交通IC卡技术规范》卡片的部分,认真去阅读(一头扎进去估计难看懂),我们知道选择目录的指令为:00A40400+lc+文件名+00;读取余额的指令为:805C000204(指令为7816报文格式) 其次我们可以去阅读NFCard这个开源项目,从源码中知道,选择的文件名为:”PAY.TICL”,lc为:08 到此,我们整理下所需指令:
commandmeaning00A40400085041592E5449434C00选择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读出余额如下图:
我们使用的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年。这样估计是各版本的卡有所不同。
接下来就是读取卡片的卡号、有效期、交易记录这些信息,并且解析数据,显示在界面上。
