NFC开发系列-第一篇:NFC开发基础与实战入门

NFC开发系列 - 第一篇:NFC开发基础与实战入门

核心要点

  1. NFC开发流程:权限配置 → 硬件检测 → Intent过滤 → 前台分发 → Tag处理
  2. NDEF标签:标准化数据格式,适合简单应用场景
  3. MIFARE Classic:加密存储卡,需要扇区认证,适合门禁、会员卡等场景
  4. 最佳实践:异步处理、异常安全、用户体验优化

参考资料

一、NFC技术基础

1.1 NFC技术原理

NFC(Near Field Communication,近场通信)是一种短距离高频无线通信技术,工作频率为13.56MHz,通信距离通常在4cm以内,基于ISO/IEC 14443标准。

三大工作模式:

  • 读卡器模式(Reader/Writer Mode):手机主动读取NFC标签
  • 卡模拟模式(Card Emulation Mode):手机模拟成NFC卡片
  • 点对点模式(P2P Mode):两个NFC设备直接通信

核心技术特点:

  • 通信距离极短(4cm以内),天然防窃听
  • 连接建立速度快(<0.1秒)
  • 支持加密和安全认证
  • 低功耗设计,适合移动设备

1.2 常见NFC卡片分类

NDEF标签(NFC Data Exchange Format)
  • 特点:支持标准化数据格式,易于读写
  • 常见型号:NTAG213/215/216
  • 应用场景:产品标签、智能海报、URL分享
  • 存储容量:144字节~888字节
MIFARE Classic卡
  • 特点:加密存储卡,需扇区认证后访问
  • 数据结构 :
    • 1K版本:16个扇区,每扇区4块,每块16字节
    • 每个扇区最后一块为控制块(存储密钥和访问权限)
  • 常见型号:M1卡(S50)、S70
  • 应用场景:门禁卡、公交卡、会员卡
CPU卡(智能卡)
  • 特点:带微处理器,支持复杂加密算法(3DES、AES)
  • 安全级别:最高(金融级安全)
  • 常见应用:金融IC卡、社保卡、身份证
  • 开发难度:需要APDU指令操作

二、NFC开发标准流程

2.1 权限与硬件声明

在AndroidManifest.xml中添加必要的配置:

xml 复制代码
<!-- NFC核心权限 -->
<uses-permission android:name="android.permission.NFC" />

<!-- 前台服务权限(Android 9.0+后台NFC需要) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- 开机自启权限(保活机制需要) -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<!-- NFC硬件特性声明(required=true表示必须具备NFC硬件) -->
<uses-feature
    android:name="android.hardware.nfc"
    android:required="true" />

权限说明:

  • NFC权限:读取NFC标签必备
  • FOREGROUND_SERVICE:Android 9.0后,后台NFC服务需要前台通知
  • RECEIVE_BOOT_COMPLETED:实现开机自启保活机制

2.2 NFC适配检测

创建NFC能力检测工具类:

java 复制代码
public class NfcChecker {
    private static final String TAG = "NfcChecker";

    /**
     * 检查设备是否支持NFC
     */
    public static boolean isNfcSupported(Context context) {
        NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(context);
        if (nfcAdapter == null) {
            Log.w(TAG, "设备不支持NFC功能");
            return false;
        }
        return true;
    }

    /**
     * 检查NFC是否已开启
     */
    public static boolean isNfcEnabled(Context context) {
        NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(context);
        return nfcAdapter != null && nfcAdapter.isEnabled();
    }

    /**
     * 引导用户开启NFC设置
     */
    public static void showNfcSettings(Activity activity) {
        Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
        activity.startActivity(intent);
    }

    /**
     * 完整的NFC检查流程
     */
    public static boolean checkNfcAvailability(Activity activity) {
        if (!isNfcSupported(activity)) {
            Toast.makeText(activity, "设备不支持NFC功能", Toast.LENGTH_SHORT).show();
            return false;
        }

        if (!isNfcEnabled(activity)) {
            new AlertDialog.Builder(activity)
                .setTitle("NFC未开启")
                .setMessage("请开启NFC功能后使用")
                .setPositiveButton("去开启", (dialog, which) -> showNfcSettings(activity))
                .setNegativeButton("取消", null)
                .show();
            return false;
        }

        return true;
    }
}

2.3 NFC Intent过滤器配置

Android系统提供三种NFC Intent分发优先级(按优先级从高到低):

xml 复制代码
<!-- 1. NDEF数据发现(最高优先级) -->
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />  <!-- 指定MIME类型 -->
</intent-filter>

<!-- 2. 技术发现(中优先级) -->
<intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
    android:name="android.nfc.action.TECH_DISCOVERED"
    android:resource="@xml/nfc_tech_filter" />

<!-- 3. 标签发现(兜底,最低优先级) -->
<intent-filter>
    <action android:name="android.nfc.action.TAG_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

技术过滤器配置 (res/xml/nfc_tech_filter.xml):

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <!-- NDEF格式标签 -->
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
    </tech-list>

    <!-- 可格式化为NDEF的标签 -->
    <tech-list>
        <tech>android.nfc.tech.NdefFormatable</tech>
    </tech-list>

    <!-- MIFARE Classic卡 -->
    <tech-list>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>

    <!-- CPU卡(ISO-DEP) -->
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
    </tech-list>
</resources>

2.4 前台分发系统(Foreground Dispatch System)

前台分发确保应用在前台时优先接收NFC事件:

java 复制代码
/**
 * NFC前台分发管理器
 */
public class NfcForegroundManager {
    private static final String TAG = "NfcForegroundManager";

    private NfcAdapter nfcAdapter;
    private Activity activity;
    private PendingIntent pendingIntent;

    public NfcForegroundManager(Activity activity) {
        this.activity = activity;
        this.nfcAdapter = NfcAdapter.getDefaultAdapter(activity);

        // 创建PendingIntent,指向当前Activity
        Intent intent = new Intent(activity, activity.getClass())
                .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

        // Android 12+ 需要明确指定FLAG_MUTABLE或FLAG_IMMUTABLE
        this.pendingIntent = PendingIntent.getActivity(
            activity,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
        );
    }

    /**
     * 启用前台分发(在onResume中调用)
     */
    public void enableForegroundDispatch() {
        if (nfcAdapter != null) {
            // 设置Intent过滤器
            IntentFilter[] filters = new IntentFilter[]{
                new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED),
                new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
                new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
            };

            try {
                filters[0].addDataType("*/*");
            } catch (MalformedMimeTypeException e) {
                Log.e(TAG, "MIME类型格式错误", e);
            }

            nfcAdapter.enableForegroundDispatch(activity, pendingIntent, filters, null);
            Log.d(TAG, "前台分发已启用");
        }
    }

    /**
     * 禁用前台分发(在onPause中调用)
     */
    public void disableForegroundDispatch() {
        if (nfcAdapter != null) {
            nfcAdapter.disableForegroundDispatch(activity);
            Log.d(TAG, "前台分发已禁用");
        }
    }
}

2.5 Activity生命周期集成

标准的NFC Activity实现模式:

java 复制代码
public class BaseNfcActivity extends AppCompatActivity {
    private static final String TAG = "BaseNfcActivity";
    private NfcForegroundManager nfcForegroundManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 初始化NFC前台管理器
        nfcForegroundManager = new NfcForegroundManager(this);

        // 检查NFC可用性
        if (!NfcChecker.checkNfcAvailability(this)) {
            // 设备不支持或未开启NFC
            return;
        }

        // 处理启动Intent中的NFC数据
        handleNfcIntent(getIntent());
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 启用前台分发
        nfcForegroundManager.enableForegroundDispatch();
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 禁用前台分发
        nfcForegroundManager.disableForegroundDispatch();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        // 处理新的NFC Intent
        handleNfcIntent(intent);
    }

    /**
     * 处理NFC Intent
     */
    private void handleNfcIntent(Intent intent) {
        String action = intent.getAction();
        Log.d(TAG, "收到NFC Intent: " + action);

        if (isNfcAction(action)) {
            Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            if (tag != null) {
                onNfcTagDiscovered(tag);
            }
        }
    }

    /**
     * 判断是否为NFC相关Action
     */
    private boolean isNfcAction(String action) {
        return action != null && (
            NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) ||
            NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
            NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
        );
    }

    /**
     * 子类重写此方法处理NFC标签
     */
    protected void onNfcTagDiscovered(Tag tag) {
        // 打印标签基本信息
        Log.d(TAG, "标签UID: " + bytesToHex(tag.getId()));
        Log.d(TAG, "支持的技术: " + Arrays.toString(tag.getTechList()));
    }

    /**
     * 字节数组转十六进制字符串
     */
    protected String bytesToHex(byte[] bytes) {
        if (bytes == null) return "";
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X", b));
        }
        return sb.toString();
    }
}

三、实战案例一:NDEF标签读取

3.1 NDEF数据格式详解

NDEF(NFC Data Exchange Format)是NFC Forum定义的标准数据格式:

NDEF消息结构:

python 复制代码
NdefMessage
├── NdefRecord 1
│   ├── TNF(类型名称格式)
│   ├── Type(记录类型)
│   ├── ID(记录标识符,可选)
│   └── Payload(有效载荷)
├── NdefRecord 2
└── NdefRecord N

常见TNF类型:

  • TNF_WELL_KNOWN (0x01):标准记录(如文本、URI)
  • TNF_MIME_MEDIA (0x02):MIME类型数据
  • TNF_ABSOLUTE_URI (0x03):绝对URI
  • TNF_EXTERNAL_TYPE (0x04):自定义类型

常见RTD(Record Type Definition):

  • RTD_TEXT:文本记录
  • RTD_URI:URI记录
  • RTD_SMART_POSTER:智能海报

3.2 NDEF读取工具类实现

java 复制代码
public class NdefReader {
    private static final String TAG = "NdefReader";

    /**
     * 读取NDEF标签内容
     */
    public static String readNdefTag(Tag tag) {
        Ndef ndef = Ndef.get(tag);
        if (ndef == null) {
            Log.w(TAG, "不是NDEF格式标签");
            return null;
        }

        try {
            // 连接标签
            ndef.connect();
            Log.d(TAG, "NDEF标签已连接");

            // 检查标签是否可写
            if (!ndef.isWritable()) {
                Log.w(TAG, "标签为只读模式");
            }

            // 读取NDEF消息
            NdefMessage ndefMessage = ndef.getNdefMessage();
            if (ndefMessage == null) {
                Log.w(TAG, "标签为空或格式不正确");
                return null;
            }

            // 解析NDEF消息
            String result = parseNdefMessage(ndefMessage);

            // 打印标签信息
            Log.d(TAG, "标签容量: " + ndef.getMaxSize() + " 字节");
            Log.d(TAG, "已使用容量: " + ndefMessage.toByteArray().length + " 字节");

            return result;

        } catch (IOException e) {
            Log.e(TAG, "读取NDEF标签失败", e);
            return null;
        } catch (FormatException e) {
            Log.e(TAG, "NDEF格式错误", e);
            return null;
        } finally {
            try {
                ndef.close();
                Log.d(TAG, "NDEF连接已关闭");
            } catch (IOException e) {
                Log.e(TAG, "关闭连接失败", e);
            }
        }
    }

    /**
     * 解析NDEF消息
     */
    private static String parseNdefMessage(NdefMessage message) {
        NdefRecord[] records = message.getRecords();
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < records.length; i++) {
            String content = parseNdefRecord(records[i]);
            if (content != null) {
                if (result.length() > 0) {
                    result.append("\n---\n");
                }
                result.append("记录 ").append(i + 1).append(":\n");
                result.append(content);
            }
        }

        return result.length() > 0 ? result.toString() : null;
    }

    /**
     * 解析单个NDEF记录
     */
    private static String parseNdefRecord(NdefRecord record) {
        short tnf = record.getTnf();

        // TNF_WELL_KNOWN:标准记录类型
        if (tnf == NdefRecord.TNF_WELL_KNOWN) {
            if (Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)) {
                return "文本:" + parseTextRecord(record);
            } else if (Arrays.equals(record.getType(), NdefRecord.RTD_URI)) {
                return "URI:" + parseUriRecord(record);
            }
        }

        // TNF_MIME_MEDIA:MIME类型
        else if (tnf == NdefRecord.TNF_MIME_MEDIA) {
            String mimeType = new String(record.getType());
            return "MIME类型:" + mimeType + "\n数据长度:" + record.getPayload().length + " 字节";
        }

        // TNF_ABSOLUTE_URI:绝对URI
        else if (tnf == NdefRecord.TNF_ABSOLUTE_URI) {
            return "绝对URI:" + new String(record.getPayload());
        }

        return "未知类型(TNF=" + tnf + ")";
    }

    /**
     * 解析文本记录
     *
     * Payload格式:
     * Byte 0: 状态字节(最高位=编码,低5位=语言代码长度)
     * Byte 1-N: 语言代码(如"en")
     * Byte N+1-End: 文本内容
     */
    private static String parseTextRecord(NdefRecord record) {
        byte[] payload = record.getPayload();

        // 第一个字节:状态字节
        int languageCodeLength = payload[0] & 0x1F; // 低5位为语言代码长度
        boolean isUtf16 = (payload[0] & 0x80) != 0;  // 最高位为编码标志

        // 提取语言代码
        String languageCode = new String(payload, 1, languageCodeLength);

        // 提取文本内容
        String text;
        try {
            text = new String(
                payload,
                languageCodeLength + 1,
                payload.length - languageCodeLength - 1,
                isUtf16 ? "UTF-16" : "UTF-8"
            );
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "文本编码解析失败", e);
            text = "编码错误";
        }

        return text + " [语言:" + languageCode + "]";
    }

    /**
     * 解析URI记录
     *
     * Payload格式:
     * Byte 0: URI标识符代码(0x00-0x23)
     * Byte 1-End: URI剩余部分
     */
    private static String parseUriRecord(NdefRecord record) {
        byte[] payload = record.getPayload();

        // 第一个字节是URI标识符代码
        int uriIdentifier = payload[0] & 0xFF;

        // 根据标识符代码获取URI前缀
        String prefix = getUriPrefix(uriIdentifier);

        // 提取URI剩余部分
        String uri = new String(payload, 1, payload.length - 1);

        return prefix + uri;
    }

    /**
     * URI前缀映射表(根据NFC Forum URI Record Type Definition)
     */
    private static String getUriPrefix(int identifier) {
        switch (identifier) {
            case 0x00: return "";
            case 0x01: return "http://www.";
            case 0x02: return "https://www.";
            case 0x03: return "http://";
            case 0x04: return "https://";
            case 0x05: return "tel:";
            case 0x06: return "mailto:";
            case 0x07: return "ftp://anonymous:anonymous@";
            case 0x08: return "ftp://ftp.";
            case 0x09: return "ftps://";
            case 0x0A: return "sftp://";
            case 0x0B: return "smb://";
            case 0x0C: return "nfs://";
            case 0x0D: return "ftp://";
            case 0x0E: return "dav://";
            case 0x0F: return "news:";
            case 0x10: return "telnet://";
            case 0x11: return "imap:";
            case 0x12: return "rtsp://";
            case 0x13: return "urn:";
            case 0x14: return "pop:";
            case 0x15: return "sip:";
            case 0x16: return "sips:";
            case 0x17: return "tftp:";
            case 0x18: return "btspp://";
            case 0x19: return "btl2cap://";
            case 0x1A: return "btgoep://";
            case 0x1B: return "tcpobex://";
            case 0x1C: return "irdaobex://";
            case 0x1D: return "file://";
            case 0x1E: return "urn:epc:id:";
            case 0x1F: return "urn:epc:tag:";
            case 0x20: return "urn:epc:pat:";
            case 0x21: return "urn:epc:raw:";
            case 0x22: return "urn:epc:";
            case 0x23: return "urn:nfc:";
            default: return "";
        }
    }

    /**
     * 检查标签是否为NDEF格式
     */
    public static boolean isNdefFormatted(Tag tag) {
        return Ndef.get(tag) != null;
    }

    /**
     * 获取NDEF标签详细信息
     */
    public static String getNdefInfo(Tag tag) {
        Ndef ndef = Ndef.get(tag);
        if (ndef == null) {
            return "非NDEF格式标签";
        }

        StringBuilder info = new StringBuilder();
        info.append("NDEF标签信息:\n");
        info.append("类型:").append(ndef.getType()).append("\n");
        info.append("最大容量:").append(ndef.getMaxSize()).append(" 字节\n");
        info.append("可写:").append(ndef.isWritable() ? "是" : "否").append("\n");

        try {
            ndef.connect();
            NdefMessage msg = ndef.getNdefMessage();
            if (msg != null) {
                info.append("已使用:").append(msg.toByteArray().length).append(" 字节\n");
                info.append("记录数量:").append(msg.getRecords().length);
            }
            ndef.close();
        } catch (Exception e) {
            Log.e(TAG, "获取NDEF信息失败", e);
        }

        return info.toString();
    }
}

3.3 NDEF读取Activity完整实现

java 复制代码
public class NdefActivity extends BaseNfcActivity {
    private static final String TAG = "NdefActivity";
    private TextView tvContent;
    private TextView tvInfo;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ndef);

        initViews();
    }

    private void initViews() {
        tvContent = findViewById(R.id.tvContent);
        tvInfo = findViewById(R.id.tvInfo);
        progressBar = findViewById(R.id.progressBar);

        tvContent.setText("请将NDEF标签靠近设备...");
    }

    @Override
    protected void onNfcTagDiscovered(Tag tag) {
        super.onNfcTagDiscovered(tag);

        Log.d(TAG, "检测到NDEF标签,UID: " + bytesToHex(tag.getId()));

        // 检查是否为NDEF格式
        if (!NdefReader.isNdefFormatted(tag)) {
            runOnUiThread(() -> {
                tvContent.setText("错误:不是NDEF格式的标签");
                Toast.makeText(this, "请使用NDEF格式标签", Toast.LENGTH_SHORT).show();
            });
            return;
        }

        // 显示标签信息
        String info = NdefReader.getNdefInfo(tag);
        runOnUiThread(() -> tvInfo.setText(info));

        // 异步读取NDEF内容
        showLoading(true);
        new Thread(() -> {
            String content = NdefReader.readNdefTag(tag);

            runOnUiThread(() -> {
                showLoading(false);
                if (content != null) {
                    tvContent.setText("NDEF内容:\n\n" + content);
                    Toast.makeText(this, "NDEF标签读取成功", Toast.LENGTH_SHORT).show();
                } else {
                    tvContent.setText("读取失败:标签为空或格式错误");
                    Toast.makeText(this, "NDEF标签读取失败", Toast.LENGTH_SHORT).show();
                }
            });
        }).start();
    }

    private void showLoading(boolean show) {
        progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
    }
}

布局文件 (res/layout/activity_ndef.xml):

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/tvInfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="14sp"
        android:textColor="@android:color/darker_gray"
        android:padding="12dp"
        android:background="#F5F5F5"
        android:layout_marginBottom="16dp" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:id="@+id/tvContent"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:lineSpacingExtra="4dp" />
    </ScrollView>
</LinearLayout>

四、实战案例二:MIFARE Classic卡读取

4.1 MIFARE Classic卡数据结构

1K卡(S50)存储结构:

css 复制代码
总容量:1024字节 = 16扇区 × 64字节

扇区结构:
扇区 0-15(每个扇区64字节 = 4块 × 16字节)
├── 块 0:数据块(16字节)
├── 块 1:数据块(16字节)
├── 块 2:数据块(16字节)
└── 块 3:控制块(16字节)- 存储密钥A、访问控制位、密钥B

控制块格式(块3):
[密钥A 6字节][访问控制 4字节][密钥B 6字节]

扇区0块0(厂商块):

kotlin 复制代码
Byte 0-3:卡片UID(唯一标识符)
Byte 4:校验字节
Byte 5-15:厂商数据
注意:扇区0块0为只读块,无法修改

访问控制位:

  • 控制每个块的读写权限
  • 可设置密钥A/B的不同访问权限
  • 默认出厂密钥:FF FF FF FF FF FF

4.2 MIFARE Classic读取工具类

java 复制代码
public class MifareClassicReader {
    private static final String TAG = "MifareReader";

    // 常见默认密钥
    private static final byte[] DEFAULT_KEY = {
        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
        (byte) 0xFF, (byte) 0xFF, (byte) 0xFF
    };

    /**
     * 读取指定扇区的指定块
     */
    public static String readBlock(Tag tag, int sector, int blockInSector) {
        MifareClassic mfc = MifareClassic.get(tag);
        if (mfc == null) {
            Log.w(TAG, "不是MIFARE Classic卡");
            return "错误:不是MIFARE Classic卡";
        }

        try {
            // 连接卡片
            mfc.connect();
            Log.d(TAG, "MIFARE Classic卡已连接");

            // 检查扇区是否有效
            if (sector >= mfc.getSectorCount()) {
                return "错误:扇区号超出范围(最大扇区号:" + (mfc.getSectorCount() - 1) + ")";
            }

            // 扇区认证(使用密钥A)
            boolean authA = mfc.authenticateSectorWithKeyA(sector, DEFAULT_KEY);
            if (!authA) {
                Log.w(TAG, "密钥A认证失败,尝试密钥B");
                // 尝试使用密钥B认证
                boolean authB = mfc.authenticateSectorWithKeyB(sector, DEFAULT_KEY);
                if (!authB) {
                    return "错误:扇区认证失败(密钥不正确)";
                }
            }

            Log.d(TAG, "扇区 " + sector + " 认证成功");

            // 计算实际块索引
            int firstBlock = mfc.sectorToBlock(sector);
            int blockIndex = firstBlock + blockInSector;

            // 检查块是否有效
            if (blockIndex >= mfc.getBlockCount()) {
                return "错误:块号超出范围";
            }

            // 防止读取控制块(每个扇区的最后一块)
            if (blockInSector == 3) {
                return "警告:块3为控制块,包含密钥信息,不应读取";
            }

            // 读取块数据
            byte[] data = mfc.readBlock(blockIndex);
            Log.d(TAG, "块 " + blockIndex + " 数据: " + bytesToHex(data));

            return bytesToHex(data);

        } catch (IOException e) {
            Log.e(TAG, "读取MIFARE卡失败", e);
            return "错误:IO操作失败 - " + e.getMessage();
        } finally {
            try {
                if (mfc.isConnected()) {
                    mfc.close();
                    Log.d(TAG, "MIFARE连接已关闭");
                }
            } catch (IOException e) {
                Log.e(TAG, "关闭连接失败", e);
            }
        }
    }

    /**
     * 读取连续多个块的数据
     */
    public static String readSector(Tag tag, int sector) {
        MifareClassic mfc = MifareClassic.get(tag);
        if (mfc == null) {
            return "错误:不是MIFARE Classic卡";
        }

        try {
            mfc.connect();

            // 扇区认证
            if (!mfc.authenticateSectorWithKeyA(sector, DEFAULT_KEY)) {
                return "错误:扇区认证失败";
            }

            StringBuilder result = new StringBuilder();
            result.append("扇区 ").append(sector).append(" 数据:\n\n");

            int firstBlock = mfc.sectorToBlock(sector);
            int blocksInSector = mfc.getBlockCountInSector(sector);

            // 读取扇区内所有数据块(跳过控制块)
            for (int i = 0; i < blocksInSector - 1; i++) {
                int blockIndex = firstBlock + i;
                byte[] data = mfc.readBlock(blockIndex);

                result.append("块 ").append(blockIndex).append(": ");
                result.append(bytesToHex(data)).append("\n");

                // 尝试解析为ASCII文本
                String ascii = bytesToAscii(data);
                if (ascii != null && !ascii.trim().isEmpty()) {
                    result.append("  [ASCII: ").append(ascii).append("]\n");
                }
            }

            return result.toString();

        } catch (IOException e) {
            Log.e(TAG, "读取扇区失败", e);
            return "错误:" + e.getMessage();
        } finally {
            try {
                if (mfc.isConnected()) {
                    mfc.close();
                }
            } catch (IOException e) {
                Log.e(TAG, "关闭连接失败", e);
            }
        }
    }

    /**
     * 读取卡片完整信息
     */
    public static String getMifareInfo(Tag tag) {
        MifareClassic mfc = MifareClassic.get(tag);
        if (mfc == null) {
            return "非MIFARE Classic卡";
        }

        StringBuilder info = new StringBuilder();
        info.append("MIFARE Classic 卡片信息\n");
        info.append("─────────────────────\n");
        info.append("UID: ").append(bytesToHex(tag.getId())).append("\n");

        // 判断卡片类型
        int type = mfc.getType();
        String typeStr;
        switch (type) {
            case MifareClassic.TYPE_CLASSIC:
                typeStr = "MIFARE Classic";
                break;
            case MifareClassic.TYPE_PLUS:
                typeStr = "MIFARE Plus";
                break;
            case MifareClassic.TYPE_PRO:
                typeStr = "MIFARE Pro";
                break;
            case MifareClassic.TYPE_UNKNOWN:
            default:
                typeStr = "未知类型";
        }
        info.append("卡片类型: ").append(typeStr).append("\n");

        // 判断卡片容量
        int size = mfc.getSize();
        String sizeStr;
        switch (size) {
            case MifareClassic.SIZE_MINI:
                sizeStr = "320字节 (MINI)";
                break;
            case MifareClassic.SIZE_1K:
                sizeStr = "1KB (S50)";
                break;
            case MifareClassic.SIZE_2K:
                sizeStr = "2KB";
                break;
            case MifareClassic.SIZE_4K:
                sizeStr = "4KB (S70)";
                break;
            default:
                sizeStr = "未知";
        }
        info.append("卡片容量: ").append(sizeStr).append("\n");
        info.append("扇区数量: ").append(mfc.getSectorCount()).append("\n");
        info.append("块数量: ").append(mfc.getBlockCount()).append("\n");

        return info.toString();
    }

    /**
     * 扫描所有扇区的认证状态
     */
    public static String scanSectors(Tag tag) {
        MifareClassic mfc = MifareClassic.get(tag);
        if (mfc == null) {
            return "非MIFARE Classic卡";
        }

        try {
            mfc.connect();
            StringBuilder result = new StringBuilder();
            result.append("扇区认证扫描结果:\n\n");

            int sectorCount = mfc.getSectorCount();
            for (int sector = 0; sector < sectorCount; sector++) {
                boolean authA = mfc.authenticateSectorWithKeyA(sector, DEFAULT_KEY);
                boolean authB = !authA && mfc.authenticateSectorWithKeyB(sector, DEFAULT_KEY);

                result.append("扇区 ").append(String.format("%2d", sector)).append(": ");
                if (authA) {
                    result.append("✓ 密钥A可用");
                } else if (authB) {
                    result.append("✓ 密钥B可用");
                } else {
                    result.append("✗ 认证失败");
                }
                result.append("\n");
            }

            return result.toString();

        } catch (IOException e) {
            Log.e(TAG, "扇区扫描失败", e);
            return "扫描失败:" + e.getMessage();
        } finally {
            try {
                if (mfc.isConnected()) {
                    mfc.close();
                }
            } catch (IOException e) {
                Log.e(TAG, "关闭连接失败", e);
            }
        }
    }

    /**
     * 字节数组转十六进制字符串(带空格)
     */
    public static String bytesToHex(byte[] bytes) {
        if (bytes == null) return "";
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString().trim();
    }

    /**
     * 字节数组转ASCII字符串(过滤不可打印字符)
     */
    private static String bytesToAscii(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            // 只保留可打印ASCII字符(32-126)
            if (b >= 32 && b <= 126) {
                sb.append((char) b);
            } else {
                sb.append('.');
            }
        }
        return sb.toString();
    }

    /**
     * 检查是否为MIFARE Classic卡
     */
    public static boolean isMifareClassic(Tag tag) {
        return MifareClassic.get(tag) != null;
    }
}

4.3 MIFARE Classic读取Activity

java 复制代码
public class MifareActivity extends BaseNfcActivity {
    private static final String TAG = "MifareActivity";

    private TextView tvInfo;
    private TextView tvScanResult;
    private TextView tvData;
    private Button btnScanSectors;
    private Button btnReadSector;
    private EditText etSectorNumber;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mifare);

        initViews();
        initListeners();
    }

    private void initViews() {
        tvInfo = findViewById(R.id.tvInfo);
        tvScanResult = findViewById(R.id.tvScanResult);
        tvData = findViewById(R.id.tvData);
        btnScanSectors = findViewById(R.id.btnScanSectors);
        btnReadSector = findViewById(R.id.btnReadSector);
        etSectorNumber = findViewById(R.id.etSectorNumber);
        progressBar = findViewById(R.id.progressBar);

        tvInfo.setText("请将MIFARE Classic卡靠近设备...");
        btnScanSectors.setEnabled(false);
        btnReadSector.setEnabled(false);
    }

    private void initListeners() {
        btnScanSectors.setOnClickListener(v -> {
            if (currentTag != null) {
                scanSectors(currentTag);
            }
        });

        btnReadSector.setOnClickListener(v -> {
            String sectorStr = etSectorNumber.getText().toString().trim();
            if (sectorStr.isEmpty()) {
                Toast.makeText(this, "请输入扇区号", Toast.LENGTH_SHORT).show();
                return;
            }

            try {
                int sector = Integer.parseInt(sectorStr);
                if (currentTag != null) {
                    readSector(currentTag, sector);
                }
            } catch (NumberFormatException e) {
                Toast.makeText(this, "扇区号格式错误", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private Tag currentTag;

    @Override
    protected void onNfcTagDiscovered(Tag tag) {
        super.onNfcTagDiscovered(tag);

        Log.d(TAG, "检测到MIFARE卡,UID: " + bytesToHex(tag.getId()));

        // 检查是否为MIFARE Classic卡
        if (!MifareClassicReader.isMifareClassic(tag)) {
            runOnUiThread(() -> {
                tvInfo.setText("错误:不是MIFARE Classic卡");
                Toast.makeText(this, "请使用MIFARE Classic卡", Toast.LENGTH_SHORT).show();
            });
            return;
        }

        currentTag = tag;

        // 显示卡片信息
        String info = MifareClassicReader.getMifareInfo(tag);
        runOnUiThread(() -> {
            tvInfo.setText(info);
            btnScanSectors.setEnabled(true);
            btnReadSector.setEnabled(true);
            Toast.makeText(this, "MIFARE Classic卡检测成功", Toast.LENGTH_SHORT).show();
        });
    }

    /**
     * 扫描所有扇区
     */
    private void scanSectors(Tag tag) {
        showLoading(true);
        new Thread(() -> {
            String result = MifareClassicReader.scanSectors(tag);

            runOnUiThread(() -> {
                showLoading(false);
                tvScanResult.setText(result);
                Toast.makeText(this, "扇区扫描完成", Toast.LENGTH_SHORT).show();
            });
        }).start();
    }

    /**
     * 读取指定扇区
     */
    private void readSector(Tag tag, int sector) {
        showLoading(true);
        new Thread(() -> {
            String result = MifareClassicReader.readSector(tag, sector);

            runOnUiThread(() -> {
                showLoading(false);
                tvData.setText(result);

                if (result.startsWith("错误")) {
                    Toast.makeText(this, "扇区读取失败", Toast.LENGTH_SHORT).show();
                } else {
                    Toast.makeText(this, "扇区读取成功", Toast.LENGTH_SHORT).show();
                }
            });
        }).start();
    }

    private void showLoading(boolean show) {
        progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
        btnScanSectors.setEnabled(!show);
        btnReadSector.setEnabled(!show);
    }
}

五、NFC开发常见问题与最佳实践

5.1 常见问题排查

问题1:Tag对象在跨进程传递后无法读取

现象:

java 复制代码
java.io.IOException: transceive failed

原因分析:

  • Tag对象包含底层硬件连接句柄
  • 跨进程传递后句柄失效
  • Android系统安全机制限制

解决方案:

  • 必须在接收NFC Intent的组件中直接处理Tag对象
  • 不要通过广播、服务传递Tag对象
  • 如需跨组件处理,传递原始数据而非Tag对象
问题2:前台分发不生效

排查步骤:

  1. 检查enableForegroundDispatch是否在onResume中调用
  2. 检查disableForegroundDispatch是否在onPause中调用
  3. 检查PendingIntent的FLAG设置(Android 12+需要FLAG_MUTABLE)
  4. 检查Activity的launchMode(建议使用singleTask)
问题3:MIFARE卡认证失败

可能原因:

  • 密钥不正确
  • 卡片已被加密
  • 卡片损坏

解决方案:

java 复制代码
// 尝试多个常见密钥
private static final byte[][] COMMON_KEYS = {
    {(byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF}, // 默认密钥
    {(byte)0xA0, (byte)0xA1, (byte)0xA2, (byte)0xA3, (byte)0xA4, (byte)0xA5}, // MAD密钥
    {(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00}, // 全零密钥
};

private boolean authenticateWithCommonKeys(MifareClassic mfc, int sector) {
    for (byte[] key : COMMON_KEYS) {
        if (mfc.authenticateSectorWithKeyA(sector, key)) {
            Log.d(TAG, "密钥A认证成功: " + bytesToHex(key));
            return true;
        }
        if (mfc.authenticateSectorWithKeyB(sector, key)) {
            Log.d(TAG, "密钥B认证成功: " + bytesToHex(key));
            return true;
        }
    }
    return false;
}

5.2 最佳实践

1. 异步处理NFC操作
java 复制代码
// ❌ 错误:在主线程读取NFC
@Override
protected void onNfcTagDiscovered(Tag tag) {
    String data = NdefReader.readNdefTag(tag); // 阻塞主线程
    tvContent.setText(data);
}

// ✅ 正确:使用异步任务
@Override
protected void onNfcTagDiscovered(Tag tag) {
    new Thread(() -> {
        String data = NdefReader.readNdefTag(tag);
        runOnUiThread(() -> tvContent.setText(data));
    }).start();
}
2. 完善的异常处理
java 复制代码
try {
    mfc.connect();
    // ... NFC操作
} catch (TagLostException e) {
    // 标签离开感应区
    showError("卡片已移除,请重新放置");
} catch (IOException e) {
    // IO异常
    showError("读取失败:" + e.getMessage());
} finally {
    try {
        if (mfc.isConnected()) {
            mfc.close();
        }
    } catch (IOException e) {
        Log.e(TAG, "关闭连接失败", e);
    }
}
3. 正确的Activity配置
xml 复制代码
<activity
    android:name=".NfcActivity"
    android:exported="true"
    android:launchMode="singleTask"
    android:screenOrientation="portrait">

    <!-- NFC Intent过滤器 -->
    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>

    <meta-data
        android:name="android.nfc.action.TECH_DISCOVERED"
        android:resource="@xml/nfc_tech_filter" />
</activity>
4. 用户体验优化
java 复制代码
// 提供清晰的操作指引
private void showNfcGuide() {
    new AlertDialog.Builder(this)
        .setTitle("NFC读取指引")
        .setMessage("1. 确保NFC功能已开启\n" +
                   "2. 将卡片平放在手机背部\n" +
                   "3. 保持卡片静止直到提示成功\n" +
                   "4. 听到提示音或震动后移开卡片")
        .setPositiveButton("知道了", null)
        .show();
}

// 震动和声音反馈
private void provideFeedback() {
    // 震动反馈
    Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
    if (vibrator != null) {
        vibrator.vibrate(100);
    }

    // 声音反馈
    MediaPlayer.create(this, R.raw.nfc_success).start();
}

相关推荐
晚风叙码31 分钟前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
素师良码3 小时前
第7篇:驱动工程师必修课-如何高效读懂芯片与Panel规格书,拒绝盲写代码
程序员
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程5 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓6 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1236 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium