🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[toc] # [Android Plugin Development Guide](http://cordova.apache.org/docs/en/latest/guide/platforms/android/plugin.html) 首先你要去看看[插件开发指南](插件开发指南.md),以便对插件开发有个总体的结构认识。本节继续演示示例echo插件,该插件从Cordova webview通信到本机平台并返回。有关另一个示例,另请参阅 [CordovaPlugin.java](https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/CordovaPlugin.java) 中的注释。 Android插件基于 Cordova-Android ,它是使用带有原生桥接(*native bridge*)的 Android WebView构建的。 Android插件的原生部分包含至少一个继承(extends) `CordovaPlugin` 类并覆盖其中的一个 `execute` 方法。 # 插件类映射 该插件的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> ~~~ `service_name` 与 `JavaScript exec` 调用中使用的名称匹配。该值是Java类的完全限定名称空间标识符。否则,插件可能会编译但仍然无法访问Cordova。 # 插件初始化和生命周期 在每个 `WebView` 的生命周期中创建一个插件对象实例。除非首先通过JavaScript调用引用插件,否则不会实例化插件,除非在 `config.xml` 中将带有`onload` `name`属性的设置为“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生命周期事件,并可以通过j集成其中一个提供的方法(`onResume`,`onDestroy`等)来处理它们。具有长时间运行请求,后台活动(如媒体播放,侦听器或内部状态)的插件应实现`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's runOnUiThread](http://developer.android.com/reference/android/app/Activity.html#runOnUiThread(java.lang.Runnable)) 方法,如下所示: ~~~ @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` 中以两种方式中的一种列出。 首选方法是使用 `<framework />` 标记(有关详细信息,请参阅[插件规范](http://cordova.apache.org/docs/en/latest/plugin_ref/spec.html#framework))。以这种方式指定库允许通过Gradle的[依赖关系管理](https://docs.gradle.org/current/userguide/dependency_management.html)逻辑来解析它们。这允许多个插件使用诸如*gson*,*android-support-v4* 和 *google-play-services* 之类的常用库而不会发生冲突。 第二个选项是使用 `<lib-file />` 标记指定 *jar* 文件的位置(有关详细信息,请参阅[插件规范](http://cordova.apache.org/docs/en/latest/plugin_ref/spec.html#framework))。只有当您确信没有其他插件依赖于您正在引用的库时(例如,如果库是特定于您的插件的话),才应该使用这种方法。 例如: ~~~ <lib-file src="src/android/PaySDK/libs/alipaySdk-20170725.jar" /> ~~~ 该jar包会被自动复制到 `platforms>android>libs` 下面! 否则,如果另一个插件添加了相同的库,则可能会导致插件用户出现构建错误。值得注意的是,Cordova应用程序开发人员不一定是原生开发人员,因此原生构建错误尤其令人沮丧。 # Echo Android插件的例子 要匹配Application Plugins中描述的JavaScript接口的echo功能,请使用 `plugin.xml` 将 `feature`规范注入原生平台的`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](http://developer.android.com/reference/android/content/Intent.html) 系统,允许进程相互通信。插件可以访问 `CordovaInterface` 对象,该对象可以访问运行应用程序的[Android Activity](http://developer.android.com/reference/android/app/Activity.html)。这是启动新[Android Intent](http://developer.android.com/reference/android/content/Intent.html)所需的[Context](http://developer.android.com/reference/android/content/Context.html)*上下文*。 CordovaInterface允许插件为结果启动[Activity](http://developer.android.com/reference/android/app/Activity.html),并为 [Intent](http://developer.android.com/reference/android/content/Intent.html) 返回应用程序时设置回调插件。 从Cordova 2.0开始,插件就不能直接访问[Context](http://developer.android.com/reference/android/content/Context.html),并且不赞成使用遗留的`ctx` 成员。所有`ctx` 方法都存在于[Context](http://developer.android.com/reference/android/content/Context.html)中,因此 `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 Developer文档](http://developer.android.com/guide/topics/security/permissions.html#perm-groups)中找到。 就插件而言,可以通过调用权限方法来请求权限;签名如下: ~~~ 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` 方法处理结果,每个插件都应覆盖该方法。下面是一个例子: ```java 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 插件所做的那样: ~~~ String [] permissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; ~~~ 然后在请求权限时,需要做的就是: ~~~ cordova.requestPermissions(this, 0, permissions); ~~~ 这会请求数组中指定的权限。提供可公开访问的权限数组是个好主意,因为这可以被使用你的插件作为依赖项的插件使用,尽管这不是必需的。 # 调试Android插件 虽然推荐使用Android studio,但可以使用Eclipse 或Android Studio 进行Android调试。由于Cordova-Android 目前用作库项目,并且支持插件作为源代码,因此可以像在本机Android应用程序中一样调试Cordova应用程序中的Java代码。 # 启动其他 Activities 如果您的插件启动将Cordova活动推送到后台的活动,则需要特别注意。如果设备内存不足,Android OS将在后台销毁活动。在这种情况下,`CordovaPlugin`实例也将被销毁。如果您的插件正在等待它所启动的Activity的结果,那么当Cordova [Activity](http://developer.android.com/reference/android/app/Activity.html) 返回到前台并获得结果时,将创建插件的新实例。但是,插件的状态不会自动保存或恢复,插件的 `CallbackContext` 将丢失。 `CordovaPlugin` 可以通过两种方法来处理这种情况: ```java /** * 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 结果所需的状态。插件的状态将不会被恢复,除非在使用 `CordovaInterface`的`startActivityForResult()` 方法获得插件请求的Activity结果并且在后台 操作系统销毁Cordova活动时。 作为 `onRestoreStateForActivityResult()` 的一部分,您的插件将被传递一个替换 CallbackContext。重要的是要意识到这个CallbackContext 与使用 Activity销毁的CallbackContext 不同。原始回调丢失,并且不会在javascript应用程序中触发。相反,此替换CallbackContext 将返回结果作为应用程序恢复时触发的[`resume`](http://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume)事件的一部分。[`resume`](http://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume)事件的有效负载遵循以下结构: ~~~ { action: "resume", pendingResult: { pluginServiceName: string, pluginStatus: string, 00000000000000000000 result: any } } ~~~ `pluginServiceName` 将匹配 `plugin.xml` 中的[name元素](http://cordova.apache.org/docs/en/latest/plugin_ref/spec.html#name)。 `pluginStatus` 是一个String,描述传递给CallbackContext的 PluginResult 的状态。有关与插件状态对应的String值,请参阅`PluginResult.java` `result` 是插件传递给`CallbackContext`的任何结果(例如 String,number,JSON object等) 此 [`resume`](http://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume) 有效负载将传递给javascript应用程序为 `resume`事件注册的任何回调中。这意味着结果*直接*进入Cordova应用程序;您的插件将无法在应用程序收到之前使用javascript处理结果。因此,您应该努力使本机代码返回的结果尽可能完整,并且在 launching activities(启动活动)时不依赖于任何javascript回调。 请务必告知Cordova应用程序如何解释他们在 [`resume`](http://cordova.apache.org/docs/en/latest/cordova/events/events.html#resume) 事件中收到的结果。 Cordova应用程序需要维护自己的状态,并记住他们提出的请求以及必要时提供的参数。但是,您仍应清楚地传达 `pluginStatus` 值的含义以及作为插件API的一部分在`resume` 字段中返回的数据类型。 启动活动的完整事件序列如下: 1. Cordova应用程序调用您的插件 2. 您的插件会为结果启动一个Activity 3. Android操作系统会破坏Cordova Activity和您的插件实例 `onSaveInstanceState()`被调用 4. 用户与您的活动进行交互,活动完成 5. 重新创建Cordova活动并接收活动结果 `onRestoreStateForActivityResult()`被调用 6. 调用 `onActivityResult()` 并且您的插件将结果传递给新的CallbackContext 7. Cordova应用程序触发并接收 `resume` 事件 Android 提供了一个开发人员设置,用于在低内存上调试 活动销毁(Activity destruction)。在您的设备或模拟器上的Developer Options菜单中启用“Don't keep activities”设置,以模拟低内存场景。如果您的插件启动了外部活动,您应该始终启用该设置进行一些测试,以确保正确地处理低内存场景。