金融支付Android扫描

[NFC] 读羊城通卡片信息

2016-12-15  本文已影响555人  河婆墟邓紫棋

学习开源项目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年。这样估计是各版本的卡有所不同。

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

上一篇下一篇

猜你喜欢

热点阅读