ICカードだけじゃない!? AndroidのNFCで読み書きしてみた

# はじめに
AndroidにNFCが搭載されたのはバージョン2.3の頃で7，8年前でしょうか、だいぶ年月が経ちました。
その後、iPhoneにも搭載されておサイフケータイやポイントサービスアプリなどで日常で利用されるシーンも多くなりました。
弊社でも「LibJeID」というマイナンバーカードや運転免許証をスマートフォンで読みとることが可能なライブラリの提供をはじめました。

AndroidのSDKでは一般にAPIが早くから公開され、私も興味がありNFCを使ったコードを書いたりしていました。
広く利用されているICカード以外にも小さなアクセサリやペラペラのシールでもNFCで読み書きが出来るのですが、今回はそういったメモリー機能だけを持ったNFCタグについての紹介をしていこうと思います。

# NFCの規格について
Androidで利用できるNFC関連の規格についてまず簡単に図で示してみました。
NFCフォーラムで既定されていないものについては()で表現しています。
![NFC図.png](https://qiita-image-store.s3.amazonaws.com/0/316107/63e0149f-e623-bf9e-23f5-2601fe0b6f66.png)

NFCの機能は4種類あり、通常私たちが利用しているのはカードエミュレーションでICカードのようにスマートフォンを利用できる機能です。
後ほど実装の紹介で利用しているのが、リーダー/ライター機能でNFCタグのNDEFフォーマットの操作をします。
RFレイヤーは通信の規約に関するタイプとなっています。TypeVはNFCフォーラムの規格にはありませんが、Androidでは利用できます。
タグタイプはNFCタグのフォーマット等に関するタイプです。RFレイヤーが同じでもこのタグタイプの違いによって読み書きの仕方が変わってきます。
Mifare Classicはヨーロッパなどで交通系ICカードで広く利用されているタイプですが、NFCフォーラムの規格にはありません。これはセキュリティ上の問題が関係しているかと思われます。
AndroidのAPIにはあるのですが端末によって読めるものと読めないものがあります。
最後にプロダクトには代表的なカードやタグを載せておきました。
この図と説明を覚えればちょっとNFCをかじった感をかもし出せます。

# Mifare Ultralight
安価なメモリータイプのNFCタグとしてMifare Ultralightを紹介します。
以下の記事にもあるとおり、2018 FIFAワールドカップ ロシアの公式チケットに利用されています。
https://www.paymentnavi.com/paymentnews/75797.html
こういった使い捨てでも可能なNFCタグとしての利用シーンがあります。
日本で2020年に開催される東京オリンピック・パラリンピックでも利用されるかもしれませんね。

Mifare UltralightはNXP社(フィリップスの半導体部門独立)製のNFCタグです。
特徴としては、
　・NFCフォーラムのタグタイプ「Type2」
　・RFレイヤーのTypeA
　・メモリーサイズ
　　Ultralight：64byte (16ページ * 4byte)
　　Ultralight C：192byte (48ページ * 4byte)
　　Ultralight X：64byte超(16ページ 超* 4byte)
　　※64byte超のUltralightXはTLVでメモリエリアを制御
　・単なるメモリーカードでアクセス制御がついている。
　・とても安価である。

# AndroidのNFC関連API
AndroidでNFCのスペックが実装されているAPIの主なものを並べてみます。

```
android.nfc.tech.IsoDep
android.nfc.tech.MifareClassic
android.nfc.tech.MifareUltralight
android.nfc.tech.Ndef
android.nfc.tech.NdefFormatable
android.nfc.tech.NfcA
android.nfc.tech.NfcB
android.nfc.tech.NfcBarcode
android.nfc.tech.NfcF
android.nfc.tech.NfcV
```
先にも述べたとおりですがNFCフォーラム以外のものもあります。
Androidに実装された当初からありましたので、さすがGoogleのエンジニアさんですね手際がいい。

このtechリストの中からMifare Ultralightではandroid.nfc.tech.NfcAとandroid.nfc.tech.Ndefを使ってテクニカル情報を取得します。

・android.nfc.tech.NfcA
　ATQA: getAtqa()
　SAK: getSak()
　Maximum transceive length: getMaxTransceiveLength()
　Maximum transceive time-out: getTimeout()
　※transceive(byte[])で直接コマンド送信することによりlengthとtime-outが取得できる
・android.nfc.tech.Ndef
　NDEFの最大サイズ:getMaxSize()
　NDEFの書き込み可:isWritable()
　NDEFの書き込み禁止可:canMakeReadOnly()

ちなみにMifare Ultralightのカード種類判定については以下のとおりです。
　SAK == 00 && getMaxSize() == 64 Ultralight
　SAK == 00 && getMaxSize() == 192 Ultralight C
　SAK == 00 && getMaxSize() != 64,192 Ultralight X

実際のNDEFのコンテンツについてはIntentから取得することができます。
Intentから受け取ったRawメッセージを以下のように取り出して表示します。
android.nfc.NdefMessage[] → android.nfc.NdefRecord[] → byte[] payload 
	
また、NdefRecordには次が定義されています。
RTD(レコード定義)：RTD_TEXT、 RTD_URI、RTD_SMART_POSTERなど
TNF(タイプネームフォーマット)：TNF_WELL_KNOWN、TNF_MIME_MEDIA、TNF_ABSOLUTE_URIなど

これでMifare Ultralightを読み書きするためのAPIの予習が完了です。

# いよいよ実装
Androidアプリの実装をこれから解説します。まず画面構成は下記のようにしました。

![画面構成.png](https://qiita-image-store.s3.amazonaws.com/0/316107/cf6c8b64-732b-ddeb-982a-04fef9ca3443.png)

アプリでNFCが利用できるようManifestに下記設定を入れるのを忘れずに。

```xml
<uses-permission android:name="android.permission.NFC"></uses-permission>
```
タブで画面を構成するフラグメントを切り替えて表示します。
AcitivityのonCreate()で画面の実装をします。

```java
	public void onCreate(Bundle savedInstanceState) {

		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_nfc_host);

		mFragmentManager = getSupportFragmentManager();
        mFragmentTransaction = mFragmentManager.beginTransaction();
		// Set up the TabHost
		mTabHost = (FragmentTabHost) findViewById(android.R.id.tabhost);
		mTabHost.setup(this, mFragmentManager, R.id.realtabcontent);

		ndefReadFragment = (NdefReadFragment) Fragment.instantiate(this,NdefReadFragment.class.getName());
		ndefTechFragment = (NdefTechFragment) Fragment.instantiate(this,NdefTechFragment.class.getName());
		ndefWriteFragment = (NdefWriteFragment) Fragment.instantiate(this,NdefWriteFragment.class.getName());

		TabSpec tab1 = mTabHost.newTabSpec("scan");
		tab1.setIndicator("SCAN");
		mTabHost.addTab(tab1,ndefReadFragment.getClass(),null);
     	TabSpec tab2 = mTabHost.newTabSpec("tech");
		tab2.setIndicator("TECH");
		mTabHost.addTab(tab2,ndefTechFragment.getClass(),null);
		TabSpec tab3 = mTabHost.newTabSpec("write");
		tab3.setIndicator("WRITE");
		mTabHost.addTab(tab3,ndefWriteFragment.getClass(),null);

		mTabHost.setOnTabChangedListener(this);

        mFragmentTransaction.add(R.id.realtabcontent,ndefTechFragment,"NdefTechFragment");
        mFragmentTransaction.add(R.id.realtabcontent,ndefWriteFragment,"NdefWriteFragment");
        mFragmentTransaction.add(R.id.realtabcontent,ndefReadFragment,"NdefReadFragment");
		mFragmentTransaction.commit();

		mLastTabId = "scan";

```

続けてIntentFilterでNFCタグを検出するための実装をします。

```java
		mNfcPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this,
				getClass()).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);

		// techList
		mTechList = new String[][] { new String[] { NfcF.class.getName()},new String[] { NfcA.class.getName()},new String[] { NfcB.class.getName()}};

		// Intent filters for reading a note from a tech.
		IntentFilter techDetected = new IntentFilter(
				NfcAdapter.ACTION_TECH_DISCOVERED);

		// Intent filters for reading a note from a tag or exchanging over p2p.
		IntentFilter ndefTextDetected = new IntentFilter(
				NfcAdapter.ACTION_NDEF_DISCOVERED);
		try {
			ndefTextDetected.addDataType("text/plain");
		} catch (MalformedMimeTypeException e) {
		}
		// Intent filters for reading a note from a tag or exchanging over p2p.
		IntentFilter ndefUriDetected = new IntentFilter(
				NfcAdapter.ACTION_NDEF_DISCOVERED);
		ndefUriDetected.addDataScheme("http");
		ndefUriDetected.addDataScheme("https");

		mReadTagFilters = new IntentFilter[] { ndefTextDetected, ndefUriDetected, techDetected };

		mWriteTagFilters = new IntentFilter[] { techDetected };

		// Intent filters for writing to a tag
		IntentFilter tagDetected = new IntentFilter(
				NfcAdapter.ACTION_TAG_DISCOVERED);
		mWriteTagFilters = new IntentFilter[] { tagDetected };

		
	}
```
onNewIntent()でタグが検出されたときの実装をします。

```java
	@Override
	protected void onNewIntent(Intent intent) {

		if (!mWriteFlg){
			// NDEF exchange mode
			if(NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())
					|| NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) {
				promptForContent(intent);
			// Tag writing mode (Button off)
			} else if(NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
				;
			} 
		}

		// Tag writing mode (Button on)
		if (mWriteFlg
				&& NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())) {
			Tag detectedTag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
			NdefWriteFragment writeFragment = (NdefWriteFragment)mFragmentManager.findFragmentByTag("NdefWriteFragment");
			writeFragment.writeTag(detectedTag);
		}
	}

	private void promptForContent(final Intent intent) {
		new AlertDialog.Builder(this)
				.setTitle("Replace current content?")
				.setPositiveButton("Yes",
						new DialogInterface.OnClickListener() {
							@Override
							public void onClick(DialogInterface arg0, int arg1) {
								NdefReadFragment ndefReadFragment = (NdefReadFragment)getSupportFragmentManager().findFragmentByTag("NdefReadFragment");
								ndefReadFragment.setReadFragment(intent);
							}
						})
				.setNegativeButton("No", new DialogInterface.OnClickListener() {
					@Override
					public void onClick(DialogInterface arg0, int arg1) {

					}
				}).show();
	}
```
onTabChanged()でタブが切り替わる際の実装をします。

```java
	public void onTabChanged(String tabId) {

		Log.i(TAG, "onTabChanged()");

		if(mLastTabId != tabId){
			mFragmentTransaction = getSupportFragmentManager().beginTransaction();
			if("scan" == tabId){
				mFragmentTransaction.replace(R.id.realtabcontent,ndefReadFragment,"NdefReadFragment");
				disableTagWriteMode();
				enableNdefExchangeMode();
			}else if("tech" == tabId){
				mFragmentTransaction.replace(R.id.realtabcontent,ndefTechFragment,"NdefTechFragment");
				disableTagWriteMode();
				enableNdefExchangeMode();
			}else if("write" == tabId){
				mFragmentTransaction.replace(R.id.realtabcontent,ndefWriteFragment,"NdefWriteFragmnet");
				disableNdefExchangeMode();
				enableTagWriteMode();
			}

			mLastTabId = tabId;
			mFragmentTransaction.commit();
		}
	}
```
Fragment①に処理が切り替わったら、検出したタグによって処理を実装します。

```java
 		// NDEF information
		if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)
				|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {

			Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
			String[] techList = tag.getTechList();
			for (String tech : techList) {
				if(tech.equals("android.nfc.tech.NfcA")){
					techFragment.setTechAFragment(intent);
				}
				if(tech.equals("android.nfc.tech.NfcB")){
					techFragment.setTechBFragment(intent);
				}
				if(tech.equals("android.nfc.tech.NfcF")){
					techFragment.setTechFFragment(intent);
				}
			}

			procNdef(intent);

 		// Felica information
		} else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {

			Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
			String[] techList = tag.getTechList();
			String techSelect = "";
			for (String tech : techList) {
				if(tech.equals("android.nfc.tech.NfcA")){
					techFragment.setTechAFragment(intent);
					techSelect = tech;
				}
				if(tech.equals("android.nfc.tech.NfcB")){
					techFragment.setTechBFragment(intent);
					techSelect = tech;
				}
				if(tech.equals("android.nfc.tech.NfcF")){
					techFragment.setTechFFragment(intent);
					techSelect = tech;
				}
			}

			// Felica Card
			if(techSelect.equals("android.nfc.tech.NfcF")){
				procFelica(intent);
			}
			if(techSelect.equals("android.nfc.tech.NfcB")){
				IsoDep isoDep = IsoDep.get(tag);
				if(isoDep != null){
					procDriversCard(intent);
				}
			}
			// Mifare Classic Card
			if(techSelect.equals("android.nfc.tech.NfcA")){
				MifareClassic mfClassic = MifareClassic.get(tag);
				if(mfClassic != null){
					procMfClassic(intent);
				}
			}

```
実際のNDEFメッセージの処理は下記のように実装します。

```java
	/**
     * NDEFメッセージの処理
	 *
	 * @param intent
	 */
	private void procNdef(final Intent intent) {

    	NdefMessage[] msgs = null;
 		StringBuilder sb = new StringBuilder();
 		StringBuilder sb2 = new StringBuilder();

		Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
		if (rawMsgs != null) {

			msgs = new NdefMessage[rawMsgs.length];
			for (int i = 0; i < rawMsgs.length; i++) {
				msgs[i] = (NdefMessage) rawMsgs[i];
				for (NdefRecord record : msgs[i].getRecords()) {

					byte[] payload = record.getPayload();

					sb.append("<Record[" + i + "]>\n");

					sb.append("TNF : ");
					switch (record.getTnf()) {
					case NdefRecord.TNF_EMPTY:
						sb.append("TNF_EMPTY");
						break;
					case NdefRecord.TNF_WELL_KNOWN:
						sb.append("TNF_WELL_KNOWN");
						break;
					case NdefRecord.TNF_MIME_MEDIA:
						sb.append("TNF_MIME_MEDIA");
						break;
					case NdefRecord.TNF_ABSOLUTE_URI:
						sb.append("TNF_ABSOLUTE_URI");
						break;
					case NdefRecord.TNF_EXTERNAL_TYPE:
						sb.append("TNF_EXTERNAL_TYPE");
						break;
					case NdefRecord.TNF_UNKNOWN:
						sb.append("TNF_UNKNOWN");
						break;
					case NdefRecord.TNF_UNCHANGED:
						sb.append("TNF_UNCHANGED");
						break;
					}
					sb.append("\n");

					sb.append("RTD : ");
					if (Arrays
							.equals(record.getType(), NdefRecord.RTD_TEXT)) {
						sb.append("text/plain\n");
						procTextPayload(payload,sb);
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_URI)) {
						sb.append("URI\n");
						procUriPayload(payload,sb);
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_SMART_POSTER)) {
						sb.append("SMART POSTER\n");
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_ALTERNATIVE_CARRIER)) {
						sb.append("ALTERNATIVE CARRIER\n");
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_HANDOVER_CARRIER)) {
						sb.append("HANDOVER CARRIER\n");
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_HANDOVER_SELECT)) {
						sb.append("HANDOVER SELECT\n");
					} else if (Arrays.equals(record.getType(),
							NdefRecord.RTD_HANDOVER_REQUEST)) {
						sb.append("HANDOVER REQUEST\n");
					}
					sb.append("\n");

				}
			}
			setPageBody(sb.toString());
		}

		Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
		NfcA nfcA = NfcA.get(tag);

		if(nfcA != null){
			int dataLimit = 0;

			try {
	        	nfcA.connect();

	        	int c3 = 0;
	        	byte blockNo = 0;
		        byte[] byteCmd = new byte[2];
		        byte[] response = new byte[16];
		        byte[] blockData = new byte[4];
		        byteCmd[0] = 0x30;
		        byteCmd[1] = blockNo;
				try {

					response = nfcA.transceive(byteCmd);
					while(response.length > 15){
						sb2.append("Block[" + Integer.valueOf(byteToInt(blockNo))+ "]");
						if(byteToInt(blockNo) < 2){
							sb2.append(" (UID)");
						} else if(byteToInt(blockNo) == 2){
							sb2.append(" (Internal/Lock)");
						} else if(byteToInt(blockNo) == 3){
							sb2.append(" (OTP)");
						} else if(byteToInt(blockNo) > 3 && byteToInt(blockNo) <= dataLimit){
							sb2.append(" (Data)");
						} else if(byteToInt(blockNo) > dataLimit){
							sb2.append(" (Lock/Reserved)");
						}
						sb2.append("\n");

						blockData = Arrays.copyOfRange(response, 0, 4);
						sb2.append(" ");
						for (byte b : blockData) {
							sb2.append(String.format("%02X", b));
						}
						sb2.append("\n");
						if(blockNo == 3){
							c3 = byteToInt(blockData[2]);
							dataLimit = (c3 * 2) + 3;
						}

						blockNo++;
						byteCmd[1] = blockNo;
						response = nfcA.transceive(byteCmd);
					}

				} catch(Exception e){
					e.printStackTrace();
				}

			} catch(Exception e){
				e.printStackTrace();
			} finally {
				try {
					nfcA.close();
				} catch(Exception e){
					e.printStackTrace();
				}
			}
			setBlockBody(sb2.toString());
		}

	}

	/**
     * ペイロードの処理 - テキスト
	 *
	 * @param payload
	 * @param sb
	 */
	private void procTextPayload(byte[] payload, StringBuilder sb) {

		String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8"
				: "UTF-16";
		sb.append("Encoding : " + textEncoding + "\n");
		int languageCodeLength = payload[0] & 0077;
		String languageCode = "en";
		try {
			languageCode = new String(payload, 1,
					languageCodeLength, "US-ASCII");
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		sb.append("Language : " + languageCode + "\n");

		sb.append("Record Size : ");
		sb.append(payload.length + "bytes");
		sb.append("\n\n");

		try {
			setNoteBody(new String(payload, 3,
					payload.length - 3, textEncoding));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}

		int idx = 0;
		for (byte data : payload) {
			sb.append(String.format("Payload[%2d] : 0x%02x \n",
					idx, data));
			idx++;
		}
		try {
			sb.append("\n");
			sb.append(new String(payload, "UTF-8"));
			sb.append("\n");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}

	/**
     * ペイロードの処理 - URI
	 *
	 * @param payload
	 * @param sb
	 */
	private void procUriPayload(byte[] payload, StringBuilder sb) {

		sb.append("Record Size : ");
		sb.append(payload.length + "bytes");
		sb.append("\n\n");

		String partSchema = "";
		if(payload[0] == 0x01){
			partSchema = "http://www.";
		} else if(payload[0] == 0x02){
			partSchema = "https://www.";
		} else if(payload[0] == 0x03){
			partSchema = "http://";
		} else if(payload[0] == 0x04){
			partSchema = "https://";
		} else if(payload[0] == 0x05){
			partSchema = "tel:";
		} else if(payload[0] == 0x06){
			partSchema = "mailto://";
		} else if(payload[0] == 0x0d){
			partSchema = "ftp://";
		} else if(payload[0] == 0x1d){
			partSchema = "file://";
		}
		String partUrl = new String(payload, 1, payload.length - 1);
		setNoteBody(partSchema + partUrl);

		int idx = 0;
		for (byte data : payload) {
			sb.append(String.format("Payload[%2d] : 0x%02x \n",
					idx, data));
			idx++;
		}
		sb.append("\n");
		sb.append(new String(payload));
		sb.append("\n");
	}


	/**
     * NDEFメッセージの表示 - テキスト
	 *
	 * @param body
	 */
	private void setNoteBody(String body) {
    	map = new HashMap<String, String>();
		map.put("Title", "Text");
    	groupData.add(map);

    	map = new HashMap<String, String>();
    	map.put("Text", body);
    	ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
    	list.add(map);
    	childData.add(list);

    	lv.expandGroup(0);
	}

	/**
     * NDEFメッセージの表示 - ページ
	 *
	 * @param page
	 */
	private void setPageBody(String page) {
    	map = new HashMap<String, String>();
		map.put("Title", "Payload");
    	groupData.add(map);
    	map = new HashMap<String, String>();
    	map.put("Text", page);
    	ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
    	list.add(map);
    	childData.add(list);

    	lv.expandGroup(1);
	}

	/**
     * Blockデータの表示
	 *
	 * @param page
	 */
	private void setBlockBody(String page) {
    	map = new HashMap<String, String>();
		map.put("Title", "Block Data");
    	groupData.add(map);
    	map = new HashMap<String, String>();
    	map.put("Text", page);
    	ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>();
    	list.add(map);
    	childData.add(list);

    	lv.expandGroup(2);
	}

```
Fragment②ではタグ情報の解析をして表示します。

```java
		byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
		sb1.append("ID:");
		if (idm != null){
			for (byte b : idm) {
				sb1.append(String.format("%02X", b));
			}
		}
		sb1.append("\n");
		Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
		String[] techList = tag.getTechList();
		short sak = 0;
		boolean hasNdef = false;
		for (String tech : techList) {
			if(tech.equals("android.nfc.tech.Ndef")){
				hasNdef = true;
			}
		}

		NfcA nfcA = NfcA.get(tag);
		byte[] atqa = nfcA.getAtqa();
		if(atqa != null){
			sb1.append("ATQA : 0x");
			for (byte b : atqa) {
				sb1.append(String.format("%02X", b));
			}
			sb1.append("\n");
		}
		sak = nfcA.getSak();
		if(atqa != null){
			sb1.append("SAK : 0x" + String.format("%02d", sak));
		}

		sb2.append("ISO/IEC 14443 Type A compatible\n");
		byte[] data = new byte[1000];
		try {
			nfcA.transceive(data);
		} catch(Exception e){
			e.printStackTrace();
		}
		int tran_length = nfcA.getMaxTransceiveLength();
            
		if(tran_length < 0){
			sb2.append("▶Maximum transceive length: unknown\n");
		} else {
			sb2.append("▶Maximum transceive length: " + Integer.valueOf(tran_length) + "bytes\n");
		}
		int timeout = nfcA.getTimeout();
		sb2.append("▶Maximum transceive time-out: " + Integer.valueOf(timeout)  + "ms\n");

		if(hasNdef){
			Ndef ndef = Ndef.get(tag);
			int ndef_length = ndef.getMaxSize();
			if(sak == 0){
				if(ndef_length == 64){
					sb3.append("Card Type : Mifare Ultralight (NXP)\n");
				} if(ndef_length == 192){
					sb3.append("Card Type : Mifare Ultralight C (NXP)\n");
				} else {
					sb3.append("Card Type : Mifare Ultralight X (NXP)\n");
				}
			}
	
			sb3.append("Maximum Memory Size: " + Integer.valueOf(ndef_length) + "bytes\n");
			sb3.append("Tag type : " + ndef.getType() + "\n");
			String writable = ndef.isWritable() == true ? "yes" : "no";
			sb3.append("Is tag writable : " + writable + "\n");
			String writeProtect = "unknown";
			try{
				writeProtect = ndef.canMakeReadOnly() == true ? "yes" : "no";
			} catch (NullPointerException e){
				e.printStackTrace();
			}
			sb3.append("Can tag be write-protected : " + writeProtect);
			
		} else {
			MifareClassic mfClassic = MifareClassic.get(tag);
			if(mfClassic != null){
				int size = mfClassic.getSize();
				if(size == MifareClassic.SIZE_1K){
					sb3.append("Card Type : Mifare Classic 1K");
				} else if(size == MifareClassic.SIZE_2K){
					sb3.append("Card Type : Mifare Classic 2K");
				} else if(size == MifareClassic.SIZE_4K){
					sb3.append("Card Type : Mifare Classic 4K");
				} else if(size == MifareClassic.SIZE_MINI){
					sb3.append("Card Type : Mifare Classic Mini");
				} else {
					sb3.append("Card Type : Unknown");
				}
				sb3.append("\n");
				int blockCnt = mfClassic.getBlockCount();
				sb3.append("Total Block Count: " + Integer.valueOf(blockCnt) + "\n");
				int sectorCnt = mfClassic.getSectorCount();
				sb3.append("Total Sector Count: " + Integer.valueOf(sectorCnt) + "\n");
			}
			
		}
```
Fragment③ではNFCタグに書込みをします。リスナーの設定とNFCタグが検出された際の書き込み処理を実装します。

```java
	private View.OnClickListener mTagWriter = new View.OnClickListener() {
		@Override
		public void onClick(View arg0) {

			((NfcHostActivity)getActivity()).setTagWriteFlg(true);
			mWriteDialog = new AlertDialog.Builder(getActivity())
					.setTitle("Touch tag to write")
					.setOnCancelListener(
							new DialogInterface.OnCancelListener() {
								@Override
								public void onCancel(DialogInterface dialog) {
								}
							}).create();
			mWriteDialog.show();

		}
	};

	boolean writeTag(Tag tag) {

		if(radioId == R.id.radio_text){
			message = getTextMessage();
		} else if(radioId == R.id.radio_url){
			message = getUriMessage();
		}
		int size = message.toByteArray().length;

		Ndef ndef = null;
		NdefFormatable format = null;
		try {
			ndef = Ndef.get(tag);
			if (ndef != null) {
				ndef.connect();

				if (!ndef.isWritable()) {
					toast("Tag is read-only.");
					return false;
				}
				if (ndef.getMaxSize() < size) {
					toast("Tag capacity is " + ndef.getMaxSize()
							+ " bytes, message is " + size + " bytes.");
					return false;
				}

				ndef.writeNdefMessage(message);
				toast("Wrote message to pre-formatted tag.");
				return true;
			} else {
				format = NdefFormatable.get(tag);
				if (format != null) {
					try {
						format.connect();
						format.format(message);
						toast("Formatted tag and wrote message");
						return true;
					} catch (IOException e) {
						toast("Failed to format tag.");
						return false;
					}
				} else {
					toast("Tag doesn't support NDEF.");
					return false;
				}
			}
		} catch (Exception e) {
			toast("Failed to write tag");
		} finally {
			try {
				if (mWriteDialog != null)
					mWriteDialog.dismiss();
				if (ndef != null && ndef.isConnected())
					ndef.close();
				if (format != null && format.isConnected())
					format.close();
				((NfcHostActivity)getActivity()).setTagWriteFlg(false);
			} catch (IOException e) {
			}
		}

		return false;
	}
```
実装の説明は以上ですが、細かい画面部品とのやり取りなどは省いてしまっています。
皆さんで考えて実装してみてください。

# 画面を動かしてみる
今回下記のような直径23㎜の小さなNFCタグを使用します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/65b126f9-d099-0bc4-405d-48fa19238098.jpeg" width="50%">

①タグに書き込みます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/332feec0-cc4d-d513-a0f5-091d38085dff.png" width="50%">

②タグメッセージを読み込みます。
1画面に収まらないので分割写真です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/86df97f4-4f93-c091-897e-d3ff3df9711a.png" width="50%">
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/725be573-1e04-66ec-3fcc-e0ffc9d88540.png" width="50%">
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/38793f9b-b773-b448-55c2-b7323fba3c0f.png" width="50%">

③タグ情報を読み込みます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/316107/97a6679a-acf6-a6d8-38ea-773d8afc7d15.png" width="50%">

# Mifrare Ultralightタグのデータ構造を分析してみる
下記図は先ほど使用したものとは違うNFCタグですが、Mifrare Ultralightタグに「あか」と書き込んでみた結果のBlockデータをHEXで表示してみたものです。
![TAG2.png](https://qiita-image-store.s3.amazonaws.com/0/316107/59541a85-aa2d-4576-624a-335af325b939.png)

仕様書とタグの中身をのぞいてみるとなんだかMifrare Ultralightの深淵に潜っていく感があります。

参考文献：
・TypeAの仕様書(NXP)
　http://www.nxp.com/documents/application_note/AN10833.pdf
・TAG Type2の仕様書(NFC Forum)
　http://apps4android.org/nfc-specifications/NFCForum-TS-Type-2-Tag_1.1.pdf

# おわりに
駆け足でNFCまわりの仕様と実装について説明してきましたがいかがでしょうか。
実際にスマホでタグの内容を読み書きできるとエンジニアとしてはちょっと楽しいかなと感じます。
ここで紹介したMifrare Ultralight以外のタグやSuicaやICカード運転免許証も実装すれば同様にAndroidアプリで表示できます。
それぞれのタグタイプで読み出し方がだいぶ変わりますので面白いと思いますので興味のある方はチャレンジしてみてください。