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 "";
}
}