NFC开发系列 - 第一篇:NFC开发基础与实战入门
核心要点
- NFC开发流程:权限配置 → 硬件检测 → Intent过滤 → 前台分发 → Tag处理
- NDEF标签:标准化数据格式,适合简单应用场景
- MIFARE Classic:加密存储卡,需要扇区认证,适合门禁、会员卡等场景
- 最佳实践:异步处理、异常安全、用户体验优化
参考资料
一、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):绝对URITNF_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:前台分发不生效
排查步骤:
- 检查
enableForegroundDispatch是否在onResume中调用 - 检查
disableForegroundDispatch是否在onPause中调用 - 检查PendingIntent的FLAG设置(Android 12+需要FLAG_MUTABLE)
- 检查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();
}