Android 4.4で追加されたNFC reader mode


盛り上がってるのか盛り上がってないのか、今ひとつ判断出来ない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モードがあるのに、新たなリーダーモードの仕組みが提供されたのか、とても不思議に思いました。単にコールバックでタグのデータを読めるだけかと思ったら、[公式サイト](http://developer.android.com/about/versions/android-4.4.html#Wireless)で以下のように解説されていました。

> 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のソースを見てみると、仕組みがさらに良く分かりました。

```java:android.nfc.NfcAdapter.java
public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, Bundle extras) {
    mNfcActivityManager.enableReaderMode(activity, callback, flags, extras);
}
```

```java: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);
        }
    }
```

```java: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との間にワンクッション置くようになったんですね。

```java: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以前）

```java: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以降で利用可能）

```java: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](https://github.com/androhi/NFCReaderModeSample)


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