[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;
flagvaluemeaning
FLAG_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
到此,我们整理下所需指令:

commandmeaning
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年。这样估计是各版本的卡有所不同。

接下来就是读取卡片的卡号、有效期、交易记录这些信息,并且解析数据,显示在界面上。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值