Android中NFC相关技术(二)

前言

上一篇主要介绍了安卓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-bytesat 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
    }


}
------ 本文结束 ------
坚持原创技术分享,您的支持将鼓励我继续创作!