androidnfc

Empty tag written if NFC write fails on Android


I am trying to update the URI written onto an NFC device. I am essentially appending some value from an edittext on the Android app's screen.

However, whenever I move the NFC device too quickly - the previous URI value gets erased leaving the device in an unusable state. This happens very rarely but it does occur especially in cases where I do multiple NFC swipes within a few seconds (Same Android phone and same NFC device).

I have included the sample code below (I have intentionally kept the URI manipulation code intact as there is a possibility that I'm doing too many things in a short duration which may be partially causing the issue to occur more frequently).

MainActivity.java:

package my.package.name;


import static my.package.name.NfcData.URI_PATTERN;

import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
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.util.Log;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    private NfcAdapter nfcAdapter;
    private TextView myTextView;
    private final String LOG_TAG = MainActivity.class.getName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myTextView = findViewById(R.id.myTextView);
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (nfcAdapter != null) {
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, this.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_MUTABLE);
            IntentFilter tagDetected = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
            tagDetected.addCategory(Intent.CATEGORY_DEFAULT);
            IntentFilter writeTagFilters[] = new IntentFilter[]{tagDetected};
            nfcAdapter.enableForegroundDispatch(this, pendingIntent, writeTagFilters, null);
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (nfcAdapter != null) {
            nfcAdapter.disableForegroundDispatch(this);
        }
    }

    private void writeNfcDataToDevice(Intent intent, String myString) {
        try {
            Tag nfcTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            NfcData nfcData = new NfcData(intent);
            if (URI_PATTERN.matcher(String.valueOf(nfcData.getUri())).matches()) {
                String preExistingUrlStr = nfcData.getUri().toString();
                Uri preExistingUri = Uri.parse(preExistingUrlStr);
                Uri newUri = preExistingUri.buildUpon().clearQuery().appendQueryParameter(NfcData.MY_PARAM, myString).build();
                NdefMessage uriNdefMessage = createUriNdefMessage(newUri);
                Ndef ndef = Ndef.get(nfcTag);
                ndef.connect();
                ndef.writeNdefMessage(uriNdefMessage);
                NdefMessage ndefMessage = ndef.getNdefMessage();
                ndef.close();
                Log.d(LOG_TAG, "NFC write successful !");
            } else {
                Log.d(LOG_TAG, "nSomething Went wrong. URI pattern does not match:" + nfcData.getUri());
            }
        }catch (Exception exception) {
            Log.e(LOG_TAG, "nSomething Went wrong: " + exception.toString());
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        if (intent != null && (NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction()))) {
            writeNfcDataToDevice(intent, myTextView.getText().toString().trim());
        }
    }

    public NdefMessage createUriNdefMessage(Uri uri) {
        NdefRecord record = NdefRecord.createUri(uri);
        NdefMessage msg = new NdefMessage(new NdefRecord[]{record});
        return msg;
    }

}

NfcData.java:

package my.package.name;

import android.content.Intent;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.os.Parcelable;

import java.util.regex.Pattern;

public final class NfcData {

    private final Uri mUri;
    public static final String MY_PARAM = "xyz";
    static Pattern URI_PATTERN = Pattern.compile("^https?://www.mywebsite.com/ax/([0-9A-Fa-f]{12})(?:\\?.*)?$");
    // both http and https protocol accepted
    // domain name www.mywebsite.com
    // string "/ax/"
    // 12 compulsory characters that may be any digit from 0 to 9 or A through F or a through f (used to denote bluetooth address)
    // Any text/characters beyond the above are optional

    public Uri getUri() {
        return mUri;
    }

    public NfcData(Intent intent) {
        Uri uri = null;
        Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
        if (rawMsgs != null) {
            for (Parcelable parcelable : rawMsgs) {
                if (!(parcelable instanceof NdefMessage)) continue;
                NdefMessage msg = (NdefMessage) parcelable;
                for (NdefRecord record : msg.getRecords()) {
                    uri = record.toUri();   
                    if(uri != null {
                       this.mUri = uri;  
                       break;
                    }          
                }
            }
        }
    }
}


Solution

  • There are multiple problems with how you are doing this.

    The main being that you have no code to handle this situation.

    The try/catch around writeNdefMessage just logs the error and does not try to recover.

    An approach I have used to recover from something similar is to store the Tag ID until the write is successful. While the Tag ID is not guaranteed to be unique in most non security critical instances with real NFC tag hardware it is good enough for retrying the write.

    for example in writeNfcDataToDevice do

    Store the Tag ID in a global variable and compare it current ID and if they same re-write the stored Ndef message e.g. sort of pseudo code below:-

    byte[] localTagID = nfcTag.getID();
    if (localTagID == globalTagID && globalNdefMessage != null) {
      // Recover from failed write
      currentNdefMessage = globalNdefMessage;
    } else {
      // This is a new Tag, so read existing NdefMessage in to currentNdefMessage and append data.
    ...
    }
    try {
      Ndef ndef = Ndef.get(nfcTag);
      ndef.connect();
      ndef.writeNdefMessage(currentNdefMessage);
      ndef.close();
      // no exception so was successfully written
      // forget the ID and message
      globalTagID = null;
      globalNdefMessage = null;
    } catch (Exception e) {
      // failed to write
      // store Tag ID
      globalTagID = localTagID;
      // store the Ndef Message to re-write
      globalNdefMessage = currentNdefMessage;
      // Tell the user to represent the Tag, so another attempt at writing the stored NdefMessage can be done.
    }
    
    

    This leads to other problems with the code:-

    You are doing writeNfcDataToDevice on the main thread and the Android Documentation for writeNdefMessage says

    This is an I/O operation and will block until complete. It must not be called from the main application thread.

    Also with Real users using the old Intent (enableForegroundDispatch) method of working with NFC is very unreliable when writing data. This is because it pauses and resumes your App to deliver the NFC tag data and the NFC system also produces a sound before it has delivered the data to your App.

    This leads the users to believe the NFC operation is complete before you App has had a chance to do anything and quite often they will remove the Tag from range leading to failed writes.

    It is much better to use the newer enableReaderMode API because it allows you to turn the platform sound off and the Tag data is delivered in a new Thread so you don't need do any thread work yourself and it does not have to pause and resume your App to do this. Once write is a success you can make your own Notification sound instead of the Platform one. So using enableReaderMode solves the two other problems with your code.

    An Example of enableReaderMode