学习资料:
感谢laocaixw
大佬,找了半天NFC
相关开发的博客,终于找到一个简单明了的,就把代码抄了下来,以便之后再看
一台支持NFC
的Android
手机,可以作为读卡器
来读取一张银行卡或者公交卡,也可以模拟成一张卡来进行刷卡消费,也就是我所说的仿真卡
,属于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
机靠近后,要求弹出卡面,指纹验证后,进行交易
写上面的代码学习时,身边没有
POS
机,也不清楚具体的指令,就使用了两个手机来学习,但手机还是和POS
机硬件有些差别的,和手机一样,POS
机厂商也会对自己的POS
机做一些有别与其他品牌的优化之类的当
POS
机发来一个指令后,当不能立即响应指令时,仿真卡在processCommandApdu ()
方法可以先返回NULL
的。例如,我们项目的一个需求,仿真卡一接到PPSE
指令时,在返回响应指令前,需要手机端先进行指纹验证时,就可以先返回NULL
,在经过指纹验证之后,再使用sendResponeApdu()
方法再来发送响应指令。需要注意的是,不同的POS
机,等待响应指令的时间可能不同
3. 最后
最近接触到了一些银行POS
业务,被POS
机交易需要用到8583
报文折磨到吐,感叹JSON
真方便
有错误,请指出
共勉 :)