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.1s6s0.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-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="19" />
    <uses-feature
        android:name="android.hardware.nfc"
        android:required="true" />

2)定义可接收Tag的Activity

Activity清单需要配置一下launchMode属性:

<activity


    android: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,界面可见时
     */
    @Override
    protected void onStart() {
        super.onStart();
        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
        //一旦截获NFC消息,就会通过PendingIntent调用窗口
        mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
    }
    /**
     * 获得焦点,按钮可以点击
     */
    @Override
    public void onResume() {
        super.onResume();
        //设置处理优于所有其他NFC的处理
        if (mNfcAdapter != null)
            mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
    }
    /**
     * 暂停Activity,界面获取焦点,按钮可以点击
     */
    @Override
    public 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";//短信
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public 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。

现在看一下效果图:

android-nfc-dev1

将NFC标签贴近手机背面,自动写入数据,此时退出所有程序,返回桌面,然后再将NFC标签贴近手机背面,将会看到自动打开了“短信”。

android-nfc-dev2
下来再看一个有趣的例子:

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")) });

其余不变。

android-nfc-dev3

上面这个功能还是比较有用的,例如我们往某些商品上贴上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)描述
01状态字节,见下表(状态字节编码格式)
1nISO/IANA语言编码。例如”en-US”,”fr-CA”。编码格式是US-ASCII,长度(n)由状态字节的后6位指定。
n+1m文本数据。编码格式是UTF-8或UTF-16。编码格式由状态字节的前3位指定。

状态字节编码格式:

字节位(0是最低位,7是最高位)含义
70:文本编码为UTF-8,1:文本编码为UTF-16
6必须设为0
5..0语言编码的长度(占用的字节个数)

下面我们动手实现NFC标签中的文本数据的读写操作:

1.读NFC标签文本数据

注意:Activity清单需要配置一下launchMode属性(后面一样要注意)

<activity

    android: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;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_read_text);
        mNfcText = (TextView) findViewById(R.id.tv_nfctext);
    }

    @Override
    public 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格式
         */
        //判断TNF
        if (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";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_write_text);
    }
    @Override
    public 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);
        //设置状态字节编码最高位数为0
        int 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标签,当写入成功会弹出“写入成功”的吐司。下面我们再验证一下是否成功写入:

android-nfc-dev5

我们看到,数据已经写入成功了,说明到此我们已经成功的读写NFC标签中的文本数据了。

5.NDEF Uri格式深度解析

与NDEF文本格式一样,存储在NFC标签中的Uri也有一定的格式,http://members.nfc-forum.org/specs/spec_dashboard

(1)Uri的格式规范要比文本格式简单一些:

Name偏移大小描述
识别码01byteUri识别码用于存储已知Uri的前缀
Uri字段1NUTF-8类型字符串用于存储剩余字符串

(2)Uri的前缀如下(都是十六进制的一个数):

十进制十六进制协议十进制十六进制协议
00x00N/A10x01http://www.
20x02https://www.30x03http://
40x04https://50x05tel:
60x06mailto:70x07ftp://anonymous:anonymous@
80x08ftp://ftp.90x09ftps://
100x0Asftp://110x0Bsmb://
120x0Cnfs://130x0Dftp://
140x0Edav://150x0Fnews:
160x10telnet://170x11imap:
180x12rtsp://190x13urn:
200x14pop:210x15sip:
220x16sips:230x17tftp:
240x18btspp://250x19btl2cap://
260x1Abtgoep://270x1Btcpobex://
280x1Cirdaobex://290x1Dfile://
300x1Eurn:epc:id:310x1Furn:epc:tag:
320x20urn:epc:pat:330x21urn:epc:raw:
340x22urn:epc:350x23urn: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的相关配置:

<activity
    android: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;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_read_uri);
            mNfcText = (TextView) findViewById(R.id.tv_nfctext);
        }


        @Override
        public 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";
    @Override
    public 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标签,写入成功后验证一下是否成功写入:

android-nfc-dev6

我们看到,数据已经写入成功了,说明到此我们已经成功的读写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 {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_read_mu);

        }

        @Override
        public 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{
        
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_write_mu);

        }


        @Override
        public 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标签,写入成功后验证一下是否成功写入:

android-nfc-dev8

我们看到,弹出了“北京上海广州天津”,说明数据已经写入成功了,说明到此我们已经成功的读写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-sdk
    android:minSdkVersion="10"
    android:targetSdkVersion="19" />
<uses-feature
    android:name="android.hardware.nfc"
    android:required="true" />


<application
    android: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 界面部分-->
    <activity
        android: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-data
            android: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";
    //nfc
    public 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 {

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

        initData();

    }

    private void initData() {

        //nfc初始化设置
        NfcUtils nfcUtils = new NfcUtils(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        //开启前台调度系统
        NfcUtils.mNfcAdapter.enableForegroundDispatch(this, NfcUtils.mPendingIntent, NfcUtils.mIntentFilter, NfcUtils.mTechList);
    }

    @Override
    protected void onPause() {
        super.onPause();
        //关闭前台调度系统
        NfcUtils.mNfcAdapter.disableForegroundDispatch(this);
    }

    @Override
    protected 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();
        }

    }


}

 至此 , 可以直接运行了

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
回答: 根据引用\[1\]中的错误信息,错误代码为500,错误消息为"There was an error processing the request.",这表明在处理请求时发生了错误。根据引用\[2\]中的解答,可以尝试在参数的RequestParm或RequestBody中添加"required=false",并在代码中判断clientInfo是否为空,如果为空则抛出参数错误的异常。另外,根据引用\[3\]中的发现,签名中传入signature_payload时,需要在url前面加上"/api"。根据提供的问题中的错误信息,可以看出服务繁忙,请稍后重试的错误是由于请求处理过程中发生了错误导致的。 #### 引用[.reference_title] - *1* [errorCode: 500, msg: , result: {"Message":"There was an error processing the request](https://blog.csdn.net/wolfking0608/article/details/79064333)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [参数异常,也返回系统异常?](https://blog.csdn.net/u013067756/article/details/112059341)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [【已解决】ftx restapi接入时登录不上 {‘success‘: false, ‘error‘: ‘not logged in‘}](https://blog.csdn.net/sxkee/article/details/119384629)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值