學(xué)習(xí)資料:
感謝laocaixw
大佬,找了半天NFC
相關(guān)開發(fā)的博客友鼻,終于找到一個(gè)簡單明了的傻昙,就把代碼抄了下來,以便之后再看
一臺(tái)支持NFC
的Android
手機(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)行交易
寫上面的代碼學(xué)習(xí)時(shí),身邊沒有
POS
機(jī)蕊玷,也不清楚具體的指令邮利,就使用了兩個(gè)手機(jī)來學(xué)習(xí),但手機(jī)還是和POS
機(jī)硬件有些差別的垃帅,和手機(jī)一樣延届,POS
機(jī)廠商也會(huì)對(duì)自己的POS
機(jī)做一些有別與其他品牌的優(yōu)化之類的當(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)指出
共勉 :)