Search code examples
androidpythonnfckivyndef

How to read NFC tag in Kivy + Python?


I'm trying to read NFC tags, and if tag has just text, reading goes well. However, it doesn't work if the tag contains a URL or is empty. I believe the problem is with the nfc_filter.xml file.

<intent-filter>
  <action android:name="android.nfc.action.TAG_DISCOVERED"/>
  <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
  <action android:name="android.nfc.action.TECH_DISCOVERED"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <data android:mimeType="*/*" />
</intent-filter>

Python code:

def nfc_init(self):
    activity.bind(on_new_intent=self.on_new_intent)
    self.j_context = context = PythonActivity.mActivity
    self.nfc_adapter = NfcAdapter.getDefaultAdapter(context)
    self.nfc_pending_intent = PendingIntent.getActivity(context, 0, Intent(context, context.getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
    return True

def on_new_intent(self, intent):
    print 'on_new_intent()', intent.getAction()
    # get TAG details
    tag = cast('android.nfc.Tag', intent.getParcelableExtra(NfcAdapter.EXTRA_TAG))
    details = self.get_ndef_details(tag)

def on_pause(self):
    print 'paused'
    return True

def on_resume(self):
    print 'resumed'

What I want is that my app would always receive the intent when it's active and you read an NFC tag. Now I can see in the log that it doesn't resume from on_pause in case the tag contains something else than text or is empty.

Can someone help me with this?


Solution

  • Your app currently receives NFC events due to the intent filter in the manifest:

    <intent-filter>
      <action android:name="android.nfc.action.TAG_DISCOVERED"/>
      <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
      <action android:name="android.nfc.action.TECH_DISCOVERED"/>
      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.BROWSABLE"/>
      <data android:mimeType="*/*" />
    </intent-filter>
    

    This intent filter has a few issues:

    1. This intent filter will match intents that have the intent action TAG_DISCOVERED, NDEF_DISCOVERED, or TECH_DISCOVERED, and at the same time contain the category DEFAULT or BROWSABLE, and at the same time contain any(?) MIME type.

      The problem with this is that only the NDEF_DISCOVERED intent may contain a MIME type. Thus, TAG_DISCOVERED and TECH_DISCOVERED will never match.

    2. The MIME type */* (i.e. match any MIME type) won't (should not?) work in the manifest intent filter since only the subtype part (i.e. the part after the slash) may contain a wildcard (*). See android:mimeType.

    3. The category BROWSABLE is useless since no NFC intent will ever contain that category.

    4. NDEF_DISCOVERED intents for tags that contain a URL don't contain a MIME type. Since you limit the NDEF_DISCOVERED intent filter to intents that contain MIME types, it won't match intents that contain URLs.

    5. The TECH_DISCOVERED intent filter requires a tech-list XML file to be declared.

    Therefore, you need to change the intent filter to match your tags. If you want to match any NDEF formatted tag, you could simply use the intent filter:

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    

    However, this comes with some limitations: Any app that registered for a more specific NDEF_DISCOVERED intent (e.g. one that contains a MIME type filter or a URL filter) will take precedence over your app and you won't receive the intent. Moreover, there are reports that an NDEF_DISCOVERED intent filter withou a <data ...> does not work on some devices.

    Consequently, in order to match MIME types and URLs you may want to use more specific intent filters, e.g. in order to match all text/, image/, and application/ MIME types, all HTTP(S) URLs, and all NFC Forum External types:

    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="text/*" />
        <data android:mimeType="image/*" />
        <data android:mimeType="application/*" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.nfc.action.NDEF_DISCOVERED" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="http" />
        <data android:scheme="https" />
        <data android:scheme="vnd.android.nfc" android:host="ext" android:pathPrefix="/" />
    </intent-filter>
    

    Still, if some other app registered a more specific intent filter, your app won't receive any intents that match those "more specific" criteria (see How NFC Tags are Dispatched to Applications).

    If your app should also be notified about tags that are not NDEF formatted, you would use the TECH_DISCOVERED intent filter (note that there is no need to specify any category for this specific intent filter). In that case, you would also need to declare an XML resource file that contains the tech-list that should be matched (the declaration must be outside the <intent-filter ... /> element!):

    <intent-filter>
        <action android:name="android.nfc.action.TECH_DISCOVERED" />
    </intent-filter>
    <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
               android:resource="@xml/nfc_tech_filter" />
    

    You would also need an XML resource nfc_tech_filter.xml (placed under res/xml/). In order to match just any tag, you could use:

    <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
        <tech-list>
            <tech>android.nfc.tech.NfcA</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcB</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcF</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcV</tech>
        </tech-list>
        <tech-list>
            <tech>android.nfc.tech.NfcBarcode</tech>
        </tech-list>
    </resources>
    

    Finally, do not use the TAG_DISCOVERED intent filter in the manifest unless you really know all its implications (particularly on user experience and user expectations). This intent filter is merely a compatibility mode for API level 9 (before Android 2.3.3) where NFC support was very, very limited and a fall-back mode that can be used to create apps that handle NFC tags that are not supported by any other app.

    Detecting tags in foreground apps

    Since you wrote that you want your app to always receive those intents "when it's active and you read NFC tag", you might want to consider removing the intent filters from the manifest completly and use the foreground dispatch system instead. In that case, your app would not be started when an NFC tag is read but it would receive all NFC discovery events and it would have precedence over all other apps while it is in the foreground.

    You could do this by simply adding this to your app (not quite sure about the Python syntax though):

    def on_pause(self):
        print 'paused'
        self.nfc_adapter.disableForegroundDispatch(PythonActivity.mActivity)
        return True
    
    def on_resume(self):
        print 'resumed'
        self.nfc_adapter.enableForegroundDispatch(PythonActivity.mActivity, self.nfc_pending_intent, None, None)