ReactNativeでFeliCaのIDmを読み取るNativeModuleを作る(Android)


ReactNativeにはFeliCaのID（IDm）を読む機能は無いので作ってみます。
[iOS13からIDm読めるようになるみたい](https://developer.apple.com/documentation/corenfc/nfcfelicatag)ですが、まだリリースされていないので、とりあえずAndroidのみ。


## 完成イメージ

機能は可能な限りシンプルに（FeliCa関連機能にフォーカスするためエラー処理とか無視）。

* UIとしてTextとButtonを2個配置。
* START POLLINGでポーリング開始。
* FeliCaを認識したらTextに表示
* STOP POLLINGでポーリング停止。

という感じ。FeliCaの認識はIntentは使わずReaderModeで処理します。

![スクリーンショット 2019-06-06 6.11.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/f6efc790-ab22-ea73-c4fa-d86d89975952.png "スクリーンショット 2019-06-06 6.11.54.png")


## 前提

* Macで作業します
* Android Studioインストール、エミュレータ作成済み
* react-native-cliインストール済み
* FeliCa raeder/write機能付、かつreader/write機能On状態の端末

## プロジェクトの作成

プロジェクトの作成にはreact-nativeコマンドを利用します。
なお、expoである程度作ってejectするという手もあります。

>expo ejectも試したのですが、run-androidコマンドがうまく動きませんでした（別にFeliCa関係ない）。別途調査。

```
react-native init FeliCaIDm
```

この時点でプロジェクトが正常に動くかどうか、一度試して置いたほうがいいでしょう。
Android Studioからエミュレータを起動した状態で、

```
react-native run-android
```

とするとアプリが起動します。

なお、実際のFeliCaの動作確認には「おサイフケータイ」対応のAndroid端末が必要です。adbがディバイスを認識している状態なら、上記コマンドで実機にも展開されるようです。


## プロジェクトの設定

ract-native initコマンドでいろいろ生成される中にandroidというフォルダがあるので、それをAndroid Studioで開きます

### Gradle Update

Updateするか？と聞かれるのでUpdateしておきましょう。

### AndroidManifest.xmlの編集

NFC機能を利用するので許可します。

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

### build.gradele(Project:xxxx)の編集

デフォルトではminSdkVersion=16となっていますが、後で使うReaderModeは19からなので19に変更します。

```
minSdkVersion = 19
```

## NativeModule開発・利用の流れ

NativeModuleの作成はおおよそ下記のような流れ。
その他、値の連携方法等は、[こちら](https://blog.mitsuruog.info/2019/02/react-native-native-module-android)の記事が大変参考になりました。ありがとうございます。

* Module Classの作成(FeliCaModule.java）：実際に利用する機能を実装する
* Package Classの作成(FeliCaPackage.java）：ReactNative側から呼び出せる準備をする
* Package Classの登録(MainApplication.java）：ReactNative側から呼び出せるよう登録する
* App.jsからの利用(ReactNative側）

という感じです。上3つはAndroid Studioで作業し、App.jsはお好みのエディタで作業する感じ。
実際のAndroid Stuidoのソース構造は下記のような感じになります。

![スクリーンショット 2019-06-06 6.06.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/f3889db7-ffde-ea1e-6b81-36ec0632612d.png "スクリーンショット 2019-06-06 6.06.12.png")


なお、[ソース全体はGitHubにあげてます](https://github.com/eizaburo/RN_FeliCaIDm)ので参考まで。


## 実装

では、実装していきます。

### FeliCaModule.java

プロジェクト内に新規にFeliCaModuleクラスを作成し、実装します。

* getName()は実装必須
* polling startでポーリングを開始してFeliCaカードの認識を待ちます
* 認識したらcallback classのonTagDiscoveredが呼ばれるので、その中でReactとしてのEventを発火。React側にFeliCaを認識したEventと読み取ったIDを通知
* polling stopでポーリング停止。

FeliCa側の処理がなれてないと？？？って感じかもしれませんが、やってることは単純。

```java:FeliCaModule.java
package com.felicaidm;

import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.util.Formatter;
import java.util.Locale;

import javax.annotation.Nullable;

public class FeliCaModule extends ReactContextBaseJavaModule {

    NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this.getReactApplicationContext());

    public FeliCaModule(ReactApplicationContext reactContext){
        super(reactContext);
    }

    @Override
    //RN側でFeliCaで呼び出せる
    public String getName(){
        return "FeliCa";
    }

    @ReactMethod
    //Polling開始（ReaderMode利用）
    public void startPolling(Callback callback){
        nfcAdapter.enableReaderMode(this.getCurrentActivity(),new MyReaderCallback(this.getReactApplicationContext()),NfcAdapter.FLAG_READER_NFC_F,null);
        callback.invoke("start polling...");
    }

    @ReactMethod
    //Polling停止
    public void stopPolling(Callback callback){
        nfcAdapter.disableReaderMode(this.getCurrentActivity());
        callback.invoke("stop polling...");

    }

    //ReaderModeの引数にわたすCallbackをinner classで実装
    //Callback内でEventを発火させる
    private class MyReaderCallback implements NfcAdapter.ReaderCallback{

        ReactApplicationContext context;

        MyReaderCallback(ReactApplicationContext reactContext){
            context = reactContext;
        }

        @Override
        public void onTagDiscovered(Tag tag){

            //IDm取得(tag.getId()でsystem0のIDmが取れる。複数systemがある場合は注意）
            String idmString = bytesToHexString(tag.getId());
            Log.d("Hoge","IDm=" + idmString);

            //必要に応じて取得したtag使ってNfcF生成してtransceive()でいろいろすればよい

            //渡すパラメータ定義(emitの際 WritableMapである必要があるため）
            WritableMap params = Arguments.createMap();
            params.putString("idm", idmString);
            //sendEvent
            sendEvent(context,"onTagDiscovered",params);

        }
    }

    //SendEvent（ここでは1つしかEventが無いが、普通は複数あるので関数化しておく）
    private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params){
        reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName,params);
    }

    //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();
    }
}
```

onTagDiscoveredの中でFeliCaコマンドを発行することでIDmの読取りだけでなくFeliCaに対するRead/Write処理も行えます。[こちら](https://qiita.com/zaburo/items/d76567086dfcaeaf3e72)なども参考にしてみてください。

### FeliCaPackage.java

次にPackage。createViewManagers()とcreateNativeModules()を実装する必要があるみたい。
createViewManagers()の方は事実上何もしない。
createNativeModules()の方で、上記で作成したFeliCaModuleをモジュールリストに追加している。

```java:FeliCaPackage.java
package com.felicaidm;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class FeliCaPakage implements ReactPackage {

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext){
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext){
        List<NativeModule> modules =  new ArrayList<>();
        //FeliCaModuleを追加
        modules.add(new FeliCaModule(reactContext));
        return modules;
    }
}
```

### MainApplication.java

MainApplication.javaにおいて追加したモージュールリストをApp.jsから呼べるように登録する。
作業で追加するのは1行だけ。複数のModuleやPackageを作成した場合は複数追加することになります。

```diff:MainApplication.java
package com.felicaidm;

import android.app.Application;

import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import java.util.Arrays;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

    @Override
    protected List<ReactPackage> getPackages() {
      //FeliCaPackageを追加
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
+            new FeliCaPakage()
      );
    }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}
```

### App.js

あとはReactNative側（App.js）で利用するだけです。
FeliCa module機能自体の呼び出しに加え、今回はデータ連携にエベントを利用しているのでその定義をしています。

```js:App.js
/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 *
 * @format
 * @flow
 */

import React, {Component} from 'react';
//必要なComponentを追加
import {Platform, StyleSheet, Text, View, Button, NativeModules, NativeEventEmitter} from 'react-native';

//module取得
const { FeliCa } = NativeModules;
//event取得
const felicaEvents = new NativeEventEmitter(FeliCa);

const instructions = Platform.select({
  ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
  android:
    'Double tap R on your keyboard to reload,\n' +
    'Shake or press menu button for dev menu',
});

type Props = {};
export default class App extends Component<Props> {

  //stateにidm定義
  state = {
    idm: "xxxxxxxx"
  }

  componentWillMount(){
    //event追加
    felicaEvents.addListener("onTagDiscovered",({idm})=>{
      this.setState({idm:idm});
    });
  }

  render() {
    return (
      <View style={{flex:1,justifyContent:'center',alignItems:'center'}}>
        <Text>IDm:{this.state.idm}</Text>
        <View style={{marginTop:20}}></View>
        <Button
          title="start polling"
          onPress={()=>this.startPolling()}
        />
        <View style={{marginTop:20}}></View>
        <Button
          title="stop polling"
          onPress={()=>this.stopPolling()}
        />
      </View>
    );
  }

  //polling開始（callback定義するとかならず処理が必要みたい）
  startPolling = () => {
    this.setState({idm:"Reading..."});
    FeliCa.startPolling(x => console.log(x));
  }

  //polling停止（callback定義するとかならず処理が必要みたい）
  stopPolling = () => {
    FeliCa.stopPolling(x => console.log(x));
  }
}
```

簡単ではありますが以上です。

## 残課題など

RNはexpoから入ったので「RNいいじゃん！」と思っていたけど、expo以外の世界は闇が深い・・・。

### npmパッケージを作る

ちゃんとエラー処理入れたバージョンでそのうち作ります。

### expo ejectで同じことをやる

~~実際の開発では、ある程度expoで開発して、最後にejectして・・・とう流れかなと。
ちらっとやった感じではエラーがでた。~~

→最新のexpo(2.20.1)でエラーは無くなりました。

### iOSでのエラーに対応

iOSで利用した場合、「このOSでは利用できません」とかやりたい。
まあ、それはPlatform判定してエラー処理すればいいのですが、そもそも

```
react-native run-ios
```

~~でうまく起動しません。Native module cannot be null.というエラー。
まあ、iOS側は何も実装してないので、そのための対応が必要な感じなのはわかるのですが、具体的に何をどうしたらよいのか。。。さて。分かる人教えて!!（とりあえず、iOS側でもダミーの機能実装したらいいのかな？）。~~

→iOS側に同じパラメータやメソッドを実装することで直りました。


![スクリーンショット 2019-06-06 6.35.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55188/347392d9-5cd7-4747-6454-a46ec1e8e1ae.png "スクリーンショット 2019-06-06 6.35.14.png")