LoginSignup
8
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-17

はじめに

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

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

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

Suicaを読み取る実装の追加

タグを検出して、画面のタブを切替えてからFragment①に処理が切り替わるまでは同じです。
検出したタグによって処理を分ける部分を再度、掲載しておきます。

        // 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)の実装をして読み取っていきます。

/**
     * 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で非暗号化の領域ということがわかります。

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

    /**
     * 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
だいぶ前に実装したものなので、適宜情報を最新にしてみてください。

    /**
     * 機器種別を取得します
     *
     * <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の技術情報を追加します。

        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
Screenshot_20191213-133232.png
Screenshot_20191213-133302.png
Screenshot_20191213-133317.png
Screenshot_20191213-133339.png

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

Screenshot_20191213-133352.png

おわりに

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

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4