美文网首页优秀案例Android 开发技术分享
Android NFC读卡器,仿真卡流程学习

Android NFC读卡器,仿真卡流程学习

作者: 英勇青铜5 | 来源:发表于2017-06-22 11:29 被阅读977次

    学习资料:

    感谢laocaixw大佬,找了半天NFC相关开发的博客,终于找到一个简单明了的,就把代码抄了下来,以便之后再看

    一台支持NFCAndroid手机,可以作为读卡器来读取一张银行卡或者公交卡,也可以模拟成一张卡来进行刷卡消费,也就是我所说的仿真卡,属于HCE相关开发

    公司现在的项目属于HCE业务项目,要模拟银行卡,也提前了解些NFC相关的东西

    本篇中的案例,需要两个支持NFC的手机才可以演示,一个作为读卡器,一个模拟卡实现仿真卡,当读卡器仿真卡贴在一起后,读卡器会先发送一个指令给仿真卡仿真卡验证指令后,就可以返回数据给读卡器

    案例中获取卡号的流程只是简单演示用的,随意返回了一个16位卡号。实际获取卡号的流程比这复杂的多,需要发送多个指令才能拿到卡的有效信息


    1.读卡器代码

    权限

    <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使用的是栈顶复用模式,activity启动自身,会执行onNewIntent()方法

    屏幕锁死了竖屏,以避免手机在横竖屏切换时,导致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;
    
        // 卡片返回来的正确信号
        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 一般设置为: SingleTop模式 ,并且锁死竖屏,以避免屏幕旋转Intent丢失
            Intent intent = new Intent(NFCActivity.this, NFCActivity.class);
    
            // 私有的请求码
            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};
    
            // 只针对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与卡进行连接
                isoDep.connect();
    
                final String AID = "F123466666";
                //转换指令为byte[]
                byte[] command = buildSelectApdu(AID);
    
                // 发送指令
                byte[] result = isoDep.transceive(command);
    
                // 截取响应数据
                int resultLength = result.length;
                byte[] statusWord = {result[resultLength - 2], result[resultLength - 1]};
                byte[] payload = Arrays.copyOf(result, resultLength - 2);
    
                // 检验响应数据
                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() 执行
         * enableForegroundDispatch()只能在onResume() 方法中,否则会报:
         * 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);
        }
    
        /**
         * 关闭检测
         */
        @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 = "手机不支付NFC功能";
                toast(info);
                return;
            }
            if (!mNfcAdapter.isEnabled()) {
                String info = "手机NFC功能没有打开";
                toast(info);
                Intent setNfc = new Intent(Settings.ACTION_NFC_SETTINGS);
                startActivity(setNfc);
            } else {
                String info = "手机NFC功能正常";
                toast(info);
            }
        }
    
        private byte[] stringToBytes(String s) {
            int len = s.length();
            if (len % 2 == 1) {
                throw new IllegalArgumentException("指令字符串长度必须为偶数 !!!");
            }
            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()分别就是,一旦在onResume()中检测到卡,会在onNewIntent()方法中执行读卡信息


    2. 仿真卡代码

    权限:

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

    配置:

    <!--仿真卡服务-->
    <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下建立一个xml文件夹,创建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"程序运行,手机亮屏不解锁的情况下,服务可以启动

    android:name="F123466666"这一行很关键

    读卡器想要识别一个卡,肯定要有一个识别的标记,这个就是指定的识别标记,需要和代码中发送的指令进行统一。这个是我瞎写的, 必须偶数位


    2.1 CardService代码

    @TargetApi(Build.VERSION_CODES.KITKAT)
    public class CardService extends HostApduService {
        // 正确信号
        private byte[] SELECT_OK = hexStringToByteArray("1000");
    
        // 错误信号
        private byte[] UNKNOWN_ERROR = hexStringToByteArray("0000");
    
        /**
         * 接收到 NFC 读卡器发送的应用协议数据单元 (APDU) 调用
         * 注意:此方法回调在UI线程,若进行联网操作时,需开子线程
         * 并先返回null,当子线程有数据结果后,再进行回调返回处理
         */
        @Override
        public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
            final String AID = "F123466666";
    
            // 将指令转换成 byte[]
            byte[] selectAPDU = buildSelectApdu(AID);
    
            // 判断是否和读卡器发来的数据相同
            if (Arrays.equals(selectAPDU, commandApdu)) {
                // 直接模拟返回16位卡号
                String account = "6222222200000001";
    
                // 获取卡号 byte[]
                byte[] accountBytes = account.getBytes();
    
                // 处理欲返回的响应数据
                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("指令字符串长度必须为偶数 !!!");
            }
            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;
        }
    }
    

    简易的一个流程就是这样,坑还很多,之后项目实际开发完成后,再来补充下实际开发中遇到的坑


    2.1 一些已经知道的浅坑

    项目中开发的需求是:当使用 App 仿真卡与 POS机靠近后,要求弹出卡面,指纹验证后,进行交易

    1. 写上面的代码学习时,身边没有POS机,也不清楚具体的指令,就使用了两个手机来学习,但手机还是和POS机硬件有些差别的,和手机一样,POS机厂商也会对自己的POS机做一些有别与其他品牌的优化之类的

    2. POS机发来一个指令后,当不能立即响应指令时,仿真卡在processCommandApdu ()方法可以先返回NULL的。例如,我们项目的一个需求,仿真卡一接到PPSE指令时,在返回响应指令前,需要手机端先进行指纹验证时,就可以先返回NULL,在经过指纹验证之后,再使用sendResponeApdu()方法再来发送响应指令。需要注意的是,不同的POS机,等待响应指令的时间可能不同


    3. 最后

    最近接触到了一些银行POS业务,被POS机交易需要用到8583报文折磨到吐,感叹JSON真方便

    有错误,请指出

    共勉 :)

    相关文章

      网友评论

      • 68f107f65b8e:奇怪,我怎么就没成功。提示“读取卡信息失败”
        两个手机触碰后,还要再各自点下确认?
        英勇青铜5:@kerros 那你排查下代码吧:smile:
        68f107f65b8e:@英勇青铜5 完全抄过来的。
        英勇青铜5:@kerros 不需要啊。是不是你手机系统的问题,不同的手机可能会有些不一样,你看看是不是我demo 里代码有问题
      • walker113:请问CardService 怎么启动的?startService?
        68f107f65b8e:层主,你启动成功过?我怎么没成功过
        walker113:@英勇青铜5 好的,感谢
        英勇青铜5:CardService extends HostApduService,HostApduService是个系统服务,我了解到的是4.4以上支持nfc的手机,这个服务会自启动的。
      • Q大疯zi:不错我测试了,可以传递数据!
        Q大疯zi:@英勇青铜5 我在看你的博客,有用的我都学习一下,嘿嘿
        英勇青铜5: @Q大疯zi 😀😀😀我现在对nfc还是一脸懵逼

      本文标题:Android NFC读卡器,仿真卡流程学习

      本文链接:https://www.haomeiwen.com/subject/qdzxcxtx.html