FeliCa(WAON)カードの情報を読んで見る


## やりたいこと

FeliCaカードの固有IDであるIDｍを読むサンプルはよくあるが、各種電子マネーの情報を読むサンプルはあまりないので、手元にあったWAONカードの情報（とりあえずWAON番号）を読んでみます。

>WAONカードはミニストップなどで300円で売っている。

## 前提

* 言語はJavaを利用
* 開発はAndroid Studio 3.4.x on Mac
* 検証実機はXperia XZ Premium(SO-04j)
* FeliCa(NFC)の操作法に重点を置きたいのでエラー処理とかは最低限
* IDmの取得もtag.getId()とかではなくFeliCaコマンドで行う

## 仕様

以下のような感じとする。

* idm, pmm, WAON番号表示用のTextViewを配置
* polling start, stopボタンを配置
* polling startボタンを押したらpollingを開始し、カードがかざされたら各種IDを取得・表示
* polling stopボタンでpollingを停止

見た目は以下のような感じ。

![スクリーンショット 2019-05-31 9.07.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/6e56c1f6-c2db-6346-b66a-b761579649a0.png "スクリーンショット 2019-05-31 9.07.07.png")

## 処理の流れ

カードを認識して読み取るまでの大まかな流れは以下の通り。

* NfcAdapterを生成し、ReaderModeで読み取る（読み取りを待ち受ける）
* 認識されたカードはTagとして取得される
* Tagを指定してNfcFを生成し、通信を行う（各種コマンドを実行する）
* Pollingコマンドでカードの認識ID(IDm)を取得
* 対象特定のためIDmを添付して、読み取りコマンド(readWithoutEncryption)を実行
* 取得した内容を表示するなどする


## 準備

NFC機能を利用するにはAndroidManifext.xmlに以下の追記。

```
<uses-permission android:name="android.permission.NFC" />
```

>そもそも、端末自体のReader/Writer機能をOnにする必要もあります。

## 実装

雑に実装してみます。

### MainActivity.java

今回はボタンで読み取り機能のOn/OffをしたいのでReaderModeを利用しています。
そのenable/disableをstartボタン、stopボタンでそれぞれ制御しています。

カード（Tag）が認識されたらCallbackクラスのonTagDiscovered()が呼び出されるので、その中で必要な処理をします。
FeliCaコマンドを実行するにはtranscive()を利用します。今回はpollingとreadWithoutEncryptionの2つを実行しています。

pollingは通信先のカードID（IDm）を特定するために必須となります。
readWithoutEncryptionでは読み取る「場所」をサービスコードで指定する必要があります。今回はWAON番号（カード番号）を読み取りたいので0x68, 0x4fを指定しています。

サービスコードは公開されていませんが、[有志によって調べられています](https://www.wdic.org/w/CUL/WAON)。

WAONの他のサービス（残高とか）、他のカードの情報を利用したい場合、サービスコードやフォーマットを入手しましょう。

>なお、FeliCaには鍵あり領域と鍵なし領域があり、チャージ等の領域は「鍵あり領域」に保持されているため第三者が操作することはできません。念の為。

```java:MainActivity.java
package jp.bluecode.waon_java;

import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Formatter;

public class MainActivity extends AppCompatActivity {

    //Widgetの宣言
    TextView txt_idm;
    TextView txt_pmm;
    TextView txt_waonno;
    Button btn_start;
    Button btn_stop;

    //NfcAdapterの宣言
    NfcAdapter nfcAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //Widgetの初期化
        txt_idm = findViewById(R.id.txt_idm);
        txt_pmm = findViewById(R.id.txt_pmm);
        txt_waonno = findViewById(R.id.txt_waonno);
        btn_start = findViewById(R.id.btn_start);
        btn_stop = findViewById(R.id.btn_stop);

        //初期設定（トグル）
        btn_stop.setEnabled(false);

        //NfcAdapterの初期化
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);

        //onClickイベントの設定
        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //トグル設定
                btn_start.setEnabled(false);
                btn_stop.setEnabled(true);

                //TextView初期化
                txt_idm.setText("");
                txt_pmm.setText("");
                txt_waonno.setText("");

                //ReaderMode On
                nfcAdapter.enableReaderMode(MainActivity.this,new MyReaderCallback(),NfcAdapter.FLAG_READER_NFC_F,null);

            }
        });

        btn_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                //トグル設定
                btn_start.setEnabled(true);
                btn_stop.setEnabled(false);

                //ReaderMode Off
                nfcAdapter.disableReaderMode(MainActivity.this);

            }
        });

    }

    //Callback用 inner class記述
    private class MyReaderCallback implements NfcAdapter.ReaderCallback{
        @Override
        public void onTagDiscovered(Tag tag){

            //タグが見つかったらとりあえずログ出力
            Log.d("Hoge","Tag Discovered!");

            //FeliCaと通信するためのNfcFを初期化
            NfcF nfc = NfcF.get(tag);

            try{

                nfc.connect();

                //とりあえずいろいろFeliCaの生コマンドで制御。idmとかならtag.getId()とかで取れる。

                //pollingコマンド（FeliCa生コマンド） 共通領域のシステムコード0xFE 0x00を指定。
                byte[] polling_request = {(byte)0x06,(byte)0x00,(byte)0xFE,(byte)0x00,(byte)0x00,(byte)0x00};
                //response受け取り用byte配列

                //コマンド送信・受信
                byte[] polling_response = nfc.transceive(polling_request);

                //idmの取り出し
                byte[] idm = Arrays.copyOfRange(polling_response,2,10);
                //pmmの取り出し（ついで）
                byte[] pmm = Arrays.copyOfRange(polling_response,11,19);

                //byte列を文字列に変換
                final String idmString = bytesToHexString(idm);
                final String pmmString = bytesToHexString(pmm);

                //waonno処理

                //waonnno request
                //カスタム関数をつかってrequestコマンドを組み立て
                byte[] waonno_request = readWithoutEncryption(idm,2);
                //コマンド送信・受信
                byte[] wannno_response = nfc.transceive(waonno_request);

                //WAON番号部分を切り取り
                byte[] waonno = Arrays.copyOfRange(wannno_response,13,21);

                //文字列変換
                final String waonnoString = bytesToHexString(waonno);

                //親スレッドのUI更新
                Handler mainHandler = new Handler(Looper.getMainLooper());
                mainHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        txt_idm.setText(idmString);
                        txt_pmm.setText(pmmString);
                        txt_waonno.setText(waonnoString);
                    }
                });

                nfc.close();

            }catch(Exception e){
                Log.e("Hoge",e.getMessage());
            }
        }
    }

    //非暗号領域読み取りコマンド（WAON番号領域特化）
    private byte[] readWithoutEncryption(byte[] idm, int blocksize) throws IOException {

        ByteArrayOutputStream bout = new ByteArrayOutputStream(100); //とりあえず

        //readWithoutEncryptionコマンド組み立て
        bout.write(0); //コマンド長（後で入れる）
        bout.write(0x06); //0x06はRead Without Encryptionを表す
        bout.write(idm); //8byte:idm
        bout.write(1); //サービス数
        bout.write(0x4f); //サービスコードリスト WAONカード番号は684F
        bout.write(0x68); //サービスコードリスト
        bout.write(blocksize); //ブロック数

        for(int i=0; i<blocksize; i++){
            bout.write(0x80); //ブロックリスト
            bout.write(i);
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte)msg.length;

        return msg;
    }

    //bytesを16進数型文字列に変換用関数
    private String bytesToHexString(byte[] bytes){
        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);
        for(byte b: bytes){
            formatter.format("%02x",b);
        }
        //大文字にして戻す（見た目の調整だけ）
        return sb.toString().toUpperCase();
    }
}
```

以上です。

### activity_main.xml

参考程度にactivity_main.xmlも記載しておきますが、お好みでレイアウトしたほうがいいでしょう。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/lbl_top"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="WAON番号 Viewer"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/lbl_idm"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="68dp"
        android:layout_marginTop="32dp"
        android:text="IDm:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_top" />

    <TextView
        android:id="@+id/txt_idm"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="32dp"
        android:background="#E8EAF6"
        app:layout_constraintStart_toEndOf="@+id/lbl_idm"
        app:layout_constraintTop_toBottomOf="@+id/lbl_top" />

    <TextView
        android:id="@+id/lbl_pmm"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="60dp"
        android:layout_marginTop="24dp"
        android:text="PMm:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_idm" />

    <TextView
        android:id="@+id/txt_pmm"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:background="#E8EAF6"
        app:layout_constraintStart_toEndOf="@+id/lbl_pmm"
        app:layout_constraintTop_toBottomOf="@+id/txt_idm" />

    <TextView
        android:id="@+id/lbl_wanno"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:text="WAON番号:"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/lbl_pmm" />

    <TextView
        android:id="@+id/txt_waonno"
        android:layout_width="180dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:background="#F3E5F5"
        app:layout_constraintStart_toEndOf="@+id/lbl_wanno"
        app:layout_constraintTop_toBottomOf="@+id/txt_pmm" />

    <Button
        android:id="@+id/btn_start"
        android:layout_width="160dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Start Polling"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txt_waonno" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="160dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="Stop Polling"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_start" />
</android.support.constraint.ConstraintLayout>
```

### AndroidManifest.xml

こちらも参考程度に。編集しているのは、冒頭に触れたNFCのパーミッションだけです。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.bluecode.waon_java">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.NFC" />

</manifest>
```