Android HCE协议卡模拟求助:NFC读卡器无法读取模拟字符串
Android HCE字符串模拟与读卡器适配方案
问题核心
需要实现Android HCE模拟自定义字符串,且读卡器能读取该字符串。目前HCE服务已正常运行,但读卡器端代码仅监听NDEF类型标签,无法与HCE模拟的ISO-DEP卡通信。
HCE端:模拟自定义字符串的正确实现
初始HCE代码问题在于APDU命令处理逻辑错误,读卡器发送的是标准SELECT APDU而非直接以自定义AID开头的命令。以下是能直接返回自定义字符串的HCE服务代码:
1. HostApduService实现
public class MyHostApduService extends HostApduService { private static final String TAG = "MyHostApduService"; // 标准NFC论坛NDEF应用AID(提升兼容性) private static final String NDEF_AID = "D2760000850101"; // 要模拟的自定义字符串 private static final String CUSTOM_STRING = "Hello HCE!"; // APDU响应成功状态字 private static final byte[] STATUS_SUCCESS = {(byte)0x90, (byte)0x00}; // APDU响应失败状态字 private static final byte[] STATUS_FAILED = {(byte)0x6A, (byte)0x81}; @Override public void onCreate() { super.onCreate(); Log.d(TAG, "HCE Service Started"); } @Override public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { String commandHex = bytesToHex(commandApdu); Log.d(TAG, "Received APDU: " + commandHex); // 处理SELECT APDU命令(选择目标AID) if (commandHex.startsWith("00A40400")) { if (commandHex.contains(NDEF_AID)) { return STATUS_SUCCESS; } } // 处理读取数据的APDU命令(00B00000XX,XX为请求长度) else if (commandHex.startsWith("00B00000")) { int requestLength = Integer.parseInt(commandHex.substring(8, 10), 16); byte[] stringBytes = CUSTOM_STRING.getBytes(java.nio.charset.StandardCharsets.UTF_8); // 返回字符串数据+成功状态字 byte[] response = new byte[Math.min(requestLength, stringBytes.length) + 2]; System.arraycopy(stringBytes, 0, response, 0, Math.min(requestLength, stringBytes.length)); System.arraycopy(STATUS_SUCCESS, 0, response, Math.min(requestLength, stringBytes.length), 2); return response; } return STATUS_FAILED; } @Override public void onDeactivated(int reason) { Log.d(TAG, "HCE Service Deactivated, Reason: " + reason); } private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X", b)); } return sb.toString(); } }
2. APDU服务配置文件(apduservice.xml)
确保AID与代码中一致:
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:requireDeviceUnlock="false"> <aid-group android:category="other"> <aid-filter android:name="D2760000850101"/> </aid-group> </host-apdu-service>
读卡器端:修复代码以读取HCE模拟的字符串
现有读卡器代码仅监听NDEF类型标签,但HCE模拟的是ISO-DEP(APDU协议)卡,需修改为监听ISO-DEP技术并发送APDU命令读取数据:
修复后的读卡器Activity代码(Kotlin)
import android.app.PendingIntent import android.app.PendingIntent.FLAG_MUTABLE import android.content.Intent import android.nfc.NfcAdapter import android.nfc.tech.IsoDep import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import android.widget.TextView import java.io.IOException class MainActivity : AppCompatActivity() { private var mNfcAdapter: NfcAdapter? = null private var mPendingIntent: PendingIntent? = null private lateinit var mTvView: TextView // 对应HCE端的NDEF AID private val NDEF_AID = "D2760000850101" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mTvView = findViewById(R.id.nfc_activity_tv_info) mNfcAdapter = NfcAdapter.getDefaultAdapter(this) if (checkNFCEnable()) { mPendingIntent = PendingIntent.getActivity( this, 0, Intent(this, this.javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), FLAG_MUTABLE ) } } override fun onResume() { super.onResume() // 监听ISO-DEP技术的NFC设备 val techLists = arrayOf(arrayOf(IsoDep::class.java.name)) mNfcAdapter?.enableForegroundDispatch(this, mPendingIntent, null, techLists) } override fun onPause() { super.onPause() mNfcAdapter?.disableForegroundDispatch(this) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) ?: return val isoDep = IsoDep.get(tag) ?: return try { isoDep.connect() // 发送SELECT APDU命令选择HCE的AID val selectApdu = buildSelectApdu(NDEF_AID) val selectResponse = isoDep.transceive(selectApdu) if (isSuccessResponse(selectResponse)) { // 发送读取数据的APDU命令(请求20字节长度) val readApdu = byteArrayOf(0x00, 0xB0, 0x00, 0x00, 0x14) val readResponse = isoDep.transceive(readApdu) if (isSuccessResponse(readResponse)) { // 提取数据(去掉最后的2字节状态字) val dataBytes = readResponse.copyOf(readResponse.size - 2) val customString = String(dataBytes, Charsets.UTF_8) mTvView.text = "读取到HCE字符串: $customString" } else { mTvView.text = "读取数据失败" } } else { mTvView.text = "选择AID失败" } } catch (e: IOException) { Log.e("NFC Reader", "通信失败", e) mTvView.text = "NFC通信异常" } finally { try { isoDep.close() } catch (e: IOException) { Log.e("NFC Reader", "关闭连接失败", e) } } } // 构建SELECT APDU命令 private fun buildSelectApdu(aid: String): ByteArray { val aidBytes = hexStringToByteArray(aid) val apdu = ByteArray(5 + aidBytes.size) apdu[0] = 0x00.toByte() // CLA apdu[1] = 0xA4.toByte() // INS apdu[2] = 0x04.toByte() // P1 apdu[3] = 0x00.toByte() // P2 apdu[4] = aidBytes.size.toByte() // Lc System.arraycopy(aidBytes, 0, apdu, 5, aidBytes.size) return apdu } // 检查APDU响应是否成功 private fun isSuccessResponse(response: ByteArray): Boolean { return response.size >= 2 && response[response.size - 2] == 0x90.toByte() && response[response.size - 1] == 0x00.toByte() } // 十六进制字符串转字节数组 private fun hexStringToByteArray(hex: String): ByteArray { val len = hex.length val data = ByteArray(len / 2) var i = 0 while (i < len) { data[i / 2] = ((Character.digit(hex[i], 16) shl 4) + Character.digit(hex[i + 1], 16)).toByte() i += 2 } return data } private fun checkNFCEnable(): Boolean { return if (mNfcAdapter == null) { mTvView.text = "设备不支持NFC" false } else if (!mNfcAdapter!!.isEnabled) { mTvView.text = "请开启NFC功能" false } else { true } } }
关键说明
- HCE端:使用标准NDEF AID可提升与各类读卡器的兼容性,同时处理标准的SELECT和READ APDU命令,返回自定义字符串数据。
- 读卡器端:必须通过
IsoDep技术与HCE设备通信,发送APDU命令完成选择AID、读取数据的流程,不能直接监听NDEF事件(HCE默认不模拟NDEF标签,除非额外实现NDEF逻辑)。
内容的提问来源于stack exchange,提问作者IvanStrasni
相关产品推荐
相关产品推荐

