盛り上がってるのか盛り上がってないのか、今ひとつ判断出来ない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のソースを見てみると、仕組みがさらに良く分かりました。
public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, Bundle extras) {
mNfcActivityManager.enableReaderMode(activity, callback, flags, extras);
}
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);
}
}
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との間にワンクッション置くようになったんですね。
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以前)
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以降で利用可能)
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を知ってる人は未だいないという…