You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
        }
    }
}

关键说明

  1. HCE端:使用标准NDEF AID可提升与各类读卡器的兼容性,同时处理标准的SELECT和READ APDU命令,返回自定义字符串数据。
  2. 读卡器端:必须通过IsoDep技术与HCE设备通信,发送APDU命令完成选择AID、读取数据的流程,不能直接监听NDEF事件(HCE默认不模拟NDEF标签,除非额外实现NDEF逻辑)。

内容的提问来源于stack exchange,提问作者IvanStrasni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 21:42:01