本文翻译自一下链接原文http://cordova.apache.org/docs/en/latest/guide/platforms/android/plugin.html#android-permissions
安卓cordove插件开发指导
这个部分介绍了怎样在安卓平台下开发cordova本地插件。在看此篇文章之前,应该先看 PluginDevelopment Guide。来获取一个整体的有关插件结构和JavaScript接口的节本了解。这篇文件接着上篇继续讲解回声示例。从cordova的webview传出字符串并传递回来的这个例子。同时可以参看代码里的注释。CordovaPlugin.java.
安卓插件都是基于cordova-android。是从android webview通过本地桥建立而成的。一个本地安卓插件是由至少一个继承自CordovaPlugin的java class。和至少实现累里面一个方法而构成。
插件类的映射
javaScript插件用到的接口使用了cordova.exec方法。如下:
exec(<successFunction>,<failFunction>,<service>, <action>,[<args>
这个函数会从webview发出一个到安卓本地的请求。有效的访问了service类里的action方法。并且附加的参数通过args数组传递。
无论你通过java文件或jar文件来开发插件。这个插件必须在安卓应用程序的res/xml/config.xml文件里说明。有关应用插件怎样使用plugin.xml文件来注入这个feature元素请看应用插件的详细信息。
<featurename="<service_name>">
<paramname="android-package"value="<full_name_including_namespace>"/>
</feature>
Service的名字对应到javaScript的里exec调用的函数。它的值是java类里面实时在在存在的类名。否则,插件可以编译,但是cordova无法调用。
插件初始化以及生命周期
每个插件的生命周期都伴随着其对应的webview的生命周期。插件不会实例化直到它们被javascript第一次调用。除非config.xml文件里的<param>的onload名字属性为“true”。例如:
<featurename="Echo">
<paramname="android-package"value="<full_name_including_namespace>"/>
<paramname="onload"value="true"/>
</feature>
插件应该使用initialize方法启动自己的逻辑部分
@Override
publicvoidinitialize(CordovaInterfacecordova,CordovaWebViewwebView){
super.initialize(cordova,webView);
// your init code here
}
插件还应该获取Android应用的生命周期事件并且可以通过扩展重写来获取其事件,包括(onResume
, onDestroy
,等)。对于要求长时间运行的插件,例如acrivity背后运行的媒体播放功能。监听者,或者是内部状态都应该实现onReset()方法。这个函数会在webview切换一个新页面或者是刷新等重新加载javascript的时候会被调用。
编写一个安卓插件
JavaScript发起对本地插件的调用。并且需要保证在config.xml里映射正确。但是最终安卓的java代码是怎样的呢?无论传递到插件里的执行函数是怎样的,大多数函数实现是像如下的样子:
@Override
publicbooleanexecute(Stringaction,JSONArrayargs,CallbackContextcallbackContext)throwsJSONException{
if("beep".equals(action)){
this.beep(args.getLong(0));
callbackContext.success();
returntrue;
}
returnfalse; // Returning false results in a "MethodNotFound" error.
}
Javascript通过exec函数的action参数来调用相应的类私有成员函数。并且加上可选的参数。
当发生例外或者错误,为了清除起见把错误的java类的错误名称尽可能的详细返回给JavaScript是很重要的,
线程
JavaScript插件并不是在webview的主线程里面运行。反而是运行在webcore的线程里。就和ececute函数一样。如果你需要和用户界面交互,你应该用 Activity's runOnUiThread来调用:就像如下方法:
@Override
publicbooleanexecute(Stringaction,JSONArrayargs,finalCallbackContextcallbackContext)throwsJSONException{
if("beep".equals(action)){
finallongduration=args.getLong(0);
cordova.getActivity().runOnUiThread(newRunnable(){
publicvoidrun(){
...
callbackContext.success();// Thread-safe.
}
});
returntrue;
}
returnfalse;
}
如果你不需要运行在ui线程里,但是也不希望阻挡webcore的线程。你需要用 ExecutorService
执行你的函数。通过cordova.getThreadPool()来获取这个服务。
@Override
publicbooleanexecute(Stringaction,JSONArrayargs,finalCallbackContextcallbackContext)throwsJSONException{
if("beep".equals(action)){
finallongduration=args.getLong(0);
cordova.getThreadPool().execute(newRunnable(){
publicvoidrun(){
...
callbackContext.success();// Thread-safe.
}
});
returntrue;
}
returnfalse;
}
增加依赖库
如果你的安卓插件有依赖库,这些库必须列在plugin.xml文件里,有两种方法:
推荐的方法是使用<framework/>标签。(从 PluginSpecification了解详细情况)使用这种方法指定库可以允许库通过gradle来管理。参考: DependencyManagement logic。一般使用的普通库有,gson,android-support-v4,google-play-services,这些库可以被多个插件调用而不发生冲突。
第二个选项是使用<lib-file/>标签来指定jar文件的路径。(详细查看: PluginSpecification )。使用这个方法的条件是你使用的这个库没有其它插件使用。(例如这个库是仅给你的插件使用的)。否则你是在冒风险。如果其另一个插件也用了你使用的插件。这样的情况对于只了解cordova调用者会造成很大的困惑和沮丧。
回声安卓插件例子
为了配合在JavaScript的回声接口,我们使用plugin.xml文件来注入一个特定的feature。来定位平台下的config.xml文件
<platformname="android">
<config-filetarget="config.xml"parent="/*">
<featurename="Echo">
<paramname="android-package"value="org.apache.cordova.plugin.Echo"/>
</feature>
</config-file>
<source-filesrc="src/android/Echo.java"target-dir="src/org/apache/cordova/plugin"/>
</platform>
然后增加如下到 src/android/Echo.java
文件:
packageorg.apache.cordova.plugin;
importorg.apache.cordova.CordovaPlugin;
importorg.apache.cordova.CallbackContext;
importorg.json.JSONArray;
importorg.json.JSONException;
importorg.json.JSONObject;
/**
* This class echoes a string called from JavaScript.
*/
publicclassEchoextendsCordovaPlugin{
@Override
publicbooleanexecute(Stringaction,JSONArrayargs,CallbackContextcallbackContext)throwsJSONException{
if(action.equals("echo")){
Stringmessage=args.getString(0);
this.echo(message,callbackContext);
returntrue;
}
returnfalse;
}
privatevoidecho(Stringmessage,CallbackContextcallbackContext){
if(message!=null&&message.length()>0){
callbackContext.success(message);
}else{
callbackContext.error("Expected one non-empty string argument.");
}
}
}
从CordovaPlugin导入所有依赖的库是非常必要的。这些execute()方法 是重写了接收消息的函数。execute()首先测试action的值。对于当前的例子只有一个有效的值echo。其他任何的值都返回false和INVALID_ACTION错误结果。对于javascript方面则是体现为一个错误回调函数。
接下来函数通过对象的getString函数处理args来检索echo字符串。指定第一个参数传递到函数方法。当参数被传递到私有的echo函数。函数检查这个参数是不是为空或者是一个空的字符串。如果是则调用javascript的allbackContext.error()函数,如果多种测试都通过了,那么就调用成功的回调函数callbackContext.success()。Message字符串就会被传递回JavaScript的成功回调函数。
与Android集成
安卓有intent机制来使进程之间互相通信。相对的,插件可以访问CordovaInterface对象。因此就可以访问安卓应用的activity。这是通过调用上下文来启动一个新的android intent。CordovaInterface可以允许插件启动一个activity并接收返回参数。当intent返回到应用的时候,可设定插件的回调函数。
对于cordova2.0来说,插件不可以直接访问context。以及ctx成员,因为ctx成员都是在context之下。因此getContext() 和 getActivity()都可以返回需要的对象。
安卓的权限
安卓的运行程序权限现在是从安装时确定的而不再是在运行是确定。所需要的权限需要在应用程序里声明。这些权限需求需要写在安卓的manifest文件里。可以通过config.xml注入的方式把权限增加到AndroidManifest.xml文件里。以下示例显示了增加通讯录的权限。
<config-filetarget="AndroidManifest.xml"parent="/*">
<uses-permissionandroid:name="android.permission.READ_CONTACTS"/>
</config-file>
运行时权限(cordove-android5.0.0+)
安卓6.0引入了一个新的权限模型。用户可以根据需要随时打开或关闭权限。这就意味着应用程序必须相应这些权限的改变。这是cordova-android5.0.0主要个更新部分。
需要用户在运行是处理权限的变化部分,可以在安卓开发文档里找到,地址:here。
只要一个插件需要一定的权限,就可以调用权限处理函数来申请所需要的权限。格式如下:
cordova.requestPermission(CordovaPluginplugin,intrequestCode,Stringpermission);
为了减少冗长,使用本地静态变量来赋值是一个很好的实践经验。
publicstaticfinalStringREAD=Manifest.permission.READ_CONTACTS;
而且有标准的定义requestcode的方法:
publicstaticfinalintSEARCH_REQ_CODE=0;
然后在,exec方法里应该这样检查权限:
if(cordova.hasPermission(READ))
{
search(executeArgs);
}
else
{
getReadPermission(SEARCH_REQ_CODE);
}
在这个例子中,我们知识调用requestPermission
protectedvoidgetReadPermission(intrequestCode)
{
cordova.requestPermission(this,requestCode,READ);
}
这将会触发一个提示对话框问用户是否开启权限。一旦用户给了权限返回结果必须处理onRequestPermissionResult方法。这个方法是每个插件必须重写的。可以看下面这个例子:
publicvoidonRequestPermissionResult(intrequestCode,String[]permissions,
int[]grantResults)throwsJSONException
{
for(intr:grantResults)
{
if(r==PackageManager.PERMISSION_DENIED)
{
this.callbackContext.sendPluginResult(newPluginResult(PluginResult.Status.ERROR,PERMISSION_DENIED_ERROR));
return;
}
}
switch(requestCode)
{
caseSEARCH_REQ_CODE:
search(executeArgs);
break;
caseSAVE_REQ_CODE:
save(executeArgs);
break;
caseREMOVE_REQ_CODE:
remove(executeArgs);
break;
}
}
上面这个switch表达式是从提示对话框里返回的结果,其中requestcode是返回的结果。应该调用这个方法。需要注意如果没有正确处理接收onRequestPermissionResult返回结果,将会导致权限提示对话框堆积。这个情况应该避免。
除了可以单独请求全权限,还可以通过定义数组来用组的方式来申请权限。权限组根据插件需要的权限定义。
String[]permissions={Manifest.permission.ACCESS_COARSE_LOCATION,Manifest.permission.ACCESS_FINE_LOCATION};
当需要请求权限的时候可以使用如下方法:
cordova.requestPermissions(this,0,permissions);
这个将会申请权限组里面的所有权限。对外提供一个可以公共访问的权限数组,这样的方式对与把你的插件做为依赖项的时候是很好的想法,当然这也不是必须的。
调试插件
安卓应用程序可以使用eclipse或Android studio来调试。当然更推荐使用Android studio 因为cordova-android作为一个库存在在项目里。而且插件是有源代码组成。因此可以在cordova应用程序里调试java代码,就像在本地安卓里一样的方法调试。
调用其他activity
当你的插件调用了一个acrivity并且使得cordova的activity放在后台,你就需要做特殊的考虑。,因为安卓系统在内存低的时候会会销毁放在后台的程序。在这种情况下,CordovaPlugin的实例也会被销毁。如果你的插件正在等待启动的activity返回结果的状态。当cordova acrivity切换到前面的时候,插件会被重新建立,并且收到Androidactivity返回的结果。插件的状态将不会被保存或重置。插件的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
*/
publicBundleonSaveInstanceState(){}
/**
* 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
*/
publicvoidonRestoreStateForActivityResult(Bundlestate,CallbackContextcallbackContext){}
你应该注意以上者两个方法只有你需要启动Android本地actiity的时候才有必要。插件的状态不会被重置。除非你的插件被通过CordovaInterface的 startActivityForResult()方法调用,并且cordova activity放在后台的时候被安卓系统销毁。
作为onRestoreStateForActivityResult()一部分。你的插件将会被传递一个复制的CallbackContext。,你必须要认识到这个CallbackContext不是原来被activity销毁的那个CallbackContext。原来的那个已经不存在了。并且不可能被JavaScript程序继续调用了。相反,当应用程序resume的时候,这个替代的CallbackContext将会返回结果作为部分的resume事件。有效的resume事件包含了一下内容。
{
action: "resume",
pendingResult: {
pluginServiceName: string,
pluginStatus: string,
result: any
}
}
pluginServiceName 将会对应到plugin.xml的name元素
pluginStatus是一个形容pluginresult传递到callbackcontext的状态字符串,。从pluginResult.java来看插件的状态字符串值。
Result plugin传递给callbackContext的任意结果。(例如:字符串,数字,json对象,等)
Resume内容将会被传递到任何在javascript应用程序注册过的resume事件回调函数。这就意味着结果会被直接传递到cordova的应用程序里。你的插件在应用程序接收之前将没有机会来处理结果。因此,你应该尽量让本地代码来尽可能完整的返回结果。而且在启动一个activity的时候尽量不要使用javascript回调函数。
一定要确认好,cordova应用程序怎样处理从resume事件接收到的结果。Cordova应该维持自己的状态和记忆自己发出了什么请求,和自己提供了什么参数。无论如何,作为插件部分API你应该清楚的知道pluginStatus意味着什么。还有resume里面返回了什么样的数据。
对于启动一个activity的完整事件顺序是如下:
1. Cordova应用程序发起一个对你插件的调用。
2. 你的插件发起一个acrivity来获取一个结果。
3. 安卓系统销毁ordova acrivity,并且你的插件实例的onSaveInstanceState()
函数被调用。
4. 用户与弹出activity交互,然后acrivity结束。
5.
Cordova activity 被重新创建,并且收到返回结果。onRestoreStateForActivityResult()
被调用。
6. onActivityResult()被调用,然后你的插件传递结果到新的CallbackContext。
7. Cordova应用程序触发resume事件,并且接收结果。
安卓提供了一个低内存acrivity销毁测试选项。在安卓设备或模拟器的开发者选项里开启“不保留acrivity”选项。来模拟低内存状态。如果你的插件启动内部activity,你就应该开启这个选项来测试你的应用。