背景
目前一个常见的业务场景是,在 App 中打开第三方网页,由于公司 App 是基于 Cordova 的混合技术栈。因此,本公司选用 InAppBrowser 插件实现该功能。 虽然 InAppBrowser 可以方便地打开第三方网页,但在实际开发中,我们希望在 InAppBrowser 打开的页面中,也能够调用原生的功能,比如拍照、选图、扫码或定位等功能。可惜的是,InAppBrowser 并没有提供相关的 API 接口。这时就需要我们对 InAppBrowser 进行扩展,来满足上述的需求。 接下来本文将分别介绍Android 及 Web 平台,针对上述功能的具体实现。不过在介绍具体实现之前,我们先来看一下 InAppBrowser 与 Cordova 插件之间的通信示意图

这里主要介绍android平台下的方案
Android平台下的通信方案
android平台下具体通信流程如下:
步骤一:在WebChromeClient增加onJsPrompt的监听
1.这里主要是监听web的调用,将对web对插件的调用转为对原有cordova插件的调用,并处理回调
inAppWebView.setWebChromeClient(new WebChromeClient(){
...
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// See if the prompt string uses the 'gap-iab' protocol. If so, the remainder should be the id of a callback to execute.
if (defaultValue != null && defaultValue.startsWith("gap")) {
if(defaultValue.startsWith("gap-iab://")) {
PluginResult scriptResult;
String scriptCallbackId = defaultValue.substring(10);
if (scriptCallbackId.startsWith("InAppBrowser")) {
if(message == null || message.length() == 0) {
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray());
} else {
try {
scriptResult = new PluginResult(PluginResult.Status.OK, new JSONArray(message));
} catch(JSONException e) {
scriptResult = new PluginResult(PluginResult.Status.JSON_EXCEPTION, e.getMessage());
}
}
thatWebView.sendPluginResult(scriptResult, scriptCallbackId);
result.confirm("");
return true;
}
}
else
{
// 处理 cordova API回调
CordovaWebViewEngine engine = webView.getEngine();
if (engine != null) {
CordovaBridge cordovaBridge = null;
try {
//反射得到CordovaBridge 的对象实例
Field bridge = engine.getClass().getDeclaredField("bridge");
bridge.setAccessible(true);
cordovaBridge = (CordovaBridge) bridge.get(engine);
bridge.setAccessible(false);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (cordovaBridge != null) {
// Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
String handledRet = cordovaBridge.promptOnJsPrompt(url, message, defaultValue);
if (handledRet != null) {
result.confirm(handledRet);
} else {
final JsPromptResult final_result = result;
CordovaDialogsHelper dialogsHelper = new CordovaDialogsHelper(webView.getContext());
dialogsHelper.showPrompt(message, defaultValue, new CordovaDialogsHelper.Result() {
@Override
public void gotResult(boolean success, String value) {
if (success) {
final_result.confirm(value);
} else {
final_result.cancel();
}
}
});
}
}else {
// Anything else with a gap: prefix should get this message
LOG.w(LOG_TAG, "InAppBrowser does not support Cordova API calls: " + url + " " + defaultValue);
result.cancel();
}
}
return true;
}
}
return false;
}
});
- 修改
Inappbrowser.js
声明全局变量__globalBrowser,保存当前使用的InAppBrowser实例
module.exports = function(strUrl, strWindowName, strWindowFeatures, callbacks) {
// Don't catch calls that write to existing frames (e.g. named iframes).
if (window.frames && window.frames[strWindowName]) {
var origOpenFunc = modulemapper.getOriginalSymbol(window, 'open');
return origOpenFunc.apply(window, arguments);
}
strUrl = urlutil.makeAbsolute(strUrl);
var iab = new InAppBrowser();
callbacks = callbacks || {};
for (var callbackName in callbacks) {
iab.addEventListener(callbackName, callbacks[callbackName]);
}
var cb = function(eventname) {
iab._eventHandler(eventname);
};
strWindowFeatures = strWindowFeatures || "";
exec(cb, cb, "InAppBrowser", "open", [strUrl, strWindowName, strWindowFeatures]);
// 声明全局变量__globalBrowser,保存当前使用的InAppBrowser实例
window.__globalBrowser = iab;
return iab;
};
3.修改cordova.js
由于 Cordova 插件是与 Cordova WebView 进行绑定,与 InAppBrowser 使用的是不同的 WebView,所以需要对插件响应进行转发处理。 对应的处理逻辑为:判断当前 Webview 上下文中的cordova.callbacks 对象中,是否含有 callbackId 对应的处理函数。若当前的 callbackId 不在 cordova.callbacks 对象中,则表示本次调用不是在 Cordova WebView 中触发的,而是在 InAppBrowser 的 WebView 上下文中触发的,所以需要把调用结果回传至 InAppBrowser 中的 WebView。
/**
* Called by native code when returning the result from an action.
*/
callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {
try {
var callback = cordova.callbacks[callbackId];
if (callback) {
if (isSuccess && status == cordova.callbackStatus.OK) {
callback.success && callback.success.apply(null, args);
} else if (!isSuccess) {
callback.fail && callback.fail.apply(null, args);
}
/*
else
Note, this case is intentionally not caught.
this can happen if isSuccess is true, but callbackStatus is NO_RESULT
which is used to remove a callback from the list without calling the callbacks
typically keepCallback is false in this case
*/
// Clear callback if not expecting any more results
if (!keepCallback) {
delete cordova.callbacks[callbackId];
}
}
else {//修改点在这:增加else逻辑
// __globalBrowser指向当前使用的InAppBrowser实例
if(window.__globalBrowser) {
var message = 'cordova.callbackFromNative("'+callbackId+'",'+isSuccess+',' + status +',' +JSON.stringify(args) + ','+ keepCallback + ')';
// 调用InAppBrowser实例的executeScript方法进行结果回传
window.__globalBrowser.executeScript({code: message});
}
}
}
catch (err) {
var msg = "Error in " + (isSuccess ? "Success" : "Error") + " callbackId: " + callbackId + " : " + err;
console && console.log && console.log(msg);
cordova.fireWindowEvent("cordovacallbackerror", { 'message': msg });
throw err;
}
},
步骤二:替换file://协议
在实际开发过程中,我们遇到了 InAppBrowser 中访问 file:// 资源的问题。即在 InAppBrowser 中是不能直接访问 file:// 协议的多媒体资源,这就导致我们的选图插件无法正常使用。针对该问题,通过内部讨论,我们也制定了相关处理方案。主要的思路如下:
-
在选图插件
ImagePicker.java中替换 file:// 协议,转换为基于 http 或 https 协议的自定义域名private JSONArray paths2JsonArray(ArrayList<String> filePaths) { JSONArray array = new JSONArray(); { for (String filePath : filePaths) { filePath=filePath.replace("file://","http://localhost");//修改点是这里 array.put(imgPath2JsonObject(filePath)); } } return array; } -
在原生层拦截自定义协议,映射为本地文件 在
InAppBrowser.java的InAppBrowserClient增加shouldInterceptRequest拦截
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
// SDK API < 21 时走这个方法
Log.i("exemyurl",url+"");
String key = "http://localhost";
if (url.startsWith(key)){
return new WebResourceResponse("image/png", "UTF-8", new ByteArrayInputStream(getBytes(url.replace(key,""))));
}
return super.shouldInterceptRequest(view, url);
}
步骤三:WebSetting支持文件相关配置
mWebSettings.setAllowFileAccess(true); //允许加载本地文件html file协议
mWebSettings.setAllowFileAccessFromFileURLs(true); //通过 file url 加载的 Javascript 读取其他的本地文件
mWebSettings.setAllowUniversalAccessFromFileURLs(true);//允许通过 file url 加载的 Javascript 可以访问其他的源,包括其他的文件和 http,https 等其他的源
步骤四:修改CordovaLib库源码
在org.apache.cordova包下
CordovaBridge.java增加如下代码
/**
* Called by cordova.js to initialize the bridge.
*/
int generateBridgeSecret()
{
SecureRandom randGen = new SecureRandom();
//llj upd 如果expectedBridgeSecret已经被赋值,直接取,不重新赋值,这是为了让inappbroswer初始化时可以只用项目已经有的expectedBridgeSecret值
if (expectedBridgeSecret < 0)
expectedBridgeSecret = randGen.nextInt(Integer.MAX_VALUE);
return expectedBridgeSecret;
}
PluginManager.java调整如下代码
/**
* Called when the webview is requesting the exec() bridge be enabled.
*/
public boolean shouldAllowBridgeAccess(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
}
}
}
// Default policy:
return url.startsWith("file://")||url.startsWith("http://") ;//llj add "http" for inappbroswer's bridge
}