InAppBrowser 与 Cordova 插件间接通信方案

·  阅读 1160

背景

目前一个常见的业务场景是,在 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;
    }     
    
});
复制代码
  1. 修改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:// 协议的多媒体资源,这就导致我们的选图插件无法正常使用。针对该问题,通过内部讨论,我们也制定了相关处理方案。主要的思路如下:

  1. 在选图插件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;
        }
    
    复制代码
  2. 在原生层拦截自定义协议,映射为本地文件 在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包下

  1. 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;
}
复制代码
  1. 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
}
复制代码
分类:
代码人生
标签:
分类:
代码人生
标签:
收藏成功!
已添加到「」, 点击更改