Android NFC - ndef.writeNdefMessage() 抛出 IOException 并擦除标签数据

Android NFC - ndef.writeNdefMessage() throws IOException and erases tag data

我的应用程序使用前台调度系统允许用户点击他们的 NFC 标签,以便在标签上执行先读后写操作。

如果用户正确点击他们的标签(即,他们在 phone 上的正确位置点击它并保持连接足够长的时间),它会很好地工作,但如果他们过早地移除标签,然后 ndef.writeNdefMessage(...) 抛出 IOException。

这意味着写入操作失败,这很正常。但是真正的问题是同样的失败操作也删除了整个ndefformatting/message标签!

我的代码是围绕 Advanced NFC | Android Developers page (as unfortunately the link to the ForegroundDispatch sample 中的片段构建的,似乎已损坏,并且没有此类示例项目可导入到 Android Studio)。

第 1 步。这是 logcat/stacktrace 用户首次点击其 NFC 标签但过早将其移开时的输出:

03-28 20:15:18.589 21278-21278/com.example.exampleapp E/NfcTestActivity: Tag error
java.io.IOException
    at android.nfc.tech.Ndef.writeNdefMessage(Ndef.java:320)
    at com.example.exampleapp.NfcTestActivity.onNewIntent(NfcTestActivity.java:170)
    at android.app.Instrumentation.callActivityOnNewIntent(Instrumentation.java:1224)
    at android.app.ActivityThread.deliverNewIntents(ActivityThread.java:2946)
    at android.app.ActivityThread.performNewIntents(ActivityThread.java:2959)
    at android.app.ActivityThread.handleNewIntent(ActivityThread.java:2968)
    at android.app.ActivityThread.access00(ActivityThread.java:181)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1554)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:145)
    at android.app.ActivityThread.main(ActivityThread.java:6145)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)
03-28 20:15:18.599 1481-17792/? E/SecNfcJni: nfaConnectionCallback: NFA_SELECT_RESULT_EVT error: status = 3
03-28 20:15:18.599 1481-1502/? E/SecNfcJni: reSelect: tag is not active

第 2 步。 接下来,同一个用户再次点击同一个标签,但它似乎不再包含 ndef 消息(我已通过更改代码并检查ndef.getCachedNdefMessage() returns null):

03-28 20:15:27.499 21278-21278/com.example.exampleapp E/NfcTestActivity: Tag error
java.lang.Exception: Tag was not ndef formatted: android.nfc.action.TECH_DISCOVERED
    at com.example.exampleapp.NfcTestActivity.onNewIntent(NfcTestActivity.java:124)
    at android.app.Instrumentation.callActivityOnNewIntent(Instrumentation.java:1224)
    at android.app.ActivityThread.deliverNewIntents(ActivityThread.java:2946)
    at android.app.ActivityThread.performNewIntents(ActivityThread.java:2959)
    at android.app.ActivityThread.handleNewIntent(ActivityThread.java:2968)
    at android.app.ActivityThread.access00(ActivityThread.java:181)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1554)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:145)
    at android.app.ActivityThread.main(ActivityThread.java:6145)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1399)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1194)

到目前为止我测试过的两台设备都遇到了这个问题 - Samsung Galaxy Core Prime(低端 phone)运行 Android 5.1.1 和 Samsung Galaxy A5(中档 phone)运行 Android 5.0.2.

我的应用程序使用的 NFC 标签包含重要信息(即,无意中删除标签数据不是一种选择!),所以我的问题是...

  1. 为什么我的代码(见下文)会像这样擦除标签数据?
  2. 如何解决根本问题,或者是否有可接受的解决方法?
  3. 我尝试使用 NfcA 或 IsoDep 而不是 Ndef 值得吗?

经过大量搜索,我很惊讶这个问题没有在其他地方讨论过,所以如果问题不是与我的代码有关,那么是否与我的 NFC 标签有关正在使用?...

我使用的标签是 NXP MIFARE Ultralight (Ultralight C) - NTAG203(标签类型:ISO 14443-3A)。其中一些是我从 ebay 购买的,一些是我从 Rapid NFC(一家信誉良好的公司)购买的,但我似乎对所有这些都有这个问题。

这是我的 activity:

的完整代码
package com.example.exampleapp;

import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Toast;

public class NfcTestActivity extends AppCompatActivity {

    private static String LOG_TAG = NfcTestActivity.class.getSimpleName();
    private static int SUCCESS_COUNT = 0;
    private static int FAILURE_COUNT = 0;

    private NfcAdapter nfcAdapter;
    private PendingIntent pendingIntent;
    private IntentFilter[] intentFiltersArray;
    private String[][] techListsArray;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_nfc_test);
        getSupportActionBar().setDisplayShowHomeEnabled(true);

        nfcAdapter = NfcAdapter.getDefaultAdapter(this);
        if (nfcAdapter == null) {

            makeToast("NFC not available!", Toast.LENGTH_LONG);
            finish();

        }
        else {

            //makeToast("NFC available");

            pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

            IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
            try {
                ndef.addDataType("*/*");    /* Handles all MIME based dispatches.
                                           You should specify only the ones that you need. */
            } catch (IntentFilter.MalformedMimeTypeException e) {
                throw new RuntimeException("fail", e);
            }
            intentFiltersArray = new IntentFilter[]{
                    ndef
            };

            techListsArray = new String[][]{
                    new String[]{
                            Ndef.class.getName()
                    }
            };
        }

    }

    @Override
    public void onPause() {
        super.onPause();

        if (nfcAdapter != null) {
            nfcAdapter.disableForegroundDispatch(this);
        }

    }

    @Override
    public void onResume() {
        super.onResume();

        if (nfcAdapter != null) {
            nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
        }

    }

    public void onNewIntent(Intent intent) {

        Ndef ndef = null;

        try {

            String action = intent.getAction();
            //makeToast("action: " + action);

            if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {

                throw new Exception("Tag was not ndef formatted: " + action); // line #124

            }
            else {

                Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
                //do something with tagFromIntent

                ndef = Ndef.get(tag);
                //makeToast("ndef: " + ndef);

                if (ndef == null) {

                    throw new Exception("ndef == null!");

                }
                else {

                    // Connect
                    ndef.connect();

                    // Get cached message
                    NdefMessage ndefMessageOld = ndef.getCachedNdefMessage();

                    if (ndefMessageOld == null) {

                        throw new Exception("No ndef message on tag!");

                    }
                    else {

                        // Get old records
                        NdefRecord[] ndefRecordsOld = ndefMessageOld.getRecords();
                        int numRecords = (ndefRecordsOld == null) ? 0 : ndefRecordsOld.length;

                        // Create/copy 'new' records

                        NdefRecord[] ndefRecordsNew = new NdefRecord[numRecords];
                        for (int i = 0; i < numRecords; i++) {
                            ndefRecordsNew[i] = ndefRecordsOld[i];
                        }

                        // Create new message
                        NdefMessage ndefMessageNew = new NdefMessage(ndefRecordsNew);

                        // Write new message
                        ndef.writeNdefMessage(ndefMessageNew); // line #170

                        SUCCESS_COUNT++;

                        // Report success
                        String msg = "Read & wrote " + numRecords + " records.";
                        makeToast(msg);
                        Log.d(LOG_TAG, msg);

                    }

                }

            }

        }
        catch(Exception e) {

            FAILURE_COUNT++;

            Log.e(LOG_TAG, "Tag error", e);
            makeToast("Tag error: " + e, Toast.LENGTH_LONG);

        }
        finally {

            try {
                if (ndef != null) {
                    ndef.close();
                }
            }
            catch(Exception e) {
                Log.e(LOG_TAG, "Error closing ndef", e);
                makeToast("Error closing ndef: " + e, Toast.LENGTH_LONG);
            }

            makeToast("Successes: " + SUCCESS_COUNT + ". Failures: " + FAILURE_COUNT);

        }

    }

    private void makeToast(final String msg) {

        makeToast(msg, Toast.LENGTH_SHORT);

    }

    private void makeToast(final String msg, final int duration) {

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {

                Toast.makeText(NfcTestActivity.this, msg, duration).show();

            }
        });
    }

}

我真的很想知道当您在覆盖数据的过程中移除存储设备时还会发生什么。

为什么我的代码(见下文)会像这样擦除标签数据?

您的代码并不是真正的 "erasing" 数据。当您中断写入时,它只是从标签内存的开头开始覆盖数据,使标签处于未定义状态。

一个NFC标签一次只支持存储一个NDEF消息。因此,当您开始编写新的 NDEF 消息时,需要覆盖旧的 NDEF 消息。因此,

ndef.writeNdefMessage(ndefMessageNew);

将从第一个块开始覆盖现有的 NDEF 消息。对于 NTAG203、MIFARE Ultralight 和 MIFARE Ultralight C(顺便说一句,这是三种不同的标签类型),第一个块将在块 4 附近。然后 writeNdefMessage 将写入新的消息块,用于用新数据替换旧数据的块。

如果写入过程被中断(例如,通过从 reader 字段中拉出标签),则只会写入部分新消息(而部分旧消息可能会保留在标签上)。由于旧消息和新消息都不完整,Android(就像任何其他 NDEF reader)无法从标签中读取有效的 NDEF 消息,因此不会检测到任何 NDEF 消息。您的应用程序仍会检测到标签,因为您还注册了 TECH_DISCOVERED 意图(不需要标签包含有效的 NDEF 消息)。

如何解决根本问题,或者是否有可接受的解决方法?

如果您的 NDEF 消息太长以至于您的用户实际上能够在写入时拉取标签,那么您对拉取本身无能为力(除了指示用户不要这样做)。 NFC 标签也没有任何形式的开箱即用保护。 IE。目前没有标签可以可靠地存储旧的 NDEF 消息,直到新的 NDEF 消息被完全写入。

您可能做的是在您的应用程序中存储旧的(或新的)NDEF 消息(可能映射到标签 ID),并让用户在失败后重新启动写入过程。尽管如此,这仍需要用户合作。

我尝试使用 NfcA 或 IsoDep 而不是 Ndef 值得吗?

这可能是另一种选择:不要对关键数据使用 NDEF,而是使用特定于应用程序的内存布局(或除 NDEF 之外)。 NTAG/MIFARE Ultralight 在 ISO 14443-3A (NFC-A) 之上有一个命令集,不支持 ISO-DEP (ISO 14443-4)。因此,您可以使用 NfcA(或 MifareUltralight)使用低级命令直接将 from/write 读取到标签。您可以将标签内存分为两个部分,用于存储旧数据和新数据:

Block x: Flag indicating which section (1 or 2) contains the valid data
Block x+1: First block of section 1
Block x+2: Second block of section 1
[...]
Block x+m: Last block of section 1
Block x+m+1: First block of section 2
Block x+m+2: Second block of section 2
[...]
Block x+2*m: Last block of section 2

其中 x 是您的自定义内存结构的第一个块(您甚至可以在一些固定的 NDEF 消息之后开始该区域)并且 m 是块中每个部分的长度(1 个块on NTAG/MF Ultralight 有 4 个字节)。

然后您将使用类似这样的方法来读取和更新您的标签:

  1. 从块 x 读取以找出包含有效(最新)数据的部分 -> 部分 s
  2. s 部分读取数据并将其用作当前数据。
  3. 将新数据写入其他部分(如果s = 1:第0节;如果s = 0:第1节)。
  4. 如果数据写入成功(且完整),请使用新的节号更新块 x

低级读写命令如下所示:

  • 阅读:

    byte[] result = nfcA.transceive(new byte[] {
            (byte)0x30,  // READ
            (byte)(blockNumber & 0x0ff)
    });
    
  • 写入:

    byte[] result = nfcA.transceive(new byte[] {
            (byte)0xA2,  // WRITE
            (byte)(blockNumber & 0x0ff),
            byte0, byte1, byte2, byte3
    });