1年に1回くらいAndroidでNFC(FeliCa)をいじる人間のメモ(2019年初夏)


FeliCaの仕事はずいぶんやってないので備忘メモ。
本当はKotlinで書きたいけど、過去のソースとか参考にしたいので一旦Javaで書きます。

やりたいこと

  • IDmを取得したい
  • とりあえずJavaを利用(後でKotlinで書き直す)
  • 開発環境はAndroid Studio(3.4をMacで利用)

仕様

ネットに多存在するサンプルの多くはNfcAdapter.enableForegroundDispatch()を利用してアプリがフォアグラウンドにある間ずっと読みっぱなしで、認識したらIntentde処理するものが多いようですが、この仕様だと読むタイミングや機能のOn/OffのコントロールしにくいのでNfcAdapter.enableReaderMode()を利用してみます。

完全にイベントドリブンで読取りってできないのかな?要はonClickで読取りみたいなことしたいのですが、スマートなやり方がわかりません。誰か教えて。

仕様の概要

アプリの動きは下記のような感じ

  • READER MODE ONボタン(btn01)で読取りスタート
  • 読み取ったらTextView(txt01)に表示
  • READER MODE OFFボタン(btn02)で読取り中止

下記のような動き

注意事項

Reader/Write機能をOnに

Reader/Writer機能を利用するアプリを開発する場合は、Android(9.0の場合)の設定で[設定]->[接続機器]->[接続の設定]->NFC[NFC/おサイフケータイ設定]->[Reader/Write,P2P]機能をOnにしておく必要があります。

既存アプリをアンインストール(可能なら)

他のNFC機能を利用するアプリ、特にバックグラウンドで待ってIntentを発生させるようなアプリ(例えば、おサイフケータイアプリ)は開発に影響があるので不要なら削除しておいたほうがいいでしょう。

一方、一般向けアプリの場合は利用者が他のNFCアプリをインストールしていることを前提に仕様を考えておく必要があるでしょう。

実装

無駄が多いですが、各種主要コード全体を貼り付けておきます。

AndroidManifest.xml

NFC利用のパーミッションを追加。

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

    <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>

activity_main.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/txt01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Read ID ..."
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.308" />

    <Button
        android:id="@+id/btn01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=" Reader Mode On"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/txt01"
        app:layout_constraintVertical_bias="0.107" />

    <Button
        android:id="@+id/btn02"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Reader Mode Off"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn01"
        app:layout_constraintVertical_bias="0.116" />

</android.support.constraint.ConstraintLayout>

MainActivity

できるだけ要点だけ短く書くために端折ってます。
IDm取得するところまでなら、NfcAdapter作って、Tagを取得すれば、Tag.getId()という感じでIDmを取得できる。
Javaには標準でbyte列をStringにする関数が無いのでカスタム関数で用意してますが、そっちのほうが長いくらい。

MainActivity.java
package jp.bluecode.buttontest;

import android.nfc.NfcAdapter;
import android.nfc.Tag;
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.util.Formatter;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    //Viewで使う変数を初期化(別にここじゃなくてもいいけど)
    TextView txt01;
    Button btn01;
    Button btn02;

    //NfcAdapterを初期化
    NfcAdapter nfcAdapter;

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

        //UIのマーツをマッピング
        txt01 = findViewById(R.id.txt01);
        btn01 = findViewById(R.id.btn01);
        btn02 = findViewById(R.id.btn02);

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

        //Reader Mode Offボタンのenabledをfalseに(トグルにするため)
        btn02.setEnabled(false);

        btn01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //トグル機能
                btn01.setEnabled(false);
                btn02.setEnabled(true);

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

        btn02.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                //トグル機能
                btn01.setEnabled(true);
                btn02.setEnabled(false);

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

                //表示初期化
                txt01.setText("Read ID ...");
            }
        });
    }

    //Callback Class
    private class MyReaderCallback implements NfcAdapter.ReaderCallback{
        @Override
        public void onTagDiscovered(Tag tag){

            Log.d("Hoge","Tag discoverd.");

            //get idm
            byte[] idm = tag.getId();
            final String idmString = bytesToHexString(idm);

            //idm取るだけじゃなくてread,writeしたい場合はtag利用してごにょごにょする

            //親スレッドのUIを更新するためごにょごにょ
            final Handler mainHandler = new Handler(Looper.getMainLooper());
            mainHandler.post(new Runnable() {
                @Override
                public void run() {
                    txt01.setText(idmString);
                }
            });

        }
    }

    //bytes列を16進数文字列に変換(めんどい)
    public static 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(Locale.getDefault());
    }
}

簡単ですが以上です。