Help us understand the problem. What is going on with this article?

Android 4.4で追加されたNFC reader mode

More than 5 years have passed since last update.

盛り上がってるのか盛り上がってないのか、今ひとつ判断出来ないNFC (※1)ですが、Android4.4(KitKat)で結構大きな機能追加がありました。

追加されたのは、以下の2点です。
* ホストカード・エミュレーション
* NFC リーダーモード

ホストカード・エミュレーション

これはAndroidアプリが、ISO14443-4(ISO-DEP)という規格のNFCカードをエミュレート出来るようになります、という機能です。
今までは、NFCタグを読み/書きするReader/Writerモードや、Android端末同士でやり取りするPeerToPeerモード(Android Beam)の機能をサポートしていましたが、新たにCardEmulationモードが追加されたことになります。

NFC リーダーモード

こちらは、拡張版リーダーモードとでも言うような機能です。
この機能を使うことで、表示しているActivityのNFCをリーダーモードに限定させます。NFCタグを検出すると、NfcAdapter.ReaderCallbackインターフェースで受け取ることが出来ます。
この記事では、こちらを掘り下げてみようと思います。

NFC リーダーモードの役割

最初に僕は、なぜAndroid4.4以前に既にReader/Writerモードがあるのに、新たなリーダーモードの仕組みが提供されたのか、とても不思議に思いました。単にコールバックでタグのデータを読めるだけかと思ったら、公式サイトで以下のように解説されていました。

This new capability, in conjunction with host card emulation, allows Android to operate on both ends of a mobile payment interface: One devices operates as the payment terminal (a device running a reader mode activity) and another device operates as the payment client (a device emulating an NFC card).

意訳すると、こんな感じでしょうか。

この新機能の目的は、例えばモバイル決済などの用途で片方のAndroid端末を支払う側、もう片方のAndroid端末を決済端末として動作させるためです。
支払う側はNFCカードをエミュレーションした状態となり、決済端末側は
reader modeが有効化されたActivityが表示された状態となります。

またNfcAdapter#enableReaderMode()の説明では、下記のような説明もありました。

In this mode the NFC controller will only act as an NFC tag reader/writer, thus disabling any peer-to-peer (Android Beam) and card-emulation modes of the NFC adapter on this device.

意訳すると、こんな感じでしょうか。

有効化されたActivityがフォアグランドにいる間、NFCコントローラをリーダモードに制限します。
このモードの間は、Android Beamやカード・エミュレーションモードは無効化されます。

なるほどですね。確かに今後、Android端末のNFCを使った決済をスムーズに行うには、こういった仕組みが必要そうですね。

このNfcAdapter.ReaderCallbackのソースを見てみると、仕組みがさらに良く分かりました。

android.nfc.NfcAdapter.java
public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, Bundle extras) {
    mNfcActivityManager.enableReaderMode(activity, callback, flags, extras);
}
android.nfc.NfcActivityManager.java
    public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, Bundle extras) {
        boolean isResumed;
        Binder token;
        synchronized (NfcActivityManager.this) {
            NfcActivityState state = getActivityState(activity);
            state.readerCallback = callback;
            state.readerModeFlags = flags;
            state.readerModeExtras = extras;
            token = state.token;
            isResumed = state.resumed;
        }
        if (isResumed) {
            setReaderMode(token, flags, extras);
        }
    }
    . . .
    public void setReaderMode(Binder token, int flags, Bundle extras) {
        if (DBG) Log.d(TAG, "Setting reader mode");
        try {
            NfcAdapter.sService.setReaderMode(token, this, flags, extras);
        } catch (RemoteException e) {
            mAdapter.attemptDeadServiceRecovery(e);
        }
    }
android.nfc.INfcAdapter.aidl
interface INfcAdapter {
    . . .
    void setReaderMode (IBinder b, IAppCallback callback, int flags, in Bundle extras);
    . . .
}

最終的にはNFCアダプターのServiceにbindしてました。さらにActivityとServiceの仲介をするようなNfcActivityManagerというクラスを、新たに生成するみたいです。
このNfcActivityManagerクラスで、NFCの各APIを制御してるようです。

比較対象として、Android4.3までで主にReader/Writerモードを有効化する方法の1つであるNfcAdapter#enableForegroundDispatch()では、Serviceへのbindしかしてませんでした。
Serviceとの間にワンクッション置くようになったんですね。

android.nfc.NfcAdapter.java
    public void enableForegroundDispatch(Activity activity, PendingIntent intent,
            IntentFilter[] filters, String[][] techLists) {
        if (activity == null || intent == null) {
            throw new NullPointerException();
        }
        if (!activity.isResumed()) {
            throw new IllegalStateException("Foreground dispatch can only be enabled " +
                    "when your activity is resumed");
        }
        try {
            TechListParcel parcel = null;
            if (techLists != null && techLists.length > 0) {
                parcel = new TechListParcel(techLists);
            }
            ActivityThread.currentActivityThread().registerOnActivityPausedListener(activity,
                    mForegroundDispatchListener);
            sService.setForegroundDispatch(intent, filters, parcel);
        } catch (RemoteException e) {
            attemptDeadServiceRecovery(e);
        }
    }

reader mode 実装の仕方を比較

Android4.4以降もNfcAdapter#enableForegroundDispatch()は使えるので、どちらが優れているとかの問題ではないけど、Readerモードを実装する選択肢が増えたということで、実装サンプルを比較してみました。

NfcAdapter#enableForegroundDispatch()メソッドの場合(主にAndroid4.3以前)

SampleActivity1.java
    public void onResume() {
        super.onResume();

        NfcAdapter nfcAdapter = getDefaultAdapter(this);
        if (nfcAdapter == null) {
            return;
        }

        PendingIntent pendingIntent = PendingIntent.getActivity(
            this, 0, new Intent(this, getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
        IntentFilter ndef = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
        try {
            ndef.addDataType(*/*”);  /* 補足するMIMEタイプを指定する */
        } catch (MalformedMimeTypeException e) {
            throw new RuntimeException(fail, e);
        }
        IntentFilter[] intentFiltersArray = new IntentFilter[] {ndef, };
        String[][] techListsArray = new String[][] { new String[] { NfcA.class.getName() } };

        nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray);
    }

    public void onNewIntent() {
        // Intentに含まれるタグの情報を取得
        Tag tag = intent.getParelableExtra(NfcAdapter.EXTRA_TAG);
    }

NfcAdapter#enableReaderMode()メソッドの場合(Android4.4以降で利用可能)

SampleActivity2.java
    public void onResume() {
        super.onResume();

        NfcAdapter nfcAdapter = getDefaultAdapter(this);
        if (nfcAdapter == null) {
            return;
        }

        nfcAdapter.enableReaderMode(this, new CustomReaderCallback(), FLAG_READER_NFC_A, null);
    }

    private class CustomReaderCallback implements NfcAdapter.ReaderCallback {
        @Override
        public void onTagDiscovered(Tag tag) {
            // タグの情報が渡される
        }
    }

検出するNFCタグのフィルタリングの記述が、かなりスッキリしたなと思いました。
書き方がいろいろあるので一概には言えませんが、少なくともIntentFilterクラスを直接セットするのではなく、フラグ(FLAG_READER_NFC_Aなど)でセットするのは簡潔でいいですよね。

まとめ

新しいReaderモードの実装については、ユースケースとしてNFCでの決済が挙げられていましたが、それ以外にもReaderモードに固定しておきたいシーンはいろいろとありそうな気がします。そういったケースではこの新しいReaderモードの仕組みは、動作の面でもソースコードを見た目の面でも、とても分かり易いなと思いました。

注意点としては、NFCタグの情報をやり取りするには、今まで通りある程度のNFCの仕様を知っておいた方がいいのは変わらないので、「NFC NDEF」といったキーワードでググると幸せになれると思います。

また簡単なNfcAdapter.ReaderCallbackのサンプルを、以下にアップしました。
ご興味ありましたら参考にどうぞ。
GitHub: NFCReaderModeSample

※1 : 日本語の書籍も増えてきたしNFC搭載端末も多くなったと思う反面、僕の親族にNFCを知ってる人は未だいないという…

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした