Cordovaアプリ開発の備忘録(プラグイン編)

前回の投稿では、Cordovaアプリの基本的な開発手順についてまとめました。
今回は、プラグインの開発手順についてまとめようと思います。

Plugin Development Guide

https://cordova.apache.org/docs/en/11.x/guide/hybrid/plugins/index.html

(2022/12/30)
プラグインからのコールバックを追記しました。

# プラグイン名を決める

まずは、プラグイン名を決めましょう。
今回は適当に「sampleplugin」とでもしておきましょうか。

フォルダ名は、慣例があり、「cordova-plugin-sampleplugin」となります。

```
> mkdir cordova-plugin-sampleplugin
> cd cordova-plugin-sampleplugin
```

# plugin.xmlを作成

```xml:cordova-plugin-sampleplugin/plugin.xml
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
    id="cordova-plugin-sampleplugin" version="0.0.1">
    <name>SamplePlugin</name>
    <js-module src="www/plugin_sampleplugin.js" name="sampleplugin">
        <clobbers target="sampleplugin" />
    </js-module>
    <platform name="android">
        // 後述
    </platform>
    <platform name="ios">
        // 後述
    </platform>
</plugin>
```

大事なところを補足します。

```xml:cordova-plugin-sampleplugin/plugin.xml
    <js-module src="www/plugin_sampleplugin.js" name="sampleplugin">
        <clobbers target="sampleplugin" />
    </js-module>
```

srcは、これから作成するネイティブプラグインとJavascript呼び出しを仲介するコードを実装するJavascriptファイルです。wwwフォルダの下に作成して置く予定です。

targetで指定した文字列を使って、他のJavascriptファイルからこのJavascriptで実装したクラスを参照できます。
この「js-module」で指定したJavascriptファイルは、自動的にWebコンテンツに追加されますので、ほかにも追加したいJavascriptファイルがある場合は、そのファイルの数だけ「js-module」を指定します。

plugin_sampleplugin.jsは、例えば以下のように作成します。
参考に、func1という関数と、func2という関数と、func3というコールバック登録関数を作成していきます。

```js:cordova-plugin-sampleplugin\www\plugin_sampleplugin.js
class SamplePlugin{
	constructor(){
	}

	func1(param1, param2, param3){
		return new Promise(function(resolve, reject){
			cordova.exec(
				function(result){
					resolve(result);
				},
				function(err){
					reject(err);
				},
				"SamplePlugin", "func1",
				[param1, param2, param3]);
		});
	}

	func2(return_type){
		return new Promise(function(resolve, reject){
			cordova.exec(
				function(result){
					resolve(result);
				},
				function(err){
					reject(err);
				},
				"SamplePlugin", "func2",
				[return_type]);
		});
	}

	func3(enable, callback){
		cordova.exec(
			function(result){
				callback(result);
			},
			function(err){
				console.error("func3 call failed");
			},
			"SamplePlugin", "func3",
			[enable]);
	}
}

module.exports = new SamplePlugin();
```

cordova.execが、ネイティブプラグインを呼び出しているところで、第3引数を後述する定義の名前と合わせる必要があります。
第4引数の文字列は、ネイティブプラグイン実装の中で、呼び出された関数を識別するための文字列になります。

# package.jsonの作成

こんなファイルを作っておきます。

```json:cordova-plugin-sampleplugin\package.json
{
  "name": "cordova-plugin-sampleplugin",
  "version": "0.0.1",
  "cordova": {
    "id": "cordova-plugin-sampleplugin",
    "platforms": [
      "android",
      "ios"
    ]
  }
}
```

今回は、AndroidとiOSのネイティブプラグインを作成します。


# Androidの場合

これからは、スマホのOSごとに異なってきますので、OSに分けて説明します。

## plugin.xml(Android)

plugin.xmlのAndroid部分は以下のようになります。

```xml:cordova-plugin-sampleplugin\plugin.xml
    <platform name="android">
        <config-file target="res/xml/config.xml" parent="/*">
            <feature name="SamplePlugin" >
                <param name="android-package" value="jp.or.sample.SamplePlugin.Main"/>
                <param name="onload" value="true" />
            </feature>
        </config-file>
        <config-file target="AndroidManifest.xml" parent="/*">
            <uses-permission android:name="android.permission.NFC" />
        </config-file>
        <source-file src="src/android/jp/or/sample/SamplePlugin/Main.java" target-dir="src/jp/or/sample/SamplePlugin" />
    </platform>
```

featureに指定したnameを、cordova.execの第3引数に指定する文字列と合わせます。
```<param name="onload" value="true" />```は、アプリ起動と同時に、ネイティブプラグインを有効にしたい場合に指定します。

```<config-file target="AndroidManifest.xml" parent="/*">```の部分は、何かAndroidManifest.xmlに定義を追加したい場合に記載します。一例ですので、指定は任意です。

source-fileに指定したsrcがこれから実装するネイティブ実装のソースコードです。通常はsrcフォルダの配下に作成します。AndroidだけでなくiOS用にも作成する予定なので、OSの区別も持たせています。また、Androidではパッケージ名に沿ったフォルダ構成にする必要があります。今回は、jp.or.sample.SamplePluginというパッケージ名にしています。
target-dirは、Androidのプロジェクトに取り込むときの取り込み先の場所を指定します。

## ネイティブ実装(Android)

以下のような関数を実装します。
・func1は、3つの引数(Int、String、[Int])を取って、3つのオブジェクトをそのまま返します。func3でコールバック関数を登録していた場合、コールバック関数も呼び出します。
・func2は、1つの引数("bool" or "int" or "string" or "array")を取って、指定した型の戻り値を返します。
・func3は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。

```java:cordova-plugin-sampleplugin\src\android\jp\or\sample\SamplePlugin\Main.java
package jp.or.sample.SamplePlugin;

import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class Main extends CordovaPlugin {
	public static String TAG = "SamplePlugin.Main";
	private Activity activity;
	private CallbackContext callback;

	@Override
	public void initialize(CordovaInterface cordova, CordovaWebView webView)
	{
		Log.d(TAG, "[Plugin] initialize called");
		super.initialize(cordova, webView);

		activity = cordova.getActivity();
	}

	@Override
	public void onResume(boolean multitasking)
	{
		Log.d(TAG, "[Plugin] onResume called");
		super.onResume(multitasking);
	}

	@Override
	public void onPause(boolean multitasking)
	{
		Log.d(TAG, "[Plugin] onPause called");
		super.onPause(multitasking);
	}

	@Override
	public void onNewIntent(Intent intent)
	{
		Log.d(TAG, "[Plugin] onNewIntent called");
		super.onNewIntent(intent);
	}

	private void sendMessageToJs(JSONObject message, CallbackContext callback) {
		final PluginResult result = new PluginResult(PluginResult.Status.OK, message);
		result.setKeepCallback(true);
		if( callback != null )
			callback.sendPluginResult(result);
	}
	
	@Override
	public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException
	{
		Log.d(TAG, "[Plugin] execute called");
		if( action.equals("func1") ){
			int arg0 = args.getInt(0);
			String arg1 = args.getString(1);
			JSONArray input_array = args.getJSONArray(2);
			int[] arg2 = new int[input_array.length()];
			for( int i = 0 ; i < arg2.length ; i++ )
				arg2[i] = input_array.getInt(i);
			
			JSONArray output_array = new JSONArray();
			for( int i = 0 ; i < arg2.length ; i++ )
				output_array.put(arg2[i]);
			
			JSONObject result = new JSONObject();
			result.put("arg0", arg0);
			result.put("arg1", arg1);
			result.put("arg2", output_array);
			callbackContext.success(result);
			
			sendMessageToJs(result, callback);
		}else
		if( action.equals("func2") ){
			String arg0 = args.getString(0);
			if( arg0.equals("int") ){
				callbackContext.success(1234);
			}else
			if( arg0.equals("string") ){
				callbackContext.success("Hello World");
			}else
			if( arg0.equals("array") ){
				JSONArray output_array = new JSONArray();
				for( int i = 0 ; i < 5 ; i++ )
					output_array.put(i);
				callbackContext.success(output_array);
			}else{
				callbackContext.error("Unknown arg0");
				return false;
			}
		}else
		if( action.equals("func3") ){
			boolean arg0 = args.getBoolean(0);
			if( arg0 ){
				callback = callbackContext;
			}else{
				callback = null;
				callbackContext.success("OK");
			}
		}else {
			String message = "Unknown action : (" + action + ") " + args.getString(0);
			Log.d(TAG, message);
			callbackContext.error(message);
			return false;
		}

		return true;
	}
}
```

コールバック関数の実装方法について補足します。
呼び出し側から呼ばれてから、普通にcallbackContext.successを呼び出すと、その呼び出しはそれで終了してしまい、コールバック関数を呼び出す前に終わってしまうので、とりあえずそれは呼び出さないでおきます。その時、次回コールバック関数呼び出せるように、callbackContextを覚えておきます。
そして、コールバックしたい時に、覚えておいたcallbackContextを使ってcallbackContext.successを呼び出すことができます。ですが、そこでそのまま呼び出すと終了してしまうので、result.setKeepCallback(true)を呼び出しておくと、また次にコールバック関数を呼べるようになります。

# iOSの場合

## plugin.xml

plugin.xmlのiOS部分は以下のようになります。

```xml:cordova-plugin-sampleplugin\plugin.xml
    <platform name="ios">
        <dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>
        <config-file target="config.xml" parent="/*">
            <feature name="SamplePlugin" >
                <param name="ios-package" value="SamplePlugin"/>
                <param name="onload" value="true" />
            </feature>
        </config-file>
        <config-file target="*-Info.plist" parent="NFCReaderUsageDescription">
            <string>NFC Scanning</string>
        </config-file>
        <source-file src="src/ios/SamplePlugin.swift" target-dir="src/ios" />
    </platform>
```

もともとCordovaのプラグインは、Object-Cで作成するのが通常でしたが、最近はSwiftでも作成できるようになっています。そのための設定が```<dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>```です。

featureに指定したnameを、cordova.execの第3引数に指定する文字列と合わせます。
```<param name="onload" value="true" />```は、アプリ起動と同時に、ネイティブプラグインを有効にしたい場合に指定します。ここらへんは、Androidと同じです。

```<config-file target="*-Info.plist" parent="NFCReaderUsageDescription">```の部分は、何かplistファイルに定義を追加したい場合に記載します。一例ですので、指定は任意です。

source-fileに指定したsrcがこれから実装するネイティブ実装のソースコードです。通常はsrcフォルダの配下に作成します。target-dirは、iOSのプロジェクトに取り込むときの取り込み先の場所を指定します。

## ネイティブ実装(iOS)

Androidの時と同様に、以下のような関数を実装します。

・func1は、3つの引数(Int、String、[Int])を取って、3つのオブジェクトをそのまま返します。func3でコールバック関数を登録していた場合、コールバック関数も呼び出します。
・func2は、1つの引数("bool" or "int" or "string" or "array")を取って、指定した型の戻り値を返します。
・func3は、コールバック関数を受け取ります。いっしょにコールバック有効・無効も指定します。

```swift:cordova-plugin-sampleplugin\src\ios\SamplePlugin.swift
import Foundation

@objc(SamplePlugin)
class SamplePlugin : CDVPlugin
{
    var callbackId: String?
    
    override
    func pluginInitialize() {
    }
    
    @objc(func1:)
    func func1(command: CDVInvokedUrlCommand)
    {
        NSLog("func1 called")
        guard let arg0 = command.arguments[0] as? Int, let arg1 = command.arguments[1] as? String, let arg2 = command.arguments[2] as? [Int] else {
            NSLog("Parameter invalid")
            let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
            self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
            return
        }

        let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: ["arg0": arg0, "arg1": arg1, "arg2": arg2] )
        commandDelegate.send(pluginResult, callbackId: command.callbackId)

        let pluginResult2 = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: ["arg0": arg0, "arg1": arg1, "arg2": arg2] )
        pluginResult2?.keepCallback = true
        if let callbackId = self.callbackId {
            commandDelegate.send(pluginResult2, callbackId: callbackId)
        }
    }

    @objc(func2:)
    func func2(command: CDVInvokedUrlCommand)
    {
        NSLog("func2 called")
        guard let arg0 = command.arguments[0] as? String else {
            NSLog("Parameter invalid")
            let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
            self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
            return
        }
        
        if arg0 == "int" {
            let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: 1234)
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }else if arg0 == "string" {
            let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "Hello World")
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }else if arg0 == "array" {
            var output_array:[Int] = []
            for i in 1 ..< 5 {
                output_array.append(i)
            }
            let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: output_array)
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }else{
            let pluginResult = CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "Invalid arg0")
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
            return
        }
        return
    }
    
    @objc(func3:)
    func func3(command: CDVInvokedUrlCommand)
    {
        NSLog("func3 called")
        guard let arg0 = command.arguments[0] as? Bool else {
            NSLog("Parameter invalid")
            let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter Invalid")
            self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
            return
        }

        if arg0 {
            self.callbackId = command.callbackId
        }else{
            self.callbackId = nil
            let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "OK" )
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }
    }
}
```

コールバック関数の実装方法について補足します。
呼び出し側から呼ばれてから、普通にcommandDelegate.sendを呼び出すと、その呼び出しはそれで終了してしまい、コールバック関数を呼び出す前に終わってしまうので、とりあえずそれは呼び出さないでおきます。その時、次回コールバック関数呼び出せるように、callbackIdを覚えておきます。
そして、コールバックしたい時に、覚えておいたcallbackIdを使ってcommandDelegate.sendを呼び出すことができます。ですが、そこでそのまま呼び出すと終了してしまうので、pluginResult2?.keepCallback = true を呼び出しておくと、また次にコールバック関数を呼べるようになります。

# Cordovaプロジェクトにプラグインを追加

以下を実行します。2通りの方法があります。

```
> cordova plugin add ..\cordova-plugin-sampleplugin
または
> cordova plugin add ..\cordova-plugin-sampleplugin --link
```

前者は、元のプラグインのコピーがプロジェクトフォルダに作成されます。後者は、単にリンクが作成されます。
ですので、プラグインのデバッグ時は後者で作成して、ネイティブコードが完成した後は前者を使うようにします。

# 実行

```
[Androidの場合]
> cordova build android
> cordova run android

[iOSの場合]
> cordova build ios
> cordova run ios
```

# プラグインのデバッグ

プラグインのネイティブ実装をデバッグしたい場合は、開発環境(Android StudioまたはXCode)で開いてデバッグします。
以下のフォルダを開きます。

[Androidの場合]
platforms\android

[iOSの場合]
platforms\ios

それぞれの開発環境において、ネイティブソースコードのところにブレークポイントが晴れるので、より詳しくデバッグができます。

iOSの場合は、XCodeで、以下の部分を設定します。
* Signing & CapabilitiesのTeam
* Build SettingsのiOS Deployment TargetのiOSバージョン

# 終わりに

以下も参考にしてください。

　[Cordovaアプリ開発の備忘録]( https://qiita.com/poruruba/items/f03b1691fd0f113735b3)

以下のページを参考にさせていただきました（ありがとうございました）

　https://tech-blog.rakus.co.jp/entry/20220207/cordva

作成したソースコードをGitHubに上げておきました。

https://github.com/poruruba/CordovaSample

以上