最近需要将先前项目的芯片识别功能优化,先前的项目使用的是 RFID 高频芯片识别 + 二维码扫描处理,后续设备大概率不会上 RFID 设备,因此需要接入 NFC 识别。

# 什么是 NFC

NFC 是近场通信 (Near Field Communication,NFC),是一种短距高频的无线电技术。

由非接触式射频识别 (RFID) 演变而来。NFC 工作频率为 13.56Hz,有效范围为 20cm 以内,其传输速度有 106 Kbit / 秒、212 Kbit / 秒或者 424 Kbit / 秒三种。NFC 采用主动和被动两种读取模式。

# 如何接入

Android 系统自带 NFC,在手机存在 NFC 硬件时就可以使用了。早在 Android2.3 时,我们就能使用 Android Beam 在同步的设备间通过 NFC 传递图片等数据。通过查阅源码我们得知,NFC 属于系统服务,在使用时自然是需要通过 context.getSystemService(Context.NFC_SERVICE); 来获取系统服务

/**
     * Use with {@link #getSystemService(String)} to retrieve a
     * {@link android.nfc.NfcManager} for using NFC.
     *
     * @see #getSystemService(String)
     */
    public static final String NFC_SERVICE = "nfc";

我们需要构建一个 NfcManager 来使用 NFC,而 NfcManager 中只有一个主角,那就是 NfcAdapter !

想要获得 NfcAdapter ,只需要使用静态方法

NfcAdapter.getDefaultAdapter(this);

其中的 context 用来启动 NFC 服务,等价于:

NfcManager manager =(NfcManager)context.getSystemService(Context.NFC_SERVICE);
NfcAdapter adapter = manager.getDefaultAdapter();

通过 NfcAdapterenableForegroundDispatch() 方法启用对给定 Activity 的前台分发,即最高优先级的分发(优先级这个后面再说)。

This method must be called from the main thread, and only when the activity is in the foreground (resumed). Also, activities must call disableForegroundDispatch before the completion of their Activity.onPause callback to disable foreground dispatch after it has been enabled.

方法的注释也写的很清楚,必须在主线程调用,同时必须当前 activity 可见 / 在前台,同样的,我们需要在 activity 不可见的时候调用 disableForegroundDispatch() 方法。

@Override
    public void onResume() {
        super.onResume();
        // 设置处理优于所有其他 NFC 的处理
        if (mNfcAdapter != null)
            mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
    }
    @Override
    public void onPause() {
        super.onPause();
        // 恢复默认状态
        if (mNfcAdapter != null)
            mNfcAdapter.disableForegroundDispatch(this);
    }

那么基本的就是这个成对出现的方法了。

enableForegroundDispatch() 方法中需要一个 PendingIntent ,下面是我们构建的 PendingIntent

// 一旦截获 NFC 消息,就会通过 PendingIntent 调用窗口
        mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);

构建 PendingIntent 时我们传入的 Intent 直接使用了 getClass() ,因此,回调的数据会到当前的 activity 中,我们需要重载 onNewIntent() 方法来处理我们接收到的数据。

# 如何读取

上面说道在 onNewIntent() 方法中就能处理我们收到的 NFC 数据了,这就不得不说另外一个重要的对象 Tag

//1. 获取 Tag 对象
        Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);

通过 Intent 获取 Tag 对象

通过注释可知:

我们可以通过 TaggetId() 方法获取 ID,通过 getTechList() 方法获取其技术集,技术集代表了当前这个芯片支持的技术,通俗点就是可以支持的格式,或者说是实现某些高级功能的基础支撑。

每次发现标记 (进入范围) 时都会创建一个新的标记对象,当发现一个标记时,将创建一个 tag 对象,并通过 NfcAdapter 传递给 activity 。没错,就是 PendingIntent 申明的那个。

接下来 Tag 会经过四个阶段的分发:

  1. 前台活动 activity 分发

    调用了 NfcAdapter.enableForegroundDispatch() 方法的 activity ,这个会被优先触发,比如我们平时打开系统 NFC,贴上 NFC 卡片后,系统会提示 “检测到 NFC 卡片”,然后会给我们一些推荐的操作,诸如打开微信,支付宝充值公交卡等等。但是当我们在调用了 NfcAdapter.enableForegroundDispatch() 方法的 activity 可见时,系统的处理就会因为优先级不够而不被触发,转而在当前 activity 回调数据

  2. NDEF 数据分发
    如果标签中包含 NDEF 数据,系统会在第一个 NdefMessage 中检查第一个 NdefRecord,

NdefRecord 可以有很多种类型,其构造方法如下

NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload)

具体参数说明可以查看源码。

我们可以在这个地方构建 Android Application Record,

通过 createApplicationRecord() 方法,例如:

NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord
                .createApplicationRecord("com.android.mms")});

我们将这样的 NdefRecord 写入标签后,下次我们识别标签就会自动打开短信应用。

NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{createUriRecord("http://www.baidu.com")});

或者

NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{NdefRecord.createUri(Uri.parse("http://www.baidu.com"))});

构建 Uri 的 Record,我们可以打开网页。

当然,我们也可以直接写入文本数据

NdefMessage ndefMessage = new NdefMessage(new NdefRecord[]{createTextRecord("Hello World")});
  1. 标签技术分发

    activity 注册时,添加了相应的 intent-filter ,例如:

    <activity android:name=".nfc.TechFilter" android:label="NFC/TechFilter">
          <!-- Add a technology filter -->
          <intent-filter>
              <action android:name="android.nfc.action.TECH_DISCOVERED" />
          </intent-filter>
     
          <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
              android:resource="@xml/filter_nfc"
          />
      </activity>
    <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
          <!-- capture anything using NfcF -->
          <tech-list>
              <tech>android.nfc.tech.NfcF</tech>
          </tech-list>
     
          <!-- OR -->
     
          <!-- capture all MIFARE Classics with NDEF payloads -->
          <tech-list>
              <tech>android.nfc.tech.NfcA</tech>
              <tech>android.nfc.tech.MifareClassic</tech>
              <tech>android.nfc.tech.Ndef</tech>
          </tech-list>
      </resources>

    intent-filter 中添加对应的 action 后,我们就能通过 action 获取数据了

  2. 备用分发

    如果所有的分发条件都不满足,就会走 NfcAdapter.ACTION_TAG_DISCOVEREDaction 来分发数据

读取数据我们可以根据当前的 techType 来读取,例如:

当前芯片的 TechList 中有 3 条数据,分别是 NFCA,MifareUltralight 和 NEDF,因此我们就能针对性的取数据了。

NEDF 通过 Ndef ndef = Ndef.get(detectedTag); 来获取 NEDF 实例;

MifareUltralight 通过 MifareUltralight ultralight = MifareUltralight.get(tag); 来获取 MifareUltralight 实例;

...

以此类推

拿到相应的实例对象后,先判断是否为空,因为数据写入的格式会决定数据写入成哪种 TechType 的数据,后面取值就简单了,拿到对应的 NdefRecord 解析就行了.

读取 NDEF:

private String readNdefTag(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 = NFCUtils.parseTextRecord(record);
                    return textRecord;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

读取 MifareUltralight:

public String readTag(Tag tag) {
        MifareUltralight ultralight = MifareUltralight.get(tag);
        try {
            ultralight.connect();
            // 从第 4 索引开始读,前 4 索引对应的为标签固化区
            byte[] data = ultralight.readPages(4);
            return new String(data, Charset.forName("GB2312"));
        } catch (Exception e) {
        } finally {
            try {
                ultralight.close();
            } catch (Exception e) {
            }
        }
        return null;
    }

MifareUltralight 读取涉及到 NFC 标签的分区,前 4 个分区 (索引 0-3) 为固有分区,后面的为数据区,我们要读,要写都要写到数据区中,并且在写的时候需要注意长度。

# 如何写入

下面是 MifareUltralight 写入的方法

public void writeTag(Tag tag,String data) {
        // 向 nfc 标签写数据第 1 步,从标签中得到 MifareUltralight
        MifareUltralight ultralight = MifareUltralight.get(tag);
        if (TextUtils.isEmpty(data)) {
            return;
        }
        try {
            // 向 nfc 标签写数据第 2 步,connect
            ultralight.connect();
            /**
            * 向 nfc 标签写数据第 3 步,正式写数据。前 4 页(0 至 3)存储了 NFC 标签相关的信息
            * 注意 Charset.forName ("GB2312")),
            * 不用 utf-8 因为一个汉字有可能用 3 个字节编码汉字,那么 2 个汉字有可能是 6 个字节.
            * 而 GB2312 始终用 2 个字节。而每页最多 4 个字节,
            */
            ultralight.writePage(4, data.substring(0,4).getBytes(Charset.forName("GB2312")));// 第 4 页,页从 0 开始.
            ultralight.writePage(5, data.substring(4,8).getBytes(Charset.forName("GB2312")));
            ToastUtils.showShort( "成功写入MifareUltralight格式数据");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                ultralight.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

汉字的编码长度在不同的编码下会有不同的占位。

M1 卡有从 0 到 15 共 16 个扇区,每个扇区配备了从 0 到 3 共 4 个段,每个段可以保存 16 字节的内容,我们一般可以存 1024 字节的数据,根据我们写入的数据长度,要确认下面的数据要写到哪个扇区里,还是比较麻烦的,另外,MifareUltralight 读取时会把后面扇区的空数据也读出来,生成乱码,所以不太推荐使用 MifareUltralight 读取。

写入 NDEF 数据就比较方便了

// 首先构建 NdefMessage 对象
NdefMessage ndefMessage = new NdefMessage(
                new NdefRecord[]{createTextRecord(mText)});
                
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;
    }

直接拿到 Ndef 对象调用 writeNdefMessage() 就可以了。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Logan 微信支付

微信支付

Logan 支付宝

支付宝

Logan 贝宝

贝宝