chariotsolutions / phonegap-nfc

PhoneGap NFC Plugin
MIT License
706 stars 557 forks source link

Android: addNdefListener not calling callback on first attempt #217

Closed homer-jay closed 6 years ago

homer-jay commented 8 years ago

Description

In Android, addNdefListener is not calling the callback on the first attempt. Any successive call works ok.

I've debugged the plugin java source code, and placed breakpoints in the parseMessage method. In the first tag read, the action is ACTION_NDEF_DISCOVERED, while every succesive read over the same tag produces an ACTION_TECH_DISCOVERED action. The plugin class fires a "ndef-mime" event in the first case, and regular "ndef" events for every successive tag read. As a consecuence of this, the callback for addNdefListener is not fired for the first attempt, since these are only relayed to callbacks registered with addMimeTypeListener instead.

This was tested using Cordova v3.7.2.

Adding <param name="onload" value="true" /> to the res/config.xml file did not fix the issue.

Steps to reproduce

Select a tag that your phone supports. Write a single plain text record (not MIME, just a WELL_KNOWN of type T) You can use any external app to verify and write the tags (e.g.: NXP's TagInfo and TagWriter are free to download from Google Play).

then in your Cordova app, run the following javascript:

window.nfc.addNdefListener(
    function(){
        console.log("Tag detected");
    },
    function(){
        console.log("listener registered");
    },
    function(e){
        console.error("Error registering listener: " + e);
    }
);

Do not register any other listener.

Expected result

The "Tag detected" line should be logged each time a properly formatted NDEF tag is read.

Actual result:

In the first attempt the plugin doesn't notify the callback even though it has detected the tag and logged the event. Consecutive reads over the very same tag work fine, and the "Tag detected" line is logged.

Examples:

TEST 1 Device: Samsung Galaxy S3 Mini OS: 4.1.2 Tag type: Mifare Classic

First attempt log: var e = document.createEvent('Events'); e.initEvent('ndef-mime'); e.tag = {"isWritable":true,"id":[12,31,18,17],"techTypes":["android.nfc.tech.NfcA","android.nfc.tech.MifareClassic","android.nfc.tech.Ndef"],"type":"NFC Forum Type 2","canMakeReadOnly":true,"maxSize":716,"ndefMessage":[{"id":[],"type":[84],"payload":[2,101,115,104,101,108,108,111],"tnf":1}]}; document.dispatchEvent(e);

Succesive attempts log: var e = document.createEvent('Events'); e.initEvent('ndef'); e.tag = {"isWritable":true,"id":[12,31,18,17],"techTypes":["android.nfc.tech.NfcA","android.nfc.tech.MifareClassic","android.nfc.tech.Ndef"],"type":"NFC Forum Type 2","canMakeReadOnly":true,"maxSize":716,"ndefMessage":[{"id":[],"type":[84],"payload":[2,101,115,104,101,108,108,111],"tnf":1}]}; document.dispatchEvent(e);

TEST 2 Device: Samsung Galaxy Core Prime LTE OS: 4.4.4 Tag type: ICODE SLIX

First attempt log: var e = document.createEvent('Events'); e.initEvent('ndef-mime'); e.tag = {"isWritable":true,"id":[-31,-96,70,0,80,1,4,-32],"techTypes":["android.nfc.tech.NfcV","android.nfc.tech.Ndef"],"type":"android.ndef.unknown","canMakeReadOnly":false,"maxSize":106,"ndefMessage":[{"id":[],"type":[84],"payload":[2,101,115,104,101,108,108,111],"tnf":1}]}; document.dispatchEvent(e);

Succesive attempts log: var e = document.createEvent('Events'); e.initEvent('ndef'); e.tag = {"isWritable":true,"id":[-31,-96,70,0,80,1,4,-32],"techTypes":["android.nfc.tech.NfcV","android.nfc.tech.Ndef"],"type":"android.ndef.unknown","canMakeReadOnly":false,"maxSize":106,"ndefMessage":[{"id":[],"type":[84],"payload":[2,101,115,104,101,108,108,111],"tnf":1}]}; document.dispatchEvent(e);

TEST 3 Device: Samsung Galaxy A3 OS: 5.0.2 Tag type: ICODE SLIX

Result: same as previous tests.

TEST 4 Device: Huawei Ascend G300 OS: 4.0.3 Tag type: ICODE SLIX

Result: same as previous tests.

homer-jay commented 8 years ago

I couldn't determine the cause of the issue. Seems to be not related to Cordova versions, and apparently it can be observed across devices of different brands and running different OS versions. Interestingly, with the same type of tags and devices used for the tests above exposed, this problem does not happen when calling addTagDiscoveredListener.

The intent processed by the parseMessage method in NfcPlugin.java contains the ACTION_NDEF_DISCOVERED action only the very first time a non MIME NDEF tag is read. If this intent comes from the OS, then this is a bug in Android source, since it shouldn't be firing this Intent for non MIME records. From the Android NFC Basics Guide:

ACTION_TECH_DISCOVERED: If no activities register to handle the ACTION_NDEF_DISCOVERED intent, the tag dispatch system tries to start an application with this intent. This intent is also directly started (without starting ACTION_NDEF_DISCOVERED first) if the tag that is scanned contains NDEF data that cannot be mapped to a MIME type or URI, or if the tag does not contain NDEF data but is of a known tag technology.

In successive reads over the same tag, the intent contains the ACTION_TECH_DISCOVERED action as expected. Which is absurd since the tag is the same.

Workaround

A workaround is proposed in this pull request. The NfcPlugin.java should also fire the more general "ndef" event so that the callbacks registered with addNdefListener are notified.

don commented 8 years ago

@homer-jay thanks for the detailed bug report. I'll go through these steps and try to reproduce when I'm back in the office. Other people have mentioned this issue before, but I have not been able to duplicate it. Unfortunately, I think that it is a hardware specific issue. I have a Samsung Galaxy S3 test device, so hopefully it behaves like your S3 mini.

For now the best work around is to add 2 listeners in JavaScript. Android will call the most specific callback for the tag scanned, so you will not get duplicate callbacks when you have multiple listeners. Use the same callback for both listeners. It doesn't matter what mime type you add.

var onNfc = function(nfcEvent) { 
    console.log("Tag detected"); 
};
nfc.addNdefListener(onNfc, success, failure);
// 2nd listener for issue #217 on Samsung devices
nfc.addMimeTypeListener("text/bogus", onNfc, success, failure);

An alternate solution could be to wire the ndef-mime event to your ndef event handler

document.addEventListener("ndef-mime", onNfc, false);
homer-jay commented 8 years ago

You are welcome. I think this might be an Android bug rather than a HW issue. I've tested in so many devices, some of them really old, and I think they have different chipsets.

BTW, I created the tag using NXP's TagWriter (the last version). You can create exactly the same tag by selecting Write tags -> New dataset -> Plain text.

don commented 6 years ago

I think I've finally tracked this one down. When the plugin starts it calls nfcAdapter.enableForegroundDispatch with empty intent filters and tech lists. This causes the plugin to capture all NFC events even if no listeners are added.

From NfcAdapter.html#enableForegroundDispatch

If you pass null for both the filters and techLists parameters that acts a wild card and will cause the foreground activity to receive all tags via the ACTION_TAG_DISCOVERED intent.

If you create a Cordova app that uses phonegap-nfc, but don't add any listeners, you still see plugin logging scans and firing events without any listeners being added. It shouldn't do this.

Here's what I think happens when the first scan is missed

  1. The plugin start, intent filters and tech lists are empty so NFC is scanning for ALL tags
  2. nfc.addNdefListener() is called. The intent filters and tech lists are updated, but the NFC adapter is not aware of the changes, it's using the old data
  3. A NFC tag is scanned, the default wildcard filter capture the scan, the plugin logic sends a ndef-mime event
  4. When the first scan is happening, Android calls onPause and onResume for the plugin
  5. onResume re-starts NFC foreground dispatching with the new intent filters and tech lists
  6. When the user scans the NFC tag again, the new filters are in place and an ndef event is fires as expected

So why does this work OK with some app or some phones? I think it a timing issue when the plugin is initialized and when the listeners are added. @homer-jay's Steps to reproduce wasn't showing the my phones. Maybe it's because I always add my listeners in onDeviceReady. If I delay the listeners, I can duplicate this every time.

// add this to app.onDeviceReady
setTimeout(function() {
  nfc.addNdefListener(
      function(){
          console.log("Tag detected");
      },
      function(){
          console.log("listener registered");
      },
      function(e){
          console.error("Error registering listener: " + e);
      }
  );
}, 3000);

What's the solution?

  1. Don't start the NFC foreground dispatching if no listeners have been added
  2. Restart the NFC foreground dispatching any time the intent filters or tech lists are modified