0%

Android 上的 NFC 开发

根据 Android 官方文档所写,支持 NFC 的 Android 设备有以下三种操作模式:

  • 读卡器模式:读取和写入 NFC 卡片(公交卡充值)
  • 仿真卡模式:将设备模拟成一张 NFC 卡片,可以通过其他 NFC 读卡器访问设备模拟的 NFC 卡(饭卡模拟)
  • 点对点模式:与 NFC 设备或其他支持非接触式射频传输的设备交换数据(Android Beam)

不同 NFC 卡片之间的差异也很大,有些只支持一次性写入,有些则可以支持读写操作,还有些 NFC 卡片内部支持加密功能。Android 对 NFC 卡片格式的支持主要为 NFC Forum 定义的 NDEF (NFC Data Exchange Format) 标准。

支持 NFC 的 Android 设备在设置内打开了 NFC 功能开关,设备就会在屏幕解锁后,在可以支持的范围内扫描 NFC 卡片。如果发现了一个可识别的 NFC 卡片,会通过 Intent 打开可以处理 NFC 操作的应用。如果设备中有多个可以处理该 NFC 卡片格式的应用,则会弹出选框由用户选择应使用哪个应用。

0x1 读卡器模式

作为一个 NFC 处理应用,第一步先要在 Manifest 中声明自己可以处理 NFC 操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- NFC 硬件支持 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />

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

<!-- singleTop 确保扫描到新卡片时调用 onNewIntent()
锁定竖屏防止丢失 Intent 信息 -->
<activity
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>

根据前文所述,Android 会通过 Intent 打开可以处理 NFC 操作的应用,所以应用也要添加 IntentFilter 来匹配想要过滤的数据类型。

由于对 NDEF 标准的支持最为完善,所以推荐使用 ACTION_NDEF_DISCOVERED;但是 Android 同样也对其他卡片格式有部分支持,可以使用 ACTION_TECH_DISCOVERED,使用此 action 需要在 xml 文件夹下指定 tech-list 来过滤想要使用的 NFC 技术标准;如果前两种 action 都无法匹配,就需要使用到 ACTION_TAG_DISCOVERED,但是此 action 过于笼统,要小心使用。

在 Manifest 中正确声明之后,当扫描到 NFC 卡片时,系统就会打开处理 NFC 操作的 Activity 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

mNfcAdapter = NfcAdapter.getDefaultAdapter(this);

val intent = Intent(this, MainActivity::class.java)
val requestCode = 0
val flag = 0
mPendingIntent = PendingIntent.getActivity(this, requestCode, intent, flag)
}

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

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

需要注意的是,在 onResume() 和 onPause() 中分别调用的 NfcAdapter 的两个方法,会使系统在扫描到新的 NFC 卡片时优先使用当前 Activity 进行处理,不再弹出 Intent 选择,以免降低用户的体验。


1
2
3
4
5
6
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)

val tag: Tag? = intent?.getParcelableExtra(NfcAdapter.EXTRA_TAG)
println(tag)
}

在 onNewIntent() 中,即可通过 Intent 来获取卡片的信息了,输出:

1
I/System.out: TAG: Tech [android.nfc.tech.NfcA, android.nfc.tech.MifareClassic, android.nfc.tech.NdefFormatable]

此处使用的是 Tag 格式的卡片实体类来获取,如果已经确定卡片是 NDEF 格式,则可以使用 NfcAdapter.EXTRA_NDEF_MESSAGES,来获取一个 NDEF 格式的卡片实体类。

对于非 NDEF 格式的卡片,就需要使用 Android 提供的其他实体类来进行操作,可以使用 Tag::getTechList() 来查看卡片所支持的标准。

0x2 仿真卡模式

Android 上对 NFC 卡片的模拟的主要是通过 HCE(基于主机的卡模拟) 来实现的 (Android 4.4+),如果使用安全元件进行卡片模拟,则需要部分安全元件的支持,例如 NFC-SIM 卡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- HCE 支持 -->
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="true" />

<!--仿真卡服务-->
<service
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>

<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
1
2
3
4
5
6
7
8
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/aiddescription"
android:category="other">
<aid-filter android:name="F0010203040506"/>
<aid-filter android:name="F0394148148100"/>
</aid-group>
</host-apdu-service>

在 Manifest 的 Service 声明中,引用到了一个 xml 文件夹下的 apduservice.xml 文件,其中 android:requireDeviceUnlock=false 可以让设备在亮屏但不解锁的情况下启动 HCE 服务;aid-filter 是必要的,Android 会根据 aid-filter 所定义的 AID 来选择合适的 HCE 服务,具体的 AID 数值参照要模拟的 NFC 卡片为准,须为十六进制格式,并且是偶数位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CardService : HostApduService() {
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle): ByteArray {
val aid = "F0010203040506"
val header = "00A40400"

val selectApdu = (header + String.format("%02X", aid.length / 2) + aid).toByteArray()
if (selectApdu.contentEquals(commandApdu)) {
return aid.toByteArray()
}
return byteArrayOf()
}

override fun onDeactivated(reason: Int) {}
}

HostApduService::processCommandApdu 中,设备将作为一张模拟的 NFC 卡片,Android 系统会将其接收到的 APDU 数据传入进来,此方法运行在 UI 线程,需要返回一个 ByteArray 作为响应数据发送回 NFC 读取设备。

0x3 点对点模式

NFC 卡片有 ID 卡与 IC 卡之分。传统的 ID 卡片只有数据存储功能,能轻易的被读写,卡片本身只是记录一个 ID 数值;IC 卡则不同,内部具有微型 CPU,数据的读写会经过卡片的 CPU 进行处理,支持加密功能,更加安全。

以笔者的卡片为例,卡片所使用的 IC 为 NXP MF1S50,ISO-14443 标准,数据的读写入需要与卡片进行 APDU(应用协议数据单元) 交互,具体的指令可以从 IC 的 datasheet 中获得。

1
2
3
4
5
val isoDep: IsoDep = IsoDep.get(tag)
isoDep.connect()

val payload = byteArrayOf(0x93.toByte(), 0x70.toByte()) // SELECT
val result = isoDep.transceive(payload)

这里使用 IsoDep::transceive() 向卡片发送指令,卡片会根据指令返回相应的数据,至于数据如何解析,就要参照不同卡片的技术规范了,可参考《中国金融集成电路 (IC卡) 规范》和《中国银联 IC 卡技术规范》。