背景
当前Android NFC手机基本都能支持基于Host的卡模拟(HCE),其实现原理上需要读卡端和模拟端互相通过校验特定AID来实现通信,这个特性就让手机模拟的普通HCE卡无法被其他手机上通用的NFC三方APP直接读取到数据,比如NFC手机通过HCE模拟出一张银行卡,只有对该HCE银行卡AID 支持的POS机或特殊定制APP才可以与该HCE 银行卡通信,完成交易。其他三方APP由于不知道该卡AID,都是无法直接与之通信的。
鉴于上面的特殊,这样限制了HCE被普通用户使用。比如我们期望自己手机通过HCE分享出BT,WIFI秘钥,或分享的联系人等信息 能够被其他手机的NFC三方APP直接读取数据。为实现这样的目的,我们就需要基于HCE实现一个Ndef格式的标签卡。因为Ndef格式是NFC联盟定义的一种NFC Data Exchange Format,NFC三方APP基本可以直接读取。
实现HCE模拟Ndef标签卡
1. 首次Android studio创建一个新的HceNdefTag APP,并保证可以正常编译出APK。
2. 在APP内创建NdefHceService.java并继承HostApduService,之后覆写相关抽象方法。
package com.example.hcendeftag;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
public class NdefHceService extends HostApduService {
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
return new byte[0];
}
@Override
public void onDeactivated(int reason) {
}
}
3.实现Ndef主卡模拟服务实现,并在其定义简单的Ndef消息。
package com.example.hcendeftag;
import android.content.ComponentName;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;
import java.util.Arrays;
public class NdefHceService extends HostApduService {
// source: https://github.com/TechBooster/C85-Android-4.4-Sample/blob/master/chapter08/NdefCard/src/com/example/ndefcard/NdefHostApduService.java
public static final ComponentName COMPONENT = new ComponentName("com.example.hcendeftag", NdefHceService.class.getName());
private final static String TAG = "NfcTest_NdefHostApduService";
private static final byte[] SELECT_APPLICATION = {
(byte) 0x00, // CLA - Class - Class of instruction
(byte) 0xA4, // INS - Instruction - Instruction code
(byte) 0x04, // P1 - Parameter 1 - Instruction parameter 1
(byte) 0x00, // P2 - Parameter 2 - Instruction parameter 2
(byte) 0x07, // Lc field - Number of bytes present in the data field of the command
(byte) 0xD2, (byte) 0x76, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01,
(byte) 0x01, // NDEF Tag Application name D2 76 00 00 85 01 01
(byte) 0x00 // Le field - Maximum number of bytes expected in the data field of
// the response to the command
};
private static final byte[] SELECT_CAPABILITY_CONTAINER = {
(byte) 0x00, // CLA - Class - Class of instruction
(byte) 0xa4, // INS - Instruction - Instruction code
(byte) 0x00, // P1 - Parameter 1 - Instruction parameter 1
(byte) 0x0c, // P2 - Parameter 2 - Instruction parameter 2
(byte) 0x02, // Lc field - Number of bytes present in the data field of the command
(byte) 0xe1, (byte) 0x03 // file identifier of the CC file
};
private static final byte[] SELECT_NDEF_FILE = {
(byte) 0x00, // CLA - Class - Class of instruction
(byte) 0xa4, // Instruction byte (INS) for Select command
(byte) 0x00, // Parameter byte (P1), select by identifier
(byte) 0x0c, // Parameter byte (P1), select by identifier
(byte) 0x02, // Lc field - Number of bytes present in the data field of the command
(byte) 0xE1, (byte) 0x04 // file identifier of the NDEF file retrieved from the CC file
};
private final static byte[] CAPABILITY_CONTAINER_FILE = new byte[]{
0x00, 0x0f, // CCLEN
0x20, // Mapping Version
0x00, 0x3b, // Maximum R-APDU data size
0x00, 0x34, // Maximum C-APDU data size
0x04, 0x06, // Tag & Length
(byte) 0xe1, 0x04, // NDEF File Identifier
(byte) 0x00, (byte) 0xff, // Maximum NDEF size, do NOT extend this value
0x00, // NDEF file read access granted
(byte) 0xff, // NDEF File write access denied
};
// Status Word success
private final static byte[] SUCCESS_SW = new byte[]{
(byte) 0x90,
(byte) 0x00,
};
// Status Word failure
private final static byte[] FAILURE_SW = new byte[]{
(byte) 0x6a,
(byte) 0x82,
};
public static String mNdefMessage = "default NDEF-message----test123456";
private byte[] mNdefRecordFile;
private boolean mAppSelected; // true when SELECT_APPLICATION detected
private boolean mCcSelected; // true when SELECT_CAPABILITY_CONTAINER detected
private boolean mNdefSelected; // true when SELECT_NDEF_FILE detected
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate");
mAppSelected = false;
mCcSelected = false;
mNdefSelected = false;
// default NDEF-message
NdefMessage ndefDefaultMessage = getNdefMessage(mNdefMessage);
// the maximum length is 246 so do not extend this value
int nlen = ndefDefaultMessage.getByteArrayLength();
mNdefRecordFile = new byte[nlen + 2];
mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
mNdefRecordFile[1] = (byte) (nlen & 0xff);
System.arraycopy(ndefDefaultMessage.toByteArray(), 0, mNdefRecordFile, 2,
ndefDefaultMessage.getByteArrayLength());
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
// intent contains a text message
if (intent.hasExtra("ndefMessage")) {
String msg = intent.getStringExtra("ndefMessage");
NdefMessage ndefMessage = ndefMessage = getNdefMessage(intent.getStringExtra("ndefMessage"));
;
Log.d(TAG, "onStartCommand msg:" + msg);
if (ndefMessage != null) {
int nlen = ndefMessage.getByteArrayLength();
mNdefRecordFile = new byte[nlen + 2];
mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
mNdefRecordFile[1] = (byte) (nlen & 0xff);
System.arraycopy(ndefMessage.toByteArray(), 0, mNdefRecordFile, 2,
ndefMessage.getByteArrayLength());
}
}
// intent contains an URL
if (intent.hasExtra("ndefUrl")) {
NdefMessage ndefMessage = getNdefUrlMessage(intent.getStringExtra("ndefUrl"));
if (ndefMessage != null) {
int nlen = ndefMessage.getByteArrayLength();
mNdefRecordFile = new byte[nlen + 2];
mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
mNdefRecordFile[1] = (byte) (nlen & 0xff);
System.arraycopy(ndefMessage.toByteArray(), 0, mNdefRecordFile, 2,
ndefMessage.getByteArrayLength());
}
}
}
return super.onStartCommand(intent, flags, startId);
}
private NdefMessage getNdefMessage(String ndefData) {
NdefRecord ndefRecord = NdefRecord.createTextRecord("en", ndefData);
return new NdefMessage(ndefRecord);
}
private NdefMessage getNdefUrlMessage(String ndefData) {
if (ndefData.length() == 0) {
return null;
}
NdefRecord ndefRecord;
ndefRecord = NdefRecord.createUri(ndefData);
return new NdefMessage(ndefRecord);
}
/**
* emulates an NFC Forum Tag Type 4
*/
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
//if (Arrays.equals(SELECT_APP, commandApdu)) {
// check if commandApdu qualifies for SELECT_APPLICATION
if (Arrays.equals(SELECT_APPLICATION, commandApdu)) {
mAppSelected = true;
mCcSelected = false;
mNdefSelected = false;
return SUCCESS_SW;
// check if commandApdu qualifies for SELECT_CAPABILITY_CONTAINER
} else if (mAppSelected && Arrays.equals(SELECT_CAPABILITY_CONTAINER, commandApdu)) {
mCcSelected = true;
mNdefSelected = false;
return SUCCESS_SW;
// check if commandApdu qualifies for SELECT_NDEF_FILE
} else if (mAppSelected && Arrays.equals(SELECT_NDEF_FILE, commandApdu)) {
// NDEF
mCcSelected = false;
mNdefSelected = true;
return SUCCESS_SW;
// check if commandApdu qualifies for // READ_BINARY
} else if (commandApdu[0] == (byte) 0x00 && commandApdu[1] == (byte) 0xb0) {
// READ_BINARY
// get the offset an le (length) data
//System.out.println("** " + Utils.bytesToHex(commandApdu) + " in else if
// (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xb0) {");
int offset = (0x00ff & commandApdu[2]) * 256 + (0x00ff & commandApdu[3]);
int le = 0x00ff & commandApdu[4];
byte[] responseApdu = new byte[le + SUCCESS_SW.length];
if (mCcSelected && offset == 0 && le == CAPABILITY_CONTAINER_FILE.length) {
System.arraycopy(CAPABILITY_CONTAINER_FILE, offset, responseApdu, 0, le);
System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.length);
return responseApdu;
} else if (mNdefSelected) {
if (offset + le <= mNdefRecordFile.length) {
System.arraycopy(mNdefRecordFile, offset, responseApdu, 0, le);
System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.length);
return responseApdu;
}
}
}
// The tag should return different errors for different reasons
// this emulation just returns the general error message
return FAILURE_SW;
}
/**
* onDeactivated is called when reading ends
* reset the status boolean values
*/
@Override
public void onDeactivated(int reason) {
mAppSelected = false;
mCcSelected = false;
mNdefSelected = false;
}
}
4.1 AndroidManifest.xml为NdefHceService增加service注册,并添加meta-data ndef_apduservice.xml,以设置AID让nfc-service可以转发读卡请求给该HCE服务。
4.2 增加NFC permission及设置NdefHceServce为disable状态,当需要激活时再使能该服务。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.NFC" />
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="true" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HceNdefTag"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.HceNdefTag">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".NdefHceService"
android:enabled="false"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<!-- category required!!! this was not included in official android HCE documentation -->
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/ndef_apduservice" />
</service>
</application>
</manifest>
5. ndef_apduservice.xml中设置读卡是亮屏读卡还是灭屏读卡,及设置aid, 这里aid D2760000850101是Android上ndef读卡通用的aid。
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:requireDeviceScreenOn="true"
android:requireDeviceUnlock="false">
<aid-group
android:category="other">
<!-- Sample for the demo application -->
<aid-filter android:name="D2760000850101" />
</aid-group>
</host-apdu-service>
6. MainActivity.java增加使能HceNdefService的逻辑。至此完成基本功能开发。
package com.example.hcendeftag;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.example.hcendeftag.databinding.ActivityMainBinding;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
enableComponent(getPackageManager(), NdefHceService.COMPONENT);
Toast.makeText(MainActivity.this, "Enable NdefService", Toast.LENGTH_SHORT).show();
}
});
}
public static void enableComponent(PackageManager pm, ComponentName component) {
pm.setComponentEnabledSetting(
component,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
}
7.编译APK安装后,点击按钮使能NdefHceService并用另一台NFC 手机APP去读卡。
7.1一个手机安装后如下图点击APP按钮去使能Ndef服务。
7.2 另一个支持NFC功能的手机安装NFC tool pro APP,背靠背读取另一台手机的ndef数据。
demo 代码及apk见下面链接
链接: https://pan.baidu.com/s/14Hw46nhzBz8ll9q-0gFpaQ?pwd=4cm5 提取码: 4cm5