ionic-Android加载H5

2020-01-17  本文已影响0人  冉桓彬

打算从以下几个步骤来学习接入Cordova
1. ionic-JS加载Android方法(JS端)

这里打算从以下几个方面分析Android侧的CordovaLib包的代码:

1. 加载H5页面
2. 插件的加载
3. JS调用Android方法(Android侧)

这里重点分析Cordova下Android加载H5的流程

一、读取config.xml配置信息
二、相关初始化操作
三、loadUrl
四、Android侧: JS调用Android方法

一、加载配置信息

1.1 CordovaActivity.loadConfig拉取配置
@SuppressWarnings("deprecation")
protected void loadConfig() {
    // 解析指定目录下的config.xml文件, 这里应该可以自定义
    ConfigXmlParser parser = new ConfigXmlParser();
    parser.parse(this);
    preferences = parser.getPreferences();
    preferences.setPreferencesBundle(getIntent().getExtras());
    launchUrl = parser.getLaunchUrl();
    pluginEntries = parser.getPluginEntries();
}

解析指定目录下的config.xml文件, 实际中可能需要对config.xml文件进行自定义. 解析包括: 读取自定义插件的配置信息, 读取要加载的html文件路径

1.2 config.xml文件解析
public void handleStartTag(XmlPullParser xml) {
    String strNode = xml.getName();
    if (strNode.equals("feature")) {// 读取插件信息
        insideFeature = true;
        service = xml.getAttributeValue(null, "name");
    } else if (insideFeature && strNode.equals("param")) {
        paramType = xml.getAttributeValue(null, "name");
        if (paramType.equals("service")) // check if it is using the older service param
            service = xml.getAttributeValue(null, "value");
        else if (paramType.equals("package") || paramType.equals("android-package"))
            pluginClass = xml.getAttributeValue(null, "value");
        else if (paramType.equals("onload"))
            onload = "true".equals(xml.getAttributeValue(null, "value"));
    } else if (strNode.equals("preference")) {
        String name = xml.getAttributeValue(null, "name").toLowerCase(Locale.ENGLISH);
        String value = xml.getAttributeValue(null, "value");
        prefs.set(name, value);
    } else if (strNode.equals("content")) {
        String src = xml.getAttributeValue(null, "src");
        if (src != null) {
            setStartUrl(src);// 读取目标html的信息
        }
    }
}

public void handleEndTag(XmlPullParser xml) {
    String strNode = xml.getName();
    if (strNode.equals("feature")) {
        // config.xml中的插件信息被加载进pluginEntries中
        pluginEntries.add(new PluginEntry(service, pluginClass, onload));
        service = "";
        pluginClass = "";
        insideFeature = false;
        onload = false;
    }
}

二、相关初始化

2.1 CordovaWebView初始化
protected void init() {
    // 初始化CordovaWebViewImpl、IonicWebViewEngine;
    appView = makeWebView();
    createViews();
    if (!appView.isInitialized()) {
        // 初始化CordovaWebViewImpl属性
        appView.init(cordovaInterface, pluginEntries, preferences);
    }
    cordovaInterface.onCordovaInit(appView.getPluginManager());
    // Wire the hardware volume controls to control media if desired.
    String volumePref = preferences.getString("DefaultVolumeStream", "");
    if ("media".equals(volumePref.toLowerCase(Locale.ENGLISH))) {
        setVolumeControlStream(AudioManager.STREAM_MUSIC);
    }
}
2.2 CordovaWebViewImpl.init属性初始化
@SuppressLint("Assert")
@Override
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
    if (this.cordova != null) {
        throw new IllegalStateException();
    }
    this.cordova = cordova;
    this.preferences = preferences;
    // 初始化插件管理类
    pluginManager = new PluginManager(this, this.cordova, pluginEntries);
    resourceApi = new CordovaResourceApi(engine.getView().getContext(), pluginManager);
    nativeToJsMessageQueue = new NativeToJsMessageQueue();
    nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.NoOpBridgeMode());
    nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.LoadUrlBridgeMode(engine, cordova));
    if (preferences.getBoolean("DisallowOverscroll", false)) {
        engine.getView().setOverScrollMode(View.OVER_SCROLL_NEVER);
    }
    // 初始化IonicWebViewEngine属性
    engine.init(this, cordova, engineClient, resourceApi, pluginManager, nativeToJsMessageQueue);
    // This isn't enforced by the compiler, so assert here.
    assert engine.getView() instanceof CordovaWebViewEngine.EngineView;
    // 添加CoreAndroid插件
    pluginManager.addService(CoreAndroid.PLUGIN_NAME, "com.sf.lib.hybrid.CoreAndroid");
    // 初始化插件管理类的属性
    pluginManager.init();
}
2.3 IonicWebViewEngine.init属性初始化
@Override
public void init(CordovaWebView parentWebView, CordovaInterface cordova, CordovaWebViewEngine.Client client,
                 CordovaResourceApi resourceApi, PluginManager pluginManager,
                 NativeToJsMessageQueue nativeToJsMessageQueue) {
    if (this.cordova != null) {
        throw new IllegalStateException();
    }
    // Needed when prefs are not passed by the constructor
    if (preferences == null) {
        preferences = parentWebView.getPreferences();
    }
    this.parentWebView = parentWebView;
    this.cordova = cordova;
    this.client = client;
    this.resourceApi = resourceApi;
    this.pluginManager = pluginManager;
    this.nativeToJsMessageQueue = nativeToJsMessageQueue;
    // 初始化SystemWebView属性
    webView.init(this, cordova);
    initWebViewSettings();
    nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() {
        @Override
        public void setNetworkAvailable(boolean value) {
            //sometimes this can be called after calling webview.destroy() on destroy()
            //thus resulting in www NullPointerException
            if (webView != null) {
                webView.setNetworkAvailable(value);
            }
        }
        @Override
        public void runOnUiThread(Runnable r) {
            SystemWebViewEngine.this.cordova.getActivity().runOnUiThread(r);
        }
    }));
    nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.EvalBridgeMode(this, cordova));
    bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue);
    // 向js注入映射对象
    exposeJsInterface(webView, bridge);
}

这里有两个很关键的地方: 1、webView.init对SystemWebView属性的初始化. 2、exposeJsInterface向JS注入映射对象.

2.4 SystemWebView.init属性初始化
void init(SystemWebViewEngine parentEngine, CordovaInterface cordova) {
    this.cordova = cordova;
    this.parentEngine = parentEngine;
    if (this.viewClient == null) {
        setWebViewClient(new SystemWebViewClient(parentEngine));
    }
    if (this.chromeClient == null) {
        // 通过SystemWebChromeClient实现JS调用Android方法的另一种方式: 
        // SystemWebChromeClient.onJsPrompt
        setWebChromeClient(new SystemWebChromeClient(parentEngine));
    }
}
2.5 IonicWebViewEngine.exposeJsInterface对象映射的注入
@SuppressLint("AddJavascriptInterface")
private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
    SystemExposedJsApi exposedJsApi = new SystemExposedJsApi(bridge);
    // 所以如果JS调用Android方法会触发SystemExposedJsApi方法的执行
    webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
}

三、Android侧: JS调用Android方法
  JS调用Android方法有两种方式: ionic两种方式都有用到, 而addJavascriptInterface方式在4.2一下存在漏洞问题, 所以需要思考: 1. ionic通过什么方式解决了addJavascriptInterface这个漏洞问题. 2. addJavascriptInterface与onJsPrompt的调用时机

3.1 addJavascriptInterface漏洞问题

  通过addJavascriptInterface方式, JS调用Android方法首先会触发SystemExposedJsApi里面相应的方法

@JavascriptInterface
public String exec(int bridgeSecret, String service, String action, String callbackId, String arguments) {
    return bridge.jsExec(bridgeSecret, service, action, callbackId, arguments);
}

public class CordovaBridge {
    public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) {
        // 首先会校验bridgeSecret这个值, 这个值在哪里被赋值? 
        if (!verifySecret("exec()", bridgeSecret)) {
            return null;
        }
    }
}

  JS通过addJavascriptInterface方式调用Android方法时, 首先会传入一个随机码bridgeSecret, 然后通过对该随机码的校验解决addJavascriptInterface在4.2以下的漏洞问题, 而这个值在是JS通过onJsPrompt让Android侧生成的ionic-JS加载Android方法(JS端)

3.2 SystemWebChromeClient.onJsPrompt
@Override
public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, final JsPromptResult result) {
    // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread.
    // 这里会触发bridgeSecret随机码的生成
    String handledRet = parentEngine.bridge.promptOnJsPrompt(origin, message, defaultValue);
    if (handledRet != null) {
        // 回调数据给JS
        result.confirm(handledRet);
    } else {
        dialogsHelper.showPrompt(message, defaultValue, new CordovaDialogsHelper.Result() {
            @Override
            public void gotResult(boolean success, String value) {
                if (success) {
                    result.confirm(value);
                } else {
                    result.cancel();
                }
            }
        });
    }
    return true;
}
3.3 CordovaDialogsHelper.showPrompt
public String promptOnJsPrompt(String origin, String message, String defaultValue) {
    else if (defaultValue != null && defaultValue.startsWith("gap_init:")) {
        // Protect against random iframes being able to talk through the bridge.
        // Trust only pages which the app would have been allowed to navigate to anyway.
        if (pluginManager.shouldAllowBridgeAccess(origin)) {
            // Enable the bridge
            int bridgeMode = Integer.parseInt(defaultValue.substring(9));
            jsMessageQueue.setBridgeMode(bridgeMode);
            // Tell JS the bridge secret.
            // 生成随机码, 然后回传给JS
            int secret = generateBridgeSecret();
            return "" + secret;
        } else {
            LOG.e(LOG_TAG, "gap_init called from restricted origin: " + origin);
        }
        return "";
    }
}
上一篇 下一篇

猜你喜欢

热点阅读