プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

【Android】 NFC を試しました

以前から興味はあったのですが、ようやく仕事で使う機会に恵まれそうなので、 NFC を試してみました。

既にネットにいろいろと情報がありますが、自分なりに気づいた点をメモしておきます。

なお、検証は Nexus 6P, Android 8.1.0 で行いました。

NFC 全般について

最初はネットの記事などをいろいろと呼んでいたのですが、今一つ腑に落ちません。ということで、ちょっと昔の本になりますが、一冊 Amazon で買いました。

NFC Hacks ―プロが教えるテクニック & ツール

NFC Hacks ―プロが教えるテクニック & ツール

私の場合、やっぱり、何か新しいものを理解しようとするときは、本で概要をつかむほうが手っ取り早いです。 NFC Forum Tag の仕様なども載っていて、NFC 触るなら手元に一冊あってもいい感じだと思います。

タグの購入

NFCのテスト用に Amazon で買いました。

NXP Ntag 213 チップ, Type-2 タグ。シールタイプ。

にしても、手軽に買えるようになりましたね。

読み込みサンプル

大きく分けて、NFC の読み込みを行う際は、

  • スマホで NFC タグにタッチした際に、アプリを起動させる
  • アプリを起動している際に、 NFC にタッチして読み込む

の2パターンがあると思います。

前者を実現するには、 AndroidManifest にインテントフィルタ(暗黙的インテント)を定義してやります。後者に対応するには、アプリ内で NfcAdapter#enableForegroundDispatch を呼び出す必要があります(NfcAdapter#enableReaderMode でもできると思いますが、今回は試していません)。

個人的には後者でやるほうが、動きがわかりやすいように思います。

なお、今回作成した NFC の読み取りと(この記事では別段書き込みに触れてるところはないですが)書き込みサンプルをそれぞれ Gist に上げておきます。何かの参考にしてください。

NFC 読み取りサンプル · GitHub

NFC 書き込みサンプル · GitHub

インテントの指定方法

Android のドキュメントには、NFC を読み込んだ際、3種類のインテントが発行されるとあります。この際、ACTION_TAG_DISCOVERED 以外のインテントにはインテント名だけでなく、インテントフィルタの定義に追加情報が必要です。

インテント 追加情報
ACTION_NDEF_DISCOVERED mimetype
ACTION_TECH_DISCOVERED tech list
ACTION_TAG_DISCOVERED

追加情報がない場合、インテントフィルタを定義していてもその、正しくインテントの呼び出しがおこなわれませんでした(追加情報なしだからといって、ワイルドカード的にはならないようです)。以下に、具体例を示しておきます。

(参考) ACTION_TECH_DISCOVERED に tech list が必要という話

Android NFC Intent-filter for all type - Stack Overflow

ACTION_NDEF_DISCOVERED の例

NDEF に text/plain の情報が書き込まれているNFCタグを読み取る場合を想定します。

AndroidManifest での定義

例えば、AndroidManifest にインテントフィルタを定義して、スマホでNFCタグにタッチした際に、アプリを起動させてみると、

指定した mimeType アプリ起動
text/plain
image/jpeg ×
なし ×

となります。

mimeType 指定が text/plain を指定

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTask">

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

mimeType 指定なし

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.nfc.action.NDEF_DISCOVERED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
NfcAdapter#enableForegroundDispatch による指定

今度は、NfcAdapter#enableForegroundDispatch を呼び出し、アプリを起動中に、NFCタグにタッチして正しく読み込めるか試すと、

mimeType 指定 読み込み(画面表示)
text/plain
image/jpeg ×
なし ×

となりました。

mimeType 指定が text/plain の場合

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

        try {
            ndefFilter.addDataType("text/plain");
        } catch (IntentFilter.MalformedMimeTypeException e) {
            e.printStackTrace();
        }

        IntentFilter[] intentFilters = new IntentFilter[] {ndefFilter};
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

mimeType 指定がなし

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter ndefFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {ndefFilter};
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

ACTION_TECH_DISCOVERED の例

次に、購入直後のNFCタグ(一度も書き込みを行っていないタグ, Type-2タグ)を読み取る場合を想定します。

AndroidManifest での定義

上記と同じく、AndroidManifest にインテントフィルタを定義して、スマホでNFCタグにタッチした際に、アプリを起動させてみると、

tech list 指定 アプリ起動
tech list あり
なし ×

となりました(下記に示すように tech list として xml/nfc_tect_list.xml を作成して、その中で Ndef を指定しています)。

AndroidManifest.xml

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

xml/nfc_tect_list.xml の中身

<?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-list>
</resources>
NfcAdapter#enableForegroundDispatch による指定

例えば、NfcAdapter#enableForegroundDispatch を呼び出し、アプリを起動中に、NFCタグにタッチして正しく読み込めるか試すと、

tech list 指定 アプリ起動
tech list あり
なし ×

となります。

tech list がある場合の NfcAdapter#enableForegroundDispatch 呼び出し

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter techFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {techFilter};

        String tech = Ndef.class.getName();
        String techlist[] = new String[]{tech};           // and 条件
        String techLists[][] = new String[][] {techlist}; // or 条件

        // TECH_DISCOVERED, tech list 必須
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, techLists);
    }

tech list がない場合の NfcAdapter#enableForegroundDispatch 呼び出し

    @Override
    protected void onResume() {
        super.onResume();
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);
        IntentFilter techFilter = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);

        IntentFilter[] intentFilters = new IntentFilter[] {techFilter};

        // TECH_DISCOVERED, tech list 必須
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null);
    }

ACTION_NDEF_DISCOVERED の例外

ドキュメントによると NfcAdapter#enableForegroundDispatch のインテントフィルタと tech list 引数の両方に null を渡した場合、 ACTION_TAG_DISCOVERED を取得できるとあります。

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass()), 0);

        // すべてを受け付ける
        mNfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null);

でも、実はこの時、 ACTION_TAG_DISCOVERED だけでなく、 ACTION_NDEF_DISCOVERED にも反応していました。この時は、上記と異なり、mimeType の指定がなくてもです。

AndroidManifest でのインテントフィルタと NfcAdapter#enableForegroundDispatch のインテントフィルタは同じように指定できると思っていたのですが、この部分については両者で異なるようです。

なお、 NfcAdapter#enableForegroundDispatch を呼び出す際に、 ACTION_TAG_DISCOVERED をインテントフィルタで明示的に指定しても、取得することができました。

ACTION_TAG_DISCOVERED に反応するアプリについて

なお、今回実機でいろいろと試していたら、下記の画面が表示されることがありました。

f:id:junichim:20200914184257p:plain

これは、購入してまだ書き込みを行っていない NFC タグを読み取ったときに出てきました。

どうもこれは、 ACTION_TECH_DISCOVERED および ACTION_TAG_DISCOVERED に対応するアプリがインストールされていないときに、おそらくシステムが用意しているアプリが反応したのだと思われます。似たような話が下記にありました。

https://webcache.googleusercontent.com/search?q=cache:dkdBGBspafcJ:https://consumer.huawei.com/jp/support/content/ja-jp00711127+&cd=1&hl=ja&ct=clnk&gl=jp

また、自分で作成したNFC読み取りサンプルが ACTION_TECH_DISCOVERED または ACTION_TAG_DISCOVERED に対応させたときは、このアプリの表示もなくなったので、ほぼあたっているかと思います。特に問題があるわけではありませんが、こういうものがありました、というメモです。

NDEFメッセージについて

Gist の処理を見るとわかりますが、 ACTION_NDEF_DISCOVERED の場合に、 NdefMessage を取得しようとすると、配列の形式で取得します。 でも、 NFC (NDEF) の仕様を眺めていても、Ndefメッセージは一つだけのようです。これはなぜなんでしょうか?

調べてみると、やはり、基本的には1つのNFCタグには1つのNDEFメッセージのようです(参考までに、1つのNDEFメッセージには複数のNDEFレコードを入れることができるようです)。

1つのNFCタグに2つのNDEFメッセージ/レコード - Android

Android のドキュメントにも通常一つのNDEFメッセージとあります。でも、将来の拡張性のため配列にして扱ってる、と書いてます。このため、 NdefMessage を取得する時、配列で戻ってきているようです。

NfcAdapter  |  Android デベロッパー  |  Android Developers

これで、だいたい感触がつかめたので、引き続きいろいろと試そうと思います。