用Kotlin在Android上玩转NFC:手把手实现一个简易门禁卡信息读取器
你是否曾经好奇过,每天进出小区或办公室时刷的那张门禁卡里到底藏着什么秘密?作为一个Android开发者,我们可以用Kotlin和NFC技术轻松揭开这个谜题。本文将带你从零开始,构建一个能够读取常见门禁卡信息的实用工具。
1. 准备工作:理解NFC与门禁卡
NFC(近场通信)技术在现代智能手机中已经相当普及,它允许设备在极短距离内(通常4厘米以内)进行无线数据交换。门禁卡大多采用RFID技术,而支持NFC的Android设备可以读取这些卡片的信息。
1.1 常见门禁卡类型
- Mifare Classic:最常见的门禁卡类型,使用13.56MHz频率
- Mifare Ultralight:更简单的卡片,通常用于一次性票证
- ISO 14443 Type A/B:另一种广泛使用的标准
提示:大多数小区门禁卡使用的是Mifare Classic 1K,它有16个扇区,每个扇区有4个块,共1KB存储空间。
2. 项目搭建与权限配置
首先创建一个新的Android项目,确保你的build.gradle中配置了Kotlin支持:
plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-android-extensions' }2.1 AndroidManifest.xml配置
<uses-permission android:name="android.permission.NFC" /> <uses-feature android:name="android.hardware.nfc" android:required="true" /> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.nfc.action.TECH_DISCOVERED" /> </intent-filter> <meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" /> </activity>2.2 创建nfc_tech_filter.xml
在res/xml目录下创建nfc_tech_filter.xml文件:
<resources> <tech-list> <tech>android.nfc.tech.MifareClassic</tech> </tech-list> <tech-list> <tech>android.nfc.tech.NfcA</tech> </tech-list> </resources>3. 核心NFC读取逻辑实现
3.1 初始化NFC适配器
在MainActivity中添加以下代码:
class MainActivity : AppCompatActivity() { private lateinit var nfcAdapter: NfcAdapter private lateinit var textView: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById(R.id.cardInfoTextView) nfcAdapter = NfcAdapter.getDefaultAdapter(this) ?: run { Toast.makeText(this, "该设备不支持NFC", Toast.LENGTH_LONG).show() finish() return } } }3.2 处理NFC Intent
override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleNfcIntent(intent) } private fun handleNfcIntent(intent: Intent) { if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) { val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) tag?.let { val mifare = MifareClassic.get(it) try { mifare.connect() readCardData(mifare) } catch (e: Exception) { textView.text = "读取卡片失败: ${e.message}" } finally { mifare.close() } } } }4. 读取并解析门禁卡数据
4.1 读取卡片基本信息
private fun readCardData(mifare: MifareClassic) { val sb = StringBuilder() // 获取卡片类型 val type = when(mifare.type) { MifareClassic.TYPE_CLASSIC -> "Mifare Classic" MifareClassic.TYPE_PLUS -> "Mifare Plus" MifareClassic.TYPE_PRO -> "Mifare Pro" else -> "未知类型" } sb.append("卡片类型: $type\n") sb.append("扇区数: ${mifare.sectorCount}\n") sb.append("块数: ${mifare.blockCount}\n") // 继续读取具体数据... textView.text = sb.toString() }4.2 读取扇区数据
for (sector in 0 until mifare.sectorCount) { if (mifare.authenticateSectorWithKeyA(sector, MifareClassic.KEY_DEFAULT)) { val blockIndex = mifare.sectorToBlock(sector) val data = mifare.readBlock(blockIndex) sb.append("\n扇区 $sector:\n") sb.append(bytesToHex(data)) } else { sb.append("\n扇区 $sector: 认证失败\n") } }4.3 字节数组转十六进制字符串工具函数
private fun bytesToHex(bytes: ByteArray): String { val hexChars = CharArray(bytes.size * 3) for (i in bytes.indices) { val v = bytes[i].toInt() and 0xFF hexChars[i * 3] = HEX_ARRAY[v ushr 4] hexChars[i * 3 + 1] = HEX_ARRAY[v and 0x0F] hexChars[i * 3 + 2] = ' ' } return String(hexChars) } companion object { private val HEX_ARRAY = "0123456789ABCDEF".toCharArray() }5. 优化用户体验
5.1 启用前台分发系统
override fun onResume() { super.onResume() nfcAdapter.enableForegroundDispatch( this, PendingIntent.getActivity( this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0 ), null, arrayOf(arrayOf("android.nfc.tech.MifareClassic")) ) } override fun onPause() { super.onPause() nfcAdapter.disableForegroundDispatch(this) }5.2 添加UI反馈
在布局文件中添加一个简单的TextView来显示卡片信息:
<TextView android:id="@+id/cardInfoTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:textSize="14sp" android:text="请将门禁卡靠近手机背面NFC区域"/>6. 安全注意事项与进阶方向
6.1 安全注意事项
- 不要尝试修改卡片数据,这可能导致卡片失效
- 某些卡片可能使用自定义密钥而非默认密钥
- 读取员工卡或门禁卡前请确保获得授权
6.2 进阶方向
如果你想进一步探索,可以考虑:
- 卡片克隆检测:比较多个卡片的UID和扇区数据
- 数据解析:尝试解析特定门禁系统的数据结构
- 写入功能:在获得授权的情况下写入测试数据
fun writeTestData(mifare: MifareClassic, sector: Int) { if (mifare.authenticateSectorWithKeyA(sector, MifareClassic.KEY_DEFAULT)) { val block = mifare.sectorToBlock(sector) val data = ByteArray(16) { 0x0F } mifare.writeBlock(block, data) } }7. 实际应用与调试技巧
在测试过程中,我发现不同品牌手机对NFC的支持有些差异。例如,华为手机通常需要将卡片放在摄像头附近,而三星手机则可能需要放在手机中部。如果遇到读取不稳定的情况,可以尝试:
- 调整卡片与手机的接触位置
- 确保手机屏幕已解锁
- 检查是否开启了NFC功能
- 尝试移除手机壳,特别是金属材质的
对于更复杂的门禁系统,可能需要分析多个扇区的数据模式。我曾经遇到过一种情况,门禁系统的有效信息存储在扇区1的块1中,而其他扇区都是空的或者包含厂商信息。