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();
}

相关推荐
消失的旧时光-19439 小时前
WebView 最佳封装模板(BaseWebActivity + WebViewHelper)
android·webview
WAsbry9 小时前
NFC开发系列 - 第二篇:NFC企业级架构设计与最佳实践
android·程序员·架构
feibafeibafeiba10 小时前
Android 14 关于imageview设置动态padding值导致图标旋转的问题
android
tangweiguo0305198711 小时前
ProcessLifecycleOwner 完全指南:优雅监听应用前后台状态
android·kotlin
介一安全12 小时前
【Frida Android】基础篇15(完):Frida-Trace 基础应用——JNI 函数 Hook
android·网络安全·ida·逆向·frida
吞掉星星的鲸鱼12 小时前
android studio创建使用开发打包教程
android·ide·android studio
陈老师还在写代码12 小时前
android studio 签名打包教程
android·ide·android studio
csj5012 小时前
android studio设置
android
hifhf12 小时前
Android Studio gradle下载失败报错
android·ide·android studio