Android 插件开发指南

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

在阅读本文之前,请参阅插件开发指南,了解插件的结构及其通用的 JavaScript 接口概述。本节将继续演示示例 echo 插件,该插件可以实现从 Cordova webview 到原生平台以及从原生平台返回的通信。有关其他示例,另请参阅 CordovaPlugin.java 中的注释。

Android 插件基于 Cordova-Android,它是从带有原生桥接的 Android WebView 构建的。Android 插件的原生部分至少包含一个 Java 类,该类继承了 CordovaPlugin 类并覆盖了其 execute 方法之一。

插件类映射

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

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

这会将请求从 WebView 编组到 Android 原生端,从而有效地使用 args 数组中传递的额外参数调用 service 类上的 action 方法。

无论您是将插件作为 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 的生命周期,都会创建一个插件对象的实例。除非 <param>onload name 属性设置为 "true",否则插件在 JavaScript 首次调用引用之前不会实例化。例如:

<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 调用会向原生端触发插件请求,并且在 config.xml 文件中正确映射了相应的 Java 插件,但是最终的 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 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 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 调用相应的方法。应该注意的是,如果执行处理不正确,权限提示可能会堆叠,应该避免这种情况。

除了请求单个权限外,还可以通过定义 permissions 数组来请求整个组的权限,就像 Geolocation 插件所做的那样。

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

然后,当请求权限时,只需要执行以下操作

cordova.requestPermissions(this, 0, permissions);

这将请求数组中指定的权限。最好提供一个公开可访问的 permissions 数组,因为使用你的插件作为依赖项的插件可以使用它,但这并非必需。

调试 Android 插件

可以使用 Eclipse 或 Android Studio 进行 Android 调试,但建议使用 Android Studio。由于 Cordova-Android 当前用作库项目,并且插件作为源代码支持,因此可以像调试原生 Android 应用程序一样调试 Cordova 应用程序内部的 Java 代码。

启动其他活动

如果你的插件启动一个将 Cordova Activity 推到后台的活动,则需要特别注意。如果设备内存不足,Android OS 会销毁后台的 Activity。在这种情况下,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 以获取结果时使用,并且仅应恢复处理该 Activity 结果所必需的状态。插件的状态不会被恢复,除非在获取使用 CordovaInterfacestartActivityForResult() 方法请求的 Activity 结果的情况下,并且 Cordova Activity 在后台被操作系统销毁。

作为 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 回调。

请务必沟通 Cordova 应用程序应如何解释在 resume 事件中收到的结果。Cordova 应用程序有责任维护自己的状态,并记住他们发出的请求以及必要时提供的参数。但是,你仍然应该清楚地沟通 pluginStatus 值的含义,以及作为插件 API 的一部分,在 resume 字段中返回的数据类型。

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

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

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