[Flutter] NFCタグを読み取る (Android)

FlutterからNFCタグを読んでみます。
Flutterを使い始めたばかりで仕様をよく理解していないため、Flutterのプラグインを用いずにごりごりJavaで書きます。
Androidのバージョンは4.4 (KitKat)以上を前提で進めます。

# おおまかな流れ
main.dart
1. MainActivityに書かれたメソッドを呼び出して結果（データ）を待つ
2. データを読んでStateを更新する

MainActivity.java
1. メソッドを呼び出されたらNFCをReaderModeにする
2. タグが検知されたらいろいろやって結果（データ）を返す

今回はデータ形式にJSONを採用します。

# main.dart
MethodChannelを用います。

```dart:main.dart
import 'dart:async'; // MainActivityとのやりとりは非同期です
import 'dart:convert'; // JSONを使います
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // 必須です

const platform = MethodChannel('com.example.hogehoge/nfcf'); // パッケージ名/任意の名前
const json = JsonCodec();

...

class _HogeState extends State<HogeWidget> {
  ...
  Future<void> _getNfcTag() async {
    try {
      final String result = await platform.invokeMethod('read');
      if (result == null) {
        throw new Exception('Invalid result');
      } else {
        final Map data = json.decode(result);
        // TODO: データ加工
      }
    } on PlatformException catch (e) {
      // TODO: エラー処理
    }
    setState(() {
      // TODO: ステート更新
    });
  }
  ...
}
```

# MainActivity.java
Activity（=this）は必要になる場面（非同期）が多いので適当な変数に格納します。
Result.success()で結果をmain.dartに返します。

```java:MainActivity.java
package com.example.hogehoge;

import android.app.Activity;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.NfcF; // FeliCaを読むことにします
import android.os.Bundle;

import io.flutter.app.FlutterActivity;
import io.flutter.view.FlutterMain;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.*;
import io.flutter.plugins.GeneratedPluginRegistrant;

import org.json.JSONArray;
import org.json.JSONObject;

public class MainActivity extends FlutterActivity {
  private Activity activity;
  private NfcAdapter adapter;
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    FlutterMain.startInitialization(this); // エラーが出なければ不要
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    
    activity = this; // Activityを格納
    // main.dartでのMethodChannelと同じ名前に
    final MethodChannel channel = new MethodChannel(getFlutterView(), "com.example.hogehoge/nfcf");
    
    channel.setMethodCallHandler(new MethodCallHandler() {
      private Result _result;
      
      @Override
      public void onMethodCall(MethodCall call, Result result) {
        // 結果を返すのがタグ検出後なので外の変数に格納します
        _result = result;
        adapter = NfcAdapter.getDefaultAdapter(activity);
        
        if (call.method == "read") {
          adapter.enableReaderMode(activity, new NfcAdapter.ReaderCallback() {
            @Override
            public void onTagDiscovered(Tag tag) {
              try {
                adapter.disableReaderMode(activity);
                final byte[] idm = tag.getId(); // System0のIDm
                final JSONObject json = new JSONObject();
                /*
                ここでいろいろやります。
                final NfcF nfcf = NfcF.get(tag);
                実際の処理は以下のように別のクラスを自作してそちら側に任せるのをおすすめします。
                final FeliCaReader reader = new FeliCaReader(tag);
                final FeliCaReader.System[] systems = reader.getSystems();
                byte[] bytes = systems[0].readWithoutEncryption(...);
                */
                _result.success(json.toString());
              } catch (Exception e) {
                _result.success(null);
              }
            }
          }, NfcAdapter.FLAG_READER_NFC_F, null); // FLAGは読みたいタグに合わせます
        } else {
          result.notImplemented();
        }
      }
    });
  }
}

```

# その他
AndroidManifest.xmlにパーミッションを追加するのを忘れずに。

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

（VSCodeユーザー向け）
デバッグで`FormatException: Bad UTF-8 encoding`が出るときは大抵の場合Javaのコンパイルエラーなので、javacをすると教えてくれます。
バッチファイルを書いておくと便利です。パスは適宜変更してください。

```text:javac.bat
@echo off
setlocal

set javac="%JAVA_HOME%\bin\javac"
set main="android\app\src\main\java\com\example\hogehoge\MainActivity.java"
set plugins="C:\flutter\.pub-cache\hosted\pub.dartlang.org\file-5.0.6\android\app\src\main\java\io\flutter\plugins\GeneratedPluginRegistrant.java"
set android="C:\Program Files (x86)\Android\android-sdk\platforms\android-28\android.jar"
set flutter="C:\flutter\bin\cache\artifacts\engine\android-arm64-dynamic-profile\flutter.jar"

%javac% %main% %plugins% -classpath %android%;%flutter%

set output="android\app\src\main\java\com\example\hogehoge\*.class"
del /q %output%

endlocal
```


# 参考
- [Writing custom platform-specific code - Flutter](https://flutter.io/docs/development/platform-integration/platform-channels) - Flutterでネイティブコードを呼び出す方法についての基本的な解説
- [android.nfc | Android Developers](https://developer.android.com/reference/android/nfc/package-summary) - NFCに関するAndroidのAPIリファレンス
- [AndroidでFelica(NFC)のブロックデータの取得 - Qiita](https://qiita.com/nshiba/items/38f94d61c020a17314b6) - FeliCaのデータ取得に関する非常にわかりやすい解説