AndroidでFeliCaを読み込むとエラーが起きるとき

今回のエントリーは、AndroidFeliCa内データへのアクセス中にExceptionが出てしまった時の対応についてです。

Android本体にNFCの機能が搭載されている場合、「作法」に従ってコーディングすれば、Suicaなどの交通系FeliCa系カード内の残高や履歴データを参照することが出来ます。

そこで試しに手元にあるPASPY(※)を読み込んでみようと思い、FeliCaリーダーアプリを作っていましたが、データの読み込み中に間欠でExceptionが発生してしまいました。PASPYのみがそのような問題が起きるのか、それともFeliCaカード全般でそうなのかは検証できていませんが、最終的には少なくともPASPYのデータであれば確実に読み込むことが出来ましたので、今回はその問題への対応を書いてみます。

PASPYは広島圏内のバスや路面電車等で使われる交通系ICカードです。

一般に、FeliCaのデータを読み込む処理はAndroidが提供しているNFCタグ周りの実装方法ではなく、FeliCa独自のデータフォーマットを使い独自のアクセスの仕方をする必要があります。詳しくはSONYの「FeliCaカード ユーザーズマニュアル 抜粋版」にその仕様が書かれています。

その仕様によると、FeliCa内の履歴データは16バイトのブロック単位でアクセスする必要があり、アクセスする場合は幾つかのバイトデータでリクエストコマンドを組み立て、それをカードへリクエストします。リクエストコマンドを正しく組み立てることができれば、レスポンスデータが正しく返却されてきますので、それを逐一解析してAndroidのUI要素へバインドすればアプリは出来上がります。

ところが当初、このリクエストコマンドの組み立ての部分が良くなかったようで、時折データアクセス時に例外が発生してしまいました。

//
// エラーが発生する書き方
//
private void readNfc(Intent intent) {
    byte[] cardId = new byte[] { 0 };
    NfcF felica= null;
    Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag != null) {
        cardId = tag.getId(); // カードのユニークIDを取得
        felica= NfcF.get(tag);
    } else {
        return;
    }

    // 1リクエストで20件分読み込もうとすると例外が発生
    try {
        // 接続
        felica.connect();

        // リクエストデータの組み立て(20件の履歴データを取得したい)
        byte[] req = buildRequest(cardId, 20);

        // カードにリクエスト送信
        byte[] res = nfc.transceive(req); // ここで例外発生(12件前後までなら読み込める…)

        // クローズ
        nfc.close();

        // 結果の解析
        parse(res);
    } catch (Exception e) {
        Log.e("TEST", e.getMessage(), e);
    }
}

private byte[] buildRequest(byte[] cardId, int blockSize)
        throws IOException {
    ByteArrayOutputStream out= new ByteArrayOutputStream(100);

    out.write(0);
    out.write(0x06); // Read Without Encryptionコマンド(カード内データを読むコマンド)
    out.write(cardId); // カードID
    out.write(1); 
    out.write(0x0f); // 履歴データを取得するための指定
    out.write(0x09); // 履歴データを取得するための指定

    out.write(blockSize); // ※ ここで取得したい全ブロック数を指定しまっていた
    for (int i = 0; i < blockSize; i++) {
        out.write(0x80); // 前述のFelica仕様書に従う
        out.write(i); // ブロック番号
    }

    byte[] byteMsg = out.toByteArray();
    byteMsg [0] = (byte) byteMsg .length;
    return byteMsg ;
}

例えば20件の履歴データを取得するために、1つのRequestで20件全てを取得しようとしたことが良くなかったようです。そこで1つのRequestで1つの履歴データを取得し、それを20回繰り返すという書き方に変えれば、問題なくデータの取得が出来ました。

//
// 正常にデータ取得できた書き方
//
private void readNfc(Intent intent) {
    byte[] cardId = new byte[] { 0 };
    NfcF felica= null;
    Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
    if (tag != null) {
        cardId = tag.getId(); // カードのユニークIDを取得
        felica= NfcF.get(tag);
    } else {
        return;
    }

    // リクエストを1個づつ20回発行
    try {
        for (int i = 0; i < 20; i++) {
            // 接続
            felica.connect();

            // リクエストデータの組み立て
            byte[] req = buildRequest(cardId, i);

            // カードにリクエスト送信
            byte[] res = nfc.transceive(req);

            // クローズ
            nfc.close();

            // 結果の解析
            parse(res);
        }
    } catch (Exception e) {
        Log.e("TEST", e.getMessage(), e);
    }
}

private byte[] buildRequest(byte[] cardId, int addr)
        throws IOException {

    ByteArrayOutputStream out= new ByteArrayOutputStream(100);

    out.write(0);
    out.write(0x06); // Read Without Encryptionコマンド(カード内データを読むコマンド)
    out.write(cardId); // カードID
    out.write(1); 
    out.write(0x0f); // 履歴データを取得するための指定
    out.write(0x09); // 履歴データを取得するための指定

    out.write(1); // ※ 取得したいブロックサイズを1に
    out.write(0x80);
    out.write(addr); // 取得対象のブロック番号

    byte[] byteMsg = out.toByteArray();
    byteMsg [0] = (byte) byteMsg .length;
    return byteMsg ;
}

振り返ってみれば仕様を細部までちゃんと咀嚼していれば良かったのですが、ハマっている最中は色々な事象に囚われて意外と気づきにくいものです。。。

ちなみに、最終的にはこのように出来上がりました。
https://play.google.com/store/apps/details?id=com.kyakujin.android.hmicchecker