Android NFC
ちょいとNFCについて調べたのでメモ。
NFCには規格がいくつかあるが、AndroidでNFCといったら、NFCフォーラム仕様のことを指す。
NFCフォーラム仕様のNFCは下記の3つに対応している
NDEF(NFC Data Exchange Format)
NFCのタグフォーマット形式。Androidで用いられる形式もNDEFに準拠
NDEFには(通常)1つのNDEF Messageが含まれ、NDEF Messageには0個以上のNDEF Recordが含まれる。
NDEF Message | |||
NDEF Record 0 | NDEF Record 1 | NDEF Record 2 | ... |
3つのモード
NFC Forum Tag
AndroidでのNFCの扱い
最低限Manifestファイルに記述する必要がある内容
必要なAPI Level は10以上. AAR(後述)の書き込みは14以上。
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.NFC" />
Play Store 配信時にNFC対応端末でしかアプリが表示されないようにしたい場合は次のように書く。
<uses-feature android:name="android.hardware.nfc" android:required="true"/>
タグテクノロジー
NFCタグを読み取った際に、そのタグの規格などを示す文字列。
- "NfcA" ISO/IEC 14443 Type A 準拠のタグ
- "NfcB" ISO/IEC 14443 Type B 準拠のタグ
- "NfcF" JIS 6319-4 準拠のタグ (Felica)
- "NtcV" ISO/IEC 15693 準拠のタグ (VはVicinityの略?)
- "IsoDep" ISO/IEC 14443で規定されているデータ交換形式DEP(Data Exchange Protocol)で通信可能なタグ
- "MifareUltralight" NfcAに基づいた規格の一つ。低速。積んでいるメモリが少ない。
- "MifareClassic" NfcAに基づいた規格の一つ。メモリが多い。※キャリアから発売しているAndroid端末では読み取れない場合があるらしい。
- "Ndef" NDEFフォーマット済みのタグ。このタグはNDEFデータを含んでいる。
- "NdefFormatable" NDEFフォーマット可能なタグ。フォーマットするとタグテクノロジーがNdefになる。
- "NfcBarcode" RFIDタグの一種。まだ普及していない。
タグディスパッチシステム
NFCタグから受け取ったデータを解析し、アプリに適切に引き渡す仕組み。
起動アプリ決定フロー
- 現在フォアグラウンドにいるアクティビティが優先的にタグを受け取る設定になっている場合、そのアクティビティで宣言されているフィルタに従い、アクティビティを起動する
- 読み取ったNFCタグにAAR(Android Application Record)が含まれているかどうかを認識する。含まれている場合、そのアプリを起動し、アプリが未インストールなら、Google Playでそのアプリを検索する。
- 端末にインストールされているアプリのAndroidManifest.xmlにて定義されたアクティビティに設定されたフィルタに従い、起動できるアクティビティの一覧を表示する。
NFCタグが読み込まれた後のIntentアクション
NFCタグが読み込まれるとタグディスパッチシステムは次の1,2,3の優先順位でNFCタグ情報をActivityへ送る
- NDEF_DISCOVEREDアクション(NDEF フォーマット済み)
- TECH_DISCOVEREDアクション(未知またはNDEFフォーマットされていないタグ)
- TAG_DISCOVEREDアクション(いずれにも当てはまらない)
AndroidManifest.xmlのインテントフィルタ
NDEF_DISCOVEREDアクションのインテントフィルタ(API Level 10)
NDEF_DISCOVEREDアクションを受け取る場合
<intent-filter > <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter>
NDEFに指定したMIMEタイプが存在すれば読み取る場合
<intent-filter > <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter>
NDEFに指定したスキームが存在すれば読み取る場合
<intent-filter > <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="http" /> <data android:scheme="https" /> </intent-filter>
TECH_DISCOVEREDアクションのインテントフィルタ(API Level 10)
指定したTECHLISTに該当するNFCを読み取る場合
<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/filter_nfc" />
TECHLISTの例(res/xml/filter_nfc.xml)
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <tech-list> <tech>android.nfc.tech.NfcA</tech> <tech>android.nfc.tech.Ndef</tech> </tech-list> <tech-list> <tech>android.nfc.tech.NfcB</tech> <tech>android.nfc.tech.Ndef</tech> </tech-list> <tech-list> <tech>android.nfc.tech.NfcF</tech> <tech>android.nfc.tech.Ndef</tech> </tech-list> <tech-List> <tech>android.nfc.tech.NfcV</tech> <tech>android.nfc.tech.Ndef</tech> </tech-List> <tech-list> <tech>android.nfc.tech.NdefFormatable</tech> </tech-list> </resources>
TAG_DISCOVREDアクションのインテントフィルタ(API Level 9)
タグテクノロジーのリストを指定しているアプリが無い、または指定してあってもそれらに該当しない場合に、
汎用的なタグ情報などを確認したいとき。
あまり使う機会は無い。
<intent-filter> <action android:name="android.nfc.action.TAG_DISCOVERED" /> </intent-filter>
NFCタグの検知
Androidで、NFCを扱うためのクラスがNfcAdapter。static メソッドgetDefualtAdapter(Context context)でインスタンスの生成を行う。
よく使うので、onCreate()で生成してメンバ変数で持っておく。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mNfcAdapter = NfcAdapter.getDefaultAdapter(this); //NFCを扱うためのインスタンスを取得 setContentView(R.layout.activity_main); }
NFCが有効になっているかどうかの確認
端末にNFC非対応の場合、NfcAdapter.getDefaultAdapter()の結果がnullになる。
また,NFC機能がオンになっているかどうかはNfcAdapter#isEnabled()で確かめる。
private boolean hasNFC() { return mNfcAdapter != null; } private boolean isEnabledNFC() { return hasNFC() && mNfcAdapter.isEnabled(); }
NFCがOFFになっているだけなら、NFCの設定画面を開いてあげると親切。
String str = ""; if (!hasNFC()) str = "NFC not supported"; if (!isEnabledNFC()) { str = "Please enable NFC"; String setting = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN ? Settings.ACTION_NFC_SETTINGS : Settings.ACTION_WIRELESS_SETTINGS; startActivity(new Intent(setting)); } if (!str.isEmpty()) Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
フォアグラウンドディスパッチ
NFCの検知をフォアグラウンドにいるActivityを優先的に処理させる(無駄にアプリ選択が面を表示させないため)には、フォアグラウンドディスパッチを有効にする。
有効にするにはNfcAdapter.enableForegroundDispatch()メソッドを使用する。通常onResume()で行う。
NfcAdapter.enableForegroundDispatch(Activity activity, PendingIntent intent, IntentFilter[] filters, String[][] techLists)
- タグをスキャンした時に最初にインテントフィルターにマッチするかをチェック
- もし、インテントフィルターにマッチしていなければ、NfcAdapter.ACTION_TECH_DISCOVERED にフォールバックするので、次にテクノロジーリストにマッチするかをチェック
インテントフィルターは、NDEFフォーマットされていて、MIMEタイプを持つものに限られる。URL や SmartPoster はMIMEタイプを持たないため、インテントフィルターではマッチせず、NfcAdapter.ACTION_TECH_DISCOVERED にフォールバックする。また、そもそもNDEFフォーマットされていなければ、NfcAdapter.ACTION_TECH_DISCOVERED にフォールバックする。nullを指定した場合、テクノロジーリストにマッチしたタグ全てを受信する。
次に テクノロジーリストにマッチするかをチェックされる。 nullを指定した場合、インテントフィルタにマッチしたタグ全てを受信する。
インテントフィルタとテクノロジーリスト共にnullなら全てのタグを受信する。
フォアグラウンドディスパッチを無効にするには、NfcAdapter.disableDispatch(Activity activity)メソッドを使用する。
通常、onPause()で記述し、Activityがバックグラウンドに回った際は、優先的に受けるのをやめる。
@Override protected void onResume() { super.onResume(); //Handle all of our received NFC intents in this activity. if (mNfcAdapter != null) { Intent intent = new Intent(this, this.getClass()).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); mNfcPendingIntent = PendingIntent.getActivity(this, 0, intent, 0); /** filters */ IntentFilter actionNdef = IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "*/*"); //= IntentFilter.create(NfcAdapter.ACTION_NDEF_DISCOVERED, "image/png"); IntentFilter[] filters //= null; //タグ内部のデータを制限しない場合 = new IntentFilter[] { actionNdef }; /** techList */ String[][] techLists //= null; //タグの種類を制限しない場合 = new String[][] { new String[] { NfcA.class.getName() }, new String[] { NfcB.class.getName() }, new String[] { NfcF.class.getName() }, new String[] { NfcV.class.getName() }, }; mNfcAdapter.enableForegroundDispatch(this, mNfcPendingIntent, filters, techLists); } } @Override protected void onPause() { super.onPause(); //Activityがバックグラウンドに回った際は、優先的に受け取るのをやめる if (mNfcAdapter != null) mNfcAdapter.disableForegroundDispatch(this); }
NFCインテントの検知
onNewIntent()でNFCタグの情報を含んだインテントを受信する。ただし、NFCのインテント以外も受信するので、NFCアクションかどうかを確認する。
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); String action = intent.getAction(); if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) || //NDEFフォーマット済み NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) || //未知もしくはNDEFフォーマットされていない 初回書き込み時には必要? NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) { //それ以外。
UIDの取得
intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)で取得。
StringBuilder sb = new StringBuilder(); //NFC-UID取得 byte[] uid = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID); if (uid != null) for (byte id: uid) sb.append(String.format("%02x", id & 0xff));
タグ情報の取得
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)で取得。
public static String[] getTechList(Intent intent) { Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); return tag != null ? tag.getTechList() : new String[0]; }
NdefMessageの取得
intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)で取得。
※注意 配列の形で返ってくる。通常、NdefMessageは1つなので一番目の要素を見る。
public static NdefMessage getNdefMessage(Intent intent) { Parcelable[] raws = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); return raws == null || raws.length == 0 ? null : (NdefMessage)raws[0]; } public static NdefMessage[] getNdefMessages(Intent intent) { Parcelable[] raws = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); if (raws == null) return new NdefMessage[0]; NdefMessage[] msgs = new NdefMessage[raws.length]; for (int i = 0; i < raws.length; ++i) msgs[i] = (NdefMessage) raws[i]; return msgs; }
NdefRecordの取得
NdefMessage.getRecords()でNdefRecordを取得
NdefRecordクラスには、ID(byte), Payload(byte), Tnf(short), Type(byte)のゲッタメソッドがある。
byte⇄Stringの変換は↓で行うが、AndroidではDefaultCharsetがUTF-8なので、Charset.forName("UTF-8")は省略してもいいはず。他のCharsetを使う場合はちゃんと指定する。
byte[] bytes = str.getBytes(Charset.forName("UTF-8")); //String -> byte[] String str = new String(bytes, Charset.forName("UTF-8")); //byte[] -> String
byte[]を16進表示にしたいなら↓みたいな感じ。
javaにunsignedが追加される日は来るのだろうか...
public static String bytesToString(byte[] byteArray) { StringBuilder sb = new StringBuilder(); for (byte b: byteArray) sb.append(String.format("%02x", b & 0xff)); return sb.toString(); }
NDEFの作り方やパースの仕方はまた今度