読者です 読者をやめる 読者になる 読者になる

kanetaiの二次記憶装置

プログラミングに関するやってみた、調べた系のものをQitaに移して、それ以外をはてブでやる運用にしようと思います。http://qiita.com/kanetai

Android NFC

ちょいとNFCについて調べたのでメモ。

NFCには規格がいくつかあるが、AndroidNFCといったら、NFCフォーラム仕様のことを指す。
NFCフォーラム仕様のNFCは下記の3つに対応している

  • NDEF(読み込みフォーマット)
  • 3つのモード(NFC通信仕様)
  • NFC Forum Tag
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つのモード
  • P2P: NFCでバイス同士通信するモード。コマンド送信側がイニシエータ。受信側がターゲット。Android Beam
  • リーダ/ライタ: NFCデバイスがNFCタグからデータの読み書きするモード。
  • カードエミュレーション: NFCデバイスが非接触ICカードの役割をするモード。Androidでは提供されていない。
NFC Forum Tag

NFC Forumで規定してある最低限対応しなければならない(NFCタグとしてフォーマット可能な)タグ。

  • Type 1 Tag: TOPAZ(Broadcom)
  • Type 2 Tag: MIFARE UltraLite (NXP)
  • Type 3 Tag: Felica (Sony)
  • Type 4 Tag: ISO/IEC 14443-4 (DESFire(NXP) etc.)

AndroidでのNFCの扱い

最低限Manifestファイルに記述する必要がある内容

必要なAPI Level は10以上. AAR(後述)の書き込みは14以上。

<uses-sdk 
	android:minSdkVersion="14"
        android:targetSdkVersion="17" />

NFCを使うためのパーミッション

<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タグから受け取ったデータを解析し、アプリに適切に引き渡す仕組み。
起動アプリ決定フロー

  1. 現在フォアグラウンドにいるアクティビティが優先的にタグを受け取る設定になっている場合、そのアクティビティで宣言されているフィルタに従い、アクティビティを起動する
  2. 読み取ったNFCタグにAAR(Android Application Record)が含まれているかどうかを認識する。含まれている場合、そのアプリを起動し、アプリが未インストールなら、Google Playでそのアプリを検索する。
  3. 端末にインストールされているアプリのAndroidManifest.xmlにて定義されたアクティビティに設定されたフィルタに従い、起動できるアクティビティの一覧を表示する。

NFCタグが読み込まれた後のIntentアクション
NFCタグが読み込まれるとタグディスパッチシステムは次の1,2,3の優先順位でNFCタグ情報をActivityへ送る

  1. NDEF_DISCOVEREDアクション(NDEF フォーマット済み)
  2. TECH_DISCOVEREDアクション(未知またはNDEFフォーマットされていないタグ)
  3. 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();

※注意 Settings.ACTION_NFC_SETTINGSはAPI Level 16以上

フォアグラウンドディスパッチ

NFCの検知をフォアグラウンドにいるActivityを優先的に処理させる(無駄にアプリ選択が面を表示させないため)には、フォアグラウンドディスパッチを有効にする。
有効にするにはNfcAdapter.enableForegroundDispatch()メソッドを使用する。通常onResume()で行う。

NfcAdapter.enableForegroundDispatch(Activity activity, PendingIntent intent, 
	IntentFilter[] filters, String[][] techLists)
  1. タグをスキャンした時に最初にインテントフィルターにマッチするかをチェック
  2. もし、インテントフィルターにマッチしていなければ、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の作り方やパースの仕方はまた今度