• Android NFC开发详解:NFC读卡实例解析及总结


    文章目录

    一、什么是NFC

    NFC是目前Android手机一个主流的配置硬件项,全称是Near Field Communication,中为近场通信,也叫做近距离无线通信技术。使用了NFC技术的设备(例如移动电话)可以在彼此靠近的情况下进行数据交换,是由非接触式射频识别(RFID)及互连互通技术整合演变而来。

    二、基础知识

    开始开发之前必须要知道的知识

    1.什么是NDEF?

    存储在NFC标签中的数据可以采用多种格式编写,但许多 Android 框架 API 都基于名为 NDEF(NFC 数据交换格式)的 NFC Forum 标准。。

    简单说就是一种普遍的数据格式标准

    2.NFC技术的操作模式

    (1) 读取器/写入器模式:支持 NFC 设备读取和/或写入被动 NFC 标签和贴纸。
    (2)点对点模式:支持 NFC 设备与其他 NFC 对等设备交换数据;Android Beam 使用的就是此操作模式。
    (3)卡模拟模式:支持 NFC 设备本身充当 NFC 卡。然后,可以通过外部 NFC 读取器(例如 NFC 销售终端)访问模拟 NFC 卡。

    本篇案例使用的主要是读写卡,就是正常的读写卡需求,后面如果有机会接触到点对点和卡模拟的需求会在此篇做补充

    3.标签的技术类型

    通常情况下每种分类的标签(卡片)都支持一种或多重技术,
    对应关系如下

    技术描述卡种
    NfcA提供NFC-A(ISO 14443-3A)的性能和I / O操作的访问。M1卡
    NfcB提供NFC-B (ISO 14443-3B)的性能和I / O操作的访问。
    NfcF提供 NFC-F (JIS 6319-4)的性能和I / O操作的访问。
    NfcV提供 NFC-V (ISO 15693)的性能和I / O操作的访问。15693卡
    IsoDep提供 ISO-DEP (ISO 14443-4)的性能和I / O操作的访问。CPU卡
    Ndef提供NFC标签已被格式化为NDEF的数据和操作的访问。
    NdefFormatable提供可能被格式化为NDEF的 formattable的标签。
    MifareClassic如果此Android设备支持MIFARE,提供访问的MIFARE Classic性能和I / O操作。m1卡
    MifareUltralight如果此Android设备支持MIFARE,提供访问的MIFARE 超轻性能和I / O操作。

    如下图,这是Demo 显示得NFC标签的信息。
    其中被我圈起来的部分是这个NFC标签支持的技术,这些后面解析数据的时候会用到,得到这些后就可以使用对应的类来解析标签数据。
     


    开发中我们有对应的方法来获取此标签支持的解析方式,后面我会介绍。

    4.实现方式的分类

    (1)Manifest注册方式:这种方式主要是在Manifest文件对应的activity下,配置过滤器,以响应不同类型NFC Action。使用这种方式,在刷卡时,如果手机中有多个应用都存在该NFC实现方案,系统会弹出能响应NFC事件的应用列表供用户选择,用户需要点击目标应用来响应本次NFC刷卡事件。

    (2)前台响应方式,无需Manifest重配置过滤器,直接使用前台activity来捕获NFC事件进行响应。

    区别如下:
    响应方式不同:Manifest注册的NFC事件由系统分发,需要选择应用去响应事件
           前台响应方式由前台activity来捕获NFC事件进行响应
    优先级不同:前台响应方式的优先级更高于Manifest注册的方式
         (如果安装多个Manifest注册的的App 和一个处于前台捕获方式的App,刷卡后 优先级最高的为前台捕获的,如果前台相应方式的App没有打开,那么将弹出列表让用户选择Manifest中注册了的符合条件的App)

    第一种更适合APP需要刷卡调用起来,并且设备没有多个响应NFC标签程序的物联网设备(因为普通安卓手机中自带的卡包APP、微信等优先级都比较高,当弹出列表选择响应的App时,操作会边得繁琐)

    第二种更适合前台界面中的读卡,且多个应用的时候
    根据自己的项目需求选择适合的实现方式。

    5.流程

    首先设备要支持NFC权限开启的前提下 不论哪种方式,都是先刷卡,等待系统分发响应的Activity 拿到Tag或者 前台Activity捕获到TAG 。然后根据这个标签支持的技术去解析数据。

    三、获取标签内容

    1.检查环境

    首先 Manifest中添加权限

        <uses-permission android:name="android.permission.NFC" />
    

    判断是否支持NFC、且打开功能

    1. NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
    2. if (null == adapter) {
    3. Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show();
    4. } else if (!adapter.isEnabled()) {
    5. Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
    6. // 根据包名打开对应的设置界面
    7. startActivity(intent);
    8. }

    2.获取NFC标签

    2.1 Manifest中注册的方式获取Tag

    这里要介绍三种意图过滤器
    前面【实现方式的分类】中对这种方式的特征做了介绍,这种由标签调度系统分发的方式需要在Manifest定义固定的意图过滤器。标签调度系统定义了三种 Intent,按优先级从高到低列出如下:

    ACTION_NDEF_DISCOVERED:如果扫描到包含 NDEF 负载的标签,并且可识别其类型,则使用此 Intent 启动 Activity。这是优先级最高的 Intent,标签调度系统会尽可能尝试使用此 Intent 启动 Activity,在行不通时才会尝试使用其他 Intent。

    ACTION_TECH_DISCOVERED:如果没有登记要处理 ACTION_NDEF_DISCOVERED Intent 的 Activity,则标签调度系统会尝试使用此 Intent 来启动应用。此外,如果扫描到的标签包含无法映射到 MIME 类型或 URI 的 NDEF 数据,或者该标签不包含 NDEF 数据,但它使用了已知的标签技术,那么也会直接启动此 Intent(无需先启动 ACTION_NDEF_DISCOVERED)。

    ACTION_TAG_DISCOVERED:如果没有处理 ACTION_NDEF_DISCOVERED 或者 ACTION_TECH_DISCOVERED Intent 的 Activity,则使用此 Intent 启动 Activity。

    添加意图过滤器
    这是第一种 最简单和优先级最高的一种,已经满足需求了

    1. <activity
    2. android:name=".NfcActivity"
    3. android:exported="false">
    4. <intent-filter>
    5. <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    6. </intent-filter>
    7. </activity>

    当然也可以选择第二种

    1. <activity
    2. android:name=".NfcActivity"
    3. android:exported="false">
    4. <intent-filter>
    5. <action android:name="android.nfc.action.TECH_DISCOVERED" />
    6. </intent-filter>
    7. <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
    8. android:resource="@xml/filter_nfc" />
    9. </activity>

    filter_nfc
    这个文件就是TECH_DISCOVERED需要配置的,其中,tech-list之间是逻辑或关系,tech之间是逻辑与关系,与方案2中的techLists原理以及用途是类似。

    1. <?xml version="1.0" encoding="utf-8"?>
    2. <resources xmlns:android="http://schemas.android.com/apk/res/android">
    3. <tech-list>
    4. <tech>android.nfc.tech.Ndef</tech>
    5. <tech>android.nfc.tech.NfcA</tech>
    6. </tech-list>
    7. <tech-list>
    8. <tech>android.nfc.tech.NfcB</tech>
    9. </tech-list>
    10. <tech-list>
    11. <tech>android.nfc.tech.NfcF</tech>
    12. </tech-list>
    13. </resources>

    还剩最后一种

    1. <activity
    2. android:name=".NfcActivity"
    3. android:exported="false">
    4. <intent-filter>
    5. <action android:name="android.nfc.action.TAG_DISCOVERED"/>
    6. <category android:name="android.intent.category.DEFAULT"/>
    7. </intent-filter>
    8. </activity>

    这种一般用不到 感觉意义不大

    然后在对应Activity的onCreate方法中就可以拿标签了

    1. class NfcActivity : AppCompatActivity() {
    2. override fun onCreate(savedInstanceState: Bundle?) {
    3. super.onCreate(savedInstanceState)
    4. setContentView(R.layout.activity_nfc)
    5. val adapter = NfcAdapter.getDefaultAdapter(this)
    6. if (null == adapter) {
    7. Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show()
    8. } else if (!adapter.isEnabled) {
    9. val intent = Intent(Settings.ACTION_NFC_SETTINGS)
    10. // 根据包名打开对应的设置界面
    11. startActivity(intent)
    12. }
    13. val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
    14. }
    15. }

    2.1 前台Activity捕获的方式获取Tag

    1. class MainActivity : AppCompatActivity() {
    2. var mNfcAdapter: NfcAdapter? = null
    3. var pIntent: PendingIntent? = null
    4. override fun onCreate(savedInstanceState: Bundle?) {
    5. super.onCreate(savedInstanceState)
    6. initNfc()
    7. }
    8. private fun initNfc() {
    9. mNfcAdapter = M1CardUtils.isNfcAble(this)
    10. pIntent = PendingIntent.getActivity(this, 0,
    11. //在Manifest里或者这里设置当前activity启动模式,否则每次响应NFC事件,activity会重复创建
    12. Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
    13. }
    14. override fun onResume() {
    15. super.onResume()
    16. mNfcAdapter?.let {
    17. val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
    18. val tag = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
    19. val tech = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)
    20. val filters = arrayOf(ndef, tag, tech)
    21. val techList = arrayOf(
    22. arrayOf(
    23. "android.nfc.tech.Ndef",
    24. "android.nfc.tech.NfcA",
    25. "android.nfc.tech.NfcB",
    26. "android.nfc.tech.NfcF",
    27. "android.nfc.tech.NfcV",
    28. "android.nfc.tech.NdefFormatable",
    29. "android.nfc.tech.MifareClassic",
    30. "android.nfc.tech.MifareUltralight",
    31. "android.nfc.tech.NfcBarcode"
    32. )
    33. )
    34. it.enableForegroundDispatch(this, pIntent, filters, techList)
    35. XLog.d("开始捕获NFC数据")
    36. }
    37. }
    38. override fun onPause() {
    39. super.onPause()
    40. mNfcAdapter?.disableForegroundDispatch(this)
    41. }
    42. override fun onNewIntent(intent: Intent?) {
    43. super.onNewIntent(intent)
    44. //这里必须setIntent,set NFC事件响应后的intent才能拿到数据
    45. setIntent(intent)
    46. val tag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG)
    47. //M1CardUtils 我后面会贴出来的
    48. if (M1CardUtils.isMifareClassic(tag)) {
    49. try {
    50. val reader = M1CardUtils.readCard(tag)
    51. XLog.d("读卡内容:$reader")
    52. val data = reader.split("|")
    53. } catch (e: IOException) {
    54. e.printStackTrace()
    55. }
    56. }
    57. }
    58. }

    四、解析标签数据

    不论使用哪种方式,当我们获取到TAG标签后,解析方式都是相同的,需要根据不同的卡类型选择对应的解析方式
     


    如图 我们能拿到卡片的信息,如图,括起来的部分分别对应的是:
    支持的技术类型
    MifareClassic 类型
    扇区存储空间
    扇区数
    扇区中的块数

    1. M1卡解析

    这里说一下基础知识,不论是NFC还是读卡模块读,解析流程都是先寻卡,然后验证扇区的密码,取扇区的数据,比如已知要读的数据在2扇区,那么寻卡后验证时把要验证的扇区号、扇区的密码,和扇区的验证密码类型A/B传过去验证通过后,就可以读取数据了。

    1. import android.app.Activity
    2. import android.nfc.NfcAdapter
    3. import android.nfc.Tag
    4. import com.hjq.toast.ToastUtils
    5. import kotlin.Throws
    6. import android.nfc.tech.MifareClassic
    7. import com.elvishew.xlog.XLog
    8. import java.io.IOException
    9. import java.lang.StringBuilder
    10. import java.nio.charset.Charset
    11. object M1CardUtils {
    12. /**
    13. * 判断是否支持NFC
    14. *
    15. * @return
    16. */
    17. fun isNfcAble(mContext: Activity?): NfcAdapter? {
    18. val mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext)
    19. if (mNfcAdapter == null) {
    20. ToastUtils.show("设备不支持NFC!")
    21. }
    22. if (!mNfcAdapter!!.isEnabled) {
    23. ToastUtils.show("请在系统设置中先启用NFC功能!")
    24. }
    25. return mNfcAdapter
    26. }
    27. /**
    28. * 监测是否支持MifareClassic
    29. *
    30. * @param tag
    31. * @return
    32. */
    33. fun isMifareClassic(tag: Tag): Boolean {
    34. val techList = tag.techList
    35. var haveMifareUltralight = false
    36. for (tech in techList) {
    37. if (tech.contains("MifareClassic")) {
    38. haveMifareUltralight = true
    39. break
    40. }
    41. }
    42. if (!haveMifareUltralight) {
    43. ToastUtils.show("不支持MifareClassic")
    44. return false
    45. }
    46. return true
    47. }
    48. /**
    49. * 读取卡片信息
    50. *
    51. * @return
    52. */
    53. @Throws(IOException::class)
    54. fun readCard(tag: Tag?): String {
    55. val mifareClassic = MifareClassic.get(tag)
    56. return try {
    57. mifareClassic.connect()
    58. val metaInfo = StringBuilder()
    59. val gbk = Charset.forName("gbk")
    60. // 获取TAG中包含的扇区数
    61. val sectorCount = mifareClassic.sectorCount
    62. // for (int j = 0; j < sectorCount; j++) {
    63. val bCount: Int //当前扇区的块数
    64. var bIndex: Int //当前扇区第一块
    65. if (m1Auth(mifareClassic, 2)) {
    66. bCount = mifareClassic.getBlockCountInSector(2)
    67. bIndex = mifareClassic.sectorToBlock(2)
    68. var length = 0
    69. for (i in 0 until bCount) {
    70. val data = mifareClassic.readBlock(bIndex)
    71. for (i1 in data.indices) {
    72. if (data[i1] == 0.toByte()) {
    73. length = i1
    74. }
    75. }
    76. val dataString = String(data, 0, length, gbk).trim { it <= ' ' }
    77. metaInfo.append(dataString)
    78. bIndex++
    79. }
    80. } else {
    81. XLog.e("密码校验失败")
    82. }
    83. // }
    84. metaInfo.toString()
    85. } catch (e: IOException) {
    86. throw IOException(e)
    87. } finally {
    88. try {
    89. mifareClassic.close()
    90. } catch (e: IOException) {
    91. throw IOException(e)
    92. }
    93. }
    94. }
    95. /**
    96. * 改写数据
    97. *
    98. * @param block
    99. * @param blockbyte
    100. */
    101. @Throws(IOException::class)
    102. fun writeBlock(tag: Tag?, block: Int, blockbyte: ByteArray?): Boolean {
    103. val mifareClassic = MifareClassic.get(tag)
    104. try {
    105. mifareClassic.connect()
    106. if (m1Auth(mifareClassic, block / 4)) {
    107. mifareClassic.writeBlock(block, blockbyte)
    108. XLog.e("writeBlock", "写入成功")
    109. } else {
    110. XLog.e("密码是", "没有找到密码")
    111. return false
    112. }
    113. } catch (e: IOException) {
    114. throw IOException(e)
    115. } finally {
    116. try {
    117. mifareClassic.close()
    118. } catch (e: IOException) {
    119. throw IOException(e)
    120. }
    121. }
    122. return true
    123. }
    124. /**
    125. * 密码校验
    126. *
    127. * @param mTag
    128. * @param position
    129. * @return
    130. * @throws IOException
    131. */
    132. @Throws(IOException::class)
    133. fun m1Auth(mTag: MifareClassic, position: Int): Boolean {
    134. if (mTag.authenticateSectorWithKeyA(position, MifareClassic.KEY_DEFAULT)) {
    135. return true
    136. } else if (mTag.authenticateSectorWithKeyB(position, MifareClassic.KEY_DEFAULT)) {
    137. return true
    138. }
    139. return false
    140. }
    141. }

    2. iso15693卡解析

    本案例中没有用到这种,只是需要M1所以不需要这个,这是别的大佬封装的类发出来供参考

    1. import android.nfc.tech.NfcV;
    2. import com.haiheng.core.util.ByteUtils;
    3. import java.io.IOException;
    4. /**
    5. * NfcV(ISO 15693)读写操作
    6. * 用法
    7. * NfcV mNfcV = NfcV.get(tag);
    8. * mNfcV.connect();
    9. *

    10. * NfcVUtils mNfcVutil = new NfcVUtils(mNfcV);
    11. * 取得UID
    12. * mNfcVutil.getUID();
    13. * 读取block在1位置的内容
    14. * mNfcVutil.readOneBlock(1);
    15. * 从位置7开始读2个block的内容
    16. * mNfcVutil.readBlocks(7, 2);
    17. * 取得block的个数
    18. * mNfcVutil.getBlockNumber();
    19. * 取得1个block的长度
    20. * mNfcVutil.getOneBlockSize();
    21. * 往位置1的block写内容
    22. * mNfcVutil.writeBlock(1, new byte[]{0, 0, 0, 0})
    23. *
    24. * @author Kelly
    25. * @version 1.0.0
    26. * @filename NfcVUtils.java
    27. * @time 2018/10/30 10:29
    28. * @copyright(C) 2018 song
    29. */
    30. public class NfcVUtils {
    31. private NfcV mNfcV;
    32. /**
    33. * UID数组行式
    34. */
    35. private byte[] ID;
    36. private String UID;
    37. private String DSFID;
    38. private String AFI;
    39. /**
    40. * block的个数
    41. */
    42. private int blockNumber;
    43. /**
    44. * 一个block长度
    45. */
    46. private int oneBlockSize;
    47. /**
    48. * 信息
    49. */
    50. private byte[] infoRmation;
    51. /**
    52. * * 初始化
    53. * * @param mNfcV NfcV对象
    54. * * @throws IOException
    55. *
    56. */
    57. public NfcVUtils(NfcV mNfcV) throws IOException {
    58. this.mNfcV = mNfcV;
    59. ID = this.mNfcV.getTag().getId();
    60. byte[] uid = new byte[ID.length];
    61. int j = 0;
    62. for (int i = ID.length - 1; i >= 0; i--) {
    63. uid[j] = ID[i];
    64. j++;
    65. }
    66. this.UID = ByteUtils.byteArrToHexString(uid);
    67. getInfoRmation();
    68. }
    69. public String getUID() {
    70. return UID;
    71. }
    72. /**
    73. * * 取得标签信息
    74. *
    75. */
    76. private byte[] getInfoRmation() throws IOException {
    77. byte[] cmd = new byte[10];
    78. cmd[0] = (byte) 0x22; // flag
    79. cmd[1] = (byte) 0x2B; // command
    80. System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
    81. infoRmation = mNfcV.transceive(cmd);
    82. blockNumber = infoRmation[12];
    83. oneBlockSize = infoRmation[13];
    84. AFI = ByteUtils.byteArrToHexString(new byte[]{infoRmation[11]});
    85. DSFID = ByteUtils.byteArrToHexString(new byte[]{infoRmation[10]});
    86. return infoRmation;
    87. }
    88. public String getDSFID() {
    89. return DSFID;
    90. }
    91. public String getAFI() {
    92. return AFI;
    93. }
    94. public int getBlockNumber() {
    95. return blockNumber + 1;
    96. }
    97. public int getOneBlockSize() {
    98. return oneBlockSize + 1;
    99. }
    100. /**
    101. * * 读取一个位置在position的block
    102. * * @param position 要读取的block位置
    103. * * @return 返回内容字符串
    104. * * @throws IOException
    105. *
    106. */
    107. public String readOneBlock(int position) throws IOException {
    108. byte cmd[] = new byte[11];
    109. cmd[0] = (byte) 0x22;
    110. cmd[1] = (byte) 0x20;
    111. System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
    112. cmd[10] = (byte) position;
    113. byte res[] = mNfcV.transceive(cmd);
    114. if (res[0] == 0x00) {
    115. byte block[] = new byte[res.length - 1];
    116. System.arraycopy(res, 1, block, 0, res.length - 1);
    117. return ByteUtils.byteArrToHexString(block);
    118. }
    119. return null;
    120. }
    121. /**
    122. * * 读取从begin开始end个block
    123. * * begin + count 不能超过blockNumber
    124. * * @param begin block开始位置
    125. * * @param count 读取block数量
    126. * * @return 返回内容字符串
    127. * * @throws IOException
    128. *
    129. */
    130. public String readBlocks(int begin, int count) throws IOException {
    131. if ((begin + count) > blockNumber) {
    132. count = blockNumber - begin;
    133. }
    134. StringBuffer data = new StringBuffer();
    135. for (int i = begin; i < count + begin; i++) {
    136. data.append(readOneBlock(i));
    137. }
    138. return data.toString();
    139. }
    140. /**
    141. * * 将数据写入到block,
    142. * * @param position 要写内容的block位置
    143. * * @param data 要写的内容,必须长度为blockOneSize
    144. * * @return false为写入失败,true为写入成功
    145. * * @throws IOException
    146. *
    147. */
    148. public boolean writeBlock(int position, byte[] data) throws IOException {
    149. byte cmd[] = new byte[15];
    150. cmd[0] = (byte) 0x22;
    151. cmd[1] = (byte) 0x21;
    152. System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
    153. //block
    154. cmd[10] = (byte) position;
    155. //value
    156. System.arraycopy(data, 0, cmd, 11, data.length);
    157. byte[] rsp = mNfcV.transceive(cmd);
    158. if (rsp[0] == 0x00)
    159. return true;
    160. return false;
    161. }
    162. }

    总结

    以上就是今天要讲的内容,文章中如有错误或者需要改进的地方欢迎补充指正,本文仅介绍了NFC的使用和M1卡的读取解析场景,关于NFC的历史、卡片类型、Intent filter类型详细描述,其他使用场景等可以参考更多文档,这里贴出来几个我看到的对我很有帮助的文章,也欢迎大家多做参考,

    NFC 各种卡类型、区别、历史介绍
    https://zhuanlan.zhihu.com/p/344426747
    各种官方资料中文说明
    https://blog.csdn.net/u013164293/article/details/124474247?spm=1001.2014.3001.5506

  • 相关阅读:
    18-Java迭代器模式 ( Iterator Pattern )
    Java简系 - Java基础概念「二」
    【OpenCV实现图像:使用OpenCV进行物体轮廓排序】
    详解JS中 call 方法的实现
    好书分享丨区块链的骨骼——密码技术
    还能这样操作?勒索软件团伙向监管部门举报受害者!
    The list of sources could not be read
    10个微服务设计模式
    使用 PyTorch 的计算机视觉简介 (6/6)
    Prometheus Operator 配置报警
  • 原文地址:https://blog.csdn.net/qq_25462179/article/details/133704531