Android 插件开发指南

本节详细介绍如何在 Android 平台上实现原生插件代码。

在阅读本节之前,请参阅 插件开发指南,了解插件结构及其常见 JavaScript 接口的概述。本节将继续演示示例 echo 插件,该插件在 Cordova webview 和原生平台之间进行通信。有关另一个示例,请参阅 CordovaPlugin.java 中的注释。

Android 插件基于 Cordova-Android,它由一个带有原生桥的 Android WebView 构建。Android 插件的原生部分至少包含一个扩展 CordovaPlugin 类并覆盖其 execute 方法之一的 Java 类。

插件类映射

插件的 JavaScript 接口使用 cordova.exec 方法,如下所示

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

这将从 WebView 传递一个请求到 Android 原生端,有效地调用 service 类上的 action 方法,并将其他参数传递到 args 数组中。

无论您是将插件作为 Java 文件还是作为自己的 jar 文件分发,都必须在您的 Cordova-Android 应用程序的 res/xml/config.xml 文件中指定插件。有关如何使用 plugin.xml 文件注入此 feature 元素的更多信息,请参阅应用程序插件。

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

服务名称与 JavaScript exec 调用中使用的名称匹配。该值是 Java 类的完全限定命名空间标识符。否则,插件可能会编译,但仍无法用于 Cordova。

插件初始化和生命周期

每个 WebView 的生命周期都会创建一个插件对象的实例。插件只有在首次被 JavaScript 调用引用时才会实例化,除非在 config.xml 中将具有 onload 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 等)来处理它们。具有长时间运行的请求、后台活动(如媒体播放)、监听器或内部状态的插件应实现 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 接口的主线程中运行;相反,它在 WebCore 线程中运行,execute 方法也是如此。如果您需要与用户界面交互,则应使用 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 ExecutorService(使用 cordova.getThreadPool() 获取)来执行您的代码,如下所示

@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 中以两种方式之一列出它们。

首选方法是使用 <framework /> 标签(有关更多详细信息,请参阅 插件规范)。以这种方式指定库允许它们通过 Gradle 的 依赖管理逻辑 进行解析。这允许多个插件使用 gsonandroid-support-v4google-play-services 等常用库,而不会发生冲突。

第二个选项是使用 <lib-file /> 标签来指定 jar 文件的位置(有关更多详细信息,请参阅 插件规范)。只有在您确定没有其他插件会依赖您引用的库(例如,如果库是特定于您的插件的)时,才应使用此方法。否则,如果您添加了相同的库,则可能会导致插件用户的构建错误。值得注意的是,Cordova 应用程序开发人员不一定是原生开发人员,因此原生平台构建错误可能尤其令人沮丧。

Echo Android 插件示例

为了匹配应用程序插件中描述的 JavaScript 接口的 echo 功能,请使用 plugin.xmlfeature 规范注入到本地平台的 config.xml 文件中

<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 值。任何其他操作都将返回 false 并导致 INVALID_ACTION 错误,这将转换为在 JavaScript 端调用的错误回调。

接下来,该方法使用 args 对象的 getString 方法检索 echo 字符串,指定传递给该方法的第一个参数。在将值传递给私有 echo 方法后,它将进行参数检查以确保它不是 null 或空字符串,在这种情况下,callbackContext.error() 会调用 JavaScript 的错误回调。如果各种检查通过,callbackContext.success() 会将原始 message 字符串作为参数传递回 JavaScript 的成功回调。

Android 集成

Android 具有一个 Intent 系统,允许进程相互通信。插件可以访问 CordovaInterface 对象,该对象可以访问运行应用程序的 Android Activity。这是启动新 Android Intent 所需的 ContextCordovaInterface 允许插件启动一个 Activity 以获取结果,并为 Intent 返回应用程序时设置回调插件。

从 Cordova 2.0 开始,插件不再可以直接访问 Context,并且旧的 ctx 成员已弃用。所有 ctx 方法都存在于 Context 上,因此 getContext()getActivity() 都可以返回所需的对象。

Android 权限

Android 权限直到最近才在安装时而不是运行时处理。这些权限需要在使用这些权限的应用程序上声明,并且需要将这些权限添加到 Android 清单中。这可以通过使用 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 调用相应的方法。需要注意的是,如果执行未正确处理,权限提示可能会堆叠,应避免这种情况。

除了请求单个权限的权限外,还可以通过定义 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 推送到后台,则需要考虑一些特殊情况。如果设备内存不足,Android 操作系统会销毁后台的活动。在这种情况下,CordovaPlugin 实例也会被销毁。如果您的插件正在等待从其启动的 Activity 获取结果,则当 Cordova Activity 返回前台并获取结果时,将创建您的插件的新实例。但是,插件的状态不会自动保存或恢复,并且插件的 CallbackContext 将丢失。您的 CordovaPlugin 可以实现两种方法来处理这种情况

/**
 * 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 以获取结果时使用,并且仅应恢复处理该活动结果所需的必要状态。插件的状态不会恢复,除非在获得您的插件使用 CordovaInterfacestartActivityForResult() 方法请求的活动结果,并且 Cordova 活动在后台被操作系统销毁的情况下。

作为 onRestoreStateForActivityResult() 的一部分,您的插件将被传递一个替换的 CallbackContext。重要的是要意识到这个 CallbackContext不是与活动一起被销毁的那个。原始回调已丢失,并且不会在 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 应用程序负责维护自己的状态并记住他们发出的请求以及他们提供的参数(如果需要)。但是,您仍然应该清楚地传达 pluginStatus 值的含义以及在 resume 字段中返回的数据类型,作为您插件 API 的一部分。

启动活动的完整事件序列如下

  1. Cordova 应用程序调用您的插件
  2. 您的插件启动一个活动以获取结果
  3. Android 操作系统销毁 Cordova 活动和您的插件实例
    • 调用 onSaveInstanceState()
  4. 用户与您的活动交互,活动结束
  5. Cordova 活动被重新创建,并接收活动结果
    • 调用 onRestoreStateForActivityResult()
  6. 调用 onActivityResult(),您的插件将结果传递给新的 CallbackContext
  7. 触发 resume 事件,并由 Cordova 应用程序接收

Android 提供了一个开发人员设置,用于调试低内存情况下的活动销毁。在您的设备或模拟器的“开发者选项”菜单中启用“不保留活动”设置,以模拟低内存情况。如果您的插件启动外部活动,您应该始终在启用此设置的情况下进行一些测试,以确保您正确处理低内存情况。