NFC是目前Android手机一个主流的配置硬件项,全称是Near Field Communication,中为近场通信,也叫做近距离无线通信技术。使用了NFC技术的设备(例如移动电话)可以在彼此靠近的情况下进行数据交换,是由非接触式射频识别(RFID)及互连互通技术整合演变而来。
开始开发之前必须要知道的知识
存储在NFC标签中的数据可以采用多种格式编写,但许多 Android 框架 API 都基于名为 NDEF(NFC 数据交换格式)的 NFC Forum 标准。。
简单说就是一种普遍的数据格式标准
(1) 读取器/写入器模式:支持 NFC 设备读取和/或写入被动 NFC 标签和贴纸。
(2)点对点模式:支持 NFC 设备与其他 NFC 对等设备交换数据;Android Beam 使用的就是此操作模式。
(3)卡模拟模式:支持 NFC 设备本身充当 NFC 卡。然后,可以通过外部 NFC 读取器(例如 NFC 销售终端)访问模拟 NFC 卡。
本篇案例使用的主要是读写卡,就是正常的读写卡需求,后面如果有机会接触到点对点和卡模拟的需求会在此篇做补充
通常情况下每种分类的标签(卡片)都支持一种或多重技术,
对应关系如下
技术 | 描述 | 卡种 |
---|---|---|
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标签支持的技术,这些后面解析数据的时候会用到,得到这些后就可以使用对应的类来解析标签数据。
开发中我们有对应的方法来获取此标签支持的解析方式,后面我会介绍。
(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时,操作会边得繁琐)
第二种更适合前台界面中的读卡,且多个应用的时候
根据自己的项目需求选择适合的实现方式。
首先设备要支持NFC权限开启的前提下 不论哪种方式,都是先刷卡,等待系统分发响应的Activity 拿到Tag或者 前台Activity捕获到TAG 。然后根据这个标签支持的技术去解析数据。
首先 Manifest中添加权限
<uses-permission android:name="android.permission.NFC" />
判断是否支持NFC、且打开功能
- NfcAdapter adapter = NfcAdapter.getDefaultAdapter(this);
- if (null == adapter) {
- Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show();
- } else if (!adapter.isEnabled()) {
- Intent intent = new Intent(Settings.ACTION_NFC_SETTINGS);
- // 根据包名打开对应的设置界面
- startActivity(intent);
- }
-
-
这里要介绍三种意图过滤器
前面【实现方式的分类】中对这种方式的特征做了介绍,这种由标签调度系统分发的方式需要在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。
添加意图过滤器
这是第一种 最简单和优先级最高的一种,已经满足需求了
- <activity
- android:name=".NfcActivity"
- android:exported="false">
- <intent-filter>
- <action android:name="android.nfc.action.NDEF_DISCOVERED" />
- </intent-filter>
- </activity>
当然也可以选择第二种
- <activity
- android:name=".NfcActivity"
- android:exported="false">
- <intent-filter>
- <action android:name="android.nfc.action.TECH_DISCOVERED" />
- </intent-filter>
- <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
- android:resource="@xml/filter_nfc" />
- </activity>
filter_nfc
这个文件就是TECH_DISCOVERED需要配置的,其中,tech-list之间是逻辑或关系,tech之间是逻辑与关系,与方案2中的techLists原理以及用途是类似。
- <?xml version="1.0" encoding="utf-8"?>
- <resources xmlns:android="http://schemas.android.com/apk/res/android">
-
- <tech-list>
- <tech>android.nfc.tech.Ndef</tech>
- <tech>android.nfc.tech.NfcA</tech>
- </tech-list>
-
- <tech-list>
- <tech>android.nfc.tech.NfcB</tech>
- </tech-list>
- <tech-list>
- <tech>android.nfc.tech.NfcF</tech>
- </tech-list>
- </resources>
还剩最后一种
- <activity
- android:name=".NfcActivity"
- android:exported="false">
- <intent-filter>
- <action android:name="android.nfc.action.TAG_DISCOVERED"/>
- <category android:name="android.intent.category.DEFAULT"/>
- </intent-filter>
- </activity>
这种一般用不到 感觉意义不大
然后在对应Activity的onCreate方法中就可以拿标签了
- class NfcActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_nfc)
- val adapter = NfcAdapter.getDefaultAdapter(this)
- if (null == adapter) {
- Toast.makeText(this, "不支持NFC功能", Toast.LENGTH_SHORT).show()
- } else if (!adapter.isEnabled) {
- val intent = Intent(Settings.ACTION_NFC_SETTINGS)
- // 根据包名打开对应的设置界面
- startActivity(intent)
- }
- val tag = intent.getParcelableExtra
(NfcAdapter.EXTRA_TAG) - }
- }
- class MainActivity : AppCompatActivity() {
- var mNfcAdapter: NfcAdapter? = null
- var pIntent: PendingIntent? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- initNfc()
-
-
- }
- private fun initNfc() {
- mNfcAdapter = M1CardUtils.isNfcAble(this)
- pIntent = PendingIntent.getActivity(this, 0,
- //在Manifest里或者这里设置当前activity启动模式,否则每次响应NFC事件,activity会重复创建
- Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
- }
-
- override fun onResume() {
- super.onResume()
- mNfcAdapter?.let {
- val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
- val tag = IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED)
- val tech = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED)
- val filters = arrayOf(ndef, tag, tech)
- val techList = arrayOf(
- arrayOf(
- "android.nfc.tech.Ndef",
- "android.nfc.tech.NfcA",
- "android.nfc.tech.NfcB",
- "android.nfc.tech.NfcF",
- "android.nfc.tech.NfcV",
- "android.nfc.tech.NdefFormatable",
- "android.nfc.tech.MifareClassic",
- "android.nfc.tech.MifareUltralight",
- "android.nfc.tech.NfcBarcode"
- )
- )
- it.enableForegroundDispatch(this, pIntent, filters, techList)
- XLog.d("开始捕获NFC数据")
- }
- }
- override fun onPause() {
- super.onPause()
- mNfcAdapter?.disableForegroundDispatch(this)
- }
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- //这里必须setIntent,set NFC事件响应后的intent才能拿到数据
- setIntent(intent)
- val tag = getIntent().getParcelableExtra
(NfcAdapter.EXTRA_TAG) - //M1CardUtils 我后面会贴出来的
- if (M1CardUtils.isMifareClassic(tag)) {
- try {
- val reader = M1CardUtils.readCard(tag)
- XLog.d("读卡内容:$reader")
- val data = reader.split("|")
- } catch (e: IOException) {
- e.printStackTrace()
- }
- }
- }
- }
不论使用哪种方式,当我们获取到TAG标签后,解析方式都是相同的,需要根据不同的卡类型选择对应的解析方式
如图 我们能拿到卡片的信息,如图,括起来的部分分别对应的是:
支持的技术类型
MifareClassic 类型
扇区存储空间
扇区数
扇区中的块数
这里说一下基础知识,不论是NFC还是读卡模块读,解析流程都是先寻卡,然后验证扇区的密码,取扇区的数据,比如已知要读的数据在2扇区,那么寻卡后验证时把要验证的扇区号、扇区的密码,和扇区的验证密码类型A/B传过去验证通过后,就可以读取数据了。
-
- import android.app.Activity
- import android.nfc.NfcAdapter
- import android.nfc.Tag
- import com.hjq.toast.ToastUtils
- import kotlin.Throws
- import android.nfc.tech.MifareClassic
- import com.elvishew.xlog.XLog
- import java.io.IOException
- import java.lang.StringBuilder
- import java.nio.charset.Charset
-
-
- object M1CardUtils {
- /**
- * 判断是否支持NFC
- *
- * @return
- */
- fun isNfcAble(mContext: Activity?): NfcAdapter? {
- val mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext)
- if (mNfcAdapter == null) {
- ToastUtils.show("设备不支持NFC!")
- }
- if (!mNfcAdapter!!.isEnabled) {
- ToastUtils.show("请在系统设置中先启用NFC功能!")
- }
- return mNfcAdapter
- }
-
- /**
- * 监测是否支持MifareClassic
- *
- * @param tag
- * @return
- */
- fun isMifareClassic(tag: Tag): Boolean {
- val techList = tag.techList
- var haveMifareUltralight = false
- for (tech in techList) {
- if (tech.contains("MifareClassic")) {
- haveMifareUltralight = true
- break
- }
- }
- if (!haveMifareUltralight) {
- ToastUtils.show("不支持MifareClassic")
- return false
- }
- return true
- }
-
- /**
- * 读取卡片信息
- *
- * @return
- */
- @Throws(IOException::class)
- fun readCard(tag: Tag?): String {
- val mifareClassic = MifareClassic.get(tag)
- return try {
- mifareClassic.connect()
- val metaInfo = StringBuilder()
- val gbk = Charset.forName("gbk")
-
- // 获取TAG中包含的扇区数
- val sectorCount = mifareClassic.sectorCount
- // for (int j = 0; j < sectorCount; j++) {
- val bCount: Int //当前扇区的块数
- var bIndex: Int //当前扇区第一块
- if (m1Auth(mifareClassic, 2)) {
- bCount = mifareClassic.getBlockCountInSector(2)
- bIndex = mifareClassic.sectorToBlock(2)
- var length = 0
- for (i in 0 until bCount) {
- val data = mifareClassic.readBlock(bIndex)
- for (i1 in data.indices) {
- if (data[i1] == 0.toByte()) {
- length = i1
- }
- }
- val dataString = String(data, 0, length, gbk).trim { it <= ' ' }
- metaInfo.append(dataString)
- bIndex++
- }
- } else {
- XLog.e("密码校验失败")
- }
- // }
- metaInfo.toString()
- } catch (e: IOException) {
- throw IOException(e)
- } finally {
- try {
- mifareClassic.close()
- } catch (e: IOException) {
- throw IOException(e)
- }
- }
- }
-
- /**
- * 改写数据
- *
- * @param block
- * @param blockbyte
- */
- @Throws(IOException::class)
- fun writeBlock(tag: Tag?, block: Int, blockbyte: ByteArray?): Boolean {
- val mifareClassic = MifareClassic.get(tag)
- try {
- mifareClassic.connect()
- if (m1Auth(mifareClassic, block / 4)) {
- mifareClassic.writeBlock(block, blockbyte)
- XLog.e("writeBlock", "写入成功")
- } else {
- XLog.e("密码是", "没有找到密码")
- return false
- }
- } catch (e: IOException) {
- throw IOException(e)
- } finally {
- try {
- mifareClassic.close()
- } catch (e: IOException) {
- throw IOException(e)
- }
- }
- return true
- }
-
- /**
- * 密码校验
- *
- * @param mTag
- * @param position
- * @return
- * @throws IOException
- */
- @Throws(IOException::class)
- fun m1Auth(mTag: MifareClassic, position: Int): Boolean {
- if (mTag.authenticateSectorWithKeyA(position, MifareClassic.KEY_DEFAULT)) {
- return true
- } else if (mTag.authenticateSectorWithKeyB(position, MifareClassic.KEY_DEFAULT)) {
- return true
- }
- return false
- }
-
-
- }
本案例中没有用到这种,只是需要M1所以不需要这个,这是别的大佬封装的类发出来供参考
- import android.nfc.tech.NfcV;
-
- import com.haiheng.core.util.ByteUtils;
-
- import java.io.IOException;
-
- /**
- * NfcV(ISO 15693)读写操作
- * 用法
- * NfcV mNfcV = NfcV.get(tag);
- * mNfcV.connect();
- *
- * NfcVUtils mNfcVutil = new NfcVUtils(mNfcV);
- * 取得UID
- * mNfcVutil.getUID();
- * 读取block在1位置的内容
- * mNfcVutil.readOneBlock(1);
- * 从位置7开始读2个block的内容
- * mNfcVutil.readBlocks(7, 2);
- * 取得block的个数
- * mNfcVutil.getBlockNumber();
- * 取得1个block的长度
- * mNfcVutil.getOneBlockSize();
- * 往位置1的block写内容
- * mNfcVutil.writeBlock(1, new byte[]{0, 0, 0, 0})
- *
- * @author Kelly
- * @version 1.0.0
- * @filename NfcVUtils.java
- * @time 2018/10/30 10:29
- * @copyright(C) 2018 song
- */
- public class NfcVUtils {
- private NfcV mNfcV;
- /**
- * UID数组行式
- */
- private byte[] ID;
- private String UID;
- private String DSFID;
- private String AFI;
- /**
- * block的个数
- */
- private int blockNumber;
- /**
- * 一个block长度
- */
- private int oneBlockSize;
- /**
- * 信息
- */
- private byte[] infoRmation;
-
- /**
- * * 初始化
- * * @param mNfcV NfcV对象
- * * @throws IOException
- *
- */
- public NfcVUtils(NfcV mNfcV) throws IOException {
- this.mNfcV = mNfcV;
- ID = this.mNfcV.getTag().getId();
- byte[] uid = new byte[ID.length];
- int j = 0;
- for (int i = ID.length - 1; i >= 0; i--) {
- uid[j] = ID[i];
- j++;
- }
- this.UID = ByteUtils.byteArrToHexString(uid);
- getInfoRmation();
- }
-
- public String getUID() {
- return UID;
- }
-
- /**
- * * 取得标签信息
- *
- */
- private byte[] getInfoRmation() throws IOException {
- byte[] cmd = new byte[10];
- cmd[0] = (byte) 0x22; // flag
- cmd[1] = (byte) 0x2B; // command
- System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
- infoRmation = mNfcV.transceive(cmd);
- blockNumber = infoRmation[12];
- oneBlockSize = infoRmation[13];
- AFI = ByteUtils.byteArrToHexString(new byte[]{infoRmation[11]});
- DSFID = ByteUtils.byteArrToHexString(new byte[]{infoRmation[10]});
- return infoRmation;
- }
-
- public String getDSFID() {
- return DSFID;
- }
-
- public String getAFI() {
- return AFI;
- }
-
- public int getBlockNumber() {
- return blockNumber + 1;
- }
-
- public int getOneBlockSize() {
- return oneBlockSize + 1;
- }
-
- /**
- * * 读取一个位置在position的block
- * * @param position 要读取的block位置
- * * @return 返回内容字符串
- * * @throws IOException
- *
- */
- public String readOneBlock(int position) throws IOException {
- byte cmd[] = new byte[11];
- cmd[0] = (byte) 0x22;
- cmd[1] = (byte) 0x20;
- System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
- cmd[10] = (byte) position;
- byte res[] = mNfcV.transceive(cmd);
- if (res[0] == 0x00) {
- byte block[] = new byte[res.length - 1];
- System.arraycopy(res, 1, block, 0, res.length - 1);
- return ByteUtils.byteArrToHexString(block);
- }
- return null;
- }
-
- /**
- * * 读取从begin开始end个block
- * * begin + count 不能超过blockNumber
- * * @param begin block开始位置
- * * @param count 读取block数量
- * * @return 返回内容字符串
- * * @throws IOException
- *
- */
- public String readBlocks(int begin, int count) throws IOException {
- if ((begin + count) > blockNumber) {
- count = blockNumber - begin;
- }
- StringBuffer data = new StringBuffer();
- for (int i = begin; i < count + begin; i++) {
- data.append(readOneBlock(i));
- }
- return data.toString();
- }
-
-
- /**
- * * 将数据写入到block,
- * * @param position 要写内容的block位置
- * * @param data 要写的内容,必须长度为blockOneSize
- * * @return false为写入失败,true为写入成功
- * * @throws IOException
- *
- */
- public boolean writeBlock(int position, byte[] data) throws IOException {
- byte cmd[] = new byte[15];
- cmd[0] = (byte) 0x22;
- cmd[1] = (byte) 0x21;
- System.arraycopy(ID, 0, cmd, 2, ID.length); // UID
- //block
- cmd[10] = (byte) position;
- //value
- System.arraycopy(data, 0, cmd, 11, data.length);
- byte[] rsp = mNfcV.transceive(cmd);
- if (rsp[0] == 0x00)
- return true;
- return false;
- }
- }
以上就是今天要讲的内容,文章中如有错误或者需要改进的地方欢迎补充指正,本文仅介绍了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