├── settings.gradle ├── app-sample-screenshot.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── sample ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-ldpi │ │ │ └── ic_launcher.png │ │ ├── drawable-mdpi │ │ │ └── ic_launcher.png │ │ ├── drawable-xhdpi │ │ │ └── ic_launcher.png │ │ └── values │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ ├── unsafe_test.html │ │ └── test.html │ │ └── java │ │ └── android │ │ └── webkit │ │ └── safe │ │ └── sample │ │ ├── JavaScriptInterface.java │ │ ├── UnsafeWebActivity.java │ │ └── WebActivity.java ├── proguard-android.txt └── build.gradle ├── library ├── doc │ ├── notRepeat.js │ └── injected.js ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── android │ │ └── webkit │ │ └── safe │ │ ├── LogUtils.java │ │ ├── JsCallback.java │ │ ├── JsCallJava.java │ │ ├── WebViewProxySettings.java │ │ └── SafeWebView.java └── build.gradle ├── .gitignore ├── gradlew.bat ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', 'sample' -------------------------------------------------------------------------------- /app-sample-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/app-sample-screenshot.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/sample/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/sample/src/main/res/drawable-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/sample/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seven456/SafeWebView/HEAD/sample/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SafeWebView 4 | 5 | -------------------------------------------------------------------------------- /library/doc/notRepeat.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | if (window.__injectFlag_xxx__) { 3 | console.log('__injectFlag_xxx__ has been injected'); 4 | return; 5 | } 6 | window.__injectFlag_xxx__ = true; 7 | 8 | xxx 9 | 10 | }()) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jun 21 11:52:11 CST 2015 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-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android-library' 2 | android { 3 | compileOptions.encoding = "UTF-8" 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion 9 9 | targetSdkVersion 19 10 | } 11 | 12 | lintOptions { 13 | abortOnError false 14 | } 15 | 16 | // defaultPublishConfig "release" // 当为“release”时,BuildConfig.DEBUG = false; 17 | } 18 | 19 | // apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' -------------------------------------------------------------------------------- /sample/proguard-android.txt: -------------------------------------------------------------------------------- 1 | #--------------- BEGIN: Gson防混淆 ---------- 2 | -keepattributes *Annotation* 3 | -keep class sun.misc.Unsafe { *; } 4 | -keep class com.idea.fifaalarmclock.entity.*** 5 | -keep class com.google.gson.stream.** { *; } 6 | #--------------- END ---------- 7 | 8 | #--------------- BEGIN: 返回给页面的自定义Java对象防混淆 ---------- 9 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface$RetJavaObj{ *; } 10 | #--------------- END ---------- 11 | 12 | #--------------- BEGIN: 注入到页面的接口类防混淆 ---------- 13 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface{ *; } 14 | #--------------- END ---------- -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/LogUtils.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.util.Log; 4 | 5 | /** 6 | * Created by zhangguojun on 2017/3/27. 7 | */ 8 | 9 | public class LogUtils { 10 | 11 | public static boolean isDebug() { 12 | return BuildConfig.DEBUG; 13 | } 14 | 15 | public static void safeCheckCrash(String tag, String msg, Throwable tr) { 16 | if (isDebug()) { 17 | throw new RuntimeException(tag + " " + msg, tr); 18 | } else { 19 | Log.e(tag, msg, tr); 20 | } 21 | } 22 | 23 | public static void e(String tag, String msg, Throwable tr) { 24 | Log.e(tag, msg, tr); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # built native files 12 | # *.o 13 | # *.so 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | 19 | # Android Studio 20 | .idea 21 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 22 | .gradle 23 | build/ 24 | 25 | */out 26 | */build 27 | */production 28 | 29 | # Local configuration file (sdk path, etc) 30 | local.properties 31 | 32 | # Proguard folder generated by Eclipse 33 | proguard/ 34 | 35 | # Log Files 36 | *.log 37 | *.iml 38 | *.iws 39 | *.ipr 40 | 41 | # Windows thumbnail db 42 | Thumbs.db 43 | 44 | # OSX files 45 | .DS_Store -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileOptions.encoding = "UTF-8" 5 | 6 | compileSdkVersion rootProject.ext.compileSdkVersion 7 | buildToolsVersion rootProject.ext.buildToolsVersion 8 | 9 | defaultConfig { 10 | minSdkVersion 9 11 | targetSdkVersion 19 12 | } 13 | 14 | lintOptions { 15 | abortOnError false 16 | } 17 | 18 | buildTypes { 19 | release { 20 | proguardFile 'proguard-android.txt' 21 | minifyEnabled true 22 | shrinkResources true 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | //compile 'cn.pedant.safewebviewbridge:library:1.4' 29 | compile project(':library') 30 | } -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/assets/unsafe_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 41 | 42 | 43 | 44 |

WebView漏洞测试(Android4.2.2以下版本可测)-(ls -l /mnt/sdcard/)

45 | 46 | 47 | -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/JavaScriptInterface.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Summary: js脚本所能执行的函数空间 3 | * Version 1.0 4 | * Date: 13-11-20 5 | * Time: 下午4:40 6 | * Copyright: Copyright (c) 2013 7 | */ 8 | 9 | package android.webkit.safe.sample; 10 | 11 | import android.os.Handler; 12 | import android.os.Looper; 13 | import android.webkit.WebView; 14 | import android.widget.Toast; 15 | 16 | import android.webkit.safe.JsCallback; 17 | 18 | public class JavaScriptInterface { 19 | private WebView mWebView; 20 | 21 | public JavaScriptInterface(WebView webView) { 22 | mWebView = webView; 23 | } 24 | 25 | /** 26 | * 短暂气泡提醒 27 | * 28 | * @param message 提示信息 29 | */ 30 | @android.webkit.JavascriptInterface 31 | public void toast(String message) { 32 | Toast.makeText(mWebView.getContext(), message, Toast.LENGTH_SHORT).show(); 33 | } 34 | 35 | /** 36 | * 弹出记录的测试JS层到Java层代码执行损耗时间差 37 | * 38 | * @param timeStamp js层执行时的时间戳 39 | */ 40 | @android.webkit.JavascriptInterface 41 | public void testLossTime(long timeStamp) { 42 | timeStamp = System.currentTimeMillis() - timeStamp; 43 | toast(String.valueOf(timeStamp)); 44 | } 45 | 46 | @android.webkit.JavascriptInterface 47 | public void delayJsCallBack(final int ms, final String backMsg, final JsCallback jsCallback) { 48 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 49 | @Override 50 | public void run() { 51 | try { 52 | jsCallback.apply(backMsg); 53 | } catch (JsCallback.JsCallbackException je) { 54 | je.printStackTrace(); 55 | } 56 | } 57 | }, ms); 58 | } 59 | } -------------------------------------------------------------------------------- /library/doc/injected.js: -------------------------------------------------------------------------------- 1 | (function(b) { 2 | console.log("Android initialization begin"); 3 | var a = { 4 | queue: [], 5 | callback: function() { 6 | var d = Array.prototype.slice.call(arguments, 0); 7 | var c = d.shift(); 8 | var e = d.shift(); 9 | this.queue[c].apply(this, d); 10 | if (!e) { 11 | delete this.queue[c] 12 | } 13 | } 14 | }; 15 | a.delayJsCallBack = a.testLossTime = a.toast = function() { 16 | var f = Array.prototype.slice.call(arguments, 0); 17 | if (f.length < 1) { 18 | throw "Android call error, message:miss method name" 19 | } 20 | var e = []; 21 | for (var h = 1; h < f.length; h++) { 22 | var c = f[h]; 23 | var j = typeof c; 24 | e[e.length] = j; 25 | if (j == "function") { 26 | var d = a.queue.length; 27 | a.queue[d] = c; 28 | f[h] = d 29 | } 30 | } 31 | var k = new Date().getTime(); 32 | var l = f.shift(); 33 | var m = prompt('SafeWebView: ' + JSON.stringify({method: l, types: e, args: f})); 34 | console.log("invoke " + l + ", time: " + (new Date().getTime() - k)); 35 | var g = JSON.parse(m); 36 | if (g.code != 200) { 37 | throw "Android call error, code:" + g.code + ", message:" + g.result 38 | } 39 | return g.result 40 | }; 41 | Object.getOwnPropertyNames(a).forEach(function(d) { 42 | var c = a[d]; 43 | if (typeof c === "function" && d !== "callback") { 44 | a[d] = function() { 45 | return c.apply(a, [d].concat(Array.prototype.slice.call(arguments, 0))) 46 | } 47 | } 48 | }); 49 | b.Android = a; 50 | console.log("Android initialization end") 51 | })(window) -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/UnsafeWebActivity.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe.sample; 2 | 3 | import android.app.Activity; 4 | import android.app.AlertDialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.webkit.JsResult; 8 | import android.webkit.WebChromeClient; 9 | import android.webkit.WebSettings; 10 | import android.webkit.WebView; 11 | 12 | public class UnsafeWebActivity extends Activity { 13 | public static final String HTML = "file:///android_asset/unsafe_test.html"; 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | WebView wv = new WebView(this); 19 | setContentView(wv); 20 | 21 | WebSettings webView = wv.getSettings(); 22 | webView.setJavaScriptEnabled(true); 23 | wv.setWebChromeClient(new InnerChromeClient()); 24 | 25 | wv.loadUrl(HTML); 26 | } 27 | 28 | public class InnerChromeClient extends WebChromeClient { 29 | 30 | @Override 31 | public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { 32 | new AlertDialog.Builder(view.getContext()) 33 | .setMessage(message) 34 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 35 | @Override 36 | public void onClick(DialogInterface dialog, int which) { 37 | result.confirm(); 38 | } 39 | }) 40 | .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 41 | @Override 42 | public void onClick(DialogInterface dialog, int which) { 43 | result.cancel(); 44 | } 45 | }) 46 | .create() 47 | .show(); 48 | return true; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /sample/src/main/assets/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SafeWebView 6 | 7 | 8 | 36 | 37 | 38 | 39 |
40 |

Java Interface 接口测试

41 |

WebView漏洞测试(Android4.2.2以下)

42 |
43 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /sample/src/main/java/android/webkit/safe/sample/WebActivity.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe.sample; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.webkit.JsPromptResult; 8 | import android.webkit.WebSettings; 9 | import android.webkit.WebView; 10 | import android.webkit.WebViewClient; 11 | import android.webkit.safe.LogUtils; 12 | import android.webkit.safe.SafeWebView; 13 | 14 | public class WebActivity extends Activity { 15 | public static final String HTML = "file:///android_asset/test.html"; 16 | 17 | @Override 18 | public void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | WebView webView = new InnerWebView(this); 21 | 22 | setContentView(webView); 23 | 24 | webView.loadUrl(HTML); 25 | } 26 | 27 | private class InnerWebView extends SafeWebView { 28 | 29 | public InnerWebView(Context context) { 30 | super(context); 31 | 32 | if (LogUtils.isDebug()) { 33 | trySetWebDebuggEnabled(); 34 | } 35 | fixedAccessibilityInjectorException(); 36 | WebSettings ws = getSettings(); 37 | ws.setJavaScriptEnabled(true); 38 | addJavascriptInterface(new JavaScriptInterface(this), "Android"); 39 | setWebChromeClient(new InnerWebChromeClient()); 40 | setWebViewClient(new InnerWebViewClient()); 41 | } 42 | 43 | public class InnerWebChromeClient extends SafeWebChromeClient { 44 | 45 | @Override 46 | public void onProgressChanged (WebView view, int newProgress) { 47 | super.onProgressChanged(view, newProgress); // 务必放在方法体的第一行执行; 48 | // to do your work 49 | // ... 50 | } 51 | 52 | @Override 53 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 54 | // to do your work 55 | // ... 56 | return super.onJsPrompt(view, url, message, defaultValue, result); // 务必放在方法体的最后一行执行,或者用if判断也行; 57 | } 58 | } 59 | 60 | private class InnerWebViewClient extends WebViewClient { 61 | @Override 62 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 63 | if (url.equals(UnsafeWebActivity.HTML)) { 64 | view.getContext().startActivity(new Intent(view.getContext(), UnsafeWebActivity.class)); 65 | return true; 66 | } 67 | return super.shouldOverrideUrlLoading(view, url); 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/JsCallback.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Summary: 异步回调页面JS函数管理对象 3 | * Version 1.0 4 | * Date: 13-11-26 5 | * Time: 下午7:55 6 | * Copyright: Copyright (c) 2013 7 | */ 8 | 9 | package android.webkit.safe; 10 | 11 | import android.util.Log; 12 | import android.webkit.WebView; 13 | 14 | import org.json.JSONArray; 15 | import org.json.JSONException; 16 | import org.json.JSONObject; 17 | 18 | import java.lang.ref.WeakReference; 19 | 20 | public class JsCallback { 21 | private static final String CALLBACK_JS_FORMAT = "javascript:%s.callback(%d, %d %s);"; 22 | private int mIndex; 23 | private boolean mCouldGoOn; 24 | private WeakReference mWebViewRef; 25 | private int mIsPermanent; 26 | private String mInjectedName; 27 | 28 | public JsCallback (WebView view, String injectedName, int index) { 29 | mCouldGoOn = true; 30 | mWebViewRef = new WeakReference(view); 31 | mInjectedName = injectedName; 32 | mIndex = index; 33 | } 34 | 35 | /** 36 | * 向网页执行js回调; 37 | * @param args 38 | * @throws JsCallbackException 39 | */ 40 | public void apply (Object... args) throws JsCallbackException { 41 | if (mWebViewRef.get() == null) { 42 | throw new JsCallbackException("the WebView related to the JsCallback has been recycled"); 43 | } 44 | if (!mCouldGoOn) { 45 | throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once"); 46 | } 47 | StringBuilder sb = new StringBuilder(); 48 | for (Object arg : args){ 49 | sb.append(","); 50 | boolean isStrArg = arg instanceof String; 51 | // 有的接口将Json对象转换成了String返回,这里不能加双引号,否则网页会认为是String而不是JavaScript对象; 52 | boolean isObjArg = isJavaScriptObject(arg); 53 | if (isStrArg && !isObjArg) { 54 | sb.append("\""); 55 | } 56 | sb.append(String.valueOf(arg)); 57 | if (isStrArg && !isObjArg) { 58 | sb.append("\""); 59 | } 60 | } 61 | String execJs = String.format(CALLBACK_JS_FORMAT, mInjectedName, mIndex, mIsPermanent, sb.toString()); 62 | if (LogUtils.isDebug()) { 63 | Log.d("JsCallBack", execJs); 64 | } 65 | mWebViewRef.get().loadUrl(execJs); 66 | mCouldGoOn = mIsPermanent > 0; 67 | } 68 | 69 | /** 70 | * 是否是JSON(JavaScript Object Notation)对象; 71 | * @param obj 72 | * @return 73 | */ 74 | private boolean isJavaScriptObject(Object obj) { 75 | if (obj instanceof JSONObject || obj instanceof JSONArray) { 76 | return true; 77 | } else { 78 | String json = obj.toString(); 79 | try { 80 | new JSONObject(json); 81 | } catch (JSONException e) { 82 | try { 83 | new JSONArray(json); 84 | } catch (JSONException e1) { 85 | return false; 86 | } 87 | } 88 | return true; 89 | } 90 | } 91 | 92 | /** 93 | * 一般传入到Java方法的js function是一次性使用的,即在Java层jsCallback.apply(...)之后不能再发起回调了; 94 | * 如果需要传入的function能够在当前页面生命周期内多次使用,请在第一次apply前setPermanent(true); 95 | * @param value 96 | */ 97 | public void setPermanent (boolean value) { 98 | mIsPermanent = value ? 1 : 0; 99 | } 100 | 101 | public static class JsCallbackException extends Exception { 102 | public JsCallbackException (String msg) { 103 | super(msg); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SafeWebView 2 | =================== 3 | 解决4个问题: 4 | 5 | 1、WebView addJavascriptInterface安全漏洞问题; 6 | 2、支持网页将JS函数(function)传到Java层,方便回调; 7 | 3、解决各种WebView的崩溃(附日志); 8 | 4、WebView设置代理(对不同Android系统版本调用Java反射实现); 9 | 10 | ![image](app-sample-screenshot.png) 11 | 12 | ## 原理 13 | 使用prompt中转反射调用Java层接口类中的方法,将方法名、参数类型、参数封装成Json进行传递; 14 | 另参照: 15 | 1、在WebView中如何让JS与Java安全地互相调用:http://www.pedant.cn/2014/07/04/webview-js-java-interface-research/,源码(20150621):https://github.com/pedant/safe-java-js-webview-bridge 16 | 2、Android WebView的Js对象注入漏洞解决方案:http://blog.csdn.net/leehong2005/article/details/11808557 17 | 18 | ## 用法 19 | 20 | ### 初始化Webview WebSettings时允许js脚本执行,SafeWebView(自己的WebView可以继承这个类)内部重写了addJavascriptInterface和setWebChromeClient方法: 21 | 22 | WebView wv = new SafeWebView(this); 23 | WebSettings ws = wv.getSettings(); 24 | ws.setJavaScriptEnabled(true); 25 | wv.addJavascriptInterface(new JavaScriptInterface(wv), "Android"); 26 | wv.setWebChromeClient(new InnerChromeClient()); 27 | wv.loadUrl("file:///android_asset/test.html"); 28 | 29 | ### 自定义WebChromeClient子类 30 | 31 | public class InnerChromeClient extends SafeWebChromeClient { 32 | 33 | @Override 34 | public void onProgressChanged (WebView view, int newProgress) { 35 | super.onProgressChanged(view, newProgress); // 务必放在方法体的第一行执行; 36 | // to do your work 37 | // ... 38 | } 39 | 40 | @Override 41 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 42 | // to do your work 43 | // ... 44 | return super.onJsPrompt(view, url, message, defaultValue, result); // 务必放在方法体的最后一行执行,或者用if判断也行; 45 | } 46 | } 47 | 48 | 注意:由于JS中数字类型不区分整型、长整型、浮点类型等,是统一由64位浮点数表示,故Java方法在定义时int/long/double被当作是一种类型double; 49 | 50 | ### 关于异步回调 51 | 举例说明,首先你可以在Java层定义如下方法,该方法的作用是延迟设定的时间之后,用你传入的参数回调Js函数: 52 | 53 | @android.webkit.JavascriptInterface 54 | public void delayJsCallBack(final int ms, final String backMsg, final JsCallback jsCallback) { 55 | new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 56 | @Override 57 | public void run() { 58 | try { 59 | jsCallback.apply(backMsg); 60 | } catch (JsCallback.JsCallbackException je) { 61 | je.printStackTrace(); 62 | } 63 | } 64 | }, ms); 65 | } 66 | 67 | 那么在网页端的调用如下: 68 | 69 | Android.delayJsCallBack(3 * 1000, 'call back haha', function (msg) { 70 | HostApp.alert(msg); 71 | }); 72 | 73 | 即3秒之后会弹出你传入的'call back haha'信息。 74 | 故从上面的例子我们可以看出,你在网页端定义的回调函数是可以附加多个参数,Java方法在执行回调时需要带入相应的实参就行了。当然这里的**回调函数的参数类型目前还不支持过复杂的类型,仅支持能够被转为字符串的类型**。 75 | 76 | 另外需要注意的是一般传入到Java方法的js function是一次性使用的,即在Java层jsCallback.apply(...)之后不能再发起回调了。如果需要传入的function能够在当前页面生命周期内多次使用,请在第一次apply前**setPermanent(true)**。例如: 77 | 78 | @android.webkit.JavascriptInterface 79 | public void test (JsCallback jsCallback) { 80 | jsCallback.setPermanent(true); 81 | ... 82 | } 83 | 84 | ### 发布时防混淆 85 | 发布时需在你的混淆配置加入像下面这样的代码,注意返回到页面的自定义Java类以及注入类都要**换成你项目中实际使用类名**: 86 | 87 | // 注入到页面的接口类防混淆 88 | -keepclassmembers class android.webkit.safe.sample.JavaScriptInterface{ *; } 89 | 90 | ## License 91 | 92 | The MIT License (MIT) 93 | 94 | Copyright (C) 2015 seven456@gmail.com 95 | 96 | Permission is hereby granted, free of charge, to any person obtaining a copy 97 | of this software and associated documentation files (the "Software"), to deal 98 | in the Software without restriction, including without limitation the rights 99 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 100 | copies of the Software, and to permit persons to whom the Software is 101 | furnished to do so, subject to the following conditions: 102 | 103 | The above copyright notice and this permission notice shall be included in all 104 | copies or substantial portions of the Software. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 108 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 109 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 110 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 111 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 112 | SOFTWARE. 113 | -------------------------------------------------------------------------------- /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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/JsCallJava.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | import android.webkit.WebView; 6 | 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | 11 | import java.lang.reflect.Method; 12 | import java.util.HashMap; 13 | 14 | public class JsCallJava { 15 | private final static String TAG = "JsCallJava"; 16 | private final static String RETURN_RESULT_FORMAT = "{\"code\": %d, \"result\": %s}"; 17 | private static final String MSG_PROMPT_HEADER = "SafeWebView:"; 18 | private static final String KEY_OBJ = "obj"; 19 | private static final String KEY_METHOD = "method"; 20 | private static final String KEY_TYPES = "types"; 21 | private static final String KEY_ARGS = "args"; 22 | private static final String[] IGNORE_UNSAFE_METHODS = {"getClass", "hashCode", "notify", "notifyAll", "equals", "toString", "wait"}; 23 | private HashMap mMethodsMap; 24 | private Object mInterfaceObj; 25 | private String mInterfacedName; 26 | private String mPreloadInterfaceJS; 27 | 28 | public JsCallJava(Object interfaceObj, String interfaceName) { 29 | try { 30 | if (TextUtils.isEmpty(interfaceName)) { 31 | throw new Exception("injected name can not be null"); 32 | } 33 | mInterfaceObj = interfaceObj; 34 | mInterfacedName = interfaceName; 35 | mMethodsMap = new HashMap(); 36 | // getMethods会获得所有继承与非继承的方法 37 | Method[] methods = mInterfaceObj.getClass().getMethods(); 38 | // 拼接的js脚本可参照备份文件:./library/doc/injected.js 39 | StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\""); 40 | sb.append(mInterfacedName); 41 | sb.append(" init begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};"); 42 | for (Method method : methods) { 43 | String sign; 44 | if ((sign = genJavaMethodSign(method)) == null) { 45 | continue; 46 | } 47 | mMethodsMap.put(sign, method); 48 | sb.append(String.format("a.%s=", method.getName())); 49 | } 50 | sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\""); 51 | sb.append(mInterfacedName); 52 | sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h 0) { 156 | Class[] methodTypes = currMethod.getParameterTypes(); 157 | int currIndex; 158 | Class currCls; 159 | while (numIndex > 0) { 160 | currIndex = numIndex - numIndex / 10 * 10 - 1; 161 | currCls = methodTypes[currIndex]; 162 | if (currCls == int.class) { 163 | values[currIndex] = argsVals.getInt(currIndex); 164 | } else if (currCls == long.class) { 165 | //WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number 166 | values[currIndex] = Long.parseLong(argsVals.getString(currIndex)); 167 | } else { 168 | values[currIndex] = argsVals.getDouble(currIndex); 169 | } 170 | numIndex /= 10; 171 | } 172 | } 173 | 174 | return getReturn(jsonObject, 200, currMethod.invoke(mInterfaceObj, values), time); 175 | } catch (Exception e) { 176 | LogUtils.safeCheckCrash(TAG, "call", e); 177 | //优先返回详细的错误信息 178 | if (e.getCause() != null) { 179 | return getReturn(jsonObject, 500, "method execute error:" + e.getCause().getMessage(), time); 180 | } 181 | return getReturn(jsonObject, 500, "method execute error:" + e.getMessage(), time); 182 | } 183 | } else { 184 | return getReturn(jsonObject, 500, "call data empty", time); 185 | } 186 | } 187 | 188 | private String getReturn(JSONObject reqJson, int stateCode, Object result, long time) { 189 | String insertRes; 190 | if (result == null) { 191 | insertRes = "null"; 192 | } else if (result instanceof String) { 193 | result = ((String) result).replace("\"", "\\\""); 194 | insertRes = "\"".concat(String.valueOf(result)).concat("\""); 195 | } else { // 其他类型直接转换 196 | insertRes = String.valueOf(result); 197 | 198 | // 兼容:如果在解决WebView注入安全漏洞时,js注入采用的是XXX:function(){return prompt(...)}的形式,函数返回类型包括:void、int、boolean、String; 199 | // 在返回给网页(onJsPrompt方法中jsPromptResult.confirm)的时候强制返回的是String类型,所以在此将result的值加双引号兼容一下; 200 | // insertRes = "\"".concat(String.valueOf(result)).concat("\""); 201 | } 202 | String resStr = String.format(RETURN_RESULT_FORMAT, stateCode, insertRes); 203 | if (LogUtils.isDebug()) { 204 | Log.d(TAG, "call time: " + (android.os.SystemClock.uptimeMillis() - time) + ", request: " + reqJson + ", result:" + resStr); 205 | } 206 | return resStr; 207 | } 208 | 209 | private static String promptMsgFormat(String object, String method, String types, String args) { 210 | StringBuilder sb = new StringBuilder(); 211 | sb.append("{"); 212 | sb.append(KEY_OBJ).append(":").append(object).append(","); 213 | sb.append(KEY_METHOD).append(":").append(method).append(","); 214 | sb.append(KEY_TYPES).append(":").append(types).append(","); 215 | sb.append(KEY_ARGS).append(":").append(args); 216 | sb.append("}"); 217 | return sb.toString(); 218 | } 219 | 220 | /** 221 | * 是否是“Java接口类中方法调用”的内部消息; 222 | * 223 | * @param message 224 | * @return 225 | */ 226 | static boolean isSafeWebViewCallMsg(String message) { 227 | return message.startsWith(MSG_PROMPT_HEADER); 228 | } 229 | 230 | static JSONObject getMsgJSONObject(String message) { 231 | message = message.substring(MSG_PROMPT_HEADER.length()); 232 | JSONObject jsonObject; 233 | try { 234 | jsonObject = new JSONObject(message); 235 | } catch (JSONException e) { 236 | e.printStackTrace(); 237 | jsonObject = new JSONObject(); 238 | } 239 | return jsonObject; 240 | } 241 | 242 | static String getInterfacedName(JSONObject jsonObject) { 243 | return jsonObject.optString(KEY_OBJ); 244 | } 245 | } -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/WebViewProxySettings.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.database.Cursor; 7 | import android.net.ConnectivityManager; 8 | import android.net.Proxy; 9 | import android.net.ProxyInfo; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.os.Parcelable; 13 | import android.text.TextUtils; 14 | import android.util.Log; 15 | import android.webkit.WebResourceRequest; 16 | import android.webkit.WebView; 17 | import android.webkit.WebViewClient; 18 | 19 | import java.lang.reflect.Constructor; 20 | import java.lang.reflect.Field; 21 | import java.lang.reflect.Method; 22 | import java.util.Arrays; 23 | import java.util.Map; 24 | 25 | /** 26 | * WebView设置代理 27 | * 适用场景:{@link WebViewClient#shouldInterceptRequest(WebView, WebResourceRequest)} 28 | *

29 | * 参考:https://github.com/li520666/WebViewProxy,20150123 30 | *

31 | * Created by zhangguojun on 2016/6/7. 32 | */ 33 | public class WebViewProxySettings { 34 | private static final boolean DEBUG = false; 35 | private static final String TAG = "WebViewProxySettings"; 36 | 37 | /** 38 | * 重置代理 39 | * 40 | * @param webView 41 | * @return 42 | */ 43 | public static boolean resetProxy(WebView webView) { 44 | if (DEBUG) { 45 | printProxy(webView.getContext()); 46 | } 47 | boolean result = setProxy(webView, "", -1); 48 | if (DEBUG) { 49 | Log.d(TAG, "resetProxy.result = " + result); 50 | printProxy(webView.getContext()); 51 | } 52 | return result; 53 | } 54 | 55 | /** 56 | * 设置代理 57 | * 58 | * @param webView 59 | * @param host 60 | * @param port 61 | * @return 62 | */ 63 | public static boolean setProxy(WebView webView, String host, int port) { 64 | boolean result; 65 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB_MR2) { // 3.2 (HC) and below 66 | result = setProxyHCAndBelow(webView, host, port); 67 | } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { // 4.0-4.3 (JB) 68 | result = setProxyJB(webView, host, port); 69 | } else { // 4.4 (KK) and above 70 | result = setProxyKKAndAbove(webView.getContext().getApplicationContext(), host, port); 71 | } 72 | return result; 73 | } 74 | 75 | /** 76 | * 设置代理(Android 3.2及以下) 77 | * 78 | * @param webView 79 | * @param host 80 | * @param port 81 | * @return 82 | */ 83 | private static boolean setProxyHCAndBelow(WebView webView, String host, int port) { 84 | Object sNetwork = invokeStaticMethod("android.webkit.Network", "getInstance", new Class[]{Context.class}, webView.getContext()); 85 | Object mRequestQueue = getFieldValue(sNetwork, "mRequestQueue"); // android.net.http.RequestQueue 86 | Object httpHost = newInstance("org.apache.http.HttpHost", new Class[]{String.class, int.class}, host, port); 87 | setFieldValue(mRequestQueue, "mProxyHost", httpHost); // org.apache.http.HttpHost 88 | return true; 89 | } 90 | 91 | /** 92 | * 设置代理(Android 4.0 - 4.3) 93 | * 94 | * @param webView 95 | * @param host 96 | * @param port 97 | * @return 98 | */ 99 | private static boolean setProxyJB(WebView webView, String host, int port) { 100 | boolean isSet = !TextUtils.isEmpty(host) && port >= 0; 101 | Object mWebViewCore = null; 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { // Android 4.0 - 4.0.3 103 | mWebViewCore = getFieldValue(webView, "mWebViewCore"); // android.webkit.WebViewClassic 104 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { // Android 4.1 - 4.3 105 | Object webViewClassic = invokeStaticMethod("android.webkit.WebViewClassic", "fromWebView", new Class[]{WebView.class}, webView); 106 | mWebViewCore = getFieldValue(webViewClassic, "mWebViewCore"); // android.webkit.WebViewClassic 107 | } 108 | Object mBrowserFrame = getFieldValue(mWebViewCore, "mBrowserFrame"); // android.webkit.BrowserFrame 109 | Object sJavaBridge = getStaticFieldValue(mBrowserFrame.getClass().getName(), "sJavaBridge"); // android.webkit.JWebCoreJavaBridge 110 | Class classProxyProperties = getClass("android.net.ProxyProperties"); 111 | Object proxyProperties = null; 112 | if (isSet) { 113 | proxyProperties = newInstance(classProxyProperties.getName(), new Class[]{String.class, Integer.TYPE, String.class}, host, port, null); 114 | } 115 | invokeMethod(sJavaBridge, "updateProxy", new Class[]{classProxyProperties}, proxyProperties); 116 | return true; 117 | } 118 | 119 | /** 120 | * 设置代理(Android 4.4及以上) 121 | * 122 | * @param applicationContext 123 | * @param host 124 | * @param port 125 | * @return 126 | */ 127 | public static boolean setProxyKKAndAbove(Context applicationContext, String host, int port) { 128 | boolean result = false; 129 | boolean isSet = !TextUtils.isEmpty(host) && port >= 0; 130 | Object mLoadedApk = getFieldValue(applicationContext, "mLoadedApk"); // android.app.LoadedApk 131 | Map mReceivers = (Map) getFieldValue(mLoadedApk, "mReceivers"); // ArrayMap> 132 | for (Object receiverMap : mReceivers.values()) { 133 | for (Object receiver : ((Map) receiverMap).keySet()) { 134 | Class clazz = receiver.getClass(); 135 | // https://src.chromium.org/svn/trunk/src/net/android/java/src/org/chromium/net/ProxyChangeListener.java#ProxyReceiver 136 | if ("org.chromium.net.ProxyChangeListener$ProxyReceiver".equals(clazz.getName())) { 137 | Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); 138 | if (isSet) { 139 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 140 | Object proxyProperties = newInstance("android.net.ProxyProperties", new Class[]{String.class, Integer.TYPE, String.class}, host, port, null); 141 | intent.putExtra("proxy", (Parcelable) proxyProperties); 142 | } else { 143 | intent.putExtra(Proxy.EXTRA_PROXY_INFO, ProxyInfo.buildDirectProxy(host, port)); 144 | } 145 | } 146 | invokeMethod(receiver, "onReceive", new Class[]{Context.class, Intent.class}, applicationContext, intent); 147 | result = true; 148 | } 149 | } 150 | } 151 | return result; 152 | } 153 | 154 | /** 155 | * 打印系统代理设置 156 | * 157 | * @param context 158 | */ 159 | private static void printProxy(Context context) { 160 | StringBuilder sb = new StringBuilder(); 161 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 162 | ProxyInfo proxyInfo = (ProxyInfo) invokeMethod(cm, "getGlobalProxy", null); 163 | sb.append("\nconnectivityManager.getGlobalProxy.proxyInfo = " + proxyInfo); 164 | sb.append("\nProxy.getDefaultHost():Proxy.getDefaultPort() = " + Proxy.getDefaultHost() + ":" + Proxy.getDefaultPort()); 165 | sb.append("\nSystem.getProperty(\"http.proxyHost\"):System.getProperty(\"http.proxyPort\") = " + System.getProperty("http.proxyHost") + ":" + System.getProperty("http.proxyPort")); 166 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT_WATCH) { 167 | String host = (String) invokeStaticMethod("android.net.ProxyProperties", "getHost", null); 168 | int port = (int) invokeStaticMethod("android.net.ProxyProperties", "getPort", null); 169 | sb.append("\nProxyProperties.getHost():ProxyProperties.getPort() = " + host + ":" + port); 170 | } 171 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 172 | Cursor cursor = null; 173 | try { 174 | cursor = context.getContentResolver().query(Uri.parse("content://telephony/carriers"), null, " apn = ? and current = 1", null, null); 175 | if (cursor != null && cursor.moveToFirst()) { 176 | cursor.moveToFirst(); 177 | String apn = cursor.getString(cursor.getColumnIndex("apn")); 178 | String proxy = cursor.getString(cursor.getColumnIndex("proxy")); 179 | int port = cursor.getInt(cursor.getColumnIndex("port")); 180 | sb.append("\nAPN:proxy:port = " + apn + ":" + proxy + ":" + port); 181 | } 182 | 183 | } finally { 184 | if (cursor != null) { 185 | cursor.close(); 186 | } 187 | } 188 | } 189 | if (DEBUG) { 190 | Log.d(TAG, "printProxy.proxy: " + sb); 191 | } 192 | } 193 | 194 | /** 195 | * 循环向上转型, 获取对象的 DeclaredField 196 | * 197 | * @param object 子类对象 198 | * @param fieldName 父类中的属性名 199 | * @return 父类中的属性对象 200 | */ 201 | private static Field getDeclaredField(Object object, String fieldName) { 202 | Class clazz = object.getClass(); 203 | while (clazz != Object.class) { 204 | try { 205 | return clazz.getDeclaredField(fieldName); 206 | } catch (Exception e) { 207 | clazz = clazz.getSuperclass(); 208 | } 209 | } 210 | throw new RuntimeException("getDeclaredField exception, object = " + object.getClass().getName() + ", fieldName = " + fieldName); 211 | } 212 | 213 | /** 214 | * 直接设置对象属性值, 忽略 private/protected 修饰符, 也不经过 setter 215 | * 216 | * @param object 子类对象 217 | * @param fieldName 父类中的属性名 218 | * @param value 将要设置的值 219 | */ 220 | private static void setFieldValue(Object object, String fieldName, Object value) { 221 | try { 222 | Field field = getDeclaredField(object, fieldName); 223 | field.setAccessible(true); 224 | field.set(object, value); 225 | } catch (Exception e) { 226 | throw new RuntimeException("setFieldValue exception, object = " + object.getClass().getName() + ", fieldName = " + fieldName, e); 227 | } 228 | } 229 | 230 | /** 231 | * 直接读取对象的属性值, 忽略 private/protected 修饰符, 也不经过 getter 232 | * 233 | * @param object 子类对象 234 | * @param fieldName 父类中的属性名 235 | * @return 父类中的属性值 236 | */ 237 | private static Object getFieldValue(Object object, String fieldName) { 238 | try { 239 | Field field = getDeclaredField(object, fieldName); 240 | field.setAccessible(true); 241 | return field.get(object); 242 | } catch (Exception e) { 243 | throw new RuntimeException("getFieldValue exception, object = " + object.getClass().getName() + ", fieldName = " + fieldName, e); 244 | } 245 | } 246 | 247 | /** 248 | * 直接读取static类的属性值, 忽略 private/protected 修饰符, 也不经过 getter 249 | * 250 | * @param className 类名称 251 | * @param fieldName 父类中的属性名 252 | * @return 父类中的属性值 253 | */ 254 | private static Object getStaticFieldValue(String className, String fieldName) { 255 | try { 256 | Field field = getDeclaredField(getClass(className), fieldName); 257 | field.setAccessible(true); 258 | return field.get(null); 259 | } catch (Exception e) { 260 | throw new RuntimeException("getFieldValue exception, className = " + className + ", fieldName = " + fieldName, e); 261 | } 262 | } 263 | 264 | /** 265 | * 循环向上转型, 获取对象的 DeclaredMethod 266 | * 267 | * @param object 子类对象 268 | * @param methodName 父类中的方法名 269 | * @param parameterTypes 父类中的方法参数类型 270 | * @return 父类中的方法对象 271 | */ 272 | private static Method getDeclaredMethod(Object object, String methodName, Class... parameterTypes) { 273 | Class clazz = object instanceof Class ? (Class) object : object.getClass(); 274 | while (clazz != Object.class) { 275 | try { 276 | return clazz.getDeclaredMethod(methodName, parameterTypes); 277 | } catch (Exception e) { 278 | clazz = clazz.getSuperclass(); 279 | } 280 | } 281 | throw new RuntimeException("getDeclaredMethod exception, object = " + object.getClass().getName() + ", methodName = " + methodName); 282 | } 283 | 284 | /** 285 | * 直接调用对象方法, 而忽略修饰符(private, protected, default) 286 | * 287 | * @param receiver 子类对象 288 | * @param methodName 父类中的方法名 289 | * @param parameterTypes 父类中的方法参数类型 290 | * @param parameters 父类中的方法参数 291 | * @return 父类中方法的执行结果 292 | */ 293 | private static Object invokeMethod(Object receiver, String methodName, Class[] parameterTypes, Object... parameters) { 294 | try { 295 | Method method = getDeclaredMethod(receiver, methodName, parameterTypes); 296 | method.setAccessible(true); 297 | return method.invoke(receiver, parameters); 298 | } catch (Exception e) { 299 | throw new RuntimeException("invokeMethod exception, receiver = " + receiver.getClass().getName() + ", methodName = " + methodName, e); 300 | } 301 | } 302 | 303 | /** 304 | * 直接调用对象静态方法, 而忽略修饰符(private, protected, default) 305 | * 306 | * @param className 子类 307 | * @param methodName 父类中的方法名 308 | * @param parameterTypes 父类中的方法参数类型 309 | * @param parameters 父类中的方法参数 310 | * @return 父类中方法的执行结果 311 | */ 312 | private static Object invokeStaticMethod(String className, String methodName, Class[] parameterTypes, Object... parameters) { 313 | try { 314 | Method method = getDeclaredMethod(getClass(className), methodName, parameterTypes); 315 | method.setAccessible(true); 316 | return method.invoke(null, parameters); 317 | } catch (Exception e) { 318 | throw new RuntimeException("invokeStaticMethod exception, className = " + className + ", methodName = " + methodName, e); 319 | } 320 | } 321 | 322 | /** 323 | * 获取Class 324 | * 325 | * @param className 类名 326 | * @return Class 327 | */ 328 | private static Class getClass(String className) { 329 | try { 330 | return Class.forName(className); 331 | } catch (ClassNotFoundException e) { 332 | throw new RuntimeException("getClass exception, className = " + className, e); 333 | } 334 | } 335 | 336 | /** 337 | * 创建一个新实例 338 | * 339 | * @param className 340 | * @param parameterTypes 341 | * @param args 342 | * @return 343 | */ 344 | private static Object newInstance(String className, Class[] parameterTypes, Object... args) { 345 | try { 346 | Constructor constructor = getClass(className).getConstructor(parameterTypes); 347 | constructor.setAccessible(true); 348 | return constructor.newInstance(args); 349 | } catch (Exception e) { 350 | throw new RuntimeException("newInstance exception, className = " + className 351 | + ", parameterTypes = " + Arrays.toString(parameterTypes) 352 | + ", args = " + Arrays.toString(args), e); 353 | } 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /library/src/main/java/android/webkit/safe/SafeWebView.java: -------------------------------------------------------------------------------- 1 | package android.webkit.safe; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.os.Build; 7 | import android.util.AndroidRuntimeException; 8 | import android.util.AttributeSet; 9 | import android.util.Log; 10 | import android.util.Pair; 11 | import android.view.ViewGroup; 12 | import android.view.ViewParent; 13 | import android.view.accessibility.AccessibilityManager; 14 | import android.webkit.CookieManager; 15 | import android.webkit.CookieSyncManager; 16 | import android.webkit.JsPromptResult; 17 | import android.webkit.WebBackForwardList; 18 | import android.webkit.WebChromeClient; 19 | import android.webkit.WebSettings; 20 | import android.webkit.WebView; 21 | import android.webkit.WebViewClient; 22 | import android.widget.Toast; 23 | 24 | import org.apache.http.client.utils.URLEncodedUtils; 25 | import org.json.JSONObject; 26 | 27 | import java.lang.reflect.Field; 28 | import java.lang.reflect.Method; 29 | import java.net.URI; 30 | import java.util.HashMap; 31 | import java.util.Map; 32 | 33 | /** 34 | * Created by zhangguojun on 2015/6/21. 35 | * 36 | * Android4.2.2以下版本WebView有远程执行代码漏洞 37 | * 乌云上的介绍:http://www.wooyun.org/bugs/wooyun-2010-067676 38 | * 测试方法:让自己的WebView加载http://drops.wooyun.org/webview.html 39 | */ 40 | public class SafeWebView extends WebView { 41 | private static final String TAG = "SafeWebView"; 42 | private Map mJsCallJavas; 43 | private Map mInjectJavaScripts; 44 | private FixedOnReceivedTitle mFixedOnReceivedTitle; 45 | private boolean mIsInited; 46 | private Boolean mIsAccessibilityEnabledOriginal; 47 | 48 | public SafeWebView(Context context) { 49 | this(context, null); 50 | } 51 | 52 | public SafeWebView(Context context, AttributeSet attrs) { 53 | super(context, attrs); 54 | removeSearchBoxJavaBridge(); 55 | 56 | // WebView跨源(加载本地文件)攻击分析:http://blogs.360.cn/360mobile/2014/09/22/webview%E8%B7%A8%E6%BA%90%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90/ 57 | // 是否允许WebView使用File协议,移动版的Chrome默认禁止加载file协议的文件; 58 | getSettings().setAllowFileAccess(false); 59 | 60 | mFixedOnReceivedTitle = new FixedOnReceivedTitle(); 61 | mIsInited = true; 62 | } 63 | 64 | /** 65 | * 经过大量的测试,按照以下方式才能保证JS脚本100%注入成功: 66 | * 1、在第一次loadUrl之前注入JS(在addJavascriptInterface里面注入即可,setWebViewClient和setWebChromeClient要在addJavascriptInterface之前执行); 67 | * 2、在webViewClient.onPageStarted中都注入JS; 68 | * 3、在webChromeClient.onProgressChanged中都注入JS,并且不能通过自检查(onJsPrompt里面判断)JS是否注入成功来减少注入JS的次数,因为网页中的JS可以同时打开多个url导致无法控制检查的准确性; 69 | * 4、注入的JS中已经在脚本(./library/doc/notRepeat.js)中检查注入的对象是否已经存在,避免注入对象被重新赋值导致网页引用该对象的方法时发生异常; 70 | * 71 | * @deprecated Android4.2.2及以上版本的addJavascriptInterface方法已经解决了安全问题,如果不使用“网页能将JS函数传到Java层”功能,不建议使用该类,毕竟系统的JS注入效率才是最高的; 72 | */ 73 | @Override 74 | @Deprecated 75 | public void addJavascriptInterface(Object interfaceObj, String interfaceName) { 76 | if (mJsCallJavas == null) { 77 | mJsCallJavas = new HashMap(); 78 | } 79 | mJsCallJavas.put(interfaceName, new JsCallJava(interfaceObj, interfaceName)); 80 | injectJavaScript(); 81 | if (LogUtils.isDebug()) { 82 | Log.d(TAG, "injectJavaScript, addJavascriptInterface.interfaceObj = " + interfaceObj + ", interfaceName = " + interfaceName); 83 | } 84 | } 85 | 86 | @Override 87 | public void setWebChromeClient(WebChromeClient client) { 88 | mFixedOnReceivedTitle.setWebChromeClient(client); 89 | super.setWebChromeClient(client); 90 | } 91 | 92 | @Override 93 | public void destroy() { 94 | if (mJsCallJavas != null) { 95 | mJsCallJavas.clear(); 96 | } 97 | if (mInjectJavaScripts != null) { 98 | mInjectJavaScripts.clear(); 99 | } 100 | removeAllViewsInLayout(); 101 | fixedStillAttached(); 102 | releaseConfigCallback(); 103 | if (mIsInited) { 104 | resetAccessibilityEnabled(); 105 | // java.lang.RuntimeException: Unable to start activity ComponentInfo{com.xxx.xxx/com.xxx.accounts.ui.a.WebViewActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void android.webkit.WebViewProvider.destroy()' on a null object reference 106 | // at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2460) 107 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2522) 108 | // at android.app.ActivityThread.access$800(ActivityThread.java:167) 109 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1417) 110 | // at android.os.Handler.dispatchMessage(Handler.java:111) 111 | // at android.os.Looper.loop(Looper.java:194) 112 | // at android.app.ActivityThread.main(ActivityThread.java:5537) 113 | // at java.lang.reflect.Method.invoke(Native Method) 114 | // at java.lang.reflect.Method.invoke(Method.java:372) 115 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:955) 116 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:750) 117 | // Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void android.webkit.WebViewProvider.destroy()' on a null object reference 118 | // at android.webkit.WebView.destroy(WebView.java:734) 119 | // at android.webkit.safe.SafeWebView.destroy(XXX:93) 120 | // at android.webkit.safe.SafeWebView.setOverScrollMode(XXX:214) 121 | // at android.view.View.(View.java:3626) 122 | // at android.view.View.(View.java:3730) 123 | // at android.view.ViewGroup.(View.java:3788) 150 | // at android.view.View.(View.java:3902) 151 | // at android.view.ViewGroup. isWebViewPackageException(Throwable e) { 157 | String messageCause = e.getCause() == null ? e.toString() : e.getCause().toString(); 158 | String trace = Log.getStackTraceString(e); 159 | if (trace.contains("android.content.pm.PackageManager$NameNotFoundException") 160 | || trace.contains("java.lang.RuntimeException: Cannot load WebView") 161 | || trace.contains("android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed")) { 162 | // java.lang.RuntimeException: Unable to start activity ComponentInfo {com.xxx.xxx/com.xxx.xxx.zhaoyaojing.FAQsActivity}: android.util.AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.android.webview 163 | // at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2567) 164 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2647) 165 | // at android.app.ActivityThread.access$800(ActivityThread.java:193) 166 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1485) 167 | // at android.os.Handler.dispatchMessage(Handler.java:111) 168 | // at android.os.Looper.loop(Looper.java:194) 169 | // at android.app.ActivityThread.main(ActivityThread.java:5759) 170 | // at java.lang.reflect.Method.invoke(Native Method) 171 | // at java.lang.reflect.Method.invoke(Method.java:372) 172 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1040) 173 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:835) 174 | // Caused by: android.util.AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.android.webview 175 | // at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:174) 176 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:109) 177 | // at android.webkit.WebView.getFactory(WebView.java:2348) 178 | // at android.webkit.WebView.ensureProviderCreated(WebView.java:2343) 179 | // at android.webkit.WebView.setOverScrollMode(WebView.java:2402) 180 | // at android.view.View.(View.java:3632) 181 | // at android.view.View.(View.java:3736) 182 | // at android.view.ViewGroup.(ViewGroup.java:524) 183 | // at android.widget.AbsoluteLayout.(AbsoluteLayout.java:55) 184 | // at android.webkit.WebView.(WebView.java:580) 185 | // at android.webkit.WebView.(WebView.java:525) 186 | // at android.webkit.WebView.(WebView.java:508) 187 | // at android.webkit.WebView.(WebView.java:495) 188 | // at android.webkit.safe.SafeWebView.(XXX:43) 189 | // at android.webkit.safe.SafeWebView.(XXX:39) 190 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.a(XXX:37) 191 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.onCreate(XXX:31) 192 | // at android.app.Activity.performCreate(Activity.java:6131) 193 | // at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1112) 194 | // at com.morgoo.droidplugin.c.b.ht.callActivityOnCreate(XXX:110) 195 | // at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2514) 196 | // ... 10 more 197 | // Caused by: android.content.pm.PackageManager$NameNotFoundException: com.android.webview 198 | // at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:125) 199 | // at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:146) 200 | // Failed to list WebView package libraries for loadNativeLibrary 201 | // android.content.pm.PackageManager$NameNotFoundException: com.android.webview 202 | // at android.app.ApplicationPackageManager.getApplicationInfo(ApplicationPackageManager.java:292) 203 | // at android.webkit.WebViewFactory.getWebViewNativeLibraryPaths(WebViewFactory.java:282) 204 | // at android.webkit.WebViewFactory.loadNativeLibrary(WebViewFactory.java:397) 205 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:103) 206 | // at android.webkit.WebView.getFactory(WebView.java:2348) 207 | // at android.webkit.WebView.ensureProviderCreated(WebView.java:2343) 208 | // at android.webkit.WebView.setOverScrollMode(WebView.java:2402) 209 | // at android.view.View.(View.java:3632) 210 | // at android.view.View.(View.java:3736) 211 | // at android.view.ViewGroup.(ViewGroup.java:524) 212 | // at android.widget.AbsoluteLayout.(AbsoluteLayout.java:55) 213 | // at android.webkit.WebView.(WebView.java:580) 214 | // at android.webkit.WebView.(WebView.java:525) 215 | // at android.webkit.WebView.(WebView.java:508) 216 | // at android.webkit.WebView.(WebView.java:495) 217 | // at android.webkit.safe.SafeWebView.(XXX:43) 218 | // at android.webkit.safe.SafeWebView.(XXX:39) 219 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.a(XXX:37) 220 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.onCreate(XXX:31) 221 | // at android.app.Activity.performCreate(Activity.java:6131) 222 | // at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1112) 223 | // at com.morgoo.droidplugin.c.b.ht.callActivityOnCreate(XXX:110) 224 | // at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2514) 225 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2647) 226 | // at android.app.ActivityThread.access$800(ActivityThread.java:193) 227 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1485) 228 | // at android.os.Handler.dispatchMessage(Handler.java:111) 229 | // at android.os.Looper.loop(Looper.java:194) 230 | // at android.app.ActivityThread.main(ActivityThread.java:5759) 231 | // at java.lang.reflect.Method.invoke(Native Method) 232 | // at java.lang.reflect.Method.invoke(Method.java:372) 233 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1040) 234 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:835) 235 | 236 | // Chromium WebView package does not exist 237 | // android.content.pm.PackageManager$NameNotFoundException: com.android.webview 238 | // at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:125) 239 | // at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:146) 240 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:109) 241 | // at android.webkit.WebView.getFactory(WebView.java:2348) 242 | // at android.webkit.WebView.ensureProviderCreated(WebView.java:2343) 243 | // at android.webkit.WebView.setOverScrollMode(WebView.java:2402) 244 | // at android.view.View.(View.java:3632) 245 | // at android.view.View.(View.java:3736) 246 | // at android.view.ViewGroup.(ViewGroup.java:524) 247 | // at android.widget.AbsoluteLayout.(AbsoluteLayout.java:55) 248 | // at android.webkit.WebView.(WebView.java:580) 249 | // at android.webkit.WebView.(WebView.java:525) 250 | // at android.webkit.WebView.(WebView.java:508) 251 | // at android.webkit.WebView.(WebView.java:495) 252 | // at android.webkit.safe.SafeWebView.(XXX:43) 253 | // at android.webkit.safe.SafeWebView.(XXX:39) 254 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.a(XXX:37) 255 | // at com.xxx.xxx.zhaoyaojing.FAQsActivity.onCreate(XXX:31) 256 | // at android.app.Activity.performCreate(Activity.java:6131) 257 | // at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1112) 258 | // at com.morgoo.droidplugin.c.b.ht.callActivityOnCreate(XXX:110) 259 | // at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2514) 260 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2647) 261 | // at android.app.ActivityThread.access$800(ActivityThread.java:193) 262 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1485) 263 | // at android.os.Handler.dispatchMessage(Handler.java:111) 264 | // at android.os.Looper.loop(Looper.java:194) 265 | // at android.app.ActivityThread.main(ActivityThread.java:5759) 266 | // at java.lang.reflect.Method.invoke(Native Method) 267 | // at java.lang.reflect.Method.invoke(Method.java:372) 268 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1040) 269 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:835) 270 | 271 | // java.lang.RuntimeException: Unable to resume activity {com.xxx.xxx/com.xxx.xxx.webview.WebViewActivity}: android.util.AndroidRuntimeException: java.lang.RuntimeException: Cannot load WebView 272 | // at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2989) 273 | // at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3018) 274 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2422) 275 | // at android.app.ActivityThread.access$800(ActivityThread.java:154) 276 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1348) 277 | // at android.os.Handler.dispatchMessage(Handler.java:110) 278 | // at android.os.Looper.loop(Looper.java:193) 279 | // at android.app.ActivityThread.main(ActivityThread.java:5328) 280 | // at java.lang.reflect.Method.invokeNative(Native Method) 281 | // at java.lang.reflect.Method.invoke(Method.java:515) 282 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:835) 283 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:651) 284 | // at dalvik.system.NativeStart.main(Native Method) 285 | // Caused by: android.util.AndroidRuntimeException: java.lang.RuntimeException: Cannot load WebView 286 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:107) 287 | // at android.webkit.WebView.getFactory(WebView.java:2087) 288 | // at android.webkit.WebView.ensureProviderCreated(WebView.java:2082) 289 | // at android.webkit.WebView.setOverScrollMode(WebView.java:2167) 290 | // at android.webkit.safe.SafeWebView.setOverScrollMode(XXX:157) 291 | // at android.view.View.(View.java:3478) 292 | // at android.view.View.(View.java:3534) 293 | // at android.view.ViewGroup.(ViewGroup.java:494) 294 | // at android.widget.AbsoluteLayout.(AbsoluteLayout.java:52) 295 | // at android.webkit.WebView.(WebView.java:525) 296 | // at android.webkit.WebView.(WebView.java:502) 297 | // at android.webkit.WebView.(WebView.java:482) 298 | // at android.webkit.WebView.(WebView.java:471) 299 | // at android.webkit.safe.SafeWebView.(XXX:51) 300 | // at com.xxx.xxx.webview.XXXWebView.(XXX:93) 301 | // at com.xxx.xxx.webview.WebViewFragment.initWebView(XXX:128) 302 | // at com.xxx.xxx.webview.WebViewFragment.init(XXX:119) 303 | // at com.xxx.xxx.webview.WebViewFragment.onResume(XXX:362) 304 | // at android.support.v4.app.Fragment.performResume(XXX:1832) 305 | // at android.support.v4.app.FragmentManagerImpl.moveToState(XXX:995) 306 | // at android.support.v4.app.FragmentManagerImpl.moveToState(XXX:1138) 307 | // at android.support.v4.app.FragmentManagerImpl.moveToState(XXX:1120) 308 | // at android.support.v4.app.FragmentManagerImpl.dispatchResume(XXX:1939) 309 | // at android.support.v4.app.FragmentActivity.onResumeFragments(XXX:447) 310 | // at android.support.v4.app.FragmentActivity.onPostResume(XXX:436) 311 | // at android.app.Activity.performResume(Activity.java:5446) 312 | // at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2975) 313 | // ... 12 more 314 | // Caused by: java.lang.RuntimeException: Cannot load WebView 315 | // at com.android.org.chromium.android_webview.AwBrowserProcess.loadLibrary(AwBrowserProcess.java:32) 316 | // at com.android.webview.chromium.WebViewChromiumFactoryProvider.(WebViewChromiumFactoryProvider.java:82) 317 | // at java.lang.Class.newInstanceImpl(Native Method) 318 | // at java.lang.Class.newInstance(Class.java:1215) 319 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:102) 320 | // ... 38 more 321 | // Caused by: com.android.org.chromium.content.common.ProcessInitException 322 | // at com.android.org.chromium.content.app.LibraryLoader.loadAlreadyLocked(LibraryLoader.java:111) 323 | // at com.android.org.chromium.content.app.LibraryLoader.loadNow(LibraryLoader.java:79) 324 | // at com.android.org.chromium.android_webview.AwBrowserProcess.loadLibrary(AwBrowserProcess.java:30) 325 | // ... 42 more 326 | // Caused by: java.lang.UnsatisfiedLinkError: dlopen failed: unknown reloc type 31 @ 0x7d570800 (139610) 327 | // at java.lang.Runtime.loadLibrary(Runtime.java:393) 328 | // at java.lang.System.loadLibrary(System.java:606) 329 | // at com.android.org.chromium.content.app.LibraryLoader.loadAlreadyLocked(LibraryLoader.java:105) 330 | // .. 44 more 331 | 332 | // java.lang.RuntimeException: Unable to resume activity {com.xxx.xxx/com.xxx.xxx.webview.WebViewActivity}: android.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed 333 | // at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3445) 334 | // at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3485) 335 | // at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2743) 336 | // at android.app.ActivityThread.-wrap12(ActivityThread.java) 337 | // at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1475) 338 | // at android.os.Handler.dispatchMessage(Handler.java:102) 339 | // at android.os.Looper.loop(Looper.java:154) 340 | // at android.app.ActivityThread.main(ActivityThread.java:6147) 341 | // at java.lang.reflect.Method.invoke(Native Method) 342 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:887) 343 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:777) 344 | // Caused by: android.util.AndroidRuntimeException: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed 345 | // at android.webkit.WebViewFactory.getProviderClass(WebViewFactory.java:371) 346 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:194) 347 | // at android.webkit.WebView.getFactory(WebView.java:2325) 348 | // at android.webkit.WebView.ensureProviderCreated(WebView.java:2320) 349 | // at android.webkit.WebView.(WebView.java:635) 350 | // at android.webkit.WebView.(WebView.java:572) 351 | // at android.webkit.WebView.(WebView.java:555) 352 | // at android.webkit.WebView.(WebView.java:542) 353 | // at android.webkit.safe.SafeWebView.(XXX:51) 354 | // at com.xxx.xxx.webview.XXXWebView.(XXX:96) 355 | // at com.xxx.xxx.webview.WebViewFragment.R(XXX:123) 356 | // at com.xxx.xxx.webview.WebViewFragment.Q(XXX:114) 357 | // at com.xxx.xxx.webview.WebViewFragment.r(XXX:422) 358 | // at android.support.v4.app.Fragment.G(XXX:1832) 359 | // at android.support.v4.app.w.a(XXX:995) 360 | // at android.support.v4.app.w.a(XXX:1138) 361 | // at android.support.v4.app.w.a(XXX:1120) 362 | // at android.support.v4.app.w.l(XXX:1939) 363 | // at android.support.v4.app.FragmentActivity.onResumeFragments(XXX:447) 364 | // at android.support.v4.app.FragmentActivity.onPostResume(XXX:436) 365 | // at android.app.Activity.performResume(Activity.java:6833) 366 | // at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3422) 367 | // ... 10 more 368 | // Caused by: android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed 369 | // at android.webkit.WebViewFactory.getWebViewContextAndSetProvider(WebViewFactory.java:270) 370 | // at android.webkit.WebViewFactory.getProviderClass(WebViewFactory.java:330) 371 | // ... 31 more 372 | 373 | LogUtils.safeCheckCrash(TAG, "isWebViewPackageException", e); 374 | return new Pair(true, "WebView load failed, " + messageCause); 375 | } 376 | return new Pair(false, messageCause); 377 | } 378 | 379 | @Override 380 | public void setOverScrollMode(int mode) { 381 | try { 382 | super.setOverScrollMode(mode); 383 | } catch (Throwable e) { 384 | Pair pair = isWebViewPackageException(e); 385 | if (pair.first) { 386 | Toast.makeText(getContext(), pair.second, Toast.LENGTH_SHORT).show(); 387 | destroy(); 388 | } else { 389 | throw e; 390 | } 391 | } 392 | } 393 | 394 | @Override 395 | public boolean isPrivateBrowsingEnabled() { 396 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 397 | && getSettings() == null) { 398 | // java.lang.NullPointerException 399 | // at android.webkit.WebView.isPrivateBrowsingEnabled(WebView.java:2458) 400 | // at android.webkit.HTML5Audio$IsPrivateBrowsingEnabledGetter$1.run(HTML5Audio.java:105) 401 | // at android.os.Handler.handleCallback(Handler.java:605) 402 | // at android.os.Handler.dispatchMessage(Handler.java:92) 403 | // at android.os.Looper.loop(Looper.java:137) 404 | // at android.app.ActivityThread.main(ActivityThread.java:4456) 405 | // at java.lang.reflect.Method.invokeNative(Native Method) 406 | // at java.lang.reflect.Method.invoke(Method.java:511) 407 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:787) 408 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:554) 409 | // at dalvik.system.NativeStart.main(Native Method) 410 | return false; // getSettings().isPrivateBrowsingEnabled() 411 | } else { 412 | return super.isPrivateBrowsingEnabled(); 413 | } 414 | } 415 | 416 | /** 417 | * 添加并注入JavaScript脚本(和“addJavascriptInterface”注入对象的注入时机一致,100%能注入成功); 418 | * 注意:为了做到能100%注入,需要在注入的js中自行判断对象是否已经存在(如:if (typeof(window.Android) = 'undefined')); 419 | * @param javaScript 420 | */ 421 | public void addInjectJavaScript(String javaScript) { 422 | if (mInjectJavaScripts == null) { 423 | mInjectJavaScripts = new HashMap(); 424 | } 425 | mInjectJavaScripts.put(String.valueOf(javaScript.hashCode()), javaScript); 426 | injectExtraJavaScript(); 427 | } 428 | 429 | private void injectJavaScript() { 430 | for (Map.Entry entry : mJsCallJavas.entrySet()) { 431 | this.loadUrl(buildNotRepeatInjectJS(entry.getKey(), entry.getValue().getPreloadInterfaceJS())); 432 | } 433 | } 434 | 435 | private void injectExtraJavaScript() { 436 | for (Map.Entry entry : mInjectJavaScripts.entrySet()) { 437 | this.loadUrl(buildNotRepeatInjectJS(entry.getKey(), entry.getValue())); 438 | } 439 | } 440 | 441 | /** 442 | * 构建一个“不会重复注入”的js脚本; 443 | * @param key 444 | * @param js 445 | * @return 446 | */ 447 | public String buildNotRepeatInjectJS(String key, String js) { 448 | String obj = String.format("__injectFlag_%1$s__", key); 449 | StringBuilder sb = new StringBuilder(); 450 | sb.append("javascript:try{(function(){if(window."); 451 | sb.append(obj); 452 | sb.append("){console.log('"); 453 | sb.append(obj); 454 | sb.append(" has been injected');return;}window."); 455 | sb.append(obj); 456 | sb.append("=true;"); 457 | sb.append(js); 458 | sb.append("}())}catch(e){console.warn(e)}"); 459 | return sb.toString(); 460 | } 461 | 462 | /** 463 | * 构建一个“带try catch”的js脚本; 464 | * @param js 465 | * @return 466 | */ 467 | public String buildTryCatchInjectJS(String js) { 468 | StringBuilder sb = new StringBuilder(); 469 | sb.append("javascript:try{"); 470 | sb.append(js); 471 | sb.append("}catch(e){console.warn(e)}"); 472 | return sb.toString(); 473 | } 474 | 475 | /** 476 | * 如果没有使用addJavascriptInterface方法,不需要使用这个类; 477 | */ 478 | public class SafeWebViewClient extends WebViewClient { 479 | 480 | @Override 481 | public void onPageStarted(WebView view, String url, Bitmap favicon) { 482 | if (mJsCallJavas != null) { 483 | injectJavaScript(); 484 | if (LogUtils.isDebug()) { 485 | Log.d(TAG, "injectJavaScript, onPageStarted.url = " + view.getUrl()); 486 | } 487 | } 488 | if (mInjectJavaScripts != null) { 489 | injectExtraJavaScript(); 490 | } 491 | mFixedOnReceivedTitle.onPageStarted(); 492 | fixedAccessibilityInjectorExceptionForOnPageFinished(url); 493 | super.onPageStarted(view, url, favicon); 494 | } 495 | 496 | @Override 497 | public void onPageFinished(WebView view, String url) { 498 | mFixedOnReceivedTitle.onPageFinished(view); 499 | if (LogUtils.isDebug()) { 500 | Log.d(TAG, "onPageFinished.url = " + view.getUrl()); 501 | } 502 | super.onPageFinished(view, url); 503 | } 504 | } 505 | 506 | /** 507 | * 如果没有使用addJavascriptInterface方法,不需要使用这个类; 508 | */ 509 | public class SafeWebChromeClient extends WebChromeClient { 510 | 511 | @Override 512 | public void onProgressChanged(WebView view, int newProgress) { 513 | if (mJsCallJavas != null) { 514 | injectJavaScript(); 515 | if (LogUtils.isDebug()) { 516 | Log.d(TAG, "injectJavaScript, onProgressChanged.newProgress = " + newProgress + ", url = " + view.getUrl()); 517 | } 518 | } 519 | if (mInjectJavaScripts != null) { 520 | injectExtraJavaScript(); 521 | } 522 | super.onProgressChanged(view, newProgress); 523 | } 524 | 525 | @Override 526 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 527 | if (mJsCallJavas != null && JsCallJava.isSafeWebViewCallMsg(message)) { 528 | JSONObject jsonObject = JsCallJava.getMsgJSONObject(message); 529 | String interfacedName = JsCallJava.getInterfacedName(jsonObject); 530 | if (interfacedName != null) { 531 | JsCallJava jsCallJava = mJsCallJavas.get(interfacedName); 532 | if (jsCallJava != null) { 533 | result.confirm(jsCallJava.call(view, jsonObject)); 534 | } 535 | } 536 | return true; 537 | } else { 538 | return super.onJsPrompt(view, url, message, defaultValue, result); 539 | } 540 | } 541 | 542 | @Override 543 | public void onReceivedTitle(WebView view, String title) { 544 | mFixedOnReceivedTitle.onReceivedTitle(); 545 | super.onReceivedTitle(view, title); 546 | } 547 | } 548 | 549 | /** 550 | * 解决部分手机webView返回时不触发onReceivedTitle的问题(如:三星SM-G9008V 4.4.2); 551 | */ 552 | private static class FixedOnReceivedTitle { 553 | private WebChromeClient mWebChromeClient; 554 | private boolean mIsOnReceivedTitle; 555 | 556 | public void setWebChromeClient(WebChromeClient webChromeClient) { 557 | mWebChromeClient = webChromeClient; 558 | } 559 | 560 | public void onPageStarted() { 561 | mIsOnReceivedTitle = false; 562 | } 563 | 564 | public void onPageFinished(WebView view) { 565 | if (!mIsOnReceivedTitle && mWebChromeClient != null) { 566 | // Samsung A8000 Android5.1.1 567 | // java.lang.NullPointerException: Attempt to invoke virtual method 'int org.chromium.content_public.browser.NavigationHistory.getCurrentEntryIndex()' on a null object reference 568 | // at com.android.webview.chromium.WebBackForwardListChromium.(WebBackForwardListChromium.java:28) 569 | // at com.android.webview.chromium.WebViewChromium.copyBackForwardList(WebViewChromium.java:1103) 570 | // at android.webkit.WebView.copyBackForwardList(WebView.java:1533) 571 | // at android.webkit.safe.SafeWebView$FixedOnReceivedTitle.onPageFinished(XXX:295) 572 | // at android.webkit.safe.SafeWebView$SafeWebViewClient.onPageFinished(XXX:227) 573 | // at com.xxx.xxx.webview.XXXWebView$InnerWebViewClient.onPageFinished(XXX:372) 574 | // at com.android.webview.chromium.WebViewContentsClientAdapter.onPageFinished(WebViewContentsClientAdapter.java:498) 575 | // at org.chromium.android_webview.AwContentsClientCallbackHelper$MyHandler.handleMessage(AwContentsClientCallbackHelper.java:163) 576 | // at android.os.Handler.dispatchMessage(Handler.java:102) 577 | // at android.os.Looper.loop(Looper.java:145) 578 | // at android.app.ActivityThread.main(ActivityThread.java:6963) 579 | // at java.lang.reflect.Method.invoke(Native Method) 580 | // at java.lang.reflect.Method.invoke(Method.java:372) 581 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404) 582 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199) 583 | WebBackForwardList list = null; 584 | try { 585 | list = view.copyBackForwardList(); 586 | } catch (NullPointerException e) { 587 | if (LogUtils.isDebug()) { 588 | e.printStackTrace(); 589 | } 590 | } 591 | if (list != null 592 | && list.getSize() > 0 593 | && list.getCurrentIndex() >= 0 594 | && list.getItemAtIndex(list.getCurrentIndex()) != null) { 595 | String previousTitle = list.getItemAtIndex(list.getCurrentIndex()).getTitle(); 596 | mWebChromeClient.onReceivedTitle(view, previousTitle); 597 | } 598 | } 599 | } 600 | 601 | public void onReceivedTitle() { 602 | mIsOnReceivedTitle = true; 603 | } 604 | } 605 | 606 | // Activity在onDestory时调用webView的destroy,可以停止播放页面中的音频 607 | private void fixedStillAttached() { 608 | // java.lang.Throwable: Error: WebView.destroy() called while still attached! 609 | // at android.webkit.WebViewClassic.destroy(WebViewClassic.java:4142) 610 | // at android.webkit.WebView.destroy(WebView.java:707) 611 | ViewParent parent = getParent(); 612 | if (parent instanceof ViewGroup) { // 由于自定义webView构建时传入了该Activity的context对象,因此需要先从父容器中移除webView,然后再销毁webView; 613 | ViewGroup mWebViewContainer = (ViewGroup) getParent(); 614 | mWebViewContainer.removeAllViewsInLayout(); 615 | } 616 | } 617 | 618 | // 解决WebView内存泄漏问题; 619 | private void releaseConfigCallback() { 620 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { // JELLY_BEAN 621 | try { 622 | Field field = WebView.class.getDeclaredField("mWebViewCore"); 623 | field = field.getType().getDeclaredField("mBrowserFrame"); 624 | field = field.getType().getDeclaredField("sConfigCallback"); 625 | field.setAccessible(true); 626 | field.set(null, null); 627 | } catch (NoSuchFieldException e) { 628 | if (LogUtils.isDebug()) { 629 | e.printStackTrace(); 630 | } 631 | } catch (IllegalAccessException e) { 632 | if (LogUtils.isDebug()) { 633 | e.printStackTrace(); 634 | } 635 | } 636 | } else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // KITKAT 637 | try { 638 | Field sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback"); 639 | if (sConfigCallback != null) { 640 | sConfigCallback.setAccessible(true); 641 | sConfigCallback.set(null, null); 642 | } 643 | } catch (NoSuchFieldException e) { 644 | if (LogUtils.isDebug()) { 645 | e.printStackTrace(); 646 | } 647 | } catch (ClassNotFoundException e) { 648 | if (LogUtils.isDebug()) { 649 | e.printStackTrace(); 650 | } 651 | } catch (IllegalAccessException e) { 652 | if (LogUtils.isDebug()) { 653 | e.printStackTrace(); 654 | } 655 | } 656 | } 657 | } 658 | 659 | /** 660 | * Android 4.4 KitKat 使用Chrome DevTools 远程调试WebView 661 | * WebView.setWebContentsDebuggingEnabled(true); 662 | * http://blog.csdn.net/t12x3456/article/details/14225235 663 | */ 664 | @TargetApi(19) 665 | protected void trySetWebDebuggEnabled() { 666 | if (LogUtils.isDebug() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 667 | try { 668 | Class clazz = WebView.class; 669 | Method method = clazz.getMethod("setWebContentsDebuggingEnabled", boolean.class); 670 | method.invoke(null, true); 671 | } catch (Throwable e) { 672 | if (LogUtils.isDebug()) { 673 | e.printStackTrace(); 674 | } 675 | } 676 | } 677 | } 678 | 679 | /** 680 | * 解决WebView远程执行代码漏洞,避免被“getClass”方法恶意利用(在loadUrl之前调用,如:MyWebView(Context context, AttributeSet attrs)里面); 681 | * 漏洞详解:http://drops.wooyun.org/papers/548 682 | *

683 | * function execute(cmdArgs) 684 | * { 685 | * for (var obj in window) { 686 | * if ("getClass" in window[obj]) { 687 | * alert(obj); 688 | * return ?window[obj].getClass().forName("java.lang.Runtime") 689 | * .getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); 690 | * } 691 | * } 692 | * } 693 | * 694 | * @return 695 | */ 696 | @TargetApi(11) 697 | protected boolean removeSearchBoxJavaBridge() { 698 | try { 699 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB 700 | && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { 701 | Method method = this.getClass().getMethod("removeJavascriptInterface", String.class); 702 | method.invoke(this, "searchBoxJavaBridge_"); 703 | return true; 704 | } 705 | } catch (Exception e) { 706 | if (LogUtils.isDebug()) { 707 | e.printStackTrace(); 708 | } 709 | } 710 | return false; 711 | } 712 | 713 | /** 714 | * 解决部分Android4.2中开启了辅助模式后,“android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.onInit中synchronized(mTextToSpeech)”空指针导致的崩溃问题; 715 | * 必放在 {@link WebSettings#setJavaScriptEnabled }之前执行; 716 | * 717 | * https://code.google.com/p/android/issues/detail?id=40944 718 | * 719 | * 如: 720 | * Xiaomi HM NOTE 1TD Android4.2.2 721 | * Samsung GT-I9500 Android4.2.2 722 | * ZTE V967S Android4.2.1 723 | * Lenovo A850 Android4.2.2 724 | * HUAWEI Y518-T00 Android4.2.2 725 | * Huawei G610-T00 Android4.2.1 726 | * Huawei U9508 Android4.2.2 727 | * OPPO R829T Android4.2.2 728 | * 729 | * 一、android.webkit.WebSettingsClassic.setJavaScriptEnabled时崩溃; 730 | * java.lang.NullPointerException 731 | * at android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.onInit(AccessibilityInjector.java:753) 732 | * at android.speech.tts.TextToSpeech.dispatchOnInit(TextToSpeech.java:653) 733 | * at android.speech.tts.TextToSpeech.initTts(TextToSpeech.java:632) 734 | * at android.speech.tts.TextToSpeech.(TextToSpeech.java:553) 735 | * at android.webkit.AccessibilityInjector$TextToSpeechWrapper.(AccessibilityInjector.java:676) 736 | * at android.webkit.AccessibilityInjector.addTtsApis(AccessibilityInjector.java:480) 737 | * at android.webkit.AccessibilityInjector.addAccessibilityApisIfNecessary(AccessibilityInjector.java:168) 738 | * at android.webkit.AccessibilityInjector.updateJavaScriptEnabled(AccessibilityInjector.java:415) 739 | * at android.webkit.WebViewClassic.updateJavaScriptEnabled(WebViewClassic.java:2017) 740 | * at android.webkit.WebSettingsClassic.setJavaScriptEnabled(WebSettingsClassic.java:1214) 741 | * 742 | * 二、android.webkit.WebViewClassic.onPageStarted时崩溃; 743 | * java.lang.NullPointerException 744 | * at android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.onInit(AccessibilityInjector.java:753) 745 | * at android.speech.tts.TextToSpeech.dispatchOnInit(TextToSpeech.java:640) 746 | * at android.speech.tts.TextToSpeech.initTts(TextToSpeech.java:619) 747 | * at android.speech.tts.TextToSpeech.(TextToSpeech.java:553) 748 | * at android.webkit.AccessibilityInjector$TextToSpeechWrapper.(AccessibilityInjector.java:676) 749 | * at android.webkit.AccessibilityInjector.addTtsApis(AccessibilityInjector.java:480) 750 | * at android.webkit.AccessibilityInjector.addAccessibilityApisIfNecessary(AccessibilityInjector.java:168) 751 | * at android.webkit.AccessibilityInjector.onPageStarted(AccessibilityInjector.java:340) 752 | * at android.webkit.WebViewClassic.onPageStarted(WebViewClassic.java:3980) 753 | * at android.webkit.CallbackProxy.handleMessage(CallbackProxy.java:312) 754 | * at android.os.Handler.dispatchMessage(Handler.java:99) 755 | * at android.os.Looper.loop(Looper.java:137) 756 | * at android.app.ActivityThread.main(ActivityThread.java:5102) 757 | * at java.lang.reflect.Method.invokeNative(Native Method) 758 | * at java.lang.reflect.Method.invoke(Method.java:511) 759 | * at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:797) 760 | * at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:564) 761 | * at dalvik.system.NativeStart.main(Native Method) 762 | * 763 | * 由于第二个崩溃无法干预,在WebView初始化时直接关闭AccessibilityInjector; 764 | */ 765 | protected void fixedAccessibilityInjectorException() { 766 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1 767 | && mIsAccessibilityEnabledOriginal == null 768 | && isAccessibilityEnabled()) { 769 | mIsAccessibilityEnabledOriginal = true; 770 | setAccessibilityEnabled(false); 771 | } 772 | } 773 | 774 | /** 775 | * 解决部分Android4.1中开启了辅助模式后,url参数不合法导致的崩溃问题; 776 | * url参数分隔符不使用“&”而是用“;”时,如:http://m.heise.de/newsticker/meldung/TomTom-baut-um-1643641.html?mrw_channel=ho;mrw_channel=ho;from-classic=1 777 | * 778 | * 参考: 779 | * https://code.google.com/p/android/issues/detail?id=35100 780 | * http://osdir.com/ml/Android-Developers/2012-07/msg02123.html 781 | * 782 | * 如: 783 | * Huawei HUAWEI C8815 4.1.2(16) 784 | * ZTE ZTE N919 4.1.2(16) 785 | * Coolpad 8190Q 4.1.2(16) 786 | * Lenovo Lenovo A706 4.1.2(16) 787 | * Xiaomi MI 2 4.1.1(16) 788 | * 789 | * java.lang.IllegalArgumentException: bad parameter 790 | * at org.apache.http.client.utils.URLEncodedUtils.parse(URLEncodedUtils.java:139) 791 | * at org.apache.http.client.utils.URLEncodedUtils.parse(URLEncodedUtils.java:76) 792 | * at android.webkit.AccessibilityInjector.getAxsUrlParameterValue(AccessibilityInjector.java:406) 793 | * at android.webkit.AccessibilityInjector.shouldInjectJavaScript(AccessibilityInjector.java:323) 794 | * at android.webkit.AccessibilityInjector.onPageFinished(AccessibilityInjector.java:282) 795 | * at android.webkit.WebViewClassic.onPageFinished(WebViewClassic.java:4129) 796 | * at android.webkit.CallbackProxy.handleMessage(CallbackProxy.java:325) 797 | * at android.os.Handler.dispatchMessage(Handler.java:99) 798 | * at android.os.Looper.loop(Looper.java:137) 799 | * at android.app.ActivityThread.main(ActivityThread.java:4794) 800 | * at java.lang.reflect.Method.invokeNative(Native Method) 801 | * at java.lang.reflect.Method.invoke(Method.java:511) 802 | * at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789) 803 | * at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:556) 804 | * at dalvik.system.NativeStart.main(Native Method) 805 | * 806 | * 需要在{@link WebViewClient#onPageFinished(WebView, String)}之前的{@link WebViewClient#onPageStarted(WebView, String, Bitmap)}中检测并设置; 807 | */ 808 | protected void fixedAccessibilityInjectorExceptionForOnPageFinished(String url) { 809 | if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN 810 | && getSettings().getJavaScriptEnabled() 811 | && mIsAccessibilityEnabledOriginal == null 812 | && isAccessibilityEnabled()) { 813 | try { 814 | try { 815 | URLEncodedUtils.parse(new URI(url), null); // AccessibilityInjector.getAxsUrlParameterValue 816 | } catch (IllegalArgumentException e) { 817 | if ("bad parameter".equals(e.getMessage())) { 818 | mIsAccessibilityEnabledOriginal = true; 819 | setAccessibilityEnabled(false); 820 | LogUtils.safeCheckCrash(TAG, "fixedAccessibilityInjectorExceptionForOnPageFinished.url = " + url, e); 821 | } 822 | } 823 | } catch (Throwable e) { 824 | if (LogUtils.isDebug()) { 825 | LogUtils.e(TAG, "fixedAccessibilityInjectorExceptionForOnPageFinished", e); 826 | } 827 | } 828 | } 829 | } 830 | 831 | private boolean isAccessibilityEnabled() { 832 | AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 833 | return am.isEnabled(); 834 | } 835 | 836 | private void setAccessibilityEnabled(boolean enabled) { 837 | AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 838 | try { 839 | Method setAccessibilityState = am.getClass().getDeclaredMethod("setAccessibilityState", boolean.class); 840 | setAccessibilityState.setAccessible(true); 841 | setAccessibilityState.invoke(am, enabled); 842 | setAccessibilityState.setAccessible(false); 843 | } catch (Throwable e) { 844 | if (LogUtils.isDebug()) { 845 | LogUtils.e(TAG, "setAccessibilityEnabled", e); 846 | } 847 | } 848 | } 849 | 850 | private void resetAccessibilityEnabled() { 851 | if (mIsAccessibilityEnabledOriginal != null) { 852 | setAccessibilityEnabled(mIsAccessibilityEnabledOriginal); 853 | } 854 | } 855 | 856 | /** 857 | * 向网页设置Cookie,设置Cookie后不需要页面刷新即可生效; 858 | * 859 | * 1、用一级域名设置Cookie:cookieManager.setCookie("360.cn", "key=value;path=/;domain=360.cn")(android所有版本都支持这种格式); 860 | * http://www.360doc.com/content/14/0903/22/9200790_406874810.shtml 861 | * 2、为何不能是“.360.cn”,在android2.3及以下版本,setCookie方法中URL参数必须是地址,如“360.cn”,而不能是“.360.cn”,否则存入webview.db-cookies表中的domain字段会为空导致无法在网页中生效; 862 | * http://zlping.iteye.com/blog/1633213 863 | */ 864 | public static void updateCookies(Context context, UpdateCookies updateCookies) { 865 | // 1、2.3及以下需要调用CookieSyncManager.createInstance; 866 | // 2、Samsung GTI9300 Android4.3,在调用cookieManager.setAcceptCookie之前不调用CookieSyncManager.createInstance会发生native崩溃:java.lang.UnsatisfiedLinkError: Native method not found: android.webkit.CookieManagerClassic.nativeSetAcceptCookie:(Z)V at android.webkit.CookieManagerClassic.nativeSetAcceptCookie(Native Method) 867 | CookieSyncManager.createInstance(context); 868 | CookieManager cookieManager = null; 869 | try { 870 | cookieManager = CookieManager.getInstance(); 871 | } catch (AndroidRuntimeException e) { // 当webview内核apk正在升级时会发生崩溃(Meizu m2 note Android5.1) 872 | // android.util.AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.android.webview 873 | // at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:174) 874 | // at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:109) 875 | // at android.webkit.CookieManager.getInstance(CookieManager.java:42) 876 | // at android.webkit.safe.SafeWebView.updateCookies(XXX:479) 877 | // at com.xxx.accounts.manager.UserLoginManager.updateCookies(XXX:669) 878 | // at com.xxx.accounts.manager.UserLoginManager.onLoginSucceed(XXX:604) 879 | // at com.xxx.accounts.manager.UserLoginManager$6.onLoaded(XXX:567) 880 | // at com.xxx.accounts.manager.UserLoginManager$6.onRpcSuccess(XXX:550) 881 | // at com.xxx.accounts.api.auth.QucRpc$LocalHandler.handleMessage(XXX:62) 882 | // at android.os.Handler.dispatchMessage(Handler.java:111) 883 | // at android.os.Looper.loop(Looper.java:194) 884 | // at android.app.ActivityThread.main(ActivityThread.java:5637) 885 | // at java.lang.reflect.Method.invoke(Native Method) 886 | // at java.lang.reflect.Method.invoke(Method.java:372) 887 | // at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:959) 888 | // at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:754) 889 | // Caused by: android.content.pm.PackageManager$NameNotFoundException: com.android.webview 890 | // at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:119) 891 | // at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:146) 892 | // ... 15 more 893 | if (LogUtils.isDebug()) { 894 | e.printStackTrace(); 895 | } 896 | } 897 | if (cookieManager != null) { 898 | cookieManager.setAcceptCookie(true); 899 | if (updateCookies != null) { 900 | updateCookies.update(cookieManager); 901 | } 902 | CookieSyncManager.getInstance().sync(); 903 | } 904 | } 905 | 906 | public interface UpdateCookies { 907 | void update(CookieManager cookieManager); 908 | } 909 | } --------------------------------------------------------------------------------