android 读取cpu+m1类型的nfc卡片,设置只读写m1的内容
前言
目前有一个需求,有一个nfc的设备,但是这个板子,是CPU+M1,APP会将M1和CPU都读一下;执行写的时候,会优先写CPU,但此时CPU没有tag标签的功能,就直接报错了。要求是只读取m1卡,绕过cpu的卡
cpu和m1
CPU卡:像一台微型计算机,安全性极高,适用于金融、身份认证等场景。
M1卡:像一个带密码的存储柜,安全性较弱,适用于门禁、公交卡等低成本场景。
M1卡 (MIFARE Classic)
M1 是恩智浦半导体(NXP)生产的 MIFARE Classic 系列芯片的简称,它是目前世界上使用量最大的非接触式IC卡之一。
它不是CPU卡,而是一种逻辑加密卡。
工作原理:你可以把它想象成一个带有很多个小抽屉(扇区)的柜子,每个抽屉都有一把锁(密码)。知道密码就能打开抽屉存取东西(数据)。
主要安全隐患:
固定算法:其加密算法已被破解,市面上常见的“IC卡复制器”主要针对的就是M1卡。
静态密钥:密码是预先设置好并固化在芯片里的,无法更改(除非有特殊权限)。
漏洞攻击:可以通过特定技术手段(如重放攻击、侧信道攻击)破解其密钥。
CPU卡
CPU卡才是真正意义上的“智能卡”,其核心是卡内集成了一个微处理器(CPU) 和一个专用的操作系统(COS - Chip Operating System)。
工作原理:它更像一台微型电脑。当你向CPU卡发送指令时,并不是直接操作数据,而是由卡内的CPU接收指令,经过COS系统的处理(如校验、加密、解密),然后再决定如何响应。所有敏感操作都在芯片内部完成,外部无法获取密钥和关键数据。
核心优势:
真正的动态加密:每次交易的密钥都不同(动态密钥),无法通过监听一次通信来复制卡片。
一卡一密:每张卡的密钥都不同。
抗攻击能力强:能防止物理和逻辑上的各种攻击手段。
M1卡就像一个日记本。你有一把锁锁着它,但如果别人撬开了锁(破解了密码),里面的所有内容就一览无余,可以被随意抄写(复制)。
CPU卡就像一个专业的银行保险库。你告诉经理(COS系统)你要取钱(一个操作),经理会 inside the vault(在芯片内部)验证你的身份、办理手续,然后只把结果(成功或失败、或取出的现金)递出来给你。你永远不知道保险库的内部结构、密码和金条的具体摆放位置。
在手机NFC功能中的体现
当你用手机的NFC功能读取卡片时:
如果是一个M1卡(如门禁卡),很多手机支持直接模拟和复制,因为算法固定。
如果是一个CPU卡(如银行卡、身份证),手机APP只能读取到一些公开的、未加密的信息(如卡号)。对于加密的敏感数据,由于无法破解其动态加密系统,根本无法复制或读取关键信息。你手机的“钱包”应用模拟门禁卡时,如果提示“此卡加密”或“不支持模拟”,那它很可能就是一张CPU卡。
要做成的效果
1.只操作m1类型的卡,可以支持写入长文本,因为每一块只有16字节,所以要跨多个块写入,读取是要将每个块的内容读取出来,组合起来
处理这种复合卡的关键在于:忽略CPU功能,只使用与M1卡通信的协议。在Android中,这通常通过MifareClassic类来实现。基本流程是:检测到NFC标签 → 获取Tag对象 → 判断是否支持MifareClassic技术 → 如果支持,则将其当作M1卡进行连接和读写。
为了明显期间,要在页面上将nfc设备内的数据打印出来,是16进制,并且在每个块的16进制后边,打印出该块的文本
app 页面
代码
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><uses-permission android:name="android.permission.NFC" /><uses-feature android:name="android.hardware.nfc" android:required="true" /><applicationandroid:allowBackup="true"android:dataExtractionRules="@xml/data_extraction_rules"android:fullBackupContent="@xml/backup_rules"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/Theme.NFCDemo"tools:targetApi="31"><activityandroid:name=".MainActivity"android:exported="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter><intent-filter><action android:name="android.nfc.action.NDEF_DISCOVERED" /><action android:name="android.nfc.action.TECH_DISCOVERED" /><action android:name="android.nfc.action.TAG_DISCOVERED" /><category android:name="android.intent.category.DEFAULT" /></intent-filter><meta-dataandroid:name="android.nfc.action.TECH_DISCOVERED"android:resource="@xml/nfc_tech_filter" /></activity></application></manifest>
xml/nfc_tech_filter.xml,只处理m1格式的nfc
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"><tech-list><tech>android.nfc.tech.MifareClassic</tech></tech-list><tech-list><tech>android.nfc.tech.NfcA</tech></tech-list>
</resources>
activity_main.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"android:background="#f5f5f5"><!-- 状态显示区域 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:background="#ffffff"android:padding="16dp"android:elevation="4dp"android:layout_marginBottom="16dp"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="NFC状态"android:textSize="18sp"android:textStyle="bold"android:textColor="#333333"android:layout_marginBottom="8dp"/><TextViewandroid:id="@+id/tv_nfc_status"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="等待检测NFC卡片..."android:textSize="16sp"android:textColor="#666666"android:layout_marginBottom="16dp"/><TextViewandroid:id="@+id/tv_tag_info"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="卡片信息: 未检测到"android:textSize="14sp"android:textColor="#888888"/></LinearLayout><!-- 文本输入区域 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:background="#ffffff"android:padding="16dp"android:elevation="4dp"android:layout_marginBottom="16dp"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="文本操作"android:textSize="16sp"android:textStyle="bold"android:textColor="#333333"android:layout_marginBottom="8dp"/><EditTextandroid:id="@+id/et_text_input"android:layout_width="match_parent"android:layout_height="100dp"android:hint="请输入要写入的文本(最多45个字符)"android:inputType="textMultiLine"android:maxLength="45"android:layout_marginBottom="8dp"/><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><Buttonandroid:id="@+id/btn_write_text"android:layout_width="0dp"android:layout_height="48dp"android:layout_weight="1"android:text="写入文本"android:textColor="#ffffff"android:layout_marginEnd="8dp"android:enabled="false"/><Buttonandroid:id="@+id/btn_read_text"android:layout_width="0dp"android:layout_height="48dp"android:layout_weight="1"android:text="读取文本"android:textColor="#ffffff"android:layout_marginStart="8dp"android:enabled="false"/></LinearLayout><Buttonandroid:id="@+id/btn_read_all_hex"android:layout_width="match_parent"android:layout_height="48dp"android:text="读取所有16进制数据"android:textColor="#ffffff"android:layout_marginBottom="16dp"android:enabled="false"/></LinearLayout><!-- 数据显示区域 --><LinearLayoutandroid:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"android:orientation="vertical"android:background="#ffffff"android:padding="16dp"android:elevation="4dp"><TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="读取结果"android:textSize="16sp"android:textStyle="bold"android:textColor="#333333"android:layout_marginBottom="8dp"/><ScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:id="@+id/tv_data_display"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="暂无数据"android:textSize="14sp"android:textColor="#555555"android:lineSpacingExtra="4dp"/></ScrollView></LinearLayout></LinearLayout>
MainActivity.java,nfc卡的默认秘钥是FFFFFFFFFFFF
package com.smart.nfcdemo;import android.app.PendingIntent;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.MifareClassic;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public class MainActivity extends AppCompatActivity {private static final String TAG = "NfcM1Activity";private static final byte[] KEY_DEFAULT = {(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF};private static final int START_SECTOR = 1; // 起始扇区private static final int START_BLOCK = 0; // 起始块private static final byte TEXT_START_MARKER = (byte) 0xAA; // 文本开始标记private static final byte TEXT_END_MARKER = (byte) 0xBB; // 文本结束标记private NfcAdapter nfcAdapter;private PendingIntent pendingIntent;private TextView tvNfcStatus;private TextView tvTagInfo;private TextView tvDataDisplay;private EditText etTextInput;private Button btnWriteText;private Button btnReadText;private Button btnReadAllHex;private Tag currentTag;private MifareClassic mifareClassic;private boolean isTagPresent = false;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initViews();initNfc();setupListeners();handleIntent(getIntent());}private void initViews() {tvNfcStatus = findViewById(R.id.tv_nfc_status);tvTagInfo = findViewById(R.id.tv_tag_info);tvDataDisplay = findViewById(R.id.tv_data_display);etTextInput = findViewById(R.id.et_text_input);btnWriteText = findViewById(R.id.btn_write_text);btnReadText = findViewById(R.id.btn_read_text);btnReadAllHex = findViewById(R.id.btn_read_all_hex);}private void initNfc() {nfcAdapter = NfcAdapter.getDefaultAdapter(this);if (nfcAdapter == null) {tvNfcStatus.setText("设备不支持NFC");return;}if (!nfcAdapter.isEnabled()) {tvNfcStatus.setText("请启用NFC功能");return;}Intent intent = new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE);tvNfcStatus.setText("NFC已就绪,请放置卡片");}private void setupListeners() {btnWriteText.setOnClickListener(v -> writeTextToCard());btnReadText.setOnClickListener(v -> readTextFromCard());btnReadAllHex.setOnClickListener(v -> readAllHexData());}@Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);handleIntent(intent);}private void handleIntent(Intent intent) {String action = intent.getAction();if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) ||NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) ||NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);if (tag != null) {processTag(tag);}}}private void processTag(Tag tag) {currentTag = tag;mifareClassic = MifareClassic.get(tag);if (mifareClassic == null) {tvNfcStatus.setText("不是MIFARE Classic卡片");return;}isTagPresent = true;updateButtonState();String tagId = bytesToHex(tag.getId());String tagInfo = String.format("卡片ID: %s\n类型: %s\n扇区数: %d\n块数: %d",tagId,getMifareTypeString(mifareClassic.getType()),mifareClassic.getSectorCount(),mifareClassic.getBlockCount());tvNfcStatus.setText("检测到M1卡片");tvTagInfo.setText(tagInfo);}private void writeTextToCard() {if (!isTagPresent || mifareClassic == null) {Toast.makeText(this, "请先放置卡片", Toast.LENGTH_SHORT).show();return;}String textToWrite = etTextInput.getText().toString().trim();if (textToWrite.isEmpty()) {Toast.makeText(this, "请输入要写入的文本", Toast.LENGTH_SHORT).show();return;}new WriteLongTextTask().execute(textToWrite);}private void readTextFromCard() {if (!isTagPresent || mifareClassic == null) {Toast.makeText(this, "请先放置卡片", Toast.LENGTH_SHORT).show();return;}new ReadLongTextTask().execute();}private void readAllHexData() {if (!isTagPresent || mifareClassic == null) {Toast.makeText(this, "请先放置卡片", Toast.LENGTH_SHORT).show();return;}new ReadAllHexDataTask().execute();}private boolean authenticateSector(int sector) {try {return mifareClassic.authenticateSectorWithKeyA(sector, KEY_DEFAULT);} catch (Exception e) {return false;}}private void updateButtonState() {btnWriteText.setEnabled(isTagPresent);btnReadText.setEnabled(isTagPresent);btnReadAllHex.setEnabled(isTagPresent);}@Overrideprotected void onResume() {super.onResume();if (nfcAdapter != null) {nfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null);}}@Overrideprotected void onPause() {super.onPause();if (nfcAdapter != null) {nfcAdapter.disableForegroundDispatch(this);}}// 写入长文本任务 - 支持跨扇区跨块private class WriteLongTextTask extends AsyncTask<String, String, String> {@Overrideprotected String doInBackground(String... params) {String textToWrite = params[0];byte[] textBytes = textToWrite.getBytes(StandardCharsets.UTF_8);try {mifareClassic.connect();publishProgress("开始写入文本,长度: " + textBytes.length + "字节");int currentSector = START_SECTOR;int currentBlock = START_BLOCK;int bytesWritten = 0;int totalBlocksUsed = 0;// 写入文本开始标记if (!writeBlockWithMarker(currentSector, currentBlock, TEXT_START_MARKER)) {return "❌ 无法写入开始标记";}publishProgress("写入开始标记到 扇区" + currentSector + "块" + currentBlock);totalBlocksUsed++;// 移动到下一个块if (!moveToNextBlock(currentSector, currentBlock)) {return "❌ 没有可用空间";}currentBlock++;if (currentBlock >= mifareClassic.getBlockCountInSector(currentSector) - 1) {currentSector++;currentBlock = 0;}// 写入文本数据while (bytesWritten < textBytes.length) {int bytesToWrite = Math.min(16, textBytes.length - bytesWritten);byte[] blockData = new byte[16];System.arraycopy(textBytes, bytesWritten, blockData, 0, bytesToWrite);if (!writeDataBlock(currentSector, currentBlock, blockData)) {return "❌ 写入数据失败 at 扇区" + currentSector + "块" + currentBlock;}publishProgress("写入数据到 扇区" + currentSector + "块" + currentBlock +" (" + bytesToWrite + "字节)");bytesWritten += bytesToWrite;totalBlocksUsed++;// 移动到下一个块或扇区if (!moveToNextBlock(currentSector, currentBlock)) {break;}currentBlock++;if (currentBlock >= mifareClassic.getBlockCountInSector(currentSector) - 1) {currentSector++;currentBlock = 0;if (currentSector >= mifareClassic.getSectorCount()) {break; // 没有更多扇区了}}}// 写入文本结束标记if (!writeBlockWithMarker(currentSector, currentBlock, TEXT_END_MARKER)) {return "❌ 无法写入结束标记";}publishProgress("写入结束标记到 扇区" + currentSector + "块" + currentBlock);totalBlocksUsed++;return "✅ 文本写入完成\n" +"总字节数: " + bytesWritten + "\n" +"使用块数: " + totalBlocksUsed + "\n" +"最后位置: 扇区" + currentSector + "块" + currentBlock;} catch (IOException e) {return "❌ 写入错误: " + e.getMessage();} finally {try {mifareClassic.close();} catch (IOException e) {e.printStackTrace();}}}private boolean writeBlockWithMarker(int sector, int block, byte marker) {try {if (!authenticateSector(sector)) return false;int absoluteBlock = mifareClassic.sectorToBlock(sector) + block;byte[] blockData = new byte[16];blockData[0] = marker;mifareClassic.writeBlock(absoluteBlock, blockData);return true;} catch (IOException e) {return false;}}private boolean writeDataBlock(int sector, int block, byte[] data) {try {if (!authenticateSector(sector)) return false;int absoluteBlock = mifareClassic.sectorToBlock(sector) + block;mifareClassic.writeBlock(absoluteBlock, data);return true;} catch (IOException e) {return false;}}private boolean moveToNextBlock(int sector, int block) {int blocksInSector = mifareClassic.getBlockCountInSector(sector);if (block + 1 < blocksInSector - 1) { // -1 是为了避开扇区尾return true;}// 移动到下一个扇区int nextSector = sector + 1;if (nextSector < mifareClassic.getSectorCount()) {return true;}return false;}@Overrideprotected void onProgressUpdate(String... values) {tvDataDisplay.append(values[0] + "\n");}@Overrideprotected void onPostExecute(String result) {Toast.makeText(MainActivity.this, result, Toast.LENGTH_LONG).show();tvDataDisplay.append(result + "\n");}}// 读取长文本任务private class ReadLongTextTask extends AsyncTask<Void, String, String> {@Overrideprotected String doInBackground(Void... voids) {try {mifareClassic.connect();publishProgress("开始读取文本数据...");List<Byte> allTextBytes = new ArrayList<>();boolean readingText = false;boolean foundStart = false;boolean foundEnd = false;// 遍历所有扇区和块来寻找文本数据for (int sector = START_SECTOR; sector < mifareClassic.getSectorCount(); sector++) {if (!authenticateSector(sector)) {publishProgress("扇区" + sector + "认证失败,跳过");continue;}int blocksInSector = mifareClassic.getBlockCountInSector(sector);for (int block = 0; block < blocksInSector - 1; block++) { // 避开扇区尾int absoluteBlock = mifareClassic.sectorToBlock(sector) + block;try {byte[] blockData = mifareClassic.readBlock(absoluteBlock);if (blockData[0] == TEXT_START_MARKER) {foundStart = true;readingText = true;publishProgress("找到开始标记 at 扇区" + sector + "块" + block);continue;}if (blockData[0] == TEXT_END_MARKER) {readingText = false;foundEnd = true;publishProgress("找到结束标记 at 扇区" + sector + "块" + block);break;}if (readingText) {// 添加所有字节(可能是文本数据)for (byte b : blockData) {allTextBytes.add(b);}publishProgress("读取数据 from 扇区" + sector + "块" + block);}} catch (IOException e) {publishProgress("读取失败 at 扇区" + sector + "块" + block + ": " + e.getMessage());}}if (foundEnd) {break; // 已经找到结束标记,停止搜索}if (!readingText && foundStart) {break; // 开始标记后没有找到数据,停止搜索}}if (!foundStart) {return "❌ 未找到文本开始标记";}if (!foundEnd) {return "❌ 未找到文本结束标记,数据可能不完整";}if (allTextBytes.isEmpty()) {return "❌ 找到标记但未找到文本数据";}// 转换为字节数组并解码为文本byte[] textBytes = new byte[allTextBytes.size()];for (int i = 0; i < allTextBytes.size(); i++) {textBytes[i] = allTextBytes.get(i);}String text = new String(textBytes, StandardCharsets.UTF_8);return "📖 读取到的文本:\n" + text +"\n\n📊 统计信息:\n" +"总字节数: " + textBytes.length + "\n" +"字符数量: " + text.length();} catch (IOException e) {return "❌ 读取错误: " + e.getMessage();} finally {try {mifareClassic.close();} catch (IOException e) {e.printStackTrace();}}}@Overrideprotected void onProgressUpdate(String... values) {tvDataDisplay.append(values[0] + "\n");}@Overrideprotected void onPostExecute(String result) {tvDataDisplay.setText(result);}}// 读取所有数据并以16进制和文本格式输出(支持中文)private class ReadAllHexDataTask extends AsyncTask<Void, String, Void> {@Overrideprotected Void doInBackground(Void... voids) {try {mifareClassic.connect();publishProgress("开始读取所有扇区数据...");publishProgress("使用密钥: FF FF FF FF FF FF");publishProgress("====================================");int sectorCount = Math.min(mifareClassic.getSectorCount(), 16);for (int sector = 0; sector < sectorCount; sector++) {if (authenticateSector(sector)) {int blockCount = mifareClassic.getBlockCountInSector(sector);int firstBlock = mifareClassic.sectorToBlock(sector);publishProgress("\n扇区 " + sector + " (块" + firstBlock + "-" + (firstBlock + blockCount - 1) + "):");for (int block = 0; block < blockCount; block++) {int absoluteBlock = firstBlock + block;try {byte[] data = mifareClassic.readBlock(absoluteBlock);String hexData = bytesToHex(data);// 转换为文本显示String textRepresentation = convertToText(data);String blockType = getBlockType(data, block, blockCount);String blockInfo = String.format(" 块%-2d: %s", block, hexData);blockInfo += " ← " + blockType;publishProgress(blockInfo);publishProgress(" 文本: \"" + textRepresentation + "\"");} catch (IOException e) {publishProgress(" 块" + block + ": 读取失败 - " + e.getMessage());}Thread.sleep(50);}} else {publishProgress("\n扇区 " + sector + ": 认证失败");}}publishProgress("\n====================================");publishProgress("数据读取完成");} catch (Exception e) {publishProgress("读取错误: " + e.getMessage());} finally {try {mifareClassic.close();} catch (IOException e) {e.printStackTrace();}}return null;}// 将字节数组转换为可显示的文本(支持中文)private String convertToText(byte[] data) {if (data == null || data.length == 0) {return "";}// 检查是否是标记块if (data[0] == TEXT_START_MARKER || data[0] == TEXT_END_MARKER) {return "[标记块]";}// 首先尝试检测是否为UTF-8编码的文本String utf8Text = tryDecodeUtf8(data);if (!utf8Text.isEmpty() && isMeaningfulText(utf8Text)) {return utf8Text;}// 如果不是有效的UTF-8文本,则显示ASCII可打印字符return convertToAsciiDisplay(data);}// 尝试解码为UTF-8文本private String tryDecodeUtf8(byte[] data) {try {// 去除尾部的空字节int length = data.length;while (length > 0 && data[length - 1] == 0) {length--;}if (length == 0) {return "";}byte[] trimmedData = Arrays.copyOfRange(data, 0, length);return new String(trimmedData, StandardCharsets.UTF_8);} catch (Exception e) {return "";}}// 检查是否是有效的文本(包含中文字符或可读文本)private boolean isMeaningfulText(String text) {if (text == null || text.trim().isEmpty()) {return false;}// 检查是否包含中文字符if (containsChinese(text)) {return true;}// 检查是否包含可读的ASCII文本int readableCount = 0;int totalCount = Math.min(text.length(), 20);for (int i = 0; i < totalCount; i++) {char c = text.charAt(i);if (c >= 32 && c <= 126) {readableCount++;} else if (c != 0 && c != ' ') {return false;}}return totalCount > 0 && (readableCount * 100 / totalCount) > 30;}// 检查是否包含中文字符private boolean containsChinese(String text) {for (int i = 0; i < text.length(); i++) {char c = text.charAt(i);// 中文字符的Unicode范围if (c >= 0x4E00 && c <= 0x9FFF) {return true;}// 中文标点符号等if (c >= 0x3000 && c <= 0x303F) {return true;}}return false;}// 转换为ASCII显示格式private String convertToAsciiDisplay(byte[] data) {StringBuilder asciiBuilder = new StringBuilder();for (byte b : data) {if (b >= 32 && b <= 126) {asciiBuilder.append((char) b);} else if (b == 0) {asciiBuilder.append(" ");} else {asciiBuilder.append(".");}}return asciiBuilder.toString();}// 获取块类型描述private String getBlockType(byte[] data, int blockIndex, int blockCount) {if (data == null || data.length == 0) {return "[空数据]";}if (data[0] == TEXT_START_MARKER) {return "[文本开始]";} else if (data[0] == TEXT_END_MARKER) {return "[文本结束]";}if (blockIndex == blockCount - 1) {return "[扇区尾]";}String utf8Text = tryDecodeUtf8(data);if (!utf8Text.isEmpty() && isMeaningfulText(utf8Text)) {if (containsChinese(utf8Text)) {return "[中文文本]";} else {return "[英文文本]";}}boolean allZeros = true;boolean allFF = true;for (byte b : data) {if (b != 0) allZeros = false;if (b != (byte) 0xFF) allFF = false;}if (allZeros) {return "[全零]";} else if (allFF) {return "[全F]";}return "[二进制数据]";}@Overrideprotected void onProgressUpdate(String... values) {tvDataDisplay.append(values[0] + "\n");}}// 工具方法:字节数组转十六进制字符串private String bytesToHex(byte[] bytes) {if (bytes == null) return "null";StringBuilder sb = new StringBuilder();for (byte b : bytes) {sb.append(String.format("%02X ", b));}return sb.toString().trim();}// 工具方法:获取MIFARE类型字符串private String getMifareTypeString(int type) {switch (type) {case MifareClassic.TYPE_CLASSIC: return "MIFARE Classic";case MifareClassic.TYPE_PLUS: return "MIFARE Plus";case MifareClassic.TYPE_PRO: return "MIFARE Pro";case MifareClassic.TYPE_UNKNOWN: return "未知类型";default: return "其他类型";}}
}