Posted at
AndroidDay 20

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を知ってる人は未だいないという…