6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-06-05

ReactNativeにはFeliCaのID(IDm)を読む機能は無いので作ってみます。
iOS13からIDm読めるようになるみたいですが、まだリリースされていないので、とりあえずAndroidのみ。

完成イメージ

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

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

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

スクリーンショット 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の作成はおおよそ下記のような流れ。
その他、値の連携方法等は、こちらの記事が大変参考になりました。ありがとうございます。

  • 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

なお、ソース全体はGitHubにあげてますので参考まで。

実装

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

FeliCaModule.java

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

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

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

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処理も行えます。こちらなども参考にしてみてください。

FeliCaPackage.java

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

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を作成した場合は複数追加することになります。

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機能自体の呼び出しに加え、今回はデータ連携にエベントを利用しているのでその定義をしています。

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

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?