前言
上一篇主要介绍了安卓NFC相关的基础知识,这节主要来看看项目中使用到的知识
支持的标签技术
Android 通过 android.nfc.tech
软件包对这些用例提供一般性支持,如表 1所述。您可以使用 getTechList()
方法确定标签支持的技术,还可以使用 android.nfc.tech
提供的一个类来创建相应的 TagTechnology
对象。
表 1. 支持的标签技术
类 | 说明 |
---|---|
TagTechnology | 这是所有标签技术类都必须实现的接口。 |
NfcA | 提供对 NFC-A (ISO 14443-3A) 属性和 I/O 操作的访问权限。 |
NfcB | 提供对 NFC-B (ISO 14443-3B) 属性和 I/O 操作的访问权限。 |
NfcF | 提供对 NFC-F (JIS 6319-4) 属性和 I/O 操作的访问权限。 |
NfcV | 提供对 NFC-V (ISO 15693) 属性和 I/O 操作的访问权限。 |
IsoDep | 提供对 ISO-DEP (ISO 14443-4) 属性和 I/O 操作的访问权限。 |
Ndef | 提供对 NDEF 格式的 NFC 标签上的 NDEF 数据和操作的访问权限。 |
NdefFormatable | 为可设置为 NDEF 格式的标签提供格式化操作。 |
Android 设备还可以选择支持以下标签技术。
表 2. 可选择支持的标签技术
类 | 说明 |
---|---|
MifareClassic | 提供对 MIFARE Classic 属性和 I/O 操作的访问权限(如果此 Android 设备支持 MIFARE)。 |
MifareUltralight | 提供对 MIFARE Ultralight 属性和 I/O 操作的访问权限(如果此 Android 设备支持 MIFARE)。 |
MifareClassic
MIFARE Classic标签分为多个扇区,每个扇区又细分为块。块大小始终为16个字节(BLOCK_SIZE。扇区大小各不相同。
- MIFARE Classic Mini的大小为320字节(SIZE_MINI),5个扇区每个扇区包含4个块。
- MIFARE Classic 1k为1024字节(SIZE_1K),16个扇区每个扇区包含4个块。
- MIFARE Classic 2k为2048字节(SIZE_2K),有32个扇区每个扇区包含4个块。
- MIFARE Classic 4k为4096字节(SIZE_4K)。前32个扇区包含4个块,后8个扇区包含16个块。
1.页面如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.card.CardActivity">
<com.khb.mpcms.customView.BaseTopbarView
android:id="@+id/topbar"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_48"
app:centerText="读卡写卡"
app:isShowRight="false"
app:layout_constraintTop_toTopOf="parent"
app:leftTopText="返回" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dp_20"
app:layout_constraintTop_toBottomOf="@id/topbar">
<TextView
android:id="@+id/tv_read"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="@dimen/dp_10"
tools:text="123456" />
<Button
android:id="@+id/btn_read"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_10"
android:background="@drawable/shape_title_gradient"
android:text="读出数据"
android:textColor="@color/white" />
<com.khb.mpcms.customView.ClearEditText
android:id="@+id/et_write"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_48"
android:layout_marginTop="@dimen/dp_10"
android:paddingStart="@dimen/dp_10"
android:maxLength="16"
tools:text="123456" />
<Button
android:id="@+id/btn_write"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_10"
android:background="@drawable/shape_title_gradient"
android:text="写入数据"
android:textColor="@color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
2.读数据
private fun readTag(
tag: Tag,
sectorIndex: Int
): String? {
val mfc = MifareClassic.get(tag)
for (tech in tag.techList) {
println("------------$tech")
}
//读取TAG
try {
var metaInfo = ""
//Enable I/O operations to the tag from this TagTechnology object.
mfc.connect()
val type = mfc.type //获取TAG的类型
val sectorCount = mfc.sectorCount //获取TAG中包含的扇区数
var typeS = ""
when (type) {
MifareClassic.TYPE_CLASSIC -> typeS = "TYPE_CLASSIC"
MifareClassic.TYPE_PLUS -> typeS = "TYPE_PLUS"
MifareClassic.TYPE_PRO -> typeS = "TYPE_PRO"
MifareClassic.TYPE_UNKNOWN -> typeS = "TYPE_UNKNOWN"
}
metaInfo += """
卡片类型:$typeS
共${sectorCount}个扇区
共${mfc.blockCount}个块
存储空间: ${mfc.size}B
""".trimIndent()
val blockIndex: Int
var cardText: String = ""
if (mfc.authenticateSectorWithKeyA(sectorIndex, MifareClassic.KEY_DEFAULT)) {
blockIndex = mfc.sectorToBlock(sectorIndex)
val data = mfc.readBlock(blockIndex)
metaInfo += "新卡 Block $blockIndex : ${String(data)}"
cardText = StringUtil.byteReplaceZeroStr(data)
} else {
toast(this, "Sector $sectorIndex:验证失败").show()
}
println(metaInfo)
return cardText
} catch (e: Exception) {
toast(this, e.message.toString()).show()
e.printStackTrace()
} finally {
if (mfc != null) {
try {
mfc.close()
} catch (e: IOException) {
toast(this, e.message.toString()).show()
}
}
}
return null
}
这里用默认工厂秘钥认证方式,获取到扇区的第一个块索引进行读写,将读出的字节数组前面有0的去掉,转化成字符串类型
3.写数据
fun writeTag(tag: Tag?, sectorIndex: Int, text: String): Boolean {
val mfc = MifareClassic.get(tag)
try {
mfc.connect()
if (mfc.authenticateSectorWithKeyA(
sectorIndex,
MifareClassic.KEY_DEFAULT
)
) { //新卡 未设密码认证 r
val block = mfc.sectorToBlock(sectorIndex)
mfc.writeBlock(block, text.toByteArray())
mfc.close()
toast(this, "新卡 写入成功").show()
} else {
toast(this, "未认证").show()
}
} catch (e: IOException) {
e.printStackTrace()
toast(this, "扇区连接异常").show()
try {
mfc.close()
} catch (e1: IOException) {
e1.printStackTrace()
}
}
return false
}
这里写数据一个块中一定要写入16个字节才会成功,否则会报错:java.lang.IllegalArgumentException: must write 16-bytes
at android.nfc.tech.MifareClassic.writeBlock
,为了不让报错我在调用方法前会将写入数据处理
btn_write.clickWithTrigger {
tag?.let {
//支持输入中文,toByteArray默认采用Utf-8,一个中文字符占了三个字节
var write = et_write.text.toString().trim()
var writeByteArray = write.toByteArray()
//如果字节数组大于16的话,循环缩减至<=16个字节
while (writeByteArray.size > 16) {
write = write.substring(0, write.length - 1)
writeByteArray = write.toByteArray()
}
for (i in 0 until (16 - writeByteArray.size)) {
write = "0$write"
}
writeTag(it, 1, write)
}
}
1.小于16个字节,前面补0,大于16个字节,循环做截取操作
2.这里还支持了中文,一个中文字符在utf-8下占三个字节,所以一个块最多只能存5个中文字符,然后前面补个0就可以正常写入
4.小插曲
当然,我们的项目中,是不支持中文字符写入的,所以加了判断
if (StringUtil.isChinese(write))
toast(
this,
"小区号中不能包含中文字符"
).show()
else writeTag(it, 1, write)
5.StringUtil类
package com.khb.architecture_mvvm.base.utils
import java.lang.Character.UnicodeBlock
/**
*创建时间:2020/7/31
*编写人:kanghb
*功能描述:nfc中关于String的一些判断
*/
object StringUtil {
// CJK_SYMBOLS_AND_PUNCTUATION 判断中文的。号
// HALFWIDTH_AND_FULLWIDTH_FORMS 判断中文的,号
private fun isChinese(c: Char): Boolean {
val ub = UnicodeBlock.of(c)
return ub === UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub === UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS || ub === UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub === UnicodeBlock.GENERAL_PUNCTUATION || ub === UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub === UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
}
fun isChinese(strName: String): Boolean {
val ch = strName.toCharArray()
for (i in ch.indices) {
val c = ch[i]
if (isChinese(c)) {
return true
}
}
return false
}
fun byteReplaceZeroStr(buffer: ByteArray): String {
return try {
var offset = 0
for (i in buffer.indices) {
if (buffer[i] != 48.toByte()) {
offset = i
break
}
}
String(buffer, offset, buffer.size - offset)
} catch (e: Exception) {
""
}
}
}
总结
当然我这里只用了一个扇区的一个块来写数据,如果需要写入更多数据,可以遍历扇区,数据块去进行读写。
最后上附上整个Activity
class CardActivity : AppCompatActivity() {
private lateinit var pendingIntent: PendingIntent
private lateinit var intentFiltersArray: Array<IntentFilter>
private lateinit var techListsArray: Array<Array<String>>
private var nfcAdapter: NfcAdapter? = null
private var tag: Tag? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_card)
topbar.setTopbarClick(object : IBaseTopbarClick {
override fun leftTopbarClick(view: View) {
finish()
}
override fun rightTopbarClick(view: View) {
}
})
btn_read.clickWithTrigger {
tag?.let {
val read = readTag(it, 1)
read?.let { text ->
tv_read.text = text
et_write.setText(text)
et_write.setSelection(text.length)
}
}
}
btn_write.clickWithTrigger {
tag?.let {
//支持输入中文,toByteArray默认采用Utf-8,一个中文字符占了三个字节
var write = et_write.text.toString().trim()
var writeByteArray = write.toByteArray()
//如果字节数组大于16的话,循环缩减至<=16个字节
while (writeByteArray.size > 16) {
write = write.substring(0, write.length - 1)
writeByteArray = write.toByteArray()
}
for (i in 0 until (16 - writeByteArray.size)) {
write = "0$write"
}
// if (StringUtil.isChinese(write))
// toast(
// this,
// "小区号中不能包含中文字符"
// ).show()
// else writeTag(it, 1, write)
}
}
btn_write.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(p0: Editable?) {
if (p0.toString().toByteArray().size >= 16) {
toast(this@CardActivity, "输入数据满16字节了").show()
}
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
val intent = Intent(this, javaClass).apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
// Check for available NFC Adapter
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
if (nfcAdapter == null) {
toast(this, "NFC is not available").show()
finish()
return
}
pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
try {
addDataType("*/*") /* Handles all MIME based dispatches.
You should specify only the ones that you need. */
} catch (e: IntentFilter.MalformedMimeTypeException) {
throw RuntimeException("fail", e)
}
}
val tech = IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED).apply {
try {
addDataType("*/*") /* Handles all MIME based dispatches.
You should specify only the ones that you need. */
} catch (e: IntentFilter.MalformedMimeTypeException) {
throw RuntimeException("fail", e)
}
}
intentFiltersArray = arrayOf(ndef, tech)
//设置应用要处理的一组标签技术。调用 Object.class.getName() 方法以获取要支持的技术的类。
techListsArray = arrayOf(
arrayOf(
NfcA::class.java.name,
MifareClassic::class.java.name,
NdefFormatable::class.java.name
)
)
}
public override fun onPause() {
super.onPause()
nfcAdapter?.disableForegroundDispatch(this)
}
public override fun onResume() {
super.onResume()
nfcAdapter?.enableForegroundDispatch(
this,
pendingIntent,
intentFiltersArray,
techListsArray
)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
//当该Activity接收到NFC标签时,运行该方法
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)?.also { rawMessages ->
val messages: List<NdefMessage> = rawMessages.map { it as NdefMessage }
// Process the messages array.
println(messages.toString())
}
}
tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
tag?.let {
for (s in it.techList) {
println(s.toString())
}
val read = readTag(it, 1)
read?.let { text ->
tv_read.text = text
et_write.setText(text)
et_write.setSelection(text.length)
}
}
}
/**
* 扇区写
* @param tag
* @param sectorIndex 扇区索引 一般16个扇区 64块
* @return
*/
fun writeTag(tag: Tag?, sectorIndex: Int, text: String): Boolean {
val mfc = MifareClassic.get(tag)
try {
mfc.connect()
if (mfc.authenticateSectorWithKeyA(
sectorIndex,
MifareClassic.KEY_DEFAULT
)
) { //新卡 未设密码认证 r
val block = mfc.sectorToBlock(sectorIndex)
mfc.writeBlock(block, text.toByteArray())
mfc.close()
toast(this, "新卡 写入成功").show()
} else {
toast(this, "未认证").show()
}
} catch (e: IOException) {
e.printStackTrace()
toast(this, "扇区连接异常").show()
try {
mfc.close()
} catch (e1: IOException) {
e1.printStackTrace()
}
}
return false
}
/**
* 读扇区
* @return
*/
private fun readTag(
tag: Tag,
sectorIndex: Int
): String? {
val mfc = MifareClassic.get(tag)
for (tech in tag.techList) {
println("------------$tech")
}
//读取TAG
try {
var metaInfo = ""
//Enable I/O operations to the tag from this TagTechnology object.
mfc.connect()
val type = mfc.type //获取TAG的类型
val sectorCount = mfc.sectorCount //获取TAG中包含的扇区数
var typeS = ""
when (type) {
MifareClassic.TYPE_CLASSIC -> typeS = "TYPE_CLASSIC"
MifareClassic.TYPE_PLUS -> typeS = "TYPE_PLUS"
MifareClassic.TYPE_PRO -> typeS = "TYPE_PRO"
MifareClassic.TYPE_UNKNOWN -> typeS = "TYPE_UNKNOWN"
}
metaInfo += """
卡片类型:$typeS
共${sectorCount}个扇区
共${mfc.blockCount}个块
存储空间: ${mfc.size}B
""".trimIndent()
val blockIndex: Int
var cardText: String = ""
if (mfc.authenticateSectorWithKeyA(sectorIndex, MifareClassic.KEY_DEFAULT)) {
blockIndex = mfc.sectorToBlock(sectorIndex)
val data = mfc.readBlock(blockIndex)
metaInfo += "新卡 Block $blockIndex : ${String(data)}"
cardText = StringUtil.byteReplaceZeroStr(data)
} else {
toast(this, "Sector $sectorIndex:验证失败").show()
}
println(metaInfo)
return cardText
} catch (e: Exception) {
toast(this, e.message.toString()).show()
e.printStackTrace()
} finally {
if (mfc != null) {
try {
mfc.close()
} catch (e: IOException) {
toast(this, e.message.toString()).show()
}
}
}
return null
}
}