Android NFC - ndef.writeNdefMessage() throws IOExc

2019-02-19 12:27发布

问题:

My app uses the foreground dispatch system to allow a user to tap their NFC tag in order to perform a read-then-write operation on the tag.

It works nicely if the user taps their tag properly (i.e., they tap it in the correct place on the phone and leave it connected for long enough), but if they physically remove the tag too early, then ndef.writeNdefMessage(...) throws an IOException.

That means the write operation fails, which is fair enough. But the real problem is that the same failed operation also deletes the entire ndef formatting/message from the tag!

My code is built around the snippets from the Advanced NFC | Android Developers page (as unfortunately the link to the ForegroundDispatch sample appears to be broken and there is no such sample project to import into Android Studio).

Step 1. Here is the logcat/stacktrace output when the user first taps their NFC tag, but moves it away too soon:

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.access$1700(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

Step 2. Next, the same user taps the same tag again but it appears to no longer contain an ndef message (which I have confirmed by altering the code and checking that 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.access$1700(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)

I am getting this issue with both devices I have tested with so far - a Samsung Galaxy Core Prime (a lower end phone) running Android 5.1.1 and a Samsung Galaxy A5 (a mid range phone) running Android 5.0.2.

The NFC tags used by my app contain important information (i.e., inadvertently deleting the tag data is not an option!), so my questions are...

  1. Why is my code (see below) erasing the tag data like this?
  2. How can I fix the underlying problem, or is there an acceptable workaround?
  3. Would it be worth me trying to use NfcA or IsoDep rather than Ndef?

Having done a lot of searching, I'm very surprised that this problem has not been discussed elsewhere, so if the problem isn't to do with my code, then could it be to do with the NFC tags I am using?...

The tags I'm using are NXP MIFARE Ultralight (Ultralight C) - NTAG203 (Tag type: ISO 14443-3A). Some of these I bought from ebay, and some I bought from Rapid NFC (a reputable company), yet I seem to have this problem with all of them.

Here is my complete code for the 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();

            }
        });
    }

}

回答1:

I really wonder what else you would expect to happen when you remove a storage device in the middle of overwriting its data.

Why is my code (see below) erasing the tag data like this?

You code is not really "erasing" data. It simply starts overwriting the data from the beginning of the tag memory leaving the tag in an undefined state when you interrupt writing.

An NFC tag only supports storing one NDEF message at a time. Consequently, when you start to write an new NDEF message, the old NDEF message needs to be overwritten. Thus,

ndef.writeNdefMessage(ndefMessageNew);

will overwrite the existing NDEF message starting at its first block. For NTAG203, MIFARE Ultralight and MIFARE Ultralight C (that's three different tag types by the way), this first block will be around block 4. writeNdefMessage will then write the new message block for block replacing old data with new data.

If the write procedure is interrupted (e.g. by pulling the tag from the reader field), then only parts of the new message are written (and parts of the old message may remain on the tag). Since neither the old nor the new message are complete, Android (just as any other NDEF reader) cannot read a valid NDEF message from the tag and, therefore, does not detect any NDEF message. The tag is still detected by your app since you also registered for the TECH_DISCOVERED intent (which does not require the tag to contain a vaild NDEF message).

How can I fix the underlying problem, or is there an acceptable workaround?

If your NDEF message is that long that your users are actually able to pull the tag while writing there is not much you can do against the pulling itself (except for instructing the users not to do so). NFC tags also do not have any form of pulling protection out-of-the-box. I.e. there are currently no tags that will reliably store the old NDEF message until the new NDEF message was written completely.

What you could possibly do is to store the old (or the new) NDEF message (possibly mapped to the tag ID) within your app and let the users restart the write procedure once it failed. Still, that would require user cooperation.

Would it be worth me trying to use NfcA or IsoDep rather than Ndef?

That might be another option: Don't use NDEF for the critical data but use an application-specific memory layout instead (or in addition to NDEF). NTAG/MIFARE Ultralight have a command set on top of ISO 14443-3A (NFC-A) and do not support ISO-DEP (ISO 14443-4). Thus, you could use NfcA (or MifareUltralight) to directly read from/write to the tags using low-level commands. You could structure the tag memory in two sections that you use to store the old and the new data:

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

Where x is the first block of your custom memory structure (you could even start that area after some fixed NDEF message) and m is the length of each section in blocks (1 block on NTAG/MF Ultralight has 4 bytes).

You would then use something like this to read and update your tag:

  1. Read from block x to find out which section contains the vaild (newest) data -> section s.
  2. Read the data from section s and use it as current data.
  3. Write the new data to the other section (if s = 1: section 0; if s = 0: section 1).
  4. If the data was written successfully (and completely), update block x with the new section number.

Low-level read and write commands look like this:

  • READ:

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

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