Android NFC讀卡器桩匪,仿真卡流程學(xué)習(xí)

學(xué)習(xí)資料:

感謝laocaixw大佬,找了半天NFC相關(guān)開發(fā)的博客友鼻,終于找到一個(gè)簡單明了的傻昙,就把代碼抄了下來,以便之后再看

一臺(tái)支持NFCAndroid手機(jī)桃移,可以作為讀卡器來讀取一張銀行卡或者公交卡屋匕,也可以模擬成一張卡來進(jìn)行刷卡消費(fèi),也就是我所說的仿真卡借杰,屬于HCE相關(guān)開發(fā)

公司現(xiàn)在的項(xiàng)目屬于HCE業(yè)務(wù)項(xiàng)目过吻,要模擬銀行卡,也提前了解些NFC相關(guān)的東西

本篇中的案例蔗衡,需要兩個(gè)支持NFC的手機(jī)才可以演示纤虽,一個(gè)作為讀卡器,一個(gè)模擬卡實(shí)現(xiàn)仿真卡绞惦,當(dāng)讀卡器仿真卡貼在一起后逼纸,讀卡器會(huì)先發(fā)送一個(gè)指令給仿真卡仿真卡驗(yàn)證指令后济蝉,就可以返回?cái)?shù)據(jù)給讀卡器

案例中獲取卡號(hào)的流程只是簡單演示用的杰刽,隨意返回了一個(gè)16位卡號(hào)菠发。實(shí)際獲取卡號(hào)的流程比這復(fù)雜的多,需要發(fā)送多個(gè)指令才能拿到卡的有效信息


1.讀卡器代碼

權(quán)限

<uses-permission android:name="android.permission.NFC" />
<!--聲明需要硬件支持nfc-->
<uses-feature
        android:name="android.hardware.nfc"
        android:required="true" />

actiivty配置

 <activity
            android:name=".NFCActivity"
            android:label="@string/nfc_name"
            android:launchMode="singleTop"
            android:screenOrientation="portrait" />

launchMode使用的是棧頂復(fù)用模式贺嫂,activity啟動(dòng)自身滓鸠,會(huì)執(zhí)行onNewIntent()方法

屏幕鎖死了豎屏,以避免手機(jī)在橫豎屏切換時(shí)第喳,導(dǎo)致Intent信息丟失


1.1 Activity代碼

NFCActivity代碼

public class NFCActivity extends AppCompatActivity {
    private final String TAG = NFCActivity.class.getSimpleName();
    private NfcAdapter mNfcAdapter;
    private PendingIntent mPendingIntent;
    private IntentFilter[] mIntentFilter;
    private String[][] mTechList;
    private TextView mTvView;

    // 卡片返回來的正確信號(hào)
    private final byte[] SELECT_OK = stringToBytes("1000");


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_nfc);

        initView();

        nfcCheck();

        init();
    }

    private void initView() {
        mTvView = (TextView) findViewById(R.id.nfc_activity_tv_info);
    }

    /**
     * 初始化
     */
    private void init() {
        // NFCActivity 一般設(shè)置為: SingleTop模式 糜俗,并且鎖死豎屏,以避免屏幕旋轉(zhuǎn)Intent丟失
        Intent intent = new Intent(NFCActivity.this, NFCActivity.class);

        // 私有的請(qǐng)求碼
        final int REQUEST_CODE = 1 << 16;

        final int FLAG = 0;
        mPendingIntent = PendingIntent.getActivity(NFCActivity.this, REQUEST_CODE, intent, FLAG);

        // 三種過濾器
        IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
        IntentFilter tech = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
        IntentFilter tag = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
        mIntentFilter = new IntentFilter[]{ndef, tech, tag};

        // 只針對(duì)ACTION_TECH_DISCOVERED
        mTechList = new String[][]{
                {IsoDep.class.getName()}, {NfcA.class.getName()}, {NfcB.class.getName()},
                {NfcV.class.getName()}, {NfcF.class.getName()}, {Ndef.class.getName()}};
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        // IsoDep卡片通信的工具類曲饱,Tag就是卡
        IsoDep isoDep = IsoDep.get((Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG));
        if (isoDep == null) {
            String info = "讀取卡信息失敗";
            toast(info);
            return;
        }
        try {
            // NFC與卡進(jìn)行連接
            isoDep.connect();

            final String AID = "F123466666";
            //轉(zhuǎn)換指令為byte[]
            byte[] command = buildSelectApdu(AID);

            // 發(fā)送指令
            byte[] result = isoDep.transceive(command);

            // 截取響應(yīng)數(shù)據(jù)
            int resultLength = result.length;
            byte[] statusWord = {result[resultLength - 2], result[resultLength - 1]};
            byte[] payload = Arrays.copyOf(result, resultLength - 2);

            // 檢驗(yàn)響應(yīng)數(shù)據(jù)
            if (Arrays.equals(SELECT_OK, statusWord)) {
                String accountNumber = new String(payload, "UTF-8");
                Log.e(TAG, "----> " + accountNumber);
                mTvView.setText(accountNumber);
            } else {
                String info = bytesToString(result);
                Log.e(TAG, "----> error" + info);
                mTvView.setText(info);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 開啟檢測,檢測到卡后悠抹,onNewIntent() 執(zhí)行
     * enableForegroundDispatch()只能在onResume() 方法中,否則會(huì)報(bào):
     * Foreground dispatch can only be enabled when your activity is resumed
     */
    @Override
    protected void onResume() {
        super.onResume();
        if (mNfcAdapter == null) return;
        mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, mIntentFilter, mTechList);
    }

    /**
     * 關(guān)閉檢測
     */
    @Override
    protected void onPause() {
        super.onPause();
        if (mNfcAdapter == null) return;
        mNfcAdapter.disableForegroundDispatch(this);
    }

    /**
     * 檢測是否支持 NFC
     */
    private void nfcCheck() {
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (mNfcAdapter == null) {
            String info = "手機(jī)不支付NFC功能";
            toast(info);
            return;
        }
        if (!mNfcAdapter.isEnabled()) {
            String info = "手機(jī)NFC功能沒有打開";
            toast(info);
            Intent setNfc = new Intent(Settings.ACTION_NFC_SETTINGS);
            startActivity(setNfc);
        } else {
            String info = "手機(jī)NFC功能正常";
            toast(info);
        }
    }

    private byte[] stringToBytes(String s) {
        int len = s.length();
        if (len % 2 == 1) {
            throw new IllegalArgumentException("指令字符串長度必須為偶數(shù) !!!");
        }
        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;
    }

    private String bytesToString(byte[] data) {
        StringBuilder sb = new StringBuilder();
        for (byte d : data) {
            sb.append(String.format("%02X", d));
        }
        return sb.toString();
    }


    private byte[] buildSelectApdu(String aid) {
        final String HEADER = "00A40400";
        return stringToBytes(HEADER + String.format("%02X", aid.length() / 2) + aid);
    }

    private void toast(String info) {
        Toast.makeText(NFCActivity.this, info, Toast.LENGTH_SHORT).show();
    }
}

onResume()onPause()分別就是關(guān)扩淀,一旦在onResume()中檢測到卡楔敌,會(huì)在onNewIntent()方法中執(zhí)行讀卡信息


2. 仿真卡代碼

權(quán)限:

    <uses-permission android:name="android.permission.NFC" />
    <!-- 聲明需要硬件支持nfc -->
    <uses-feature
        android:name="android.hardware.nfc.hce"
        android:required="true" />

配置:

<!--仿真卡服務(wù)-->
<service
    android:name=".CardService"
    android:exported="true"
    android:permission="android.permission.BIND_NFC_SERVICE">

    <intent-filter>
         z<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
                <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    
    <meta-data
          android:name="android.nfc.cardemulation.host_apdu_service"
          android:resource="@xml/aid_list" />
</service>

在res下建立一個(gè)xml文件夾,創(chuàng)建aid_li文件st

<?xml version="1.0" encoding="utf-8"?>

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/service_name" 
    android:requireDeviceUnlock="false">
    <aid-group
        android:category="other"
        android:description="@string/card_title">
        <aid-filter
            android:name="F123466666" />
    </aid-group>

</host-apdu-service>

android:requireDeviceUnlock="false"程序運(yùn)行驻谆,手機(jī)亮屏不解鎖的情況下梁丘,服務(wù)可以啟動(dòng)

android:name="F123466666"這一行很關(guān)鍵

讀卡器想要識(shí)別一個(gè)卡,肯定要有一個(gè)識(shí)別的標(biāo)記旺韭,這個(gè)就是指定的識(shí)別標(biāo)記,需要和代碼中發(fā)送的指令進(jìn)行統(tǒng)一掏觉。這個(gè)是我瞎寫的区端, 必須偶數(shù)位


2.1 CardService代碼

@TargetApi(Build.VERSION_CODES.KITKAT)
public class CardService extends HostApduService {
    // 正確信號(hào)
    private byte[] SELECT_OK = hexStringToByteArray("1000");

    // 錯(cuò)誤信號(hào)
    private byte[] UNKNOWN_ERROR = hexStringToByteArray("0000");

    /**
     * 接收到 NFC 讀卡器發(fā)送的應(yīng)用協(xié)議數(shù)據(jù)單元 (APDU) 調(diào)用
     * 注意:此方法回調(diào)在UI線程,若進(jìn)行聯(lián)網(wǎng)操作時(shí),需開子線程
     * 并先返回null澳腹,當(dāng)子線程有數(shù)據(jù)結(jié)果后织盼,再進(jìn)行回調(diào)返回處理
     */
    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        final String AID = "F123466666";

        // 將指令轉(zhuǎn)換成 byte[]
        byte[] selectAPDU = buildSelectApdu(AID);

        // 判斷是否和讀卡器發(fā)來的數(shù)據(jù)相同
        if (Arrays.equals(selectAPDU, commandApdu)) {
            // 直接模擬返回16位卡號(hào)
            String account = "6222222200000001";

            // 獲取卡號(hào) byte[]
            byte[] accountBytes = account.getBytes();

            // 處理欲返回的響應(yīng)數(shù)據(jù)
            return concatArrays(accountBytes, SELECT_OK);
        } else {
            return UNKNOWN_ERROR;
        }
    }

    @Override
    public void onDeactivated(int reason) {

    }

    private byte[] hexStringToByteArray(String s) throws IllegalArgumentException {
        int len = s.length();
        if (len % 2 == 1) {
            throw new IllegalArgumentException("指令字符串長度必須為偶數(shù) !!!");
        }
        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;
    }

    private byte[] buildSelectApdu(String aid) {
        final String HEADER = "00A40400";
        return hexStringToByteArray(HEADER + String.format("%02X", aid.length() / 2) + aid);
    }

    private byte[] concatArrays(byte[] first, byte[]... rest) {
        int totalLength = first.length;
        for (byte[] array : rest) {
            totalLength += array.length;
        }
        byte[] result = Arrays.copyOf(first, totalLength);
        int offset = first.length;
        for (byte[] array : rest) {
            System.arraycopy(array, 0, result, offset, array.length);
            offset += array.length;
        }
        return result;
    }
}

簡易的一個(gè)流程就是這樣,坑還很多酱塔,之后項(xiàng)目實(shí)際開發(fā)完成后沥邻,再來補(bǔ)充下實(shí)際開發(fā)中遇到的坑


2.1 一些已經(jīng)知道的淺坑

項(xiàng)目中開發(fā)的需求是:當(dāng)使用 App 仿真卡與 POS機(jī)靠近后,要求彈出卡面羊娃,指紋驗(yàn)證后唐全,進(jìn)行交易

  1. 寫上面的代碼學(xué)習(xí)時(shí),身邊沒有POS機(jī)蕊玷,也不清楚具體的指令邮利,就使用了兩個(gè)手機(jī)來學(xué)習(xí),但手機(jī)還是和POS機(jī)硬件有些差別的垃帅,和手機(jī)一樣延届,POS機(jī)廠商也會(huì)對(duì)自己的POS機(jī)做一些有別與其他品牌的優(yōu)化之類的

  2. 當(dāng)POS機(jī)發(fā)來一個(gè)指令后,當(dāng)不能立即響應(yīng)指令時(shí)贸诚,仿真卡在processCommandApdu ()方法可以先返回NULL的方庭。例如厕吉,我們項(xiàng)目的一個(gè)需求,仿真卡一接到PPSE指令時(shí)械念,在返回響應(yīng)指令前头朱,需要手機(jī)端先進(jìn)行指紋驗(yàn)證時(shí),就可以先返回NULL订讼,在經(jīng)過指紋驗(yàn)證之后髓窜,再使用sendResponeApdu()方法再來發(fā)送響應(yīng)指令。需要注意的是欺殿,不同的POS機(jī)寄纵,等待響應(yīng)指令的時(shí)間可能不同


3. 最后

最近接觸到了一些銀行POS業(yè)務(wù),被POS機(jī)交易需要用到8583報(bào)文折磨到吐脖苏,感嘆JSON真方便

有錯(cuò)誤程拭,請(qǐng)指出

共勉 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市棍潘,隨后出現(xiàn)的幾起案子恃鞋,更是在濱河造成了極大的恐慌,老刑警劉巖亦歉,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恤浪,死亡現(xiàn)場離奇詭異,居然都是意外死亡肴楷,警方通過查閱死者的電腦和手機(jī)水由,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赛蔫,“玉大人砂客,你說我怎么就攤上這事『腔郑” “怎么了鞠值?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渗钉。 經(jīng)常有香客問我彤恶,道長,這世上最難降的妖魔是什么鳄橘? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任粤剧,我火速辦了婚禮,結(jié)果婚禮上挥唠,老公的妹妹穿的比我還像新娘抵恋。我一直安慰自己,他們只是感情好宝磨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布弧关。 她就那樣靜靜地躺著盅安,像睡著了一般。 火紅的嫁衣襯著肌膚如雪世囊。 梳的紋絲不亂的頭發(fā)上别瞭,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音株憾,去河邊找鬼蝙寨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嗤瞎,可吹牛的內(nèi)容都是我干的墙歪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼贝奇,長吁一口氣:“原來是場噩夢啊……” “哼虹菲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起掉瞳,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤毕源,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后陕习,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霎褐,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年该镣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瘩欺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拌牲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出歌粥,到底是詐尸還是另有隱情塌忽,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布失驶,位于F島的核電站土居,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏嬉探。R本人自食惡果不足惜擦耀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望涩堤。 院中可真熱鬧眷蜓,春花似錦、人聲如沸胎围。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至汽纤,卻和暖如春上岗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蕴坪。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工肴掷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人背传。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓呆瞻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親续室。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栋烤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 《非銀行支付機(jī)構(gòu)網(wǎng)絡(luò)支付業(yè)務(wù)管理辦法》條款釋義 - 中國支付網(wǎng) - 中國支付行業(yè)第一門戶網(wǎng)站2016年7月1日...
    菜菜苔閱讀 7,569評(píng)論 1 44
  • 1.固定pos機(jī) 優(yōu)點(diǎn)是:1、軟件升級(jí)和維護(hù)比較容易挺狰;2明郭、網(wǎng)絡(luò)撥號(hào)方式,撥號(hào)速度快丰泊;3薯定、POS交易清算比較容易;缺...
    嗝喯唲閱讀 1,159評(píng)論 1 2
  • 愛吃的小胖紙閱讀 296評(píng)論 0 1
  • 夏天年堆,尤其最近雨水多,好像蚊子多了一些盏浇。怎樣不讓蚊子咬变丧,上周在電視上新學(xué)了一招,這幾天試了一下绢掰,還挺靈痒蓬。很簡單,只...
    聞道解惑閱讀 310評(píng)論 0 1