Starting app when MIFARE Classic is detect with Android NFC
Asked Answered
C

2

7

I've got a lot of experience working with MIFARE (since 1996, when working with cards manufactured by GEMPLUS). I've even written low-level code to emulate MIFARE cards... but now that things are much simpler and higher level I can't make it to work with Java, Android and Android Studio! I think I'm getting dumber with time...

All I'm trying to do is to launch an application when a MIFARE card is detected. I know it can be done because I've used NFC Card Info app in my device and it is launched correctly in the presence of the MIFARE card. I've uninstalled it to make sure the only NFC app was my own. I've tried to follow the less than satisfying documentation found on http://developer.android.com/guide/topics/connectivity/nfc/nfc.html and http://developer.android.com/guide/topics/connectivity/nfc/advanced-nfc.html.

The problem is that my app is never launched. The code is very simple, and, AFAIK should be working... Here is the app manifest:

    <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.nfctest.app" >

    <uses-sdk android:minSdkVersion="10"/>
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.nfctest.app.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
            <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" />
        </activity>
    </application>
</manifest>

It filters by NDEF_DISCOVERED because TECH_DISCOVERED didn't work alone.

Here is the nfc_tech_filter resource file:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

And here is the activity code:

package com.example.nfctest.app;

import android.content.Intent;
import android.nfc.NfcAdapter;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {

    TextView label;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        label = (TextView)findViewById(R.id.label);
    }

    @Override
    public void onResume() {
        super.onResume();
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
            label.setText("NDEF_DISCOVERED");
        } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(getIntent().getAction())) {
            label.setText("TECH_DISCOVERED");
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        label.setText("onNewIntent!!!");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

The activity layout is as simple as it gets:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.nfctest.app.MainActivity">
    <TextView
        android:id="@+id/label"
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

All I needed to get started programming the actual NFC code was to have the app launched whenever the TAG as detected. The documentation makes it sound so easy that I'm certain that it is me, again, being stupid again...

UPDATE:

I am able to use the foregroundDispatcher to read the tag UID correctly by changing the activity code to:

package com.example.nfctest.app;

import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.widget.TextView;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class MainActivity extends ActionBarActivity {

    TextView label;
    IntentFilter[] filters;
    String[][] techs;
    PendingIntent pendingIntent;
    NfcAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        label = (TextView)findViewById(R.id.label);
        pendingIntent = PendingIntent.getActivity(
                this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
        IntentFilter mifare = new IntentFilter((NfcAdapter.ACTION_TECH_DISCOVERED));
        filters = new IntentFilter[] { mifare };
        techs = new String[][] { new String[] {  NfcA.class.getName() } };
        adapter = NfcAdapter.getDefaultAdapter(this);
    }

    public void onPause() {
        super.onPause();
        adapter.disableForegroundDispatch(this);
    }

    public void onResume() {
        super.onResume();
        adapter.enableForegroundDispatch(this, pendingIntent, filters, techs);
    }

    public void onNewIntent(Intent intent) {
        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        byte[] id = tag.getId();
        ByteBuffer wrapped = ByteBuffer.wrap(id);
        wrapped.order(ByteOrder.LITTLE_ENDIAN);
        int signedInt = wrapped.getInt();
        long number = signedInt & 0xffffffffl;
        label.setText("Tag detected: " + number);
    }
}

I don't even need any intent filters set up in the manifest for this to work, just a simple manifest like this will do:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.nfctest.app" >

    <uses-sdk android:minSdkVersion="10"/>
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.nfctest.app.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

With this code I can read the MIFARE card serial number when my app is in the foreground, but I'm still unable to set things up so that ANDROID launches my activity when it is in the background...

I'm still at a loss on how to get ANDROID to launch my app/activity when a tag (MIFARE or NOT) is detected.

Carollcarolle answered 7/5, 2014 at 23:55 Comment(5)
What device are you testing with? Is it one that will support that tag? I seem to recall that the MiFARE tags were a bit off-spec and therefore only work with certain NFC chips.Washy
I've made it certain that the device could read the MIFARE card by testing it with another app (NFC Tag Info). The device is a Motorola Razr i...Carollcarolle
I also believe that even if the hardware didn't support MIFARE completely, at least it can detect it and read its UID (AFAIK all NFC readers can read a MIFARE "tag id"/serial number)Carollcarolle
@Washy Now I'm 100% sure that all NFC-capable ANDROID devices can at least read the MIFARE UID... In my update I was able to read the card using the foregroundDispatcher and setting up the "tech" array with NfcA instead of MIFARE and it works... Now all that is left is to make ANDROID start my activity when it is not in the foreground.Carollcarolle
@Carollcarolle Several Android devices from Samsung based on the Broadcom NFC chipset (e.g. the S4) cannot read (at least this was the case with pre-4.4 Android versions, don't know if that has changed since) MIFARE Classic cards (not even the UID). This, however, was a software design decision by Samsung. Devices with Broadcom chipset can normally read the UID (as the anti-collision of MF Classic is equivalent to ISO 14443-3). Also note that only MF Classic (due to its non NfcA framing) and MF Plus in MF Classic-legacy mode are affected, but not MF Ultralight, DESFire, etc.Karlene
K
7

Your problem is the tech filter XML file (as you finally correctly found out yourself). The tech filter you were originally using,

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

does not make any sense. <tech> entries within one <tech-filter> entry are combined using a logical AND.

So you would need to have a tag that is Ndef and NdefFormatable and MifareClassic and MifareUltralight. This is impossible for two reasons:

  1. Ndef and NdefFormatable are mutually exclusive. A tag can either already contain NDEF data/an empty NDEF message (-> Ndef) or it may be ready to be formatted with an NDEF message (-> NdefFormatable).
  2. MifareClassic and MifareUltralight are mutually exclusive. A tag can either be a MIFARE Classic tag or a MIFARE Ultralight tag, but not both at the same time.

So, for instance, a proper tech filter that triggers upon tags that contain an NDEF message and are either MIFARE Classic or MIFARE Ultralight would look like this:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

All <tech-list> entries are combined with logical OR then: (Ndef and MifareClassic) or Ndef and MifareUltralight).

In your case, you seem to be less interested in NDEF but more in getting anything from a MIFARE card. Assuming that by MIFARE you mean a MIFARE Classic card (since you were talking about 1996 ;-) ), your tech filter should look like this:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.MifareClassic</tech>
    </tech-list>
</resources>

But note that this only works on devices with NXP chipset (MIFARE Classic is proprietary NXP technology. While NXP licenses the card-side to other manufacturers, they do not license the reader-side. Consequently, only1 NXP's reader products read MIFARE Classic.). On devices with Broadcom chipset, MIFARE Classic cards2 are not detected as MIFARE Classic. But since MIFARE Classic uses the standard ISO 14443-3 Type A anti-collision and activation sequence, these devices usually3 detect such cards as NfcA:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.NfcA</tech>
    </tech-list>
</resources>

Note that using a second entry with MifareClassic tag technology would be redundant as every device that detects MIFARE Classic will detect it as both, MifareClassic and NfcA.


1) There are exceptions to that rule as some manufacturers do not agree with NXP's position (or simply ignore it?) that implementing MIFARE Classic support on the reader-side infringes their rights.

2) And only MIFARE Classic. This does not apply to standards conformant MIFARE products like Ultralight, DESFire, NTAG, etc.

3) Some Samsung devices with Broadcom NFC chipset, like the S4, are exceptions to that rule. On these devices Samsung decided to ban MIFARE Classic completely and instead display a "tag not supported" error. As they claim, to improve user experience as user's would otherwise not understand why they can't write data to those tags. Or as I interpret it, to make your users hate you as an app developer as you can't make your app work on their phones with their tags. See the negative reviews on the ReTag app or on my own app.

Karlene answered 8/5, 2014 at 16:19 Comment(4)
Thanks Michael for the very good explanation... I've realized the tech filter issue after trial and error. The Andorid NFC Basics and Advanced documentation is not clear on this subject. They don't explain the tech filters in detail, only en passant... I've chosen to use NfcA as it will handle both MIFARE and ULTRALIGHT. Since I'm only interested in the tag's serial number I really believe that all NFC-powered ANDROID phones will be able to read it. I'm also using Foreground Dispatcher to make sure my app gets any tag when it is in the foreground. Thank you again.Carollcarolle
After reading the docs again, they mention the AND and OR semantics... Maybe it was my mind meandering after 12 hours of continuous work :-)Carollcarolle
The Android NFC documentation does explain the concept with ANDing and ORing but it gives two invalid examples ;-)Karlene
@Carollcarolle Your code helped me alot. I have a problem : if the app is minimized and i read a card it loads the app mainactivity but shows Hello world and doesnt read the card numberMingmingche
C
4

After a LOT of searching and frustration I started experimenting and realized that you can't filter for all Nfc tag types. Some of them doesn't play well with others (NfcA and MifareClassic for example). I didn't tried to figure out all the possible invalid combinations, but filtering only by NfcA did the trick (MifareClassic and Ultralight will be detected too as they behave like NfcA).

The tech list was changed to this:

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

The Manifest was changed to:

<uses-sdk android:minSdkVersion="10"/>
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
    <activity
        android:name="com.example.nfctest.app.MainActivity"
        android:label="@string/app_name" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
            <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" />
    </activity>
</application>

And then it worked! The activity was launched as expected.

There was no mention to this in the ANDROID documentation. I find it rather lacking most of the time I need to refer to it! But that's life... Another sleepless night (4AM here in Brazil), one less problem for today. :-)

Carollcarolle answered 8/5, 2014 at 6:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.