Android NFC 标签读写读取快速开发教程 ( 整理来自 https://blog.csdn.net/wolfking0608/article/details/72675180 )
本文整理完全参考 : https://blog.csdn.net/wolfking0608/article/details/72675180
只是将参考文章做了浏览处理
我在文章结尾放入了我的实现方式 , 分为: Activity , xml 配置 , NfcUtils
Demo下载
1.NFC的工作模式
NFC支持如下3种工作模式:读卡器模式(Reader/writer mode)、仿真卡模式(Card Emulation Mode)、点对点模式(P2P mode)。
下来分别看一下这三种模式:
(1)读卡器模式
数据在NFC芯片中,可以简单理解成“刷标签”。本质上就是通过支持NFC的手机或其它电子设备从带有NFC芯片的标签、贴纸、名片等媒介中读写信息。通常NFC标签是不需要外部供电的。当支持NFC的外设向NFC读写数据时,它会发送某种磁场,而这个磁场会自动的向NFC标签供电。
(2)仿真卡模式
数据在支持NFC的手机或其它电子设备中,可以简单理解成“刷手机”。本质上就是将支持NFC的手机或其它电子设备当成借记卡、公交卡、门禁卡等IC卡使用。基本原理是将相应IC卡中的信息凭证封装成数据包存储在支持NFC的外设中 。
在使用时还需要一个NFC射频器(相当于刷卡器)。将手机靠近NFC射频器,手机就会接收到NFC射频器发过来的信号,在通过一系列复杂的验证后,将IC卡的相应信息传入NFC射频器,最后这些IC卡数据会传入NFC射频器连接的电脑,并进行相应的处理(如电子转帐、开门等操作)。
(3)点对点模式
该模式与蓝牙、红外差不多,用于不同NFC设备之间进行数据交换,不过这个模式已经没有有“刷”的感觉了。其有效距离一般不能超过4厘米,但传输建立速度要比红外和蓝牙技术快很多,传输速度比红外块得多,如过双方都使用Android4.2,NFC会直接利用蓝牙传输。这种技术被称为Android Beam。所以使用Android Beam传输数据的两部设备不再限于4厘米之内。
点对点模式的典型应用是两部支持NFC的手机或平板电脑实现数据的点对点传输,例如,交换图片或同步设备联系人。因此,通过NFC,多个设备如数字相机,计算机,手机之间,都可以快速连接,并交换资料或者服务。
下面看一下NFC、蓝牙和红外之间的差异:
对比项 | NFC | 蓝牙 | 红外 |
---|---|---|---|
网络类型 | 点对点 | 单点对多点 | 点对点 |
有效距离 | <=0.1m | <=10m,最新的蓝牙4.0有效距离可达100m | 一般在1m以内,热技术连接,不稳定 |
传输速度 | 最大424kbps | 最大24Mbps | 慢速115.2kbps,快速4Mbps |
建立时间 | <0.1s | 6s | 0.5s |
安全性 | 安全,硬件实现 | 安全,软件实现 | 不安全,使用IRFM时除外 |
通信模式 | 主动-主动/被动 | 主动-主动 | 主动-主动 |
成本 | 低 | 中 | 低 |
2.Android对NFC的支持
不同的NFC标签之间差异很大,有的只支持简单的读写操作,有时还会采用支持一次性写入的芯片,将NFC标签设计成只读的。当然,也存在一些复杂的NFC标签,例如,有一些NFC标签可以通过硬件加密的方式限制对某一区域的访问。还有一些标签自带操作环境,允许NFC设备与这些标签进行更复杂的交互。这些标签中的数据也会采用不同的格式。但Android SDK API主要支持NFC论坛标准(Forum Standard),这种标准被称为NDEF(NFC Data Exchange Format,NFC数据交换格式)。
NDEF格式其实就类似于硬盘的NTFS,下面我们看一下NDEF数据:
(1)NDEF数据的操作
Android SDK API支持如下3种NDEF数据的操作:
1)从NFC标签读取NDEF格式的数据。
2)向NFC标签写入NDEF格式的数据。
3)通过Android Beam技术将NDEF数据发送到另一部NFC设备。
用于描述NDEF格式数据的两个类:
1)NdefMessage:描述NDEF格式的信息,实际上我们写入NFC标签的就是NdefMessage对象。
2)NdefRecord:描述NDEF信息的一个信息段,一个NdefMessage可能包含一个或者多个NdefRecord。
NdefMessage和NdefRecord是Android NFC技术的核心类,无论读写NDEF格式的NFC标签,还是通过Android Beam技术传递Ndef格式的数据,都需要这两个类。
(2)非NDEF数据的操作
对于某些特殊需求,可能要存任意的数据,对于这些数据,我们就需要自定义格式。这些数据格式实际上就是普通的字节流,至于字节流中的数据代表什么,就由开发人员自己定义了。
(3)编写NFC程序的基本步骤
1)设置权限,限制Android版本、安装的设备:
在 AndroidManifest 配置权限
<uses-permission android:name="android.permission.NFC" /><uses-sdkandroid:minSdkVersion="10"android:targetSdkVersion="19" /><uses-featureandroid:name="android.hardware.nfc"android:required="true" />
2)定义可接收Tag的Activity
Activity清单需要配置一下launchMode属性:
<activityandroid:name=".TagTextActivity"android:launchMode="singleTop"/>
而Activity中,我们也抽取了一个通用的BaseNfcActivity,如下(后面的Activity实现都继承于BaseNfcActivity):
/*** 1.子类需要在onCreate方法中做Activity初始化。* 2.子类需要在onNewIntent方法中进行NFC标签相关操作。* 当launchMode设置为singleTop时,第一次运行调用onCreate方法,* 第二次运行将不会创建新的Activity实例,将调用onNewIntent方法* 所以我们获取intent传递过来的Tag数据操作放在onNewIntent方法中执行* 如果在栈中已经有该Activity的实例,就重用该实例(会调用实例的onNewIntent())* 只要NFC标签靠近就执行*/
public class BaseNfcActivity extends AppCompatActivity {private NfcAdapter mNfcAdapter;private PendingIntent mPendingIntent;/*** 启动Activity,界面可见时*/@Overrideprotected void onStart() {super.onStart();mNfcAdapter = NfcAdapter.getDefaultAdapter(this);//一旦截获NFC消息,就会通过PendingIntent调用窗口mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);}/*** 获得焦点,按钮可以点击*/@Overridepublic void onResume() {super.onResume();//设置处理优于所有其他NFC的处理if (mNfcAdapter != null)mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);}/*** 暂停Activity,界面获取焦点,按钮可以点击*/@Overridepublic void onPause() {super.onPause();//恢复默认状态if (mNfcAdapter != null)mNfcAdapter.disableForegroundDispatch(this);}
}
注意:通常来说,所有处理NFC的Activity都要设置launchMode属性为singleTop或者singleTask,保证了无论NFC标签靠近手机多少次,Activity实例只有一个。
接下来看几个具体的NFC标签应用实例,通过情景学习快速掌握NFC技术:
3.两个NFC标签的简单实例
1.利用NFC标签让Android自动运行程序
场景是这样的:现将应用程序的包写到NFC程序上,然后我们将NFC标签靠近Android手机,手机就会自动运行包所对应的程序,这个是NFC比较基本的一个应用。下面以贴近标签自动运行Android自带的“短信”为例。
向NFC标签写入数据一般分为三步:
1)获取Tag对象
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
2)判断NFC标签的数据类型(通过Ndef.get方法)
Ndef ndef = Ndef.get(tag);
3)写入数据
ndef.writeNdefMessage(ndefMessage);
详细实现代码如下:
public class RunAppActivity extends BaseNfcActivity{private String mPackageName = "com.android.mms";//短信@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}@Overridepublic void onNewIntent(Intent intent) {if (mPackageName == null)return;//1.获取Tag对象Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);writeNFCTag(detectedTag);}/*** 往标签写数据的方法** @param tag*/public void writeNFCTag(Tag tag) {if (tag == null) {return;}NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord.createApplicationRecord(mPackageName)});//转换成字节获得大小int size = ndefMessage.toByteArray().length;try {//2.判断NFC标签的数据类型(通过Ndef.get方法)Ndef ndef = Ndef.get(tag);//判断是否为NDEF标签if (ndef != null) {ndef.connect();//判断是否支持可写if (!ndef.isWritable()) {return;}//判断标签的容量是否够用if (ndef.getMaxSize() < size) {return;}//3.写入数据ndef.writeNdefMessage(ndefMessage);Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();} else { //当我们买回来的NFC标签是没有格式化的,或者没有分区的执行此步//Ndef格式类NdefFormatable format = NdefFormatable.get(tag);//判断是否获得了NdefFormatable对象,有一些标签是只读的或者不允许格式化的if (format != null) {//连接format.connect();//格式化并将信息写入标签format.format(ndefMessage);Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();}}} catch (Exception e) {}}
}
注意:设置 RunAppActivity 的 launchMode 属性为 singleTop。
现在看一下效果图:
将NFC标签贴近手机背面,自动写入数据,此时退出所有程序,返回桌面,然后再将NFC标签贴近手机背面,将会看到自动打开了“短信”。
下来再看一个有趣的例子:
2.利用NFC标签让Android自动打开网页
如何让NFC标签贴近手机,手机可以自动打开一个网页呢?
首先我们创建一个NdefRecord,Android已经为我们提供好了这样的方法:
//直接接受一个Uri
public NdefRecord createUri(String uriString);//接受一个Uri的对象
public NdefRecord createUri(Uri uri);
实现代码对比“3.利用NFC标签让Android自动运行程序”部分只是修改了writeNFCTag方法中
NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord.createApplicationRecord(mPackageName)});
为
NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord.createUri(Uri.parse("http://www.nfchome.org")) });
其余不变。
上面这个功能还是比较有用的,例如我们往某些商品上贴上NFC标签,里面写入该商品的详细介绍网页Uri,当用户贴近商品时,就会自动打开该商品的详情介绍。
通过上面这两个案例的学习相信很多人已经对NFC感起了兴趣,那么下来渗透式的分析一下NDEF文本格式,看看NDEF到底是个什么东西。
4.NDEF文本格式深度解析
获取NFC标签中的数据要通过 NdefRecord.getPayload 方法完成。当然,在处理这些数据之前,最好判断一下NdefRecord对象中存储的是不是NDEF文本格式数据。
(1)判断数据是否为NDEF格式
1)TNF(类型名格式,Type Name Format)必须是NdefRecord.TNF_WELL_KNOWN。
2)可变的长度类型必须是NdefRecord.RTD_TEXT。
如果这两个标准同时满足,那么就为NDEF格式。
(2)NDEF文本格式规范
不管什么格式的数据本质上都是由一些字节组成的。对于NDEF文本格式来说,这些数据的第1个字节描述了数据的状态,然后若干个字节描述文本的语言编码,最后剩余字节表示文本数据。这些数据格式由NFC Forum的相关规范定义,可以通过 http://members.nfc-forum.org/specs/spec_dashboard 下载相关的规范。
下面这两张表是规范中 3.2节 相对重要的翻译部分:
NDEF文本数据格式:
偏移量 | 长度(bytes) | 描述 |
---|---|---|
0 | 1 | 状态字节,见下表(状态字节编码格式) |
1 | n | ISO/IANA语言编码。例如”en-US”,”fr-CA”。编码格式是US-ASCII,长度(n)由状态字节的后6位指定。 |
n+1 | m | 文本数据。编码格式是UTF-8或UTF-16。编码格式由状态字节的前3位指定。 |
状态字节编码格式:
字节位(0是最低位,7是最高位) | 含义 |
---|---|
7 | 0:文本编码为UTF-8,1:文本编码为UTF-16 |
6 | 必须设为0 |
5..0 | 语言编码的长度(占用的字节个数) |
下面我们动手实现NFC标签中的文本数据的读写操作:
1.读NFC标签文本数据
注意:Activity清单需要配置一下launchMode属性(后面一样要注意)
<activityandroid:name=".ReadTextActivity"android:launchMode="singleTop"/>
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Parcelable;
import android.os.Bundle;
import android.widget.TextView;
import android.widget.Toast;import java.util.Arrays;public class ReadTextActivity extends BaseNfcActivity {private TextView mNfcText;private String mTagText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_read_text);mNfcText = (TextView) findViewById(R.id.tv_nfctext);}@Overridepublic void onNewIntent(Intent intent) {//1.获取Tag对象Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//2.获取Ndef的实例Ndef ndef = Ndef.get(detectedTag);if (ndef!=null) {mTagText = ndef.getType() + "\nmaxsize:" + ndef.getMaxSize() + "bytes\n\n";readNfcTag(intent);mNfcText.setText(mTagText);}else{Toast.makeText(this, " 没有获取到 NDEF 实例! ", Toast.LENGTH_SHORT).show();}}/*** 读取NFC标签文本数据*/private void readNfcTag(Intent intent) {if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);NdefMessage msgs[] = null;int contentSize = 0;if (rawMsgs != null) {msgs = new NdefMessage[rawMsgs.length];for (int i = 0; i < rawMsgs.length; i++) {msgs[i] = (NdefMessage) rawMsgs[i];contentSize += msgs[i].toByteArray().length;}}try {if (msgs != null) {NdefRecord record = msgs[0].getRecords()[0];String textRecord = parseTextRecord(record);mTagText += textRecord + "\n\ntext\n" + contentSize + " bytes";}} catch (Exception e) {}}}/*** 解析NDEF文本数据,从第三个字节开始,后面的文本数据* @param ndefRecord* @return*/public static String parseTextRecord(NdefRecord ndefRecord) {/*** 判断数据是否为NDEF格式*///判断TNFif (ndefRecord.getTnf() != NdefRecord.TNF_WELL_KNOWN) {return null;}//判断可变的长度的类型if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {return null;}try {//获得字节数组,然后进行分析byte[] payload = ndefRecord.getPayload();//下面开始NDEF文本数据第一个字节,状态字节//判断文本是基于UTF-8还是UTF-16的,取第一个字节"位与"上16进制的80,16进制的80也就是最高位是1,//其他位都是0,所以进行"位与"运算后就会保留最高位String textEncoding = ((payload[0] & 0x80) == 0) ? "UTF-8" : "UTF-16";//3f最高两位是0,第六位是1,所以进行"位与"运算后获得第六位int languageCodeLength = payload[0] & 0x3f;//下面开始NDEF文本数据第二个字节,语言编码//获得语言编码String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");//下面开始NDEF文本数据后面的字节,解析出文本String textRecord = new String(payload, languageCodeLength + 1,payload.length - languageCodeLength - 1, textEncoding);return textRecord;} catch (Exception e) {throw new IllegalArgumentException();}}
}
2.写NFC标签文本数据
public class WriteTextActivity extends BaseNfcActivity {private String mText = "NFC-NewText-123";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_write_text);}@Overridepublic void onNewIntent(Intent intent) {if (mText == null)return;//获取Tag对象Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);NdefMessage ndefMessage = new NdefMessage(new NdefRecord[] { createTextRecord(mText) });boolean result = writeTag(ndefMessage, detectedTag);if (result){Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();}}/*** 创建NDEF文本数据* @param text* @return*/public static NdefRecord createTextRecord(String text) {byte[] langBytes = Locale.CHINA.getLanguage().getBytes(Charset.forName("US-ASCII"));Charset utfEncoding = Charset.forName("UTF-8");//将文本转换为UTF-8格式byte[] textBytes = text.getBytes(utfEncoding);//设置状态字节编码最高位数为0int utfBit = 0;//定义状态字节char status = (char) (utfBit + langBytes.length);byte[] data = new byte[1 + langBytes.length + textBytes.length];//设置第一个状态字节,先将状态码转换成字节data[0] = (byte) status;//设置语言编码,使用数组拷贝方法,从0开始拷贝到data中,拷贝到data的1到langBytes.length的位置System.arraycopy(langBytes, 0, data, 1, langBytes.length);//设置文本字节,使用数组拷贝方法,从0开始拷贝到data中,拷贝到data的1 + langBytes.length//到textBytes.length的位置System.arraycopy(textBytes, 0, data, 1 + langBytes.length, textBytes.length);//通过字节传入NdefRecord对象//NdefRecord.RTD_TEXT:传入类型 读写NdefRecord ndefRecord = new NdefRecord(NdefRecord.TNF_WELL_KNOWN,NdefRecord.RTD_TEXT, new byte[0], data);return ndefRecord;}/*** 写数据* @param ndefMessage 创建好的NDEF文本数据* @param tag 标签* @return*/public static boolean writeTag(NdefMessage ndefMessage, Tag tag) {try {Ndef ndef = Ndef.get(tag);ndef.connect();ndef.writeNdefMessage(ndefMessage);return true;} catch (Exception e) {}return false;}
}
我们将手机贴近NFC标签,当写入成功会弹出“写入成功”的吐司。下面我们再验证一下是否成功写入:
我们看到,数据已经写入成功了,说明到此我们已经成功的读写NFC标签中的文本数据了。
5.NDEF Uri格式深度解析
与NDEF文本格式一样,存储在NFC标签中的Uri也有一定的格式,http://members.nfc-forum.org/specs/spec_dashboard
(1)Uri的格式规范要比文本格式简单一些:
Name | 偏移 | 大小 | 值 | 描述 |
---|---|---|---|---|
识别码 | 0 | 1byte | Uri识别码 | 用于存储已知Uri的前缀 |
Uri字段 | 1 | N | UTF-8类型字符串 | 用于存储剩余字符串 |
(2)Uri的前缀如下(都是十六进制的一个数):
十进制 | 十六进制 | 协议 | 十进制 | 十六进制 | 协议 |
---|---|---|---|---|---|
0 | 0x00 | N/A | 1 | 0x01 | http://www. |
2 | 0x02 | https://www. | 3 | 0x03 | http:// |
4 | 0x04 | https:// | 5 | 0x05 | tel: |
6 | 0x06 | mailto: | 7 | 0x07 | ftp://anonymous:anonymous@ |
8 | 0x08 | ftp://ftp. | 9 | 0x09 | ftps:// |
10 | 0x0A | sftp:// | 11 | 0x0B | smb:// |
12 | 0x0C | nfs:// | 13 | 0x0D | ftp:// |
14 | 0x0E | dav:// | 15 | 0x0F | news: |
16 | 0x10 | telnet:// | 17 | 0x11 | imap: |
18 | 0x12 | rtsp:// | 19 | 0x13 | urn: |
20 | 0x14 | pop: | 21 | 0x15 | sip: |
22 | 0x16 | sips: | 23 | 0x17 | tftp: |
24 | 0x18 | btspp:// | 25 | 0x19 | btl2cap:// |
26 | 0x1A | btgoep:// | 27 | 0x1B | tcpobex:// |
28 | 0x1C | irdaobex:// | 29 | 0x1D | file:// |
30 | 0x1E | urn:epc:id: | 31 | 0x1F | urn:epc:tag: |
32 | 0x20 | urn:epc:pat: | 33 | 0x21 | urn:epc:raw: |
34 | 0x22 | urn:epc: | 35 | 0x23 | urn:nfc: |
每一个协议,都是用十六进制来存储于识别码位置(占1byte)。
是不是相对简单了些,那么下来我们来解析一个Uri。
(3)预先定义已知Uri前缀
这里我们定义一个UriPrefix类,以便方便的获取Uri前缀:
public class UriPrefix {public static final Map<Byte, String> URI_PREFIX_MAP =new HashMap<Byte,String>();//预先定义已知Uri前缀static {URI_PREFIX_MAP.put((byte) 0x00,"");URI_PREFIX_MAP.put((byte)0x01, "http://www.");URI_PREFIX_MAP.put((byte)0x02,"https://www.");URI_PREFIX_MAP.put((byte)0x03,"http://");URI_PREFIX_MAP.put((byte)0x04,"https://");URI_PREFIX_MAP.put((byte)0x05,"tel:");URI_PREFIX_MAP.put((byte) 0x06, "mailto:");URI_PREFIX_MAP.put((byte)0x07, "ftp://anonymous:anonymous@");URI_PREFIX_MAP.put((byte)0x08,"ftp://ftp.");URI_PREFIX_MAP.put((byte)0x09, "ftps://");URI_PREFIX_MAP.put((byte) 0x0A,"sftp://");URI_PREFIX_MAP.put((byte)0x0B, "smb://");URI_PREFIX_MAP.put((byte) 0x0C, "nfs://");URI_PREFIX_MAP.put((byte) 0x0D, "ftp://");URI_PREFIX_MAP.put((byte)0x0E, "dav://");URI_PREFIX_MAP.put((byte)0x0F, "news:");URI_PREFIX_MAP.put((byte)0x10,"telnet://");URI_PREFIX_MAP.put((byte)0x11,"imap:");URI_PREFIX_MAP.put((byte) 0x12,"rtsp://");URI_PREFIX_MAP.put((byte)0x13, "urn:");URI_PREFIX_MAP.put((byte) 0x14, "pop:");URI_PREFIX_MAP.put((byte) 0x15,"sip:");URI_PREFIX_MAP.put((byte) 0x16, "sips:");URI_PREFIX_MAP.put((byte) 0x17, "tftp:");URI_PREFIX_MAP.put((byte) 0x18,"btspp://");URI_PREFIX_MAP.put((byte) 0x19, "btl2cap://");URI_PREFIX_MAP.put((byte)0x1A,"btgoep://");URI_PREFIX_MAP.put((byte)0x1B, "tcpobex://");URI_PREFIX_MAP.put((byte) 0x1C, "irdaobex://");URI_PREFIX_MAP.put((byte)0x1D, "file://");URI_PREFIX_MAP.put((byte) 0x1E, "urn:epc:id:");URI_PREFIX_MAP.put((byte)0x1F, "urn:epc:tag:");URI_PREFIX_MAP.put((byte)0x20, "urn:epc:pat:");URI_PREFIX_MAP.put((byte) 0x21,"urn:epc:raw:");URI_PREFIX_MAP.put((byte) 0x22,"urn:epc:");URI_PREFIX_MAP.put((byte) 0x23,"urn:nfc:");}}
然后我们来看一下清单文件中Activity的相关配置:
<activityandroid:name=".ReadWriteUriActivity"android:label="读写NFC标签的Uri"android:launchMode="singleTop" ><intent-filter><action android:name="android.nfc.action.NDEF_DISCOVERED" /><category android:name="android.intent.category.DEFAULT" /><!-- 拦截NFC标签中存储有以下Uri前缀的 --><data android:scheme="http" /><data android:scheme="https" /><data android:scheme="ftp" /></intent-filter><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>
好了,接下来就可以进行读写NFC标签中的Uri数据了:
1.读NFC标签中的Uri数据
public class ReadUriActivity extends BaseNfcActivity {private TextView mNfcText;private String mTagText;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_read_uri);mNfcText = (TextView) findViewById(R.id.tv_nfctext);}@Overridepublic void onNewIntent(Intent intent) {//获取Tag对象Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//获取Ndef的实例Ndef ndef = Ndef.get(detectedTag);mTagText = ndef.getType() + "\n max size:" + ndef.getMaxSize() + " bytes\n\n";readNfcTag(intent);mNfcText.setText(mTagText);}/*** 读取NFC标签Uri*/private void readNfcTag(Intent intent) {if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {Parcelable[] rawMsgs = intent.getParcelableArrayExtra( NfcAdapter.EXTRA_NDEF_MESSAGES);NdefMessage ndefMessage = null;int contentSize = 0;if (rawMsgs != null) {if(rawMsgs.length > 0) {ndefMessage = (NdefMessage) rawMsgs[0];contentSize = ndefMessage.toByteArray().length;} else {return;}}try {NdefRecord ndefRecord = ndefMessage.getRecords()[0];Log.i("JAVA", ndefRecord.toString());Uri uri = parse(ndefRecord);Log.i("JAVA", "uri:" + uri.toString());mTagText += uri.toString()+ "\n\nUri\n"+contentSize + " bytes";} catch(Exception e) {}}}/*** 解析NdefRecord中Uri数据** @param record* @return*/public static Uri parse(NdefRecord record) {short tnf = record.getTnf();if (tnf == NdefRecord.TNF_WELL_KNOWN) {return parseWellKnown(record);} else if(tnf == NdefRecord.TNF_ABSOLUTE_URI) {return parseAbsolute(record);}throw new IllegalArgumentException("UnknownTNF"+ tnf);}/*** 处理绝对的Uri* <p>* <p>* 没有Uri识别码,也就是没有Uri前缀,存储的全部是字符串** @param ndefRecord 描述NDEF信息的一个信息段,一个NdefMessage可能包含一个或者多个NdefRecord* @return*/private static Uri parseAbsolute(NdefRecord ndefRecord) {//获取所有的字节数据byte[] payload = ndefRecord.getPayload();Uri uri = Uri.parse(new String(payload,Charset.forName("UTF-8")));return uri;}/*** 处理已知类型的Uri** @param ndefRecord* @return*/private static Uri parseWellKnown(NdefRecord ndefRecord) {//判断数据是否是Uri类型的if (!Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_URI))return null;//获取所有的字节数据byte[] payload = ndefRecord.getPayload();String prefix = UriPrefix.URI_PREFIX_MAP.get(payload[0]);byte[] prefixBytes = prefix.getBytes(Charset.forName("UTF-8"));byte[] fullUri = new byte[prefixBytes.length+ payload.length - 1];System.arraycopy(prefixBytes,0, fullUri,0,prefixBytes.length);System.arraycopy(payload,1,fullUri,prefixBytes.length,payload.length- 1);Uri uri = Uri.parse(new String(fullUri, Charset.forName("UTF-8")));return uri;}}
2.写NFC标签中的Uri数据
public class WriteUriActivity extends BaseNfcActivity {private String mUri = "http://www.baidu.com";@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_write_uri);}public void onNewIntent(Intent intent) {Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{createUriRecord(mUri)});boolean result = writeTag(ndefMessage, detectedTag);if (result){Toast.makeText(this, "写入成功", Toast.LENGTH_SHORT).show();} else {Toast.makeText(this, "写入失败", Toast.LENGTH_SHORT).show();}}/*** 将Uri转成NdefRecord* @param uriStr* @return*/public static NdefRecord createUriRecord(String uriStr) {byte prefix = 0;for (Byte b : UriPrefix.URI_PREFIX_MAP.keySet()) {String prefixStr = UriPrefix.URI_PREFIX_MAP.get(b).toLowerCase();if ("".equals(prefixStr))continue;if (uriStr.toLowerCase().startsWith(prefixStr)) {prefix = b;uriStr = uriStr.substring(prefixStr.length());break;}}byte[] data = new byte[1 + uriStr.length()];data[0] = prefix;System.arraycopy(uriStr.getBytes(), 0, data, 1, uriStr.length());NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, new byte[0], data);return record;}/*** 写入标签* @param message* @param tag* @return*/public static boolean writeTag(NdefMessage message, Tag tag) {int size = message.toByteArray().length;try {Ndef ndef = Ndef.get(tag);if (ndef != null) {ndef.connect();if (!ndef.isWritable()) {return false;}if (ndef.getMaxSize() < size) {return false;}ndef.writeNdefMessage(message);return true;}} catch (Exception e) {}return false;}
}
我们将手机贴近NFC标签,写入成功后验证一下是否成功写入:
我们看到,数据已经写入成功了,说明到此我们已经成功的读写NFC标签中的Uri数据了。
到这里,NDEF格式就大致说完了,那么接下来看一下非NDEF格式的数据。
6.非NDEF格式深度解析
1.MifareUltralight数据格式
将NFC标签的存储区域分为16个页,每一个页可以存储4个字节,一个可存储64个字节(512位)。页码从0开始(0至15)。前4页(0至3)存储了NFC标签相关的信息(如NFC标签的序列号、控制位等)。从第5页开始存储实际的数据(4至15页)。
使用MifareUltralight.get方法获取MifareUltralight对象,然后调用MifareUltralight.connect方法进行连接,并使用MifareUltralight.writePage方法每次写入1页(4个字节)。也可以使用MifareUltralight.readPages方法每次连续读取4页。如果读取的页的序号超过15,则从头开始读。例如,从第15页(序号为14)开始读。readPages方法会读取14、15、0、1页的数据。
2.读MifareUltralight格式数据
public class ReadMUActivity extends BaseNfcActivity {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_read_mu);}@Overridepublic void onNewIntent(Intent intent) {Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);String[] techList = tag.getTechList();boolean haveMifareUltralight = false;for (String tech : techList) {if (tech.indexOf("MifareUltralight") >= 0) {haveMifareUltralight = true;break;}}if (!haveMifareUltralight) {Toast.makeText(this, "不支持MifareUltralight数据格式", Toast.LENGTH_SHORT).show();return;}String data = readTag(tag);if (data != null) Toast.makeText(this, data, Toast.LENGTH_SHORT).show();}public String readTag(Tag tag) {MifareUltralight ultralight = MifareUltralight.get(tag);try {ultralight.connect();byte[] data = ultralight.readPages(4);return new String(data, Charset.forName("GB2312"));} catch (Exception e) {} finally {try {ultralight.close();} catch (Exception e) {}}return null;}}
3.写MifareUltralight格式数据
public class WriteMUActivity extends BaseNfcActivity{@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_write_mu);}@Overridepublic void onNewIntent(Intent intent){Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);String[] techList = tag.getTechList();boolean haveMifareUltralight = false;for(String tech:techList){if (tech.indexOf("MifareUltralight")>= 0){haveMifareUltralight =true;break;}}if (!haveMifareUltralight){Toast.makeText(this, "不支持MifareUltralight数据格式", Toast.LENGTH_SHORT).show();return;}writeTag(tag);}public void writeTag(Tag tag){MifareUltralight ultralight = MifareUltralight.get(tag);try{ultralight.connect();//写入八个汉字,从第五页开始写,中文需要转换成GB2312格式ultralight.writePage(4,"北京".getBytes(Charset.forName("GB2312")));ultralight.writePage(5, "上海".getBytes(Charset.forName("GB2312")));ultralight.writePage(6,"广州".getBytes(Charset.forName("GB2312")));ultralight.writePage(7,"天津".getBytes(Charset.forName("GB2312")));Toast.makeText(this, "写入成功",Toast.LENGTH_SHORT).show();} catch(Exception e) {} finally{try{ultralight.close();}catch(Exception e){}}}}
我们将手机贴近NFC标签,写入成功后验证一下是否成功写入:
我们看到,弹出了“北京上海广州天津”,说明数据已经写入成功了,说明到此我们已经成功的读写NFC非NDEF格式的数据了。
NFC标签开发深度解析到此就结束了!
以上为整理大神的文章内容 , 以下为我自己使用的工具类及使用 :
以下内容分为 : xml 配置 , activity 部分 , NfcUtils
xml --> androidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fn.fn_nfc"><!--todo 权限部分,必须配置-->
<uses-permission android:name="android.permission.NFC" />
<uses-sdkandroid:minSdkVersion="10"android:targetSdkVersion="19" />
<uses-featureandroid:name="android.hardware.nfc"android:required="true" /><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><!--todo 界面部分--><activityandroid:name=".MainActivity"android:launchMode="singleTask"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter><intent-filter><action android:name="android.nfc.action.TECH_DISCOVERED" /></intent-filter><intent-filter><action android:name="android.nfc.action.NDEF_DISCOVERED" /><category android:name="android.intent.category.DEFAULT" /><!-- 拦截NFC标签中存储有以下Uri前缀的 --><data android:scheme="http" /><data android:scheme="https" /><data android:scheme="ftp" /></intent-filter><intent-filter><action android:name="android.nfc.action.NDEF_DISCOVERED" /><category android:name="android.intent.category.DEFAULT" /><!-- 定义可以拦截文本 --><data android:mimeType="text/plain" /></intent-filter><meta-dataandroid:name="android.nfc.action.TECH_DISCOVERED"android:resource="@xml/nfc_tech_filter" /></activity></application></manifest>
xml --> res 下的 xml 文件夹下的文件 nfc_tech_filter.xml
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"><!-- 可以处理所有Android支持的NFC类型 --><tech-list><tech>android.nfc.tech.IsoDep</tech></tech-list><tech-list><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><tech-list><tech>android.nfc.tech.NfcV</tech></tech-list><tech-list><tech>android.nfc.tech.Ndef</tech></tech-list><tech-list><tech>android.nfc.tech.NdefFormatable</tech></tech-list><tech-list><tech>android.nfc.tech.MifareUltralight</tech></tech-list><tech-list><tech>android.nfc.tech.MifareClassic</tech></tech-list>
</resources>
NfcUtils 会有重复的功能 , 但可以相互参考 :
package com.fn.fn_nfc;import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.FormatException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.nfc.tech.NdefFormatable;
import android.os.Parcelable;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;import java.io.IOException;
import java.io.UnsupportedEncodingException;/*** Created by 77167 on 2018/10/15.*/public class NfcUtils {private static final String TAG = "NfcUtils";//nfcpublic static NfcAdapter mNfcAdapter;public static IntentFilter[] mIntentFilter = null;public static PendingIntent mPendingIntent = null;public static String[][] mTechList = null;private final Activity mActivity;/*** 构造函数,用于初始化nfc*/public NfcUtils(Activity activity) {mNfcAdapter = NfcCheck(activity);this.mActivity = activity;NfcInit(activity);}/*** 检查NFC是否打开*/public static NfcAdapter NfcCheck(Activity activity) {NfcAdapter mNfcAdapter = NfcAdapter.getDefaultAdapter(activity);if (mNfcAdapter == null) {return null;} else {if (!mNfcAdapter.isEnabled()) {Intent setNfc = new Intent(Settings.ACTION_NFC_SETTINGS);activity.startActivity(setNfc);}}return mNfcAdapter;}/*** 初始化nfc设置*/public static void NfcInit(Activity activity) {mPendingIntent = PendingIntent.getActivity(activity, 0, new Intent(activity, activity.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);IntentFilter filter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);IntentFilter filter2 = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);try {filter.addDataType("*/*");} catch (IntentFilter.MalformedMimeTypeException e) {e.printStackTrace();}mIntentFilter = new IntentFilter[]{filter, filter2};mTechList = null;}/*** 往nfc写入数据*/public static void writeNFCToTag(String data, Intent intent) throws IOException, FormatException {//1. 获取Tag对象Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);//额外的//2. 判断 NFC 标签的数据类型Ndef ndef = Ndef.get(tag);ndef.connect();NdefRecord ndefRecord = null;//这里android sdk 版本, 只有大于 21 ,才能使用录入功能if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {ndefRecord = NdefRecord.createTextRecord(null, data);}NdefRecord[] records = {ndefRecord};NdefMessage ndefMessage = new NdefMessage(records);//3. 写入数据ndef.writeNdefMessage(ndefMessage);}/*** 读取NFC的数据*/public static String readNFCFromTag(Intent intent) throws UnsupportedEncodingException {Parcelable[] rawArray = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);if (rawArray != null) {NdefMessage mNdefMsg = (NdefMessage) rawArray[0];NdefRecord mNdefRecord = mNdefMsg.getRecords()[0];if (mNdefRecord != null) {String readResult = new String(mNdefRecord.getPayload(), "UTF-8");return readResult;}}return "";}/*** 读取nfcID*/public static String readNFCId(Intent intent) throws UnsupportedEncodingException {Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);String id = ByteArrayToHexString(tag.getId());return id;}/*** 将字节数组转换为字符串*/private static String ByteArrayToHexString(byte[] inarray) {int i, j, in;String[] hex = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"};String out = "";for (j = 0; j < inarray.length; ++j) {in = (int) inarray[j] & 0xff;i = (in >> 4) & 0x0f;out += hex[i];i = in & 0x0f;out += hex[i];}return out;}/*** 读取 NFC** @return*/public static String readNFCFromTag(Tag tag){if (tag == null) {return "";}//2.判断NFC标签的数据类型(通过Ndef.get方法)Ndef ndef = Ndef.get(tag);return "";}/*** 读取 NFC TAG 方法*/public void readNFCTag(Intent intent){//1.获取Tag对象Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);}/*** 往标签写数据的方法* https://blog.csdn.net/wolfking0608/article/details/72675180* @param tag*/public void writeNFCTag(Tag tag) {if (tag == null) {return;}//NdefMessage 描述NDEF格式的信息,实际上我们写入NFC标签的就是NdefMessage对象。//NdefRecord 描述NDEF信息的一个信息段,一个NdefMessage可能包含一个或者多个NdefRecord。NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord.createApplicationRecord(mActivity.getPackageName())});//转换成字节获得大小int size = ndefMessage.toByteArray().length;try {//2.判断NFC标签的数据类型(通过Ndef.get方法)Ndef ndef = Ndef.get(tag);//判断是否为NDEF标签if (ndef != null) {ndef.connect();//判断是否支持可写if (!ndef.isWritable()) {return;}//判断标签的容量是否够用if (ndef.getMaxSize() < size) {return;}//3.写入数据ndef.writeNdefMessage(ndefMessage);Log.i(TAG, "writeNFCTag: 写入成功");} else {//当我们买回来的NFC标签是没有格式化的,或者没有分区的执行此步//Ndef格式类NdefFormatable format = NdefFormatable.get(tag);//判断是否获得了NdefFormatable对象,有一些标签是只读的或者不允许格式化的if (format != null) {//连接format.connect();//格式化并将信息写入标签format.format(ndefMessage);Log.i(TAG, "writeNFCTag: 写入成功");} else {Log.i(TAG, "writeNFCTag: 写入失败");}}} catch (Exception e) {}}}
Activity 只是在 onNewIntent(Intent intent) 读取了 TagID 用 Toast 弹出 :
package com.fn.fn_nfc;import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;import java.io.UnsupportedEncodingException;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initData();}private void initData() {//nfc初始化设置NfcUtils nfcUtils = new NfcUtils(this);}@Overrideprotected void onResume() {super.onResume();//开启前台调度系统NfcUtils.mNfcAdapter.enableForegroundDispatch(this, NfcUtils.mPendingIntent, NfcUtils.mIntentFilter, NfcUtils.mTechList);}@Overrideprotected void onPause() {super.onPause();//关闭前台调度系统NfcUtils.mNfcAdapter.disableForegroundDispatch(this);}@Overrideprotected void onNewIntent(Intent intent) {super.onNewIntent(intent);//当该Activity接收到NFC标签时,运行该方法//调用工具方法,读取NFC数据try {String str = NfcUtils.readNFCId(intent);Log.i("fn_tag", "读取到的标签数据: " + str);Toast.makeText(this, "读取到的标签数据: " + str, Toast.LENGTH_SHORT).show();} catch (UnsupportedEncodingException e) {e.printStackTrace();}}}
至此 , 可以直接运行了
Android NFC 标签读写读取快速开发教程 ( 整理来自 https://blog.csdn.net/wolfking0608/article/details/72675180 )相关推荐
- Android工程师遇到瓶颈后转行什么最好?https://blog.csdn.net/feiyu1947/article/details/86438086
前言: Android不热门了,很多Android开发已经转行了,有的正走在转行的路上,还有的迟疑不决,到底要不要转呢?我们来分析下. 1. 转什么最好 每个人的自身情况不同,转的方向也会有所区别,转 ...
- 转载 java开发基础 https://blog.csdn.net/jiangjiewudi/article/details/9565749
Java就是用来做项目的!Java的主要应用领域就是企业级的项目开发!要想从事企业级的项目开发,你必须掌握如下要点: 1.掌握项目开发的基本步骤 2.具备极强的面向对象的分析与设计技巧 3.掌握用例驱 ...
- Jmeter压力测试简单教程(包括服务器状态监控)-----转载自lsoqvle 的博客(https://blog.csdn.net/cbzcbzcbzcbz/article/details/780)
步骤一 安装Jmeter 我用的版本是3.1版本,为什么是3.1,因为3.2有问题,我也是跑了一段时间后才知道3.2版本太新了还是什么的,有些功能就是不行,在此建议大家,不要轻易使用最新版本,次新版 ...
- Android NFC 标签读写Demo与历史漏洞概述
文章目录 前言 NFC基础 1.1 RFID区别 1.2 工作模式 1.3 日常应用 NFC标签 2.1 标签应用 2.2 应用实践 2.3 标签预览 2.4 前台调度 NFC开发 3.1 NDEF数 ...
- GitHub 优秀的 Android 开源项目 转自http://blog.csdn.net/shulianghan/article/details/18046021
原文地址为http://www.trinea.cn/android/android-open-source-projects-view/,作者Trinea 主要介绍那些不错个性化的View,包括Lis ...
- Android Volley完全解析(一),初识Volley的基本用法 转载地址:http://blog.csdn.net/guolin_blog/article/details/17482095
转载地址:http://blog.csdn.net/guolin_blog/article/details/17482095 1. Volley简介 我们平时在开发Android应用的时候不可避免地都 ...
- Android Volley完全解析(二),使用Volley加载网络图片 转载:http://blog.csdn.net/guolin_blog/article/details/174
转载:http://blog.csdn.net/guolin_blog/article/details/17482165 在上一篇文章中,我们了解了Volley到底是什么,以及它的基本用法.本篇文章中 ...
- solr教程,值得刚接触搜索开发人员一看(转载:http://blog.csdn.net/awj3584/article/details/16963525)
Solr调研总结 开发类型 全文检索相关开发 Solr版本 4.2 文件内容 本文介绍solr的功能使用及相关注意事项;主要包括以下内容:环境搭建及调试;两个核心配置文件介绍;维护索引;查询索引,和在 ...
- 移动WebApp开发-phoneGap+android入门(http://blog.csdn.net/cwb1128/article/details/18004505)
随着HTML5的快速发展,以及大家对于跨平台的移动App开发的渴望,使用PhoneGap开发的需求也会越来越多.根据网络上的资料,并结合自己的实践,搭建了基于phoneGap的android开发环 ...
- Windows Phone开发(28):隔离存储B 转:http://blog.csdn.net/tcjiaan/article/details/7436959...
上一节我们聊了目录的操作,这一节我们继续来看看如何读写文件. 首先说一下题外话,许多朋友都在摇摆不定,三心二意,其实这样的学习态度是很不好的,如果你对Windows phone开发有兴趣,如果你真想学 ...
最新文章
- eclipse安装Log4E插件以及简单使用
- 多线程随机数组生成+双线程快速排序(C++实现)(0.2秒排100W个数字)
- 谷歌 Daydream 实验室:VR中学习新技能是一种怎样的体验?
- wordpress 首页调用文章 不同样式的方法
- 【Python】集合的交、并、补、差集怎么算?
- 空投坐标怎么看6_装修时,怎么确认自己买的“瓷砖”是优等品?看“6点”很重要...
- VS2012 快捷键 VS RESHARPER 设置
- js setTimeout 使用方法
- select中option解析
- ExceptionError
- 刚刚修复的Windows 0day和Chrome 0day 已被组合用于 WizardOpium 攻击(详解)
- Coolite Toolkit学习笔记七:常用控件TreePanel
- 【互联网协议】北邮国际学院大三上基础知识
- 使用树莓派4B最新官方烧录软件烧录镜像设置密码,直接登录wifi
- 牛客网华为机试在线训练JavaScript版解答
- 前端优化之二 图片优化——质量与性能的博弈
- c语言大一期中考试知识点,大一期末考试复习计划
- 【python】计算圆周率到任意位支持任意位(速度快)
- Linux学习(六):proftpd搭建,完美解决vsftpd中文引号bug
- MATLAB图像数字水印的方案
热门文章
- web开发中多线程下载文件
- org.apache.commons.fileupload.DiskFileUpload1
- git-svn使用教程:git与svn进行同步
- 写一篇meta分析要多少时间?如何写好一篇Meta分析,你需要这样做
- ROS学习—【在solidworks环境中将六自由度机械臂转换为URDF模型】
- iChart--组件定制
- 现代通信原理:第七章部分习题答案
- 力软框架java_力软敏捷框架 jfGrid 使用例子之一
- (转)中国著名黑客你知道多少?
- Linux CentOS 7 下 安装SimHei字体