小白也能看懂的 Android nfc 开发

2,599 阅读30分钟

前言:

最近公司业务开发比较忙,做了一些公司比较看重的 Wi-Fi 设备的批量配网新模式、单点设备的群组功能等等。因此好久没有更新文档了。最近支付宝在市面上推出了 NFC 的接触式无感支付的事情不知道大家都用过了没有,因此小弟也研究了一下 android NFC 相关的开发文档,市面上很多的文章都是拷贝来拷贝去,android 的官方文档写的也比较吃力(这里的吃力是因为如果没有一些基础看起来还是比较费劲的意思),因此为了自己再巩固一下知识,也方便后续有想要了解 NFC 开发的同学能找到个比较好的入门资料,这里就做一个小结,不出意外,该篇章也是今年最后一篇的掘金文章了。好了,那么废话不多说,我们开始今天我们的主题:NFC

什么是 NFC ?

近场通信(英语:Near-field communication,NFC),又称感应通信近距离无线通信近距离通信,是一套通信协议,让两个电子设备(其中一个通常是移动设备,例如智能手机)在相距几厘米之内进行通信

以上这段话是摘自维基百科里面关于 NFC 的简介(大家也可以自己 google 和 baidu)。意思就是,NFC 是一种近距离的无线通信技术,他的特点是:接触式几厘米近距离 的一种通信协议。没错,他就是一种无线通信协议。
讲到这里可能大家还是有点迷惑,我们平日里也听过 NFC,但是他到底是个什么东西的(通常本人去理解一个新事物的时候,喜欢化抽象为具体)?为了让大家更加了解 NFC,我们找个大家熟知的一个东西(也是自己比较熟悉的东西,嘿嘿)——蓝牙。我们知道,蓝牙、Wi-Fi 都是一种用无线电波传输的通信协议,只不过蓝牙是工作在 2.4Ghz 频道的,Wi-Fi 是工作在 2.4Ghz/5Ghz 频道上的。因此 NFC 和蓝牙/Wi-Fi 的区别大家可以理解成是你在老式汽车里面听到的不同频道的广播频道一样。

image.png

那么既然都说到蓝牙了,那么就对蓝牙和 NFC 进行一个对比:

不同点

特性NFC蓝牙(Bluetooth)
频率13.56 MHz(高频)2.4 GHz(ISM 频段)
通信范围通常为 4-10 厘米(接触式或非常近的距离)最远可达 100 米(视版本和功率而定)
配对方式无需配对,直接通信(主动或被动模式)通常需要配对,建立连接后才能通信
传输速度较低(106 kbps、212 kbps、424 kbps)更高(BLE 为 125 kbps 至 2 Mbps,经典蓝牙更高)
功耗极低,几乎可以忽略BLE 功耗较低,但经典蓝牙功耗相对更高
通信模式主动模式(两端都有电源)或被动模式(如 NFC 卡无需电源)主动模式(双方都需要供电)
典型应用场景付款、门禁、身份认证、数据共享、移动支付音频传输、数据共享、物联网设备连接、文件传输等

具体的实际生活当中的应用

最近支付宝推出的接触支付,我们常用的公交车的 IC 卡、回家用的门禁卡、电梯卡等等,都是应用了 NFC 的这项技术。

支付宝近距离支付公交 IC 卡电梯卡
image.pngimage.pngimage.png

NFC 基础知识

看到这里相信大家对 NFC 有了一个基本的概念,那么我们继续了解一下,NFC 的一些基础知识。

NFC 通信的角色

跟蓝牙类似,当我们使用 NFC 通信的时候,我们要想完成一次 NFC 的通信,就是读/写,因此在这个过程一定要有两个角色,那就是提供数据载体的 NFC-Card 和 数据操作的 NFC 操作机器。我们拿上面的应用例子做个类比。

  • 公交 IC 卡:我们使用公交 IC 卡去靠近公交车上的车费扣款仪,就完成了一次公交付费。这里我们的 IC卡就是一个提供数据载体的 NFC-Card车费扣款仪就是数据操作的 NFC 操作机器
  • 电梯卡/门锁卡:我们使用电梯卡/门锁卡去接触一下我们电梯或门锁,此时电梯和门锁就可以自动解锁了。这里我们的电梯卡/门锁卡就是一个提供数据载体的 NFC-Card,电梯或者门锁就是数据操作的 NFC 操作机器
  • 支付宝:我们去便利店付钱的时候,店员向机器里面输入需要支付的价格,然后我们通过手机碰一下,就可以完成支付。这个场景我思考了一下,其实手机和机器都有可能是提供数据载体的 NFC-Card,而另外一个是数据操作的 NFC 操作机器。后来我仔细思考了下,我认为,还是支付宝的那个仪器充当的是提供数据载体的 NFC-Card,然后由手机读取要付的款项进行支付。因为,不管是出于 android / iOS 的实现,、对于仪器的 OTA 还是安全性来说,这种模式我认为是更佳的。

NFC 通信标准

了解了 NFC 的通讯过程,我们继续了解下,NFC 的通信的数据格式是如何的。 NFC(近场通信)的通信标准由多个国际标准组织定义,主要包括 ISO/IECECMANFC ForumEMVCo 等。以下是 NFC 的主要通信标准及其详细介绍:


1. ISO/IEC 标准

ISO(国际标准化组织)和 IEC(国际电工委员会)定义了 NFC 的基础通信协议。这些标准定义了 NFC 的物理层和数据链路层。

标准编号描述
ISO/IEC 14443- 定义近场通信的基础协议,用于非接触式智能卡通信(如 NFC 卡)。 - 包括 Type A 和 Type B 两种类型。
ISO/IEC 15693- 定义用于近场通信的高频长距离通信协议,适用于 NFC 的标签和智能设备通信。
ISO/IEC 18092- 定义 NFC 的主动和被动模式通信,包括点对点通信。
ISO/IEC 21481- 定义 NFC 的通信干扰防护机制,支持与 RFID 系统的兼容性。

2. ECMA 标准

ECMA(欧洲计算机制造商协会)扩展和细化了 NFC 的通信协议,提供了更具体的实现细节。

标准编号描述
ECMA-340- 定义 NFC 的物理层和链路层协议,是 ISO/IEC 18092 的技术基础。
ECMA-352- 定义 NFC 的活动模式和数据交换机制。
ECMA-356- 定义 NFC 与 ISO/IEC 14443 的互操作性。

3. NFC Forum 标准

NFC Forum 是一个行业联盟,定义了 NFC 的互操作性、标签格式以及应用规范。

规范名称描述
NFC Data Exchange Format (NDEF)- 定义 NFC 数据交换的格式,确保不同设备之间的兼容性和数据可解析性。
Tag Operation Specifications- 定义 NFC 标签的格式和操作规范,支持 Type 1 至 Type 5 的标签。
RTD(Record Type Definition)- 定义常见数据类型(如文本、URI)的存储和解析方式。

4. EMVCo 标准

EMVCo(Europay, Mastercard, Visa)定义了 NFC 在支付场景中的标准。其主要作用是确保 NFC 设备与支付终端的互操作性。

标准编号描述
EMV Contactless Specifications- 定义 NFC 用于支付时的通信协议和安全规范,广泛用于移动支付(如 Apple Pay、Google Pay)。

NFC 模式和支持的标准

NFC 支持以下三种工作模式,每种模式对应的通信标准如下:

模式描述相关标准
读卡器模式- NFC 设备作为读取器,读取非接触式标签的数据。ISO/IEC 14443、ISO/IEC 15693
卡模拟模式- NFC 设备模拟成智能卡,与终端设备进行交互(如支付场景)。ISO/IEC 14443、EMVCo
点对点模式- 两个 NFC 设备之间点对点传输数据。ISO/IEC 18092

NFC 的通信标准涵盖了物理层、协议层和应用层,支持各种应用场景(如支付、数据交换、门禁等)。其中,ISO/IEC 14443NFC Forum 的 NDEF 规范 是最常见和核心的标准,确保了 NFC 的设备互操作性和广泛兼容性。

许多 Android 框架 API 都基于名为 NDEF(NFC 数据交换格式)的 NFC Forum 标准。

通讯数据格式

了解了上述的 NFC 的标准协议,我不知道大家到底对 NFC 的概念是否还是出于一种比较抽象的概念,至少当时我看文档的时候,虽然已经知道 NFC 是个什么东西了,但是我脑袋里还是对 NFC 的数据比较抽象。NFC Forum 的格式到底是怎么样的?那么我们就来了解下,NFC 到底传输了啥东西。

我们先来看几个数据例子:

以下是几种常见的 NFC NDEF 数据格式示例,包括文本记录URI记录URL记录等。NDEF(NFC Data Exchange Format)是 NFC Forum 定义的标准数据格式,用于设备之间的数据交换。


1. 文本记录 (Text Record)

描述:
存储一段简单的文本,如 "Hello, NFC!".

数据结构:

  • TNF (Type Name Format): 0x01 (Well-known Type)
  • Type: T (文本)
  • Payload: 包含语言编码和文本内容

NDEF 数据样例 (Hex):

D1 01 0F 54 02 65 6E 48 65 6C 6C 6F 2C 20 4E 46 43 21

解释:

  • D1: TNF = Well-known, 单条消息,短格式
  • 01: 类型长度 = 1 字节
  • 0F: 有效载荷长度 = 15 字节
  • 54: 类型标识符,表示文本 (T)
  • 02: 状态字节,语言编码长度为 2
  • 65 6E: 语言编码为 en (英语)
  • 48 65 6C 6C 6F 2C 20 4E 46 43 21: 内容为 Hello, NFC!

2. URI 记录 (URI Record)

描述:
存储一个 URI,如 https://example.com.

数据结构:

  • TNF: 0x01 (Well-known Type)
  • Type: U (URI)
  • Payload: 包含 URI 前缀和内容

NDEF 数据样例 (Hex):

D1 01 0D 55 03 65 78 61 6D 70 6C 65 2E 63 6F 6D

解释:

  • D1: TNF = Well-known, 单条消息,短格式
  • 01: 类型长度 = 1 字节
  • 0D: 有效载荷长度 = 13 字节
  • 55: 类型标识符,表示 URI (U)
  • 03: URI 标识符码(https://
  • 65 78 61 6D 70 6C 65 2E 63 6F 6D: 内容为 example.com

完整 URI: https://example.com


3. URL 记录 (URL Record)

描述:
存储一个完整的 URL,如 http://www.nfcforum.org.

数据结构: 与 URI 记录类似,但通常省略协议前缀。

NDEF 数据样例 (Hex):

D1 01 13 55 01 6E 66 63 66 6F 72 75 6D 2E 6F 72 67

解释:

  • D1: TNF = Well-known, 单条消息,短格式
  • 01: 类型长度 = 1 字节
  • 13: 有效载荷长度 = 19 字节
  • 55: 类型标识符,表示 URI (U)
  • 01: URI 标识符码(http://www.
  • 6E 66 63 66 6F 72 75 6D 2E 6F 72 67: 内容为 nfcforum.org

完整 URL: http://www.nfcforum.org


4. MIME 类型记录 (MIME Type Record)

描述:
存储一段二进制数据,附加 MIME 类型说明。

数据结构:

  • TNF: 0x02 (MIME Type)
  • Type: MIME 类型字符串
  • Payload: MIME 数据

NDEF 数据样例 (Hex):

D2 12 1B 61 70 70 6C 69 63 61 74 69 6F 6E 2F 6A 73 6F 6E 7B 22 6B 65 79 22 3A 22 76 61 6C 75 65 22 7D

解释:

  • D2: TNF = MIME Type, 单条消息,短格式
  • 12: 类型长度 = 18 字节
  • 1B: 有效载荷长度 = 27 字节
  • 61 70 70 6C 69 63 61 74 69 6F 6E 2F 6A 73 6F 6E: MIME 类型为 application/json
  • 7B 22 6B 65 79 22 3A 22 76 61 6C 75 65 22 7D: 内容为 JSON 数据 {"key":"value"}

5. Smart Poster 记录

描述:
包含多个 NDEF 记录,存储复合数据(如 URL 和标题)。

数据结构:

  • Smart Poster 是一个 NDEF Message,包含多个子记录。
  • 子记录可能包括 URI、文本描述、图标等。

NDEF 数据样例 (Hex):

D1 02 2F 53 D1 01 0D 55 03 6E 66 63 66 6F 72 75 6D 2E 6F 72 67 D1 01 0F 54 02 65 6E 53 6D 61 72 74 20 50 6F 73 74 65 72

解释:

  1. D1 02 2F 53: Smart Poster 记录开始

    • D1: TNF = Well-known, 多条消息
    • 02 2F: 包含多个子记录,长度 47 字节
    • 53: 类型标识符,表示 Smart Poster (S)
  2. 子记录 1: URI

    • D1 01 0D 55 03 6E 66 63 66 6F 72 75 6D 2E 6F 72 67: URI 子记录,内容为 http://nfcforum.org
  3. 子记录 2: 文本

    • D1 01 0F 54 02 65 6E 53 6D 61 72 74 20 50 6F 73 74 65 72: 文本子记录,内容为 Smart Poster

完整内容:

  • URI: http://nfcforum.org
  • 文本: Smart Poster

这些样本展示了 NFC NDEF 数据的不同用途和结构。根据你的应用场景,可以选择合适的 NDEF 类型和数据格式来实现 NFC 数据交换。例如:

  • 使用文本记录实现简单的文本传输。
  • 使用URI 记录Smart Poster记录传递链接或复合数据。
  • 使用MIME 类型记录传递特定格式的数据(如 JSON、图片等)。

相信看了下以上的数据,大家对 NFC 的大致数据有个比较具体的印象了。哦,原来他是这么传递数据的,可以传输文本、链接、图片、JSON 甚至复合数据。

不过,既然都已经看到了这里了,我们就继续对协议本身继续有个更深入的了解(其实 android 框架里面已经帮我们封装好了数据的组装和解析,但是听我 iOS 的同事说 iOS 是需要自己解析的,因此我认为还是要知其所以然)。

我们继续介绍下 NDEF 的协议数据格式。大家有个大致了解,如果在开发过程中遇到问题,也能知道如何解决。

NDEF 数据格式简单介绍

NDEF 通用数据格式结构

一个 NDEF 消息由多个 NDEF 记录 (NDEF Record) 组成,每条记录包含以下关键部分:

NDEF 记录通用结构

┌─────────────┬────────────┬───────────────┬─────────────────┬─────────────────────────────┐
│ Header Byte │ Type Length│ Payload Length│ ID Length (可选) | Type / ID / Payload Data    │
└─────────────┴────────────┴───────────────┴─────────────────┴─────────────────────────────┘

字段说明

NDEF Header Byte (1 字节) 是每个 NDEF 记录的开头,它定义了记录的元信息和数据的处理方式。Header Byte 包括以下 8 位,每个位或位组都有特定的含义。


1. Header Byte 位结构
7   6   5   4   3   2   1   0
MB  ME  CF  SR  IL  TNF TNF TNF
字段含义
  • MB (Message Begin, 第 7 位) : 是否为消息的第一个记录。

    • 1: 表示这是整个消息的第一条记录。
    • 0: 表示不是消息的第一条记录。
    • 示例: 如果有多条记录组成一个消息,只有第一条记录的 MB 位会被设置为 1
  • ME (Message End, 第 6 位) : 是否为消息的最后一个记录。

    • 1: 表示这是整个消息的最后一条记录。
    • 0: 表示不是消息的最后一条记录。
    • 示例: 如果有多条记录组成一个消息,只有最后一条记录的 ME 位会被设置为 1
  • CF (Chunk Flag, 第 5 位) : 分块标志(Chunk Flag),用于表示数据是否被分块。

    • 1: 表示这是分块记录的一部分。
    • 0: 表示数据未被分块。
    • 说明: 分块模式下,一条大记录会被分成多个小记录发送,直到最后一块数据时 CF 置为 0
  • SR (Short Record, 第 4 位) : 短记录标志,用于表示 Payload Length 是否为 1 字节(短记录)还是 4 字节(长记录)。

    • 1: 表示是短记录,Payload Length 占 1 字节。

    • 0: 表示是长记录,Payload Length 占 4 字节。

    • 示例:

      • 短记录: 数据长度较短(<= 255 字节)。
      • 长记录: 数据长度较长(> 255 字节)。
  • IL (ID Length Flag, 第 3 位) : ID 长度标志,表示是否存在 ID Length 字段。

    • 1: 表示记录包含 ID 字段,并紧随 Type Length 之后。
    • 0: 表示记录不包含 ID 字段。
  • TNF (Type Name Format, 第 0-2 位) : 类型名格式,定义记录的类型数据如何解释。TNF 取值如下:

    • 0x0: 空类型(Empty Record)
    • 0x1: Well-Known 类型(标准类型,如 TextURI
    • 0x2: MIME 类型(如 application/json
    • 0x3: URI 类型(外部类型)
    • 0x4: 外部类型(External Type)
    • 0x5: 未定义类型
    • 0x6: 保留类型
    • 0x7: 未知类型(Unknown Type)

Header Byte 示例

以下是一些示例 Header Byte 的解析:

示例 1: 0xD1

  • 二进制表示: 11010001

    • MB: 1(消息的第一条记录)
    • ME: 1(消息的最后一条记录)
    • CF: 0(数据未分块)
    • SR: 1(短记录)
    • IL: 0(没有 ID 字段)
    • TNF: 0x1(Well-Known 类型)

示例 2: 0x53

  • 二进制表示: 01010011

    • MB: 0(不是第一条记录)
    • ME: 1(消息的最后一条记录)
    • CF: 0(数据未分块)
    • SR: 1(短记录)
    • IL: 0(没有 ID 字段)
    • TNF: 0x3(URI 类型)

示例 3: 0x02

  • 二进制表示: 00000010

    • MB: 0(不是第一条记录)
    • ME: 0(不是最后一条记录)
    • CF: 0(数据未分块)
    • SR: 0(长记录)
    • IL: 0(没有 ID 字段)
    • TNF: 0x2(MIME 类型)
2. Type Length (1 字节)

表示 Type 字段的长度(单位:字节)。

3. Payload Length (1 或 4 字节)

表示有效负载的长度:

-   若 SR = 1,长度字段占 1 字节。
-   若 SR = 0,长度字段占 4 字节。
4. ID Length (可选,1 字节)

若 IL = 1,表示 ID 字段的长度。

5. Type (可变长度)

定义记录的类型(例如 T 表示文本,U 表示 URI)。

6. ID (可选,可变长度)

唯一标识符(根据需求定义)。

7. Payload Data (可变长度)

实际的数据内容。


NDEF 示例记录说明

以下是一些常见的 NDEF 记录格式的详细描述:

1. 文本记录 (Text Record)
Header Byte:    D1
Type Length:    1
Payload Length: 可变
Type:           T (0x54)
Payload Data:   状态字节 + 语言代码 + 文本内容
  • 状态字节 (1 字节) : 指定语言代码的长度及文本编码格式(如 UTF-8)。

    • 高位: 保留位,通常为 0。
    • 低位: 语言代码长度。
  • 语言代码 (可变) : 如 en 表示英语。

  • 文本内容 (可变) : 具体的文本字符串。

示例: "Hello, NFC!" 的数据如下:

D1 01 0F 54 02 65 6E 48 65 6C 6C 6F 2C 20 4E 46 43 21

2. URI 记录 (URI Record)
Header Byte:    D1
Type Length:    1
Payload Length: 可变
Type:           U (0x55)
Payload Data:   URI 前缀码 + URI 内容
  • URI 前缀码 (1 字节) : 用于压缩常见协议前缀。

    • 0x00: 无前缀
    • 0x01: http://www.
    • 0x02: https://www.
    • 0x03: http://
    • 0x04: https://
  • URI 内容 (可变) : 实际的 URI 数据。

示例: https://example.com 的数据如下:

D1 01 0D 55 03 65 78 61 6D 70 6C 65 2E 63 6F 6D

Uri 和 NFC 的前缀匹配如下: 在 NFC 技术中,UriPrefix 是指一种标准化的 URI 前缀,用于 NFC 标签中的 NDEF(NFC Data Exchange Format)记录,以便更高效地存储和传输 URL 或其他资源标识符。

NFC 的 UriPrefix 使用了紧凑编码,将常见的 URL 前缀映射为一个字节值,从而节省存储空间。以下是 UriPrefix 的标准映射列表,依据 NFC Forum 的 TNF (Type Name Format) 规范定义:

NFC URI 前缀映射表

字节值URI 前缀
0x00(无前缀,直接存储完整 URI)
0x01http://www.
0x02https://www.
0x03http://
0x04https://
0x05tel:
0x06mailto:
0x07ftp://anonymous:anonymous@
0x08ftp://ftp.
0x09ftps://
0x0Asftp://
0x0Bsmb://
0x0Cnfs://
0x0Dftp://
0x0Edav://
0x0Fnews:
0x10telnet://
0x11imap:
0x12rtsp://
0x13urn:
0x14pop:
0x15sip:
0x16sips:
0x17tftp:
0x18btspp://
0x19btl2cap://
0x1Abtgoep://
0x1Btcpobex://
0x1Cirdaobex://
0x1Dfile://
0x1Eurn:epc:id:
0x1Furn:epc:tag:
0x20urn:epc:pat:
0x21urn:epc:raw:
0x22urn:epc:
0x23urn:nfc:

通过正确使用 URI 前缀,你可以实现高效的 NFC 数据交互和管理。

3. MIME 类型记录 (MIME Type Record)
Header Byte:    D2
Type Length:    可变
Payload Length: 可变
Type:           MIME 类型字符串
Payload Data:   MIME 数据
  • Type: MIME 类型字符串(如 application/jsonimage/png)。
  • Payload Data: MIME 类型对应的实际数据(如 JSON 字符串或图片)。

示例: MIME 类型 application/json,内容为 {"key":"value"}

D2 12 1B 61 70 70 6C 69 63 61 74 69 6F 6E 2F 6A 73 6F 6E
7B 22 6B 65 79 22 3A 22 76 61 6C 75 65 22 7D

4. Smart Poster 记录
Header Byte:    D1
Type Length:    1
Payload Length: 可变
Type:           S (0x53)
Payload Data:   包含多个 NDEF 记录
  • Payload Data:

    • 子记录 1: URI
    • 子记录 2: 文本描述
    • 子记录 3: 图标(可选)

示例: Smart Poster 包含 URI 和标题:

D1 02 2F 53 D1 01 0D 55 03 6E 66 63 66 6F 72 75 6D 2E 6F 72 67
D1 01 0F 54 02 65 6E 53 6D 61 72 74 20 50 6F 73 74 65 72

常见应用总结

  1. 文本传输:使用 Text Record
  2. 链接传输:使用 URI Record
  3. 自定义数据格式:使用 MIME Type Record,适合传递 JSON 或图片。
  4. 复合数据:使用 Smart Poster,可组合多个记录。

通过灵活使用 NDEF 记录格式,可以实现多种 NFC 数据传输需求。

至此:我们对 NFC 的概念、数据传输格式、数据协议字段含义都有个初步的了解,更细致可以查看官方的一些资料。可能大家看到这里觉得会有一些啰嗦,但是我个人是比较喜欢把一个东西的本质看明白了,用起来才能比较信手拈来的。都看到这里,大家就继续看下来去吧,后续就是 Android 里面的 NFC。

Android 里面的 NFC

大家刚刚看完上面的数据传输结构,希望不要一下子忘记掉,因为后续要介绍的标签系统是跟上面的数据协议密不可分的。Android 的 NFC 通信框架都是基于一个特殊的标签系统进行的。

下面我会根据 android 的官方系统的说明的流程和大家继续解释 NFC 在 Android 里面是怎么使用的。

NFC 标签调度系统

Android 设备通常会在屏幕解锁后查找 NFC 标签,除非设备的“设置”菜单中停用了 NFC 功能。当 Android 设备发现 NFC 标签时,期望的行为是让最合适的 activity 处理 intent,而不询问用户要使用哪个应用。由于设备是在非常短的范围内扫描 NFC 标签,因此让用户手动选择 activity 可能会迫使他们将设备远离标签并断开连接。您应以适当方式开发您的 activity,使其仅处理所关注的 NFC 标签,以避免 Activity 选择器出现。

没错,上面这段话是摘自官方的。翻译过来就是: Android 手机在锁屏解锁之后,如果 NFC 传感器能感应到有 NFC 到数据的时候(通常是通过手机背面),会通过 Android 的 Intent 的系统找到最符合 NFC 标签的 Activity 处理器去进行处理。因为在实际操作的时候,我们的 NFC 卡是贴在手机后面的,如果我们不优先帮助用户去选择 NFC 的处理器而让用户自己去找到目标的 APP 和目标的 activity,这种体验会非常的差。举个例子:现在你去一个便利店买东西,使用了 NFC 去支付,但是这个设备支持多种支付方式,包括支付宝支付、微信支付、Apple 支付,你还需要自己找到这次支付使用的 app,然后打开具体的确认页面,是不是会容易出错,效率低下,因此我们最好是主动帮用户找到最适合处理数据的 app以及对应的 activity,具体怎么做我后面会介绍。

Android 的 NFC 的具体流程如下:

graph LR
Android收到NFC的数据 --> 通过标签系统拉起对应的APP --> 将数据封装到Intent --> 打开对应的Activity界面 --> 开发者在onNewIntent里面进行数据获取和处理

因此,标签调度系统,可以理解为是 Android 系统解析 NFC 数据然后进行处理的一个流程。

Android 上的 NFC 的开发

  1. 在 Android 清单中请求 NFC 访问权限

    <uses-permission android:name="android.permission.NFC" />
    
  2. 您的应用支持的最低 SDK 版本。API 级别 9 仅通过 ACTION_TAG_DISCOVERED 支持有限的标签调度,并且只能通过 EXTRA_NDEF_MESSAGES extra 提供对 NDEF 消息的访问权限。无法访问其他任何标签属性或 I/O 操作。API 级别 10 提供全面的读取器/写入器支持以及前台 NDEF 推送功能;API 级别 14 则提供了用于创建 NDEF 记录的其他便捷方法。(这里目前大家的 minSdkVersion 基本上都已经满足了)

    <uses-sdk android:minSdkVersion="10"/>
    
  3. uses-feature 元素,以便您的应用仅在那些具备 NFC 硬件的设备的 Google Play 中显示:

    <uses-feature android:name="android.hardware.nfc" android:required="true/false" />
    

    如果你的应用必须要求使用 NFC 的话,那么设置为 true,如果你的应用当中,NFC 是其中的一个功能,他并非必须要有的功能。那么你就可以设置成 false,当使用到 NFC 相关功能的时候,通过以下代码去判断当前设备是否支持 NFC。

    // 获取 Nfc adapter
    fun getNfcAdapter(ctx : Context) : NfcAdapter? = NfcAdapter.getDefaultAdapter(context)
    
    // 如果 Nfc adapter 不为空,则代表当前设备是支持 Nfc 
    fun isSupportNfc(ctx : Context) : Boolean = getNfcAdapter(ctx) != null
    
    
  4. 判断 NFC 开关是否打开?如果没打开,则跳转到系统开关让用户自主打开 NFC 开关

    // 判断 NFC 是否打开
    fun isNfcEnable(ctx : Context) : Boolean = NfcAdapter.getDefaultAdapter(context)?.isEnable == true
    
    // 跳转到 NFC 开关页
    fun jumpNfcSetting(activity : Context) = activity.startActivity(Intent(Settings.ACTION_NFC_SETTINGS))

  1. 在你的 activity 里面去匹配你要读取的 NFC 标签的数据
class ReadNfcActivity : AppCompatActivity() {

    private lateinit var mNfcText: TextView
    private var nfcData: String = ""
    private var mNfcAdapter: NfcAdapter? = null
    private lateinit var mPendingIntent: PendingIntent

    private val scheme = "https"
    private val host = "lpp.test.nfc"
    private val pathPrefix = "/uri.html"
    private val mUri = "$scheme://$host$pathPrefix"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_read_uri)
        mNfcText = findViewById(R.id.tv_nfctext)
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val nfcData = intent?.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
        nfcData?.let {
            val ndef = Ndef.get(it)
            nfcData = "${ndef.type}\n max size: ${ndef.maxSize} bytes\n\n"
            readNfcTag(intent)
            mNfcText.text = nfcData
        }
    }

    /**
     * 读取NFC标签Uri
     */
    private fun readNfcTag(intent: Intent) {
        if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
            val rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
            val ndefMessage = rawMsgs?.getOrNull(0) as? NdefMessage
            ndefMessage?.let {
                try {
                    val ndefRecord = it.records[0]
                    Log.i("JAVA", ndefRecord.toString())
                    val uri = parse(ndefRecord)
                    Log.i("JAVA", "uri: $uri")
                    nfcData += "$uri\n\nUri\n${it.toByteArray().size} bytes"
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    }

    /**
     * 解析NdefRecord中Uri数据
     */
    private fun parse(record: NdefRecord): Uri {
        return when (record.tnf) {
            NdefRecord.TNF_WELL_KNOWN -> parseWellKnown(record)
            NdefRecord.TNF_ABSOLUTE_URI -> parseAbsolute(record)
            else -> throw IllegalArgumentException("Unknown TNF ${record.tnf}")
        }
    }

    /**
     * 处理绝对的Uri
     */
    private fun parseAbsolute(ndefRecord: NdefRecord): Uri {
        val payload = ndefRecord.payload
        return Uri.parse(String(payload, Charsets.UTF_8))
    }

    /**
     * 处理已知类型的Uri
     */
    private fun parseWellKnown(ndefRecord: NdefRecord): Uri {
        if (!ndefRecord.type.contentEquals(NdefRecord.RTD_URI)) return Uri.EMPTY
        val payload = ndefRecord.payload
        val prefix = UriPrefix.URI_PREFIX_MAP[payload[0].toInt()]
        val prefixBytes = prefix.toByteArray(Charsets.UTF_8)
        val fullUri = ByteArray(prefixBytes.size + payload.size - 1)
        System.arraycopy(prefixBytes, 0, fullUri, 0, prefixBytes.size)
        System.arraycopy(payload, 1, fullUri, prefixBytes.size, payload.size - 1)
        return Uri.parse(String(fullUri, Charsets.UTF_8))
    }

    override fun onStart() {
        super.onStart()
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this)
        mPendingIntent = PendingIntent.getActivity(
            this, 0, Intent(this, javaClass),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
        )
    }

    override fun onResume() {
        super.onResume()
        mNfcAdapter?.enableForegroundDispatch(this, mPendingIntent, null, null)
    }

    override fun onPause() {
        super.onPause()
        mNfcAdapter?.disableForegroundDispatch(this)
    }
}

到这里的话,你已经可以通过在 activity 里面主动去读取 NFC 的数据了,但是我们之前说过,google 官方不建议我们直接在某一个 activity 里让用户自己主动去读取数据,而是应该直接弹出页面去获取 NFC 数据。因此我们在 AndroidManifest.xml 里面去设置一个 Intent-Filter 的标签,如下所示:

<!-- 启动 activity -->
<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- 匹配 https 开头的标签 -->
<intent-filter>
    <!-- 匹配 NDEF_DISCOVERED 类型数据的 NFC 标签 -->
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
     <!-- 匹配 标签支持声明的 NFC 技术,这个通常是其他非 NDEF 类型的 NFC 标签 -->
    <action android:name="android.nfc.action.TECH_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="application/*" />
    <data android:scheme="https" />
</intent-filter>

这样,即便你的应用是在 app 处理后台,android 系统也会主动拉起你的 app 去进行处理。注意,拉起的 activity 通常是你的启动 activity,然后你需要在你的启动 activity 里面 onNewIntent 里面进行处理,如果处理数据的 activity 和 LaunchActivity 不是一个 activity,则需要自主进行跳转。或者通过在你的 activity 进行设置 export = true 的设置,如下所示:


<activity
    android:name=".example.activity.NFCDemoActivity"
    android:exported="true" />

以下这段是摘自官方的文档:

Google Document Start

如何将 NFC 标签分发到应用

当标签调度系统创建完用于封装 NFC 标签及其标识信息的 intent 后,它会将该 intent 发送给感兴趣的应用,由这些应用对其进行过滤。如果有多个应用可处理该 intent,系统会显示 activity 选择器,供用户选择要使用的 activity。代码植入调度系统定义了三种 intent,按优先级从高到低列出如下:

  1. ACTION_NDEF_DISCOVERED:如果扫描到包含 NDEF 负载的标签,并且可识别其类型,则使用此 intent 启动 activity。这是优先级最高的 intent,标签调度系统会尽可能尝试使用此 intent 启动 activity,在行不通时才会尝试使用其他 intent。
  2. ACTION_TECH_DISCOVERED:如果没有登记要处理 ACTION_NDEF_DISCOVERED intent 的 activity,则标签调度系统会尝试使用此 intent 来启动应用。此外,如果扫描到的标签包含无法映射到 MIME 类型或 URI 的 NDEF 数据,或者该标签不包含 NDEF 数据,但它使用了已知的标签技术,那么也会直接启动此 intent(无需先启动 ACTION_NDEF_DISCOVERED)。
  3. ACTION_TAG_DISCOVERED:如果没有处理 ACTION_NDEF_DISCOVERED 或者 ACTION_TECH_DISCOVERED intent 的 activity,则使用此 intent 启动 activity。

标签调度系统的基本工作方式如下:

  1. 在解析 NFC 标签(ACTION_NDEF_DISCOVERED 或 ACTION_TECH_DISCOVERED)时,尝试使用由标签调度系统创建的 intent 启动 activity。
  2. 如果不存在过滤该 intent 的 activity,则尝试使用下一优先级的 intent(ACTION_TECH_DISCOVERED 或 ACTION_TAG_DISCOVERED)启动 activity,直到应用过滤该 intent 或者直到标签调度系统试完所有可能的 intent。
  3. 如果没有应用过滤任何 Intent,则不执行任何操作。

图 1. 标签调度系统

尽可能使用 NDEF 消息和 ACTION_NDEF_DISCOVERED intent,因为它是三种 intent 中最具体的一种。与另外两个 intent 相比,此 intent 可让您在更合适的时间启动应用,从而为用户提供更好的体验。

Google Document End

NFC 支持的标签(NDEF 和 非 NDEF标签)

看到上面,大家对 Android 的 NFC 功能开发应该有了一个基本的了解了,但是这里我们继续讨论一下NFC 支持的标签(NDEF 和 非 NDEF标签。

在 Android 中,ACTION_NDEF_DISCOVEREDACTION_TECH_DISCOVERED 都是与 NFC 标签交互时使用的 Intent Actions。它们的主要作用和区别如下:


1. ACTION_NDEF_DISCOVERED

主要功能
用于处理 NDEF(NFC Data Exchange Format)格式化标签。NDEF 是一种规范化的数据格式,常用于存储如文本、URL、URI 等数据。

触发条件

  • 只有当 NFC 标签包含 NDEF 数据记录时,系统才会触发 ACTION_NDEF_DISCOVERED
  • 且该 Intent 的优先级最高:如果匹配到了此 Action,系统不会再尝试匹配其他 Actions(如 ACTION_TECH_DISCOVEREDACTION_TAG_DISCOVERED)。

Intent-Filter 配置
可以在 AndroidManifest.xml 中通过 intent-filter 声明,匹配特定的 MIME 类型或 URI。

<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>
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:scheme="http" android:host="example.com" /> <!-- 匹配特定 URI -->
</intent-filter>

适用场景

  • 处理标准化的 NDEF 数据(如文本、URI、URL 等)。
  • 需要通过 MIME 类型或 URI 进行精确匹配。

解析数据示例
使用 getParcelableExtra(NfcAdapter.EXTRA_NDEF_MESSAGES) 获取标签中的 NDEF 消息数据。


2. ACTION_TECH_DISCOVERED

主要功能
用于处理支持的 特定 NFC 技术 的标签,例如 MifareClassic、NfcA、NfcB、IsoDep 等。系统会根据应用的声明去尝试匹配。

触发条件

  • 标签不一定包含 NDEF 数据,但支持某些 NFC 技术。
  • 优先级低于 ACTION_NDEF_DISCOVERED,如果标签的数据可以被 ACTION_NDEF_DISCOVERED 处理,则不会触发 ACTION_TECH_DISCOVERED

Intent-Filter 配置
在 AndroidManifest.xml 中通过 tech-list 声明支持的 NFC 技术。

<intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

<!-- 声明支持的 NFC 技术 -->
<meta-data android:name="android.nfc.action.TECH_DISCOVERED" android:resource="@xml/nfc_tech_filter" />

nfc_tech_filter.xml 示例

<resources xmlns:xlmns="http://schemas.android.com/apk/res/android">
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech> <!-- 支持 NfcA 技术 -->
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech> <!-- 支持 IsoDep 技术 -->
    </tech-list>
</resources>

activity处理数据 示例

import android.app.Activity;
import android.content.Intent;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.NfcA;
import android.os.Bundle;
import android.widget.TextView;
import java.io.IOException;

public class NfcAExampleActivity extends Activity {

    private NfcAdapter nfcAdapter;
    private TextView statusText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        statusText = findViewById(R.id.statusText);
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);

        if (nfcAdapter == null) {
            statusText.setText("NFC is not available on this device.");
            return;
        }
    }

    @Override
    protected void onResume() {
        super.onResume();

        if (nfcAdapter != null && nfcAdapter.isEnabled()) {
            Intent intent = getIntent();
            if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
                handleTag(intent);
            }
        }
    }

    private void handleTag(Intent intent) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        if (tag == null) {
            statusText.setText("No NFC tag detected.");
            return;
        }

        // 查看是否支持 NfcA
        NfcA nfcA = NfcA.get(tag);
        if (nfcA == null) {
            statusText.setText("Tag does not support NfcA.");
            return;
        }

        try {
            // 如果支持就进行连接目标设备
            nfcA.connect();

            // Example: 发送一个数据给 NFC 芯片
            byte[] command = new byte[]{(byte) 0x30, (byte) 0x04}; 
            // 发送完之后看设备是否有回复
            byte[] response = nfcA.transceive(command);

            // 查看设备回复的消息
            String responseData = bytesToHex(response);
            statusText.setText("Response: " + responseData);

        } catch (IOException e) {
            e.printStackTrace();
            statusText.setText("Error communicating with the tag.");
        } finally {
            try {
                nfcA.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }
}

适用场景

  • 处理非 NDEF 格式的标签。
  • 需要根据 NFC 标签的底层技术特性进行处理(如读写块数据、身份验证等)。

解析数据示例
使用 getParcelableExtra(NfcAdapter.EXTRA_TAG) 获取标签的 Tag 对象,从而与底层技术 API 交互。


3. 主要区别

特性ACTION_NDEF_DISCOVEREDACTION_TECH_DISCOVERED
触发条件标签包含 NDEF 数据标签支持声明的 NFC 技术
优先级最高次高
用途解析 NDEF 数据记录使用底层技术与标签交互
Intent 过滤基于 MIME 类型或 URI基于声明的 NFC 技术
数据获取NDEFMessage 对象Tag 对象,需进一步与技术类交互
适用场景标准化的 NDEF 标签(文本、URL 等)非标准化标签(如 MifareClassic、NfcA 等)

4. 综合使用建议

  • 优先配置 ACTION_NDEF_DISCOVERED,用于处理规范化的 NDEF 数据标签。
  • 同时配置 ACTION_TECH_DISCOVERED,以确保可以处理支持的 NFC 技术标签。
  • 通过优先级机制和 Intent 数据,确保不同类型的标签可以被正确处理。

关于 android NFC 的其他支持

其实,android 不仅仅可以读取和写入 nfc 卡,还能将自己设置为一个 nfc 卡片提供,这个的话,后续我们再继续介绍。

总结:

NFC 是一种近距离接触式的通信方式,在 Android 当中,NFC 是以一种特殊的标签系统的来进行数据的读取和处理的。他支持 NDEF 和 非 NDEF 的数据交换格式。其中,数据的走向结合了 Android 的 Intent 方式来进行处理。