Androidプラグイン開発ガイド

このセクションでは、Androidプラットフォームでネイティブプラグインコードを実装する方法について詳しく説明します。

これを読む前に、プラグインの構造とその共通のJavaScriptインターフェイスの概要については、プラグイン開発ガイドを参照してください。このセクションでは、CordovaのWebViewからネイティブプラットフォームとの間で通信するサンプルの *echo* プラグインを引き続き説明します。別のサンプルについては、CordovaPlugin.javaのコメントも参照してください。

Androidプラグインは、ネイティブブリッジを備えたAndroid WebViewから構築されたCordova-Androidに基づいています。Androidプラグインのネイティブ部分は、少なくとも1つのJavaクラスで構成され、そのクラスはCordovaPluginクラスを拡張し、そのexecuteメソッドの1つをオーバーライドします。

プラグインクラスのマッピング

プラグインのJavaScriptインターフェイスは、次のようにcordova.execメソッドを使用します

exec(<successFunction>, <failFunction>, <service>, <action>, [<args>]);

これにより、WebViewからAndroidネイティブ側へのリクエストがマーシャリングされ、実質的にargs配列で渡された追加の引数を使用して、serviceクラスのactionメソッドが呼び出されます。

プラグインをJavaファイルまたは独自の*jar*ファイルとして配布するかどうかにかかわらず、プラグインはCordova-Androidアプリケーションのres/xml/config.xmlファイルで指定する必要があります。このfeature要素を注入するためのplugin.xmlファイルの使用方法の詳細については、アプリケーションプラグインを参照してください

<feature name="<service_name>">
    <param name="android-package" value="<full_name_including_namespace>" />
</feature>

サービス名は、JavaScriptのexec呼び出しで使用されているものと一致します。値は、Javaクラスの完全修飾名前空間識別子です。そうしないと、プラグインはコンパイルされてもCordovaで利用できない可能性があります。

プラグインの初期化とライフタイム

プラグインオブジェクトの1つのインスタンスは、各WebViewのライフタイムの間作成されます。プラグインは、JavaScriptからの呼び出しで最初に参照されるまでインスタンス化されません。ただし、config.xmlonload name属性を持つ<param>"true"に設定されている場合は例外です。次に例を示します。

<feature name="Echo">
    <param name="android-package" value="<full_name_including_namespace>" />
    <param name="onload" value="true" />
</feature>

プラグインは、起動ロジックにinitializeメソッドを使用する必要があります。

@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
    super.initialize(cordova, webView);
    // your init code here
}

プラグインは、Androidのライフサイクルイベントにもアクセスでき、提供されているメソッド(onResumeonDestroyなど)の1つを拡張することで、それらを処理できます。長時間実行されるリクエスト、メディア再生などのバックグラウンドアクティビティ、リスナー、または内部状態を持つプラグインは、onReset()メソッドを実装する必要があります。これは、WebViewが新しいページに移動するか、JavaScriptをリロードするリフレッシュを行うときに実行されます。

Android Javaプラグインの記述

JavaScript呼び出しによって、ネイティブ側へのプラグインリクエストが開始され、対応するJavaプラグインがconfig.xmlファイルで適切にマッピングされていますが、最終的なAndroid Javaプラグインクラスはどのようなものになるのでしょうか?JavaScriptのexec関数でプラグインにディスパッチされるものはすべて、プラグインクラスのexecuteメソッドに渡されます。ほとんどのexecute実装は次のようになります。

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        this.beep(args.getLong(0));
        callbackContext.success();
        return true;
    }
    return false;  // Returning false results in a "MethodNotFound" error.
}

JavaScriptのexec関数のactionパラメーターは、オプションのパラメーターを使用してディスパッチするプライベートクラスメソッドに対応します。

例外をキャッチしてエラーを返す場合、JavaScriptに返されるエラーが、可能な限りJavaの例外名と一致するようにすることが重要です。

スレッド

プラグインのJavaScriptは、WebViewインターフェイスのメインスレッドでは*実行されません*。代わりに、executeメソッドと同様に、WebCoreスレッドで実行されます。ユーザーインターフェイスと対話する必要がある場合は、次のようにActivityのrunOnUiThreadメソッドを使用する必要があります。

@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        final long duration = args.getLong(0);
        cordova.getActivity().runOnUiThread(new Runnable() {
            public void run() {
                ...
                callbackContext.success(); // Thread-safe.
            }
        });
        return true;
    }
    return false;
}

UIスレッドで実行する必要はないが、WebCoreスレッドもブロックしたくない場合は、次のようにcordova.getThreadPool()で取得したCordova ExecutorServiceを使用してコードを実行する必要があります。

@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if ("beep".equals(action)) {
        final long duration = args.getLong(0);
        cordova.getThreadPool().execute(new Runnable() {
            public void run() {
                ...
                callbackContext.success(); // Thread-safe.
            }
        });
        return true;
    }
    return false;
}

依存ライブラリの追加

Androidプラグインに追加の依存関係がある場合は、plugin.xmlに2つの方法のいずれかでリストする必要があります。

推奨される方法は、<framework />タグを使用することです(詳細については、プラグイン仕様を参照してください)。この方法でライブラリを指定すると、Gradleの依存関係管理ロジックを介して解決できます。これにより、*gson*、*android-support-v4*、*google-play-services*などの一般的に使用されるライブラリを、競合なしに複数のプラグインで使用できます。

2番目のオプションは、<lib-file />タグを使用してjarファイルの場所を指定することです(詳細については、プラグイン仕様を参照してください)。この方法は、参照しているライブラリに他のプラグインが依存しないことが確実な場合にのみ使用する必要があります(例:ライブラリがプラグインに固有の場合)。そうしないと、別のプラグインが同じライブラリを追加した場合、プラグインのユーザーのビルドエラーが発生するリスクがあります。Cordovaアプリの開発者は必ずしもネイティブ開発者ではないため、ネイティブプラットフォームのビルドエラーは特に不満の原因となる可能性があることに注意してください。

Echo Androidプラグインの例

アプリケーションプラグインで説明されているJavaScriptインターフェイスの*echo*機能に一致させるには、plugin.xmlを使用して、ローカルプラットフォームのconfig.xmlファイルにfeature仕様を挿入します

<platform name="android">
    <config-file target="config.xml" parent="/*">
        <feature name="Echo">
            <param name="android-package" value="org.apache.cordova.plugin.Echo"/>
        </feature>
    </config-file>

    <source-file src="src/android/Echo.java" target-dir="src/org/apache/cordova/plugin" />
</platform>

次に、src/android/Echo.javaファイルに以下を追加します

package org.apache.cordova.plugin;

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;

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

/**
* This class echoes a string called from JavaScript.
*/
public class Echo extends CordovaPlugin {

    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (action.equals("echo")) {
            String message = args.getString(0);
            this.echo(message, callbackContext);
            return true;
        }
        return false;
    }

    private void echo(String message, CallbackContext callbackContext) {
        if (message != null && message.length() > 0) {
            callbackContext.success(message);
        } else {
            callbackContext.error("Expected one non-empty string argument.");
        }
    }
}

ファイルの先頭に必要なインポートは、CordovaPluginからクラスを拡張し、そのexecute()メソッドをオーバーライドしてexec()からのメッセージを受信します。execute()メソッドは、最初にactionの値をテストします。この場合、有効なecho値は1つだけです。他のアクションはfalseを返し、INVALID_ACTIONエラーになります。これは、JavaScript側で呼び出されたエラーコールバックに変換されます。

次に、メソッドはargsオブジェクトのgetStringメソッドを使用してecho文字列を取得し、メソッドに渡された最初のパラメーターを指定します。値がプライベートなechoメソッドに渡された後、パラメーターチェックを実行して、nullまたは空の文字列でないことを確認します。その場合は、callbackContext.error()がJavaScriptのエラーコールバックを呼び出します。さまざまなチェックに合格した場合、callbackContext.success()は元のmessage文字列をパラメーターとしてJavaScriptの成功コールバックに渡します。

Android統合

Androidには、プロセスが相互に通信できるようにするIntentシステムが搭載されています。プラグインは、アプリケーションを実行するAndroid ActivityにアクセスできるCordovaInterfaceオブジェクトにアクセスできます。これが、新しいAndroid Intentを起動するために必要なContextです。CordovaInterfaceを使用すると、プラグインは結果を得るためにActivityを開始し、Intentがアプリケーションに戻ったときにコールバックプラグインを設定できます。

Cordova 2.0以降、プラグインはContextに直接アクセスできなくなり、レガシーctxメンバーは非推奨になりました。すべてのctxメソッドはContextに存在するため、getContext()getActivity()の両方で必要なオブジェクトを返すことができます。

Androidの権限

最近まで、Androidの権限はランタイムではなくインストール時に処理されていました。これらの権限は、権限を使用するアプリケーションで宣言する必要があり、これらの権限はAndroid Manifestに追加する必要があります。これは、config.xmlを使用して、これらの権限をAndroidManifest.xmlファイルに挿入することで実現できます。以下の例では、連絡先の権限を使用しています。

<config-file target="AndroidManifest.xml" parent="/*">
    <uses-permission android:name="android.permission.READ_CONTACTS" />
</config-file>

ランタイム権限(Cordova-Android 5.0.0以降)

Android 6.0「Marshmallow」では、必要に応じてユーザーが権限をオン/オフにできる新しい権限モデルが導入されました。つまり、アプリケーションはこれらの権限の変更を処理して将来に対応する必要があり、これはCordova-Android 5.0.0リリースの焦点でした。

ランタイムで処理する必要がある権限は、Android開発者ドキュメントのこちらに記載されています。

プラグインに関する限り、権限は許可メソッドを呼び出すことで要求できます。そのシグネチャは次のとおりです

cordova.requestPermission(CordovaPlugin plugin, int requestCode, String permission);

冗長性を減らすために、これをローカルの静的変数に割り当てるのが標準的な方法です

public static final String READ = Manifest.permission.READ_CONTACTS;

requestCodeを次のように定義することも標準的な方法です

public static final int SEARCH_REQ_CODE = 0;

次に、execメソッドで、権限をチェックする必要があります

if(cordova.hasPermission(READ))
{
    search(executeArgs);
}
else
{
    getReadPermission(SEARCH_REQ_CODE);
}

この場合、requestPermissionを呼び出すだけです

protected void getReadPermission(int requestCode)
{
    cordova.requestPermission(this, requestCode, READ);
}

これにより、アクティビティが呼び出され、権限を求めるプロンプトが表示されます。ユーザーが権限を取得すると、結果はすべてのプラグインがオーバーライドする必要があるonRequestPermissionResultメソッドで処理する必要があります。以下に例を示します。

public void onRequestPermissionResult(int requestCode, String[] permissions,
                                         int[] grantResults) throws JSONException
{
    for(int r:grantResults)
    {
        if(r == PackageManager.PERMISSION_DENIED)
        {
            this.callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, PERMISSION_DENIED_ERROR));
            return;
        }
    }
    switch(requestCode)
    {
        case SEARCH_REQ_CODE:
            search(executeArgs);
            break;
        case SAVE_REQ_CODE:
            save(executeArgs);
            break;
        case REMOVE_REQ_CODE:
            remove(executeArgs);
            break;
    }
}

上記のswitchステートメントはプロンプトから戻り、渡されたrequestCodeに応じて、それぞれのメソッドを呼び出します。実行が正しく処理されない場合、権限プロンプトがスタックする可能性があり、これを避ける必要があることに注意してください。

単一の権限に対する権限を要求することに加えて、Geolocationプラグインで行われるように、permissions配列を定義してグループ全体の権限を要求することも可能です

String [] permissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };

次に、権限を要求するときは、次の操作を行うだけです

cordova.requestPermissions(this, 0, permissions);

これにより、配列で指定された権限が要求されます。これは必須ではありませんが、プラグインを依存関係として使用するプラグインで使用できるため、一般にアクセス可能なpermissions配列を提供することをお勧めします。

Androidプラグインのデバッグ

Android のデバッグは、Eclipse または Android Studio のいずれかで行うことができますが、Android Studio が推奨されます。Cordova-Android は現在ライブラリプロジェクトとして使用されており、プラグインはソースコードとしてサポートされているため、ネイティブの Android アプリケーションと同様に、Cordova アプリケーション内の Java コードをデバッグすることができます。

他のアクティビティの起動

プラグインが Cordova のActivityをバックグラウンドにプッシュする Activity を起動する場合、特別な考慮事項があります。Android OS は、デバイスのメモリが不足している場合、バックグラウンドにある Activity を破棄します。その場合、CordovaPlugin インスタンスも破棄されます。プラグインが起動したActivityからの結果を待機している場合、Cordova のActivityがフォアグラウンドに戻され、結果が取得されると、プラグインの新しいインスタンスが作成されます。ただし、プラグインの状態は自動的に保存または復元されず、プラグインの CallbackContext は失われます。この状況を処理するために、CordovaPlugin が実装できる 2 つのメソッドがあります。

/**
 * Called when the Activity is being destroyed (e.g. if a plugin calls out to an
 * external Activity and the OS kills the CordovaActivity in the background).
 * The plugin should save its state in this method only if it is awaiting the
 * result of an external Activity and needs to preserve some information so as
 * to handle that result; onRestoreStateForActivityResult() will only be called
 * if the plugin is the recipient of an Activity result
 *
 * @return  Bundle containing the state of the plugin or null if state does not
 *          need to be saved
 */
public Bundle onSaveInstanceState() {}

/**
 * Called when a plugin is the recipient of an Activity result after the
 * CordovaActivity has been destroyed. The Bundle will be the same as the one
 * the plugin returned in onSaveInstanceState()
 *
 * @param state             Bundle containing the state of the plugin
 * @param callbackContext   Replacement Context to return the plugin result to
 */
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}

上記のメソッドは、プラグインが結果を求めるActivityを起動する場合にのみ使用する必要があり、その Activity の結果を処理するために必要な状態のみを復元する必要があることに注意してください。プラグインの状態は、CordovaInterfacestartActivityForResult() メソッドを使用してプラグインが要求した Activity 結果が取得され、バックグラウンドで Cordova Activity が OS によって破棄された場合を除き、復元されません。

onRestoreStateForActivityResult() の一部として、プラグインには代替の CallbackContext が渡されます。この CallbackContext は、Activity と共に破棄されたものと同じではないことを理解することが重要です。元のコールバックは失われ、JavaScript アプリケーションで発行されることはありません。代わりに、この代替の CallbackContext は、アプリケーションが再開されたときに発生するresumeイベントの一部として結果を返します。 resume イベントのペイロードは、次の構造に従います。

{
    action: "resume",
    pendingResult: {
        pluginServiceName: string,
        pluginStatus: string,
        result: any
    }
}
  • pluginServiceName は、プラグインの plugin.xml の name 要素と一致します。
  • pluginStatus は、CallbackContext に渡された PluginResult のステータスを説明する文字列です。プラグインステータスに対応する文字列値については、PluginResult.java を参照してください。
  • result は、プラグインが CallbackContext に渡す結果 (文字列、数値、JSON オブジェクトなど) になります。

このresume ペイロードは、JavaScript アプリケーションがresume イベントに登録したコールバックに渡されます。これは、結果が *直接* Cordova アプリケーションに渡されることを意味します。プラグインは、アプリケーションが結果を受け取る前に、JavaScript で結果を処理する機会はありません。したがって、ネイティブコードから返される結果を可能な限り完全なものにし、アクティビティを起動するときに JavaScript コールバックに依存しないように努める必要があります。

resume イベントで受け取った結果を Cordova アプリケーションがどのように解釈すべきかを必ず伝えてください。必要な場合、Cordova アプリケーションは独自の状態を維持し、行った要求と提供した引数を覚えておく必要があります。ただし、プラグインの API の一部として、pluginStatus 値の意味と、resume フィールドでどのような種類のデータが返されるかを明確に伝える必要があります。

アクティビティを起動するための一連のイベントの完全な流れは次のとおりです。

  1. Cordova アプリケーションがプラグインを呼び出します。
  2. プラグインが結果を求めるアクティビティを起動します。
  3. Android OS が Cordova Activity とプラグインインスタンスを破棄します。
    • onSaveInstanceState() が呼び出されます。
  4. ユーザーがアクティビティを操作し、アクティビティが終了します。
  5. Cordova Activity が再作成され、アクティビティの結果が受信されます。
    • onRestoreStateForActivityResult() が呼び出されます。
  6. onActivityResult() が呼び出され、プラグインが新しい CallbackContext に結果を渡します。
  7. resume イベントが発生し、Cordova アプリケーションで受信されます。

Android には、メモリ不足時のアクティビティ破棄のデバッグ用の開発者向け設定が用意されています。デバイスまたはエミュレーターの開発者向けオプションメニューで [アクティビティを保持しない] 設定を有効にして、メモリ不足のシナリオをシミュレートします。プラグインが外部アクティビティを起動する場合は、この設定を有効にして、メモリ不足のシナリオを適切に処理していることを確認するためのテストを常に行う必要があります。