AndroidのNFCでSuicaの履歴を読んでみる

# はじめに
去年のアドベントカレンダーでAndroidのNFCでMifare Ultralightのタグを読み取る方法について以下の記事を書きました。

「ICカードだけじゃない!? AndroidのNFCで読み書きしてみた」
https://qiita.com/kurotsu/items/0748961a59a33a045d33

1年ぶりですが、続編ということで今度は皆さんにもおなじみのSuicaを使ってみたいと思います。
NFCやAndroidのAPIに関して基本的な事柄を去年の記事で解説しています。
また、画面遷移について同じ実装でSuicaの情報を読み取るところだけ追加していきますので、
一度、去年の記事を参照してみてください。

# Suicaを読み取る実装の追加

タグを検出して、画面のタブを切替えてから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);
				}
			}

```
Suicaのカードをかざした場合はタグのtechListに"android.nfc.tech.NfcF"が設定されてきます。
procFelica(intent)の実装をして読み取っていきます。

```java
/**
     * Felica Cardの処理
	 *
	 * @param intent
	 */
	private void procFelica(final Intent intent) {

		byte[] idm = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID);
		Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
		NfcF nfcF = NfcF.get(tag);
		byte[] systemCode = nfcF.getSystemCode();
 		StringBuilder sb = new StringBuilder();
 		StringBuilder sb2 = new StringBuilder();
 		StringBuilder sb3 = new StringBuilder();

        // Suica Transaction
        if(byteToInt(systemCode) == 0x0003){
            byte addr = 0;
            int serviceCode = 0x090f; // サービスコマンド（Suica履歴情報）
            byte[] serviceCmd = new byte[]{(byte) (serviceCode & 0xff), (byte) (serviceCode >> 8)};
            byte[] read_wo_encryption_command = new byte[]{(byte)0x06}; // 非暗号化領域読込コマンド read_wo_encryption_command:0x06
            byte[] data = new byte[]{
                    (byte) 0x01,			// サービス数
                    (byte) serviceCmd[0],	// サービスコード (little endian)
                    (byte) serviceCmd[1],
                    (byte) 0x01,			// 同時読み込みブロック数
                    (byte) 0x80, addr // ブロックリスト
            };
            int length = 16; // コマンド長(length(1)+ idm(8)+ cmd(1)+ data(6))

            ByteBuffer buff = ByteBuffer.allocate(length);
            byte byteLength = (byte)length;
            buff.put(byteLength).put(read_wo_encryption_command).put(idm).put(data);
            byte[] tranCmd = buff.array();

            byte[] response;
            Long remain;
            SimpleDateFormat sdf;
            String strDate;
            byte[] contents;
            NumberFormat nf = NumberFormat.getCurrencyInstance();
            nf.setMaximumFractionDigits(0);

            try {
				nfcF.connect();
				response = nfcF.transceive(tranCmd);
				while(response != null){
					if(response.length < 13){
						break;
					}
					if((response[13] & 0xff) == 0xc7 || (response[13] & 0xff) == 0xc8){
						sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss",Locale.JAPANESE);
						strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),byteToInt(new byte[]{response[19], response[20]})));
					} else {
						sdf = new SimpleDateFormat("yyyy/MM/dd",Locale.JAPANESE);
						strDate = sdf.format(getProccessDate(byteToInt(new byte[]{response[17], response[18]}),0));
					}
					sb.append("日時：" + strDate + "\n");

					int cType = byteToInt(new byte[]{response[13]});
					sb.append("機器：" + getConsoleType(cType) + "\n");

					int proc = byteToInt(new byte[]{response[14]});
					sb.append("処理：" + getProcessType(proc) + "\n");

					remain = Long.valueOf((byteToInt(new byte[]{response[24], response[23]})));
					sb.append("残高：" + nf.format(remain) + "\n");
					sb.append("残高(b)：");
					sb.append(String.format("%02X", response[24]));
					sb.append(String.format("%02X", response[23]));
					sb.append("\n");

					contents = Arrays.copyOfRange(response, 10, response.length);
					for (byte b : contents) {
						sb.append(String.format("%02X", b));
					}
					sb.append("\n");
					sb.append("---------------------------------------------------------\n");

					addr++;
					tranCmd[tranCmd.length-1] = addr;
					response = nfcF.transceive(tranCmd);
				}
			} catch(Exception e){
				e.printStackTrace();
			} finally {
				try {
					nfcF.close();
				} catch(Exception e){
					e.printStackTrace();
				}
			}

        // nanaco Transaction
        } else if(byteToInt(systemCode) == 0x04c7){
　　　　　　// TODO: nanaco implementation　　　　

		} else {

			sb.append("none\n");
		}
		setFelicaTransactionBody(sb.toString());

        // System Code List
        byte[] command_request_systemcode = new byte[]{(byte)0x0c}; // SystemCode検索コマンド command_request_systemcode:0x0c
        int length = 10; // コマンド長(length(1)+ idm(8)+ cmd(1))

        ByteBuffer buff = ByteBuffer.allocate(length);
        byte byteLength = (byte)length;
        buff.put(byteLength).put(command_request_systemcode).put(idm);
        byte[] systemCodeCmd = buff.array();

        byte[] response;

        try {
			nfcF.connect();
			response = nfcF.transceive(systemCodeCmd);
			int array_length = (int)response[10];
			byte[] serviceCodes;
	        for (int i=0; i < array_length; i++) {
	        	serviceCodes = Arrays.copyOfRange(response, 11+i*2, 13+i*2);
				for (byte b : serviceCodes) {
					sb2.append(String.format("%02X", b));
				}
				sb2.append("\n");
	        }

		} catch(Exception e){
			e.printStackTrace();
		} finally {
			try {
				nfcF.close();
			} catch(Exception e){
				e.printStackTrace();
			}
		}
		setFelicaSystemCodeBody(sb2.toString());


        // Service Command List
        byte[] command_search_servicecode = new byte[]{(byte)0x0a}; // SystemCode検索コマンド command_search_servicecode:0x0a
        length = 12; // コマンド長(length(1)+ idm(8)+ cmd(1) + index(2))
        int addr = 0;

        byte[] indexByte = new byte[]{(byte)(addr & 0xff), (byte)(addr >> 8)};

        buff = ByteBuffer.allocate(length);
        byteLength = (byte)length;
        buff.put(byteLength).put(command_search_servicecode).put(idm).put(indexByte);
        byte[] serviceCodeCmd = buff.array();
        byte[] contents;
        byte[] serviceCode = new byte[2];

        try {
            nfcF.connect();
            response = nfcF.transceive(serviceCodeCmd);
            while(response != null){

                contents = Arrays.copyOfRange(response, 10, response.length);
                if (contents.length != 2 && contents.length != 4) break; // 2 or 4 バイトじゃない場合終了
                if (contents.length == 2) {
                    if (contents[0] == (byte)0xff && contents[1] == (byte)0xff) break; // FFFF が終了コード

                    // little endian
                    serviceCode[0] = contents[1];
                    serviceCode[1] = contents[0];
                    for (byte b : serviceCode) {
                        sb3.append(String.format("%02X", b));
                    }
                    int accessInfo = serviceCode[1] & 0x3F; // 下位6bitがアクセス情報
                    switch (accessInfo) {
                        case 0x09: sb3.append(" 固定長RW"); break; // RW: ReadWrite
                        case 0x0b: sb3.append(" 固定長RO"); break; // RO: ReadOnly
                        case 0x0d: sb3.append(" 循環RW"); break;
                        case 0x0f: sb3.append(" 循環RO"); break;
                        case 0x11: sb3.append(" 加減算直接"); break;
                        case 0x13: sb3.append(" 加減算戻入"); break;
                        case 0x15: sb3.append(" 加減算減算"); break;
                        case 0x17: sb3.append(" 加減算RO"); break;
                        case 0x08: sb3.append(" 固定長RW(Locked)"); break; // RW: ReadWrite
                        case 0x0a: sb3.append(" 固定長RO(Locked)"); break; // RO: ReadOnly
                        case 0x0c: sb3.append(" 循環RW(Locked)"); break;
                        case 0x0e: sb3.append(" 循環RO(Locked)"); break;
                        case 0x10: sb3.append(" 加減算直接(Locked)"); break;
                        case 0x12: sb3.append(" 加減算戻入(Locked)"); break;
                        case 0x14: sb3.append(" 加減算減算(Locked)"); break;
                        case 0x16: sb3.append(" 加減算RO(Locked)"); break;
                    }

                    sb3.append("\n");
                }

                addr++;
                serviceCodeCmd[serviceCodeCmd.length-2] = (byte)(addr & 0xff);
                serviceCodeCmd[serviceCodeCmd.length-1] = (byte)(addr >> 8);
                response = nfcF.transceive(serviceCodeCmd);
            }

		} catch(Exception e){
			e.printStackTrace();
		} finally {
			try {
				nfcF.close();
			} catch(Exception e){
				e.printStackTrace();
			}
		}
		setFelicaServiceCodeBody(sb3.toString());

	}
```
Felicaのカードにはシステムコードと呼ばれるカードの種類によって決められたコードが規定されています。
Suicaは”0003”です。NanacoやWAONなどFelicaを採用しているカードにもそれぞれシステムコードが割り振られています。
それから、カードの中の情報についてはアクセス先を表すサービスコードが設定されています。
Suicaのカードでチャージをしたり、タッチして品物を購入したときの履歴情報は”090f”です。
サービスコードやidmを利用して、AndroidのnfcFのAPIを使ってコマンドを送信してレスポンスを受け取ります。
ブロックアドレスを変更しながら、最大20件の履歴情報を読み取ることが出来ます。

「// System Code List」のコメントから先はカードに設定されているシステムコードの一覧を表示しています。
Suicaでは”0003”の他にもコードが設定されているようです。
「// Service Command List」のコメントから先はカードに設定されているサービスコードの一覧を表示しています。
この情報からアクセスできる情報のサービスコードを確認することができます。
履歴情報は”090f”は循環型のデータ構造でReadOnlyで非暗号化の領域ということがわかります。

取得した情報を設定している処理が下記になります。

```java
	/**
     * Felica履歴 の表示 - ページ
	 *
	 * @param page
	 */
	private void setFelicaTransactionBody(String page) {
    	map = new HashMap<String, String>();
		map.put("Title", "Transaction");
    	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(0);
	}

	/**
     * Felica System Code の一覧 - ページ
	 *
	 * @param page
	 */
	private void setFelicaSystemCodeBody(String page) {
    	map = new HashMap<String, String>();
		map.put("Title", "System Code List");
    	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);
	}

	/**
     * Felica Service Code の一覧 - ページ
	 *
	 * @param page
	 */
	private void setFelicaServiceCodeBody(String page) {
    	map = new HashMap<String, String>();
		map.put("Title", "Service Code List");
    	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);
	}
```

履歴情報のタイプを変換するメソッドが下記となります。
コメントがあるとおり、有志の方が調べてくれた情報の下記サイトを参考にしています。
http://sourceforge.jp/projects/felicalib/wiki/suica
だいぶ前に実装したものなので、適宜情報を最新にしてみてください。

```java
	/**
     * 機器種別を取得します
     *
     * <pre>
     * http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
     * </pre>
     *
     * @param cType
     *            コンソールタイプをセット
     * @return String 機器タイプが文字列で戻ります
	 */
	public static final String getConsoleType(int cType) {
		switch (cType & 0xff) {
            case 0x03:
                return "精算機";
            case 0x04:
                return "携帯型端末";
            case 0x05:
                return "等車載端末"; // bus
            case 0x07:
                return "券売機";
            case 0x08:
                return "券売機";
            case 0x09:
                return "入金機(クイックチャージ機)";
            case 0x12:
                return "券売機(東京モノレール)";
            case 0x13:
                return "券売機等";
            case 0x14:
                return "券売機等";
            case 0x15:
                return "券売機等";
            case 0x16:
                return "改札機";
            case 0x17:
                return "簡易改札機";
            case 0x18:
                return "窓口端末";
            case 0x19:
                return "窓口端末(みどりの窓口)";
            case 0x1a:
                return "改札端末";
            case 0x1b:
                return "携帯電話";
            case 0x1c:
                return "乗継清算機";
            case 0x1d:
                return "連絡改札機";
            case 0x1f:
                return "簡易入金機";
            case 0x46:
                return "VIEW ALTTE";
            case 0x48:
                return "VIEW ALTTE";
            case 0xc7:
                return "物販端末"; // sales
            case 0xc8:
                return "自販機"; // sales
            default:
                return "???";
		}
	}

    /**
     * 処理種別を取得します
     *
     * <pre>
     * http://sourceforge.jp/projects/felicalib/wiki/suicaを参考にしています
     * </pre>
     *
     * @param proc
     *            処理タイプをセット
     * @return String 処理タイプが文字列で戻ります
     */
    public static final String getProcessType(int proc) {
        switch (proc & 0xff) {
            case 0x01:
                return "運賃支払(改札出場)";
            case 0x02:
                return "チャージ";
            case 0x03:
                return "券購(磁気券購入";
            case 0x04:
                return "精算";
            case 0x05:
                return "精算(入場精算)";
            case 0x06:
                return "窓出(改札窓口処理)";
            case 0x07:
                return "新規(新規発行)";
            case 0x08:
                return "控除(窓口控除)";
            case 0x0d:
                return "バス(PiTaPa系)"; // byBus
            case 0x0f:
                return "バス(IruCa系)"; // byBus
            case 0x11:
                return "再発(再発行処理)";
            case 0x13:
                return "支払(新幹線利用)";
            case 0x14:
                return "入A(入場時オートチャージ)";
            case 0x15:
                return "出A(出場時オートチャージ)";
            case 0x1f:
                return "入金(バスチャージ)"; // byBus
            case 0x23:
                return "券購 (バス路面電車企画券購入)"; // byBus
            case 0x46:
                return "物販"; // sales
            case 0x48:
                return "特典(特典チャージ)";
            case 0x49:
                return "入金(レジ入金)"; // sales
            case 0x4a:
                return "物販取消"; // sales
            case 0x4b:
                return "入物 (入場物販)"; // sales
            case 0xc6:
                return "物現 (現金併用物販)"; // sales
            case 0xcb:
                return "入物 (入場現金併用物販)"; // sales
            case 0x84:
                return "精算 (他社精算)";
            case 0x85:
                return "精算 (他社入場精算)";
            default:
                return "???";
        }
    }
```

Fragment②ではFelicaの技術情報を追加します。

```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);
		NfcF nfcF = NfcF.get(tag);

		byte[] manufacturer = nfcF.getManufacturer();
		if(manufacturer != null){
			sb1.append("PMm : ");
			for (byte b : manufacturer) {
				sb1.append(String.format("%02X", b));
			}
			sb1.append("\n");
		}

		sb2.append("JIS X 6319-4 compatible\n");
		byte[] data = new byte[1000];
		try {
			nfcF.connect();
			nfcF.transceive(data);
		} catch(Exception e){
			e.printStackTrace();
		} finally {
			try {
				nfcF.close();
			} catch(Exception e){
				e.printStackTrace();
			}
		}
		int tran_length = nfcF.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 = nfcF.getTimeout();
		sb2.append("▶Maximum transceive time-out: " + Integer.valueOf(timeout) + "ms\n");

    	byte[] systemCode = nfcF.getSystemCode();
		if(systemCode != null){
			sb3.append("System Code : ");
			for (byte b : systemCode) {
				sb3.append(String.format("%02X", b));
			}
			sb3.append("\n");
		}
		if(NdefReadFragment.byteToInt(systemCode) == 0x0003){
			sb3.append("Card Type : Suica\n");
		}

		
		String[] techList = tag.getTechList();
		for(int i = 0; i <= techList.length - 1; i++){
			sb4.append("▶" + techList[i]);
			if(i < techList.length - 1)	sb4.append("\n");
		}
		
		setTechFPage(sb1.toString(),sb2.toString(),sb3.toString(),sb4.toString());
```

これで実装の説明は終わりです。

# Suicaの履歴情報を読み取ってみる
いよいよ実際にSuicaを読み取りたいと思います。

Suicaをアプリで読み取って際の分割写真を張り付けています。
SCANタブの情報です。

![Screenshot_20191213-133159.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/bd16e05f-8296-e365-d5e4-795bca827633.png)
![Screenshot_20191213-133232.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/59fd26a7-f07a-f8f1-fa72-39979b98200e.png)
![Screenshot_20191213-133302.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/0121c46d-d5d6-c1f6-6a56-bda2c8ebcc81.png)
![Screenshot_20191213-133317.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/17114d99-33dd-8b2f-7678-1b7159e08922.png)
![Screenshot_20191213-133339.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/43bcc1be-99f7-eb09-b7c0-edaad1c781ff.png)

TECHタブの情報が下記になります。

![Screenshot_20191213-133352.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316107/7c994e40-5324-83d4-0504-b389dd4b07e6.png)

# おわりに
実装の細部などは端折ってしまっていますが、皆さんで考えながら実装してみてください。
皆さんの手元にあるカードでFelicaを採用しているものもあるかと思います。
システムコードやサービスコードの情報がこの実装で読み取ることが出来ると思いますので、
実際にかざしてみて確認してみてください。