├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ ├── java │ │ │ └── uk │ │ │ │ └── co │ │ │ │ └── alt236 │ │ │ │ └── webviewdebugsampleapp │ │ │ │ └── MainActivity.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── uk │ │ │ └── co │ │ │ └── alt236 │ │ │ └── webviewdebugsampleapp │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── uk │ │ └── co │ │ └── alt236 │ │ └── webviewdebugsampleapp │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── library ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── uk │ │ │ └── co │ │ │ └── alt236 │ │ │ └── webviewdebug │ │ │ └── webviewclient │ │ │ ├── LogControl.java │ │ │ ├── LogEngine.java │ │ │ ├── StringUtils.java │ │ │ ├── OnUnhandledInputEventMethodProxy.java │ │ │ ├── Validation.java │ │ │ ├── DebugWebViewClient.java │ │ │ └── DebugWebViewClientLogger.java │ ├── androidTest │ │ └── java │ │ │ └── uk │ │ │ └── co │ │ │ └── alt236 │ │ │ └── webviewdebug │ │ │ └── ExampleInstrumentedTest.java │ └── test │ │ └── java │ │ └── uk │ │ └── co │ │ └── alt236 │ │ └── webviewdebug │ │ └── webviewclient │ │ ├── OnUnhandledInputEventMethodProxyTestValidApi.java │ │ ├── OnUnhandledInputEventMethodProxyTestInvalidApi.java │ │ ├── DebugWebViewClientTest.java │ │ └── DebugWebViewClientLoggerTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── .idea ├── vcs.xml ├── compiler.xml ├── modules.xml └── misc.xml ├── gradle.properties ├── gradlew.bat ├── .circleci └── config.yml ├── gradlew ├── LICENSE-2.0.html └── README.md /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | webviewdebug 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alt236/webviewdebug/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/LogControl.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | interface LogControl { 4 | boolean isLoggingEnabled(); 5 | 6 | void setLoggingEnabled(boolean enabled); 7 | 8 | boolean isLogKeyEventsEnabled(); 9 | 10 | void setLogKeyEventsEnabled(boolean enabled); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/uk/co/alt236/webviewdebugsampleapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebugsampleapp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/LogEngine.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.util.Log; 4 | 5 | class LogEngine { 6 | private final String tag; 7 | 8 | public LogEngine(final String tag) { 9 | this.tag = tag; 10 | } 11 | 12 | public void log(final String message) { 13 | Log.i(tag, message); 14 | } 15 | 16 | public void logError(final String message) { 17 | Log.e(tag, message); 18 | } 19 | 20 | public void logSecurity(final String message) { 21 | Log.w(tag, message); 22 | } 23 | 24 | public void logKeyEvent(final String message) { 25 | Log.e(tag, message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /app/src/main/java/uk/co/alt236/webviewdebugsampleapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebugsampleapp; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebView; 5 | import android.webkit.WebViewClient; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | import uk.co.alt236.webviewdebug.webviewclient.DebugWebViewClient; 9 | 10 | public class MainActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | final WebView webView = new WebView(this); 16 | setContentView(webView); 17 | 18 | final DebugWebViewClient debugWebViewClient = new DebugWebViewClient(new WebViewClient()); 19 | debugWebViewClient.setLoggingEnabled(true); 20 | webView.setWebViewClient(debugWebViewClient); 21 | webView.loadUrl("https://www.google.com"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/src/androidTest/java/uk/co/alt236/webviewdebug/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Assert; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumentation test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | @Test 19 | public void useAppContext() throws Exception { 20 | // Context of the app under test. 21 | Context appContext = InstrumentationRegistry.getTargetContext(); 22 | 23 | Assert.assertEquals("uk.co.alt236.webviewdebug.test", appContext.getPackageName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/uk/co/alt236/webviewdebugsampleapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebugsampleapp; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("uk.co.alt236.webviewdebug", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/alex/Dev/android-sdk-linux/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/alex/Dev/android-sdk-linux/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply from: "${project.rootDir}/buildsystem/dependency-versions.gradle" 3 | 4 | android { 5 | compileSdkVersion compile_sdk_version 6 | 7 | defaultConfig { 8 | applicationId "uk.co.alt236.webviewdebugsampleapp" 9 | minSdkVersion min_sdk_version 10 | targetSdkVersion target_sdk_version 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation project(path: ':library') 27 | 28 | implementation "androidx.appcompat:appcompat:$android_x_annotations" 29 | 30 | testImplementation "junit:junit:$junit_version" 31 | 32 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { 33 | exclude group: 'com.android.support', module: 'support-annotations' 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /library/src/test/java/uk/co/alt236/webviewdebug/webviewclient/OnUnhandledInputEventMethodProxyTestValidApi.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.view.InputEvent; 4 | import android.webkit.WebView; 5 | import android.webkit.WebViewClient; 6 | 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.robolectric.RobolectricTestRunner; 13 | import org.robolectric.annotation.Config; 14 | 15 | import static org.junit.Assert.assertTrue; 16 | 17 | @RunWith(RobolectricTestRunner.class) 18 | @Config(manifest = Config.NONE, sdk = {21, 27}) 19 | public class OnUnhandledInputEventMethodProxyTestValidApi { 20 | 21 | private OnUnhandledInputEventMethodProxy proxy; 22 | 23 | @Before 24 | public void setUp() { 25 | WebViewClient client = Mockito.mock(WebViewClient.class); 26 | proxy = new OnUnhandledInputEventMethodProxy(client); 27 | } 28 | 29 | @After 30 | public void tearDown() { 31 | proxy = null; 32 | } 33 | 34 | @Test 35 | public void testOnUnhandledInputEvent() { 36 | final WebView webView = Mockito.any(WebView.class); 37 | final InputEvent inputEvent = Mockito.any(InputEvent.class); 38 | 39 | final boolean handled = proxy.onUnhandledInputEvent(webView, inputEvent); 40 | 41 | assertTrue("Method should have been present", proxy.isMethodPresent()); 42 | assertTrue("Method call should have been handled", handled); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/src/test/java/uk/co/alt236/webviewdebug/webviewclient/OnUnhandledInputEventMethodProxyTestInvalidApi.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.view.InputEvent; 4 | import android.webkit.WebView; 5 | import android.webkit.WebViewClient; 6 | 7 | import org.junit.After; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.Mockito; 12 | import org.robolectric.RobolectricTestRunner; 13 | import org.robolectric.annotation.Config; 14 | 15 | import static org.junit.Assert.assertFalse; 16 | 17 | @RunWith(RobolectricTestRunner.class) 18 | @Config(manifest = Config.NONE, sdk = {17, 18, 19}) 19 | public class OnUnhandledInputEventMethodProxyTestInvalidApi { 20 | 21 | private OnUnhandledInputEventMethodProxy proxy; 22 | 23 | @Before 24 | public void setUp() { 25 | WebViewClient client = Mockito.mock(WebViewClient.class); 26 | proxy = new OnUnhandledInputEventMethodProxy(client); 27 | } 28 | 29 | @After 30 | public void tearDown() { 31 | proxy = null; 32 | } 33 | 34 | @Test 35 | public void testOnUnhandledInputEvent() { 36 | final WebView webView = Mockito.mock(WebView.class); 37 | final InputEvent inputEvent = Mockito.mock(InputEvent.class); 38 | 39 | final boolean handled = proxy.onUnhandledInputEvent(webView, inputEvent); 40 | 41 | assertFalse("Method should not have been present", proxy.isMethodPresent()); 42 | assertFalse("Method call should not have been handled", handled); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/StringUtils.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.os.Build; 4 | import android.webkit.ClientCertRequest; 5 | import android.webkit.WebResourceResponse; 6 | import android.webkit.WebViewClient; 7 | 8 | import java.util.Arrays; 9 | 10 | import androidx.annotation.RequiresApi; 11 | 12 | @SuppressWarnings("WeakerAccess") 13 | public class StringUtils { 14 | 15 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 16 | public static String toString(final ClientCertRequest request) { 17 | if (request == null) { 18 | return ""; 19 | } else { 20 | return request.getHost() + ":" + request.getPort() + " " 21 | + Arrays.toString(request.getKeyTypes()) 22 | + " " 23 | + Arrays.toString(request.getPrincipals()); 24 | } 25 | } 26 | 27 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 28 | public static String toString(final WebResourceResponse response) { 29 | if (response == null) { 30 | return ""; 31 | } else { 32 | return response.getStatusCode() + " " + response.getReasonPhrase(); 33 | } 34 | } 35 | 36 | public static String resolveThreatType(final int threatType) { 37 | final String threat; 38 | 39 | switch (threatType) { 40 | case WebViewClient.SAFE_BROWSING_THREAT_MALWARE: 41 | threat = "Malware"; 42 | break; 43 | case WebViewClient.SAFE_BROWSING_THREAT_PHISHING: 44 | threat = "Phishing"; 45 | break; 46 | case WebViewClient.SAFE_BROWSING_THREAT_UNWANTED_SOFTWARE: 47 | threat = "Unwanted Software"; 48 | break; 49 | case WebViewClient.SAFE_BROWSING_THREAT_UNKNOWN: 50 | default: 51 | threat = "Unkwnown"; 52 | } 53 | 54 | return threat + " (" + threatType + ")"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/OnUnhandledInputEventMethodProxy.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.util.Log; 4 | import android.view.InputEvent; 5 | import android.webkit.WebView; 6 | import android.webkit.WebViewClient; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | 11 | import androidx.annotation.NonNull; 12 | import uk.co.alt236.webviewdebug.BuildConfig; 13 | 14 | class OnUnhandledInputEventMethodProxy { 15 | private static final String TAG = BuildConfig.DEFAULT_LOG_TAG; 16 | private static final String METHOD_NAME = "onUnhandledInputEvent"; 17 | private final WebViewClient client; 18 | private final Method method; 19 | 20 | OnUnhandledInputEventMethodProxy(@NonNull final WebViewClient client) { 21 | this.client = client; 22 | this.method = getMethod(); 23 | } 24 | 25 | public boolean onUnhandledInputEvent(final WebView view, final InputEvent event) { 26 | boolean executed = false; 27 | if (method != null) { 28 | try { 29 | method.invoke(client, view, event); 30 | executed = true; 31 | } catch (IllegalAccessException e) { 32 | Log.e(TAG, "IllegalAccessException() " + e.getMessage(), e); 33 | e.printStackTrace(); 34 | } catch (InvocationTargetException e) { 35 | Log.e(TAG, "InvocationTargetException() " + e.getMessage(), e); 36 | e.printStackTrace(); 37 | } 38 | } 39 | return executed; 40 | } 41 | 42 | public boolean isMethodPresent() { 43 | return method != null; 44 | } 45 | 46 | @SuppressWarnings({"JavaReflectionMemberAccess", "PrivateApi"}) 47 | // necessary for debugging- should not be used in production 48 | private Method getMethod() { 49 | try { 50 | return WebViewClient.class.getDeclaredMethod(METHOD_NAME, WebView.class, InputEvent.class); 51 | } catch (NoSuchMethodException e) { 52 | Log.i(TAG, "WebViewClient does not implement " + METHOD_NAME); 53 | return null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/Validation.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.util.Log; 4 | import android.webkit.WebViewClient; 5 | 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Modifier; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import uk.co.alt236.webviewdebug.BuildConfig; 14 | 15 | final class Validation { 16 | private static final String TAG = BuildConfig.DEFAULT_LOG_TAG; 17 | 18 | // We need to validate that the DebugClient overrides all methods 19 | // overridden in the wrapped client. 20 | 21 | public boolean validate( 22 | final Class wrappedClient, 23 | final Class debugClient) { 24 | 25 | final List unimplementedMethods = new ArrayList<>(); 26 | final Method[] wrappedClientMethods = wrappedClient.getMethods(); 27 | Method candidate; 28 | 29 | for (final Method method : wrappedClientMethods) { 30 | if (!isIgnorable(method)) { 31 | candidate = getMethod(debugClient, method); 32 | if (candidate == null 33 | && (getMethod(Object.class, method) == null)) { 34 | unimplementedMethods.add(method); 35 | } 36 | } 37 | } 38 | 39 | if (unimplementedMethods.isEmpty()) { 40 | Log.i(TAG, "All methods implemented :)"); 41 | } else { 42 | Log.e(TAG, "-----------------------------"); 43 | for (final Method method : unimplementedMethods) { 44 | Log.e(TAG, debugClient.getSimpleName() + " does not implement: " + method); 45 | } 46 | Log.e(TAG, "-----------------------------"); 47 | } 48 | 49 | return unimplementedMethods.isEmpty(); 50 | } 51 | 52 | 53 | private static boolean isIgnorable(@NonNull final Method method) { 54 | return Modifier.isPrivate(method.getModifiers()) || Modifier.isStatic(method.getModifiers()); 55 | } 56 | 57 | @Nullable 58 | private static Method getMethod(final Class clazz, final Method method) { 59 | try { 60 | return clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); 61 | } catch (NoSuchMethodException e) { 62 | return null; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | ## 4 | ## REFERENCES 5 | ################################################################ 6 | references: 7 | 8 | # 9 | # CONFIGURATION 10 | # 11 | workspace: &workspace 12 | ~/code 13 | 14 | environment_config: &environment_config 15 | working_directory: *workspace 16 | docker: 17 | - image: cimg/android:2022.04 18 | environment: 19 | JVM_OPTS: -Xmx3584m 20 | 21 | # 22 | # 23 | # CACHE CONTROL 24 | # 25 | generate_dependency_hashfile: &generate_dependency_hashfile 26 | run: 27 | name: Generate Dependency Hashfile 28 | command: ./buildsystem/generate_dependency_hashfile.sh ./ ./dependency_hashfile.tmp 29 | 30 | print_dependency_hashfile: &print_dependency_hashfile 31 | run: 32 | name: Print Dependency Hashfile Contents 33 | command: cat ./dependency_hashfile.tmp 34 | 35 | cache_key: &cache_key 36 | key: cache-{{ checksum "dependency_hashfile.tmp" }} 37 | 38 | restore_cache: &restore_cache 39 | restore_cache: 40 | <<: *cache_key 41 | 42 | save_cache: &save_cache 43 | save_cache: 44 | <<: *cache_key 45 | paths: 46 | - ~/.gradle 47 | - ~/.m2 48 | 49 | # 50 | # DOWNLOADING DEPENDENCIES 51 | # 52 | download_android_dependencies: &download_android_dependencies 53 | run: 54 | name: Download Dependencies 55 | command: ./gradlew dependencies androidDependencies --no-daemon 56 | 57 | # 58 | # RUNNING TESTS 59 | # 60 | run_android_lint: &run_android_lint 61 | run: 62 | name: Running lint 63 | command: ./gradlew lint -PpreDexEnable=false --stacktrace --no-daemon 64 | 65 | run_unit_tests: &run_unit_tests 66 | run: 67 | name: Running unit tests 68 | command: ./gradlew test -PpreDexEnable=false --stacktrace --no-daemon 69 | 70 | # 71 | # BUILDING 72 | # 73 | build_project: &build_project 74 | run: 75 | name: Building project 76 | command: ./gradlew assemble -PpreDexEnable=false --stacktrace --no-daemon 77 | 78 | ## 79 | ## COMMANDS 80 | ################################################################ 81 | commands: 82 | upload_test_results_and_artifacts: 83 | description: "Upload the test reports and artifacts for a given module" 84 | parameters: 85 | module_name: 86 | type: string 87 | default: "[MISSING]" 88 | steps: 89 | - store_artifacts: 90 | path: << parameters.module_name >>/build/reports 91 | destination: reports-<< parameters.module_name >> 92 | - store_test_results: 93 | path: << parameters.module_name >>/build/test-results 94 | 95 | setup_dependency_cache: 96 | description: "Restore (if present) populate and save (if needed) the dependency cache" 97 | steps: 98 | - *generate_dependency_hashfile 99 | - *print_dependency_hashfile 100 | - *restore_cache 101 | - *download_android_dependencies 102 | 103 | ## 104 | ## JOBS 105 | ################################################################ 106 | jobs: 107 | build: 108 | <<: *environment_config 109 | steps: 110 | - checkout 111 | - setup_dependency_cache 112 | - *save_cache 113 | - *build_project 114 | - *run_android_lint 115 | - *run_unit_tests 116 | - upload_test_results_and_artifacts: 117 | module_name: "app" 118 | - upload_test_results_and_artifacts: 119 | module_name: "library" -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'com.jfrog.bintray' 4 | apply from: "${project.rootDir}/buildsystem/dependency-versions.gradle" 5 | 6 | final int versionMajor = 1 7 | final int versionMinor = 0 8 | final int versionPatch = 0 9 | 10 | final String gitHubRepo = 'alt236/webviewdebug' 11 | final String artifactName = 'webviewdebug' 12 | final String artifactDesc = 'Provides a custom WebViewClient which will log info each time one of its methods is called' 13 | final String[] tags = ['webview', 'webviewclient', 'android'] 14 | 15 | final String artifactGroupId = 'uk.co.alt236' 16 | final String semanticVersion = "${versionMajor}.${versionMinor}.${versionPatch}" 17 | final String gitHubUrl = "https://github.com/${gitHubRepo}" 18 | 19 | android { 20 | compileSdkVersion compile_sdk_version 21 | 22 | defaultConfig { 23 | minSdkVersion min_sdk_version 24 | targetSdkVersion target_sdk_version 25 | 26 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 27 | 28 | buildConfigField "String", "DEFAULT_LOG_TAG", "\"DebugWVClient\"" 29 | } 30 | 31 | buildTypes { 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | } 38 | 39 | dependencies { 40 | compileOnly "androidx.annotation:annotation:$android_x_annotations" 41 | 42 | testImplementation "junit:junit:$junit_version" 43 | 44 | testImplementation "org.robolectric:robolectric:4.1" 45 | testImplementation "org.mockito:mockito-core:3.9.0" 46 | 47 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { 48 | exclude group: 'com.android.support', module: 'support-annotations' 49 | }) 50 | } 51 | 52 | publishing { 53 | publications { 54 | mavenJava(MavenPublication) { 55 | groupId artifactGroupId 56 | artifactId artifactName 57 | version semanticVersion 58 | 59 | // artifact sourcesJar 60 | // artifact javadocJar 61 | 62 | // this can go horribly horribly wrong if the directory is not 63 | // named after its module 64 | String moduleName = ((File) buildDir).getParentFile().getName() 65 | 66 | artifact "$buildDir/outputs/aar/$moduleName-release.aar" 67 | } 68 | } 69 | } 70 | 71 | bintray { 72 | user = project.hasProperty('alt236JCenterUser') ? alt236JCenterUser : "" 73 | key = project.hasProperty('alt236JCenterToken') ? alt236JCenterToken : "" 74 | 75 | dryRun = false 76 | publish = false 77 | publications = ['mavenJava'] 78 | pkg { 79 | repo = 'maven' 80 | // userOrg = 'myorg' //An optional organization name when the repo belongs to one of the user's orgs 81 | name = artifactName 82 | desc = artifactDesc 83 | websiteUrl = "${gitHubUrl}" 84 | issueTrackerUrl = "${gitHubUrl}/issues" 85 | vcsUrl = "${gitHubUrl}.git" 86 | labels = tags 87 | licenses = ['Apache-2.0'] 88 | publicDownloadNumbers = true 89 | githubRepo = "${gitHubRepo}" 90 | githubReleaseNotesFile = 'README.md' 91 | 92 | version { 93 | name = semanticVersion 94 | //desc = 'optional, version-specific description' 95 | mavenCentralSync { 96 | sync = false 97 | // Optional (true by default). Determines whether to sync the version to Maven Central. 98 | user = 'userToken' //OSS user token 99 | password = 'paasword' //OSS user password 100 | close = '1' 101 | // Optional property. By default the staging repository is closed and artifacts are released to Maven Central. 102 | // You can optionally turn this behaviour off (by puting 0 as value) and release the version manually. 103 | } 104 | } 105 | } 106 | 107 | if (project.hasProperty("android")) { // Android libraries 108 | task sourcesJar(type: Jar) { 109 | classifier = 'sources' 110 | from android.sourceSets.main.java.srcDirs 111 | } 112 | 113 | task javadoc(type: Javadoc) { 114 | source = android.sourceSets.main.java.srcDirs 115 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 116 | } 117 | } else { // Java libraries 118 | task sourcesJar(type: Jar, dependsOn: classes) { 119 | classifier = 'sources' 120 | from sourceSets.main.allSource 121 | } 122 | } 123 | 124 | task javadocJar(type: Jar, dependsOn: javadoc) { 125 | classifier = 'javadoc' 126 | from javadoc.destinationDir 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/DebugWebViewClient.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Bitmap; 5 | import android.net.http.SslError; 6 | import android.os.Build; 7 | import android.os.Message; 8 | import android.util.Log; 9 | import android.view.InputEvent; 10 | import android.view.KeyEvent; 11 | import android.webkit.ClientCertRequest; 12 | import android.webkit.HttpAuthHandler; 13 | import android.webkit.RenderProcessGoneDetail; 14 | import android.webkit.SafeBrowsingResponse; 15 | import android.webkit.SslErrorHandler; 16 | import android.webkit.WebResourceError; 17 | import android.webkit.WebResourceRequest; 18 | import android.webkit.WebResourceResponse; 19 | import android.webkit.WebView; 20 | import android.webkit.WebViewClient; 21 | 22 | import androidx.annotation.NonNull; 23 | import androidx.annotation.RequiresApi; 24 | 25 | public class DebugWebViewClient extends WebViewClient implements LogControl { 26 | private final WebViewClient client; 27 | private final DebugWebViewClientLogger logger; 28 | private final OnUnhandledInputEventMethodProxy onUnhandledInputEventMethodProxy; 29 | 30 | public DebugWebViewClient() { 31 | this(new WebViewClient()); 32 | } 33 | 34 | public DebugWebViewClient(@NonNull final WebViewClient client) { 35 | this(client, new DebugWebViewClientLogger()); 36 | } 37 | 38 | @SuppressWarnings("WeakerAccess") 39 | public DebugWebViewClient( 40 | @NonNull final WebViewClient client, 41 | @NonNull final DebugWebViewClientLogger logger) { 42 | this.logger = logger; 43 | this.client = client; 44 | this.onUnhandledInputEventMethodProxy = new OnUnhandledInputEventMethodProxy(client); 45 | validate(); 46 | } 47 | 48 | private void validate() { 49 | if (!new Validation().validate(client.getClass(), this.getClass())) { 50 | Log.e(DebugWebViewClient.class.getSimpleName(), 51 | "invalid: the DebugClient does not override all methods overridden in the wrapped client"); 52 | } 53 | } 54 | 55 | @RequiresApi(api = Build.VERSION_CODES.M) 56 | @Override 57 | public void onReceivedError(final WebView view, final WebResourceRequest request, final WebResourceError error) { 58 | logger.onReceivedError(view, request, error); 59 | client.onReceivedError(view, request, error); 60 | } 61 | 62 | @Override 63 | @Deprecated 64 | public void onReceivedError(final WebView view, final int errorCode, final String description, final String failingUrl) { 65 | logger.onReceivedError(view, errorCode, description, failingUrl); 66 | //noinspection deprecation 67 | client.onReceivedError(view, errorCode, description, failingUrl); 68 | } 69 | 70 | @RequiresApi(api = Build.VERSION_CODES.M) 71 | @Override 72 | public void onReceivedHttpError(final WebView view, final WebResourceRequest request, final WebResourceResponse errorResponse) { 73 | logger.onReceivedHttpError(view, request, errorResponse); 74 | client.onReceivedHttpError(view, request, errorResponse); 75 | } 76 | 77 | @Override 78 | public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { 79 | logger.onReceivedSslError(view, handler, error); 80 | client.onReceivedSslError(view, handler, error); 81 | } 82 | 83 | @RequiresApi(api = Build.VERSION_CODES.N) 84 | @Override 85 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 86 | final boolean retVal = client.shouldOverrideUrlLoading(view, request); 87 | logger.shouldOverrideUrlLoading(view, request, retVal); 88 | return retVal; 89 | } 90 | 91 | @Override 92 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 93 | //noinspection deprecation 94 | final boolean retVal = client.shouldOverrideUrlLoading(view, url); 95 | logger.shouldOverrideUrlLoading(view, url, retVal); 96 | return retVal; 97 | } 98 | 99 | @Override 100 | public void onLoadResource(WebView view, String url) { 101 | logger.onLoadResource(view, url); 102 | client.onLoadResource(view, url); 103 | } 104 | 105 | @TargetApi(Build.VERSION_CODES.M) 106 | @Override 107 | public void onPageCommitVisible(WebView view, String url) { 108 | logger.onPageCommitVisible(view, url); 109 | client.onPageCommitVisible(view, url); 110 | } 111 | 112 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 113 | @Deprecated 114 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 115 | //noinspection deprecation 116 | final WebResourceResponse retVal = client.shouldInterceptRequest(view, url); 117 | logger.shouldInterceptRequest(view, url, retVal); 118 | return retVal; 119 | } 120 | 121 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 122 | @Override 123 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 124 | final WebResourceResponse retVal = client.shouldInterceptRequest(view, request); 125 | logger.shouldInterceptRequest(view, request, retVal); 126 | return retVal; 127 | } 128 | 129 | @Override 130 | @Deprecated 131 | @SuppressWarnings("deprecation") //for use with older versions 132 | public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) { 133 | logger.onTooManyRedirects(view, cancelMsg, continueMsg); 134 | //noinspection deprecation 135 | client.onTooManyRedirects(view, cancelMsg, continueMsg); 136 | } 137 | 138 | @Override 139 | public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { 140 | logger.onReceivedHttpAuthRequest(view, handler, host, realm); 141 | client.onReceivedHttpAuthRequest(view, handler, host, realm); 142 | } 143 | 144 | @Override 145 | public void onPageStarted(WebView view, String url, Bitmap facIcon) { 146 | logger.onPageStarted(view, url, facIcon); 147 | client.onPageStarted(view, url, facIcon); 148 | } 149 | 150 | @Override 151 | public void onPageFinished(WebView view, String url) { 152 | logger.onPageFinished(view, url); 153 | client.onPageFinished(view, url); 154 | } 155 | 156 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 157 | @Override 158 | public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { 159 | logger.onReceivedClientCertRequest(view, request); 160 | client.onReceivedClientCertRequest(view, request); 161 | } 162 | 163 | @Override 164 | public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { 165 | logger.onFormResubmission(view, dontResend, resend); 166 | client.onFormResubmission(view, dontResend, resend); 167 | } 168 | 169 | @Override 170 | public void doUpdateVisitedHistory(final WebView view, final String url, final boolean isReload) { 171 | logger.doUpdateVisitedHistory(view, url, isReload); 172 | client.doUpdateVisitedHistory(view, url, isReload); 173 | } 174 | 175 | @Override 176 | public boolean shouldOverrideKeyEvent(final WebView view, final KeyEvent event) { 177 | final boolean retVal = client.shouldOverrideKeyEvent(view, event); 178 | logger.shouldOverrideKeyEvent(view, event, retVal); 179 | return retVal; 180 | } 181 | 182 | //this is a valid, method but it was fully removed from the SDK in API24 183 | // it was added in API21. 184 | //@Override 185 | @SuppressWarnings("unused") 186 | public void onUnhandledInputEvent(final WebView view, final InputEvent event) { 187 | logger.onUnhandledInputEvent(view, event); 188 | onUnhandledInputEventMethodProxy.onUnhandledInputEvent(view, event); 189 | } 190 | 191 | @Override 192 | public void onUnhandledKeyEvent(final WebView view, final KeyEvent event) { 193 | logger.onUnhandledKeyEvent(view, event); 194 | client.onUnhandledKeyEvent(view, event); 195 | } 196 | 197 | @Override 198 | public void onScaleChanged(final WebView view, final float oldScale, final float newScale) { 199 | logger.onScaleChanged(view, oldScale, newScale); 200 | client.onScaleChanged(view, oldScale, newScale); 201 | } 202 | 203 | @Override 204 | public void onReceivedLoginRequest(final WebView view, final String realm, final String account, final String args) { 205 | logger.onReceivedLoginRequest(view, realm, account, args); 206 | client.onReceivedLoginRequest(view, realm, account, args); 207 | } 208 | 209 | @RequiresApi(api = Build.VERSION_CODES.O) 210 | @Override 211 | public boolean onRenderProcessGone(final WebView view, final RenderProcessGoneDetail detail) { 212 | final boolean retVal = client.onRenderProcessGone(view, detail); 213 | logger.onRenderProcessGone(view, detail, retVal); 214 | return retVal; 215 | } 216 | 217 | @RequiresApi(api = Build.VERSION_CODES.O_MR1) 218 | @Override 219 | public void onSafeBrowsingHit(final WebView view, final WebResourceRequest request, final int threatType, final SafeBrowsingResponse callback) { 220 | logger.onSafeBrowsingHit(view, request, threatType, callback); 221 | client.onSafeBrowsingHit(view, request, threatType, callback); 222 | } 223 | 224 | @Override 225 | public boolean isLoggingEnabled() { 226 | return logger.isLoggingEnabled(); 227 | } 228 | 229 | @Override 230 | public void setLoggingEnabled(final boolean enabled) { 231 | logger.setLoggingEnabled(enabled); 232 | } 233 | 234 | @Override 235 | public boolean isLogKeyEventsEnabled() { 236 | return logger.isLogKeyEventsEnabled(); 237 | } 238 | 239 | @Override 240 | public void setLogKeyEventsEnabled(final boolean enabled) { 241 | logger.setLogKeyEventsEnabled(enabled); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /library/src/test/java/uk/co/alt236/webviewdebug/webviewclient/DebugWebViewClientTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.graphics.Bitmap; 4 | import android.net.http.SslError; 5 | import android.os.Message; 6 | import android.view.KeyEvent; 7 | import android.webkit.ClientCertRequest; 8 | import android.webkit.HttpAuthHandler; 9 | import android.webkit.RenderProcessGoneDetail; 10 | import android.webkit.SafeBrowsingResponse; 11 | import android.webkit.SslErrorHandler; 12 | import android.webkit.WebResourceError; 13 | import android.webkit.WebResourceRequest; 14 | import android.webkit.WebResourceResponse; 15 | import android.webkit.WebView; 16 | import android.webkit.WebViewClient; 17 | 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.mockito.Mockito; 23 | import org.robolectric.RobolectricTestRunner; 24 | import org.robolectric.annotation.Config; 25 | 26 | @RunWith(RobolectricTestRunner.class) 27 | @Config(manifest = Config.NONE, sdk = 27) 28 | public class DebugWebViewClientTest { 29 | 30 | private WebView webView; 31 | private DebugWebViewClient debugClient; 32 | private WebViewClient wrappedClient; 33 | private DebugWebViewClientLogger logger; 34 | 35 | @Before 36 | public void setUp() { 37 | webView = Mockito.mock(WebView.class); 38 | wrappedClient = Mockito.mock(WebViewClient.class); 39 | logger = Mockito.mock(DebugWebViewClientLogger.class); 40 | debugClient = new DebugWebViewClient(wrappedClient, logger); 41 | } 42 | 43 | @After 44 | public void tearDown() { 45 | webView = null; 46 | wrappedClient = null; 47 | logger = null; 48 | debugClient = null; 49 | } 50 | 51 | @Test 52 | public void onReceivedError() { 53 | final int code = 500; 54 | final String message = "foo"; 55 | final String url = "bar"; 56 | 57 | //noinspection deprecation 58 | debugClient.onReceivedError(webView, code, message, url); 59 | verifyLogger().onReceivedError(webView, code, message, url); 60 | verifyWrappedClient().onReceivedError(webView, code, message, url); 61 | } 62 | 63 | @Test 64 | public void onReceivedError_api23() { 65 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 66 | final WebResourceError error = Mockito.mock(WebResourceError.class); 67 | 68 | debugClient.onReceivedError(webView, request, error); 69 | verifyLogger().onReceivedError(webView, request, error); 70 | verifyWrappedClient().onReceivedError(webView, request, error); 71 | } 72 | 73 | @Test 74 | public void onReceivedHttpError() { 75 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 76 | final WebResourceResponse response = Mockito.mock(WebResourceResponse.class); 77 | 78 | debugClient.onReceivedHttpError(webView, request, response); 79 | verifyLogger().onReceivedHttpError(webView, request, response); 80 | verifyWrappedClient().onReceivedHttpError(webView, request, response); 81 | } 82 | 83 | @Test 84 | public void onReceivedSslError() { 85 | final SslErrorHandler errorHandler = Mockito.mock(SslErrorHandler.class); 86 | final SslError sslError = Mockito.mock(SslError.class); 87 | 88 | debugClient.onReceivedSslError(webView, errorHandler, sslError); 89 | verifyLogger().onReceivedSslError(webView, errorHandler, sslError); 90 | verifyWrappedClient().onReceivedSslError(webView, errorHandler, sslError); 91 | } 92 | 93 | @Test 94 | public void shouldOverrideUrlLoading() { 95 | final String url = "foo"; 96 | 97 | final boolean retVal = debugClient.shouldOverrideUrlLoading(webView, url); 98 | verifyLogger().shouldOverrideUrlLoading(webView, url, retVal); 99 | verifyWrappedClient().shouldOverrideUrlLoading(webView, url); 100 | } 101 | 102 | @Test 103 | public void shouldOverrideUrlLoading1() { 104 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 105 | 106 | final boolean retVal = debugClient.shouldOverrideUrlLoading(webView, request); 107 | verifyLogger().shouldOverrideUrlLoading(webView, request, retVal); 108 | verifyWrappedClient().shouldOverrideUrlLoading(webView, request); 109 | } 110 | 111 | @Test 112 | public void onLoadResource() { 113 | final String url = "foo"; 114 | 115 | debugClient.onLoadResource(webView, url); 116 | verifyLogger().onLoadResource(webView, url); 117 | verifyWrappedClient().onLoadResource(webView, url); 118 | } 119 | 120 | @Test 121 | public void onPageCommitVisible() { 122 | final String url = "foo"; 123 | 124 | debugClient.onPageCommitVisible(webView, url); 125 | verifyLogger().onPageCommitVisible(webView, url); 126 | verifyWrappedClient().onPageCommitVisible(webView, url); 127 | } 128 | 129 | @Test 130 | public void shouldInterceptRequest() { 131 | final String url = "foo"; 132 | 133 | //noinspection deprecation 134 | final WebResourceResponse retVal = debugClient.shouldInterceptRequest(webView, url); 135 | verifyLogger().shouldInterceptRequest(webView, url, retVal); 136 | verifyWrappedClient().shouldInterceptRequest(webView, url); 137 | } 138 | 139 | @Test 140 | public void shouldInterceptRequest_api21() { 141 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 142 | 143 | final WebResourceResponse retVal = debugClient.shouldInterceptRequest(webView, request); 144 | verifyLogger().shouldInterceptRequest(webView, request, retVal); 145 | verifyWrappedClient().shouldInterceptRequest(webView, request); 146 | } 147 | 148 | @Test 149 | @SuppressWarnings("deprecation") 150 | public void onTooManyRedirects() { 151 | final Message cancelMsg = Mockito.mock(Message.class); 152 | final Message continueMsg = Mockito.mock(Message.class); 153 | 154 | debugClient.onTooManyRedirects(webView, cancelMsg, continueMsg); 155 | verifyLogger().onTooManyRedirects(webView, cancelMsg, continueMsg); 156 | verifyWrappedClient().onTooManyRedirects(webView, cancelMsg, continueMsg); 157 | } 158 | 159 | @Test 160 | public void onReceivedHttpAuthRequest() { 161 | final HttpAuthHandler handler = Mockito.mock(HttpAuthHandler.class); 162 | final String host = "foo"; 163 | final String realm = "bar"; 164 | 165 | debugClient.onReceivedHttpAuthRequest(webView, handler, host, realm); 166 | verifyLogger().onReceivedHttpAuthRequest(webView, handler, host, realm); 167 | verifyWrappedClient().onReceivedHttpAuthRequest(webView, handler, host, realm); 168 | } 169 | 170 | @Test 171 | public void onPageStarted() { 172 | final String url = "foo"; 173 | final Bitmap bitmap = Mockito.mock(Bitmap.class); 174 | 175 | debugClient.onPageStarted(webView, url, bitmap); 176 | verifyLogger().onPageStarted(webView, url, bitmap); 177 | verifyWrappedClient().onPageStarted(webView, url, bitmap); 178 | } 179 | 180 | @Test 181 | public void onPageFinished() { 182 | final String url = "foo"; 183 | 184 | debugClient.onPageFinished(webView, url); 185 | verifyLogger().onPageFinished(webView, url); 186 | verifyWrappedClient().onPageFinished(webView, url); 187 | } 188 | 189 | @Test 190 | public void onReceivedClientCertRequest() { 191 | final ClientCertRequest request = Mockito.mock(ClientCertRequest.class); 192 | 193 | debugClient.onReceivedClientCertRequest(webView, request); 194 | verifyLogger().onReceivedClientCertRequest(webView, request); 195 | verifyWrappedClient().onReceivedClientCertRequest(webView, request); 196 | } 197 | 198 | @Test 199 | public void onFormResubmission() { 200 | final Message dontResend = Mockito.mock(Message.class); 201 | final Message resend = Mockito.mock(Message.class); 202 | 203 | debugClient.onFormResubmission(webView, dontResend, resend); 204 | verifyLogger().onFormResubmission(webView, dontResend, resend); 205 | verifyWrappedClient().onFormResubmission(webView, dontResend, resend); 206 | } 207 | 208 | @Test 209 | public void doUpdateVisitedHistory() { 210 | final String url = "foo"; 211 | final boolean reload = true; 212 | 213 | debugClient.doUpdateVisitedHistory(webView, url, reload); 214 | verifyLogger().doUpdateVisitedHistory(webView, url, reload); 215 | verifyWrappedClient().doUpdateVisitedHistory(webView, url, reload); 216 | } 217 | 218 | @Test 219 | public void shouldOverrideKeyEvent() { 220 | final KeyEvent keyEvent = Mockito.mock(KeyEvent.class); 221 | 222 | final boolean retVal = debugClient.shouldOverrideKeyEvent(webView, keyEvent); 223 | verifyLogger().shouldOverrideKeyEvent(webView, keyEvent, retVal); 224 | verifyWrappedClient().shouldOverrideKeyEvent(webView, keyEvent); 225 | } 226 | 227 | @Test 228 | public void onUnhandledKeyEvent() { 229 | final KeyEvent keyEvent = Mockito.mock(KeyEvent.class); 230 | 231 | debugClient.onUnhandledKeyEvent(webView, keyEvent); 232 | verifyLogger().onUnhandledKeyEvent(webView, keyEvent); 233 | verifyWrappedClient().onUnhandledKeyEvent(webView, keyEvent); 234 | } 235 | 236 | @Test 237 | public void onScaleChanged() { 238 | final float oldScale = 1.0f; 239 | final float newScale = 2.0f; 240 | 241 | debugClient.onScaleChanged(webView, oldScale, newScale); 242 | verifyLogger().onScaleChanged(webView, oldScale, newScale); 243 | verifyWrappedClient().onScaleChanged(webView, oldScale, newScale); 244 | } 245 | 246 | @Test 247 | public void onReceivedLoginRequest() { 248 | final String realm = "realm"; 249 | final String account = "account"; 250 | final String args = "args"; 251 | 252 | debugClient.onReceivedLoginRequest(webView, realm, account, args); 253 | verifyLogger().onReceivedLoginRequest(webView, realm, account, args); 254 | verifyWrappedClient().onReceivedLoginRequest(webView, realm, account, args); 255 | } 256 | 257 | @Test 258 | public void onRenderProcessGone() { 259 | final RenderProcessGoneDetail detail = Mockito.mock(RenderProcessGoneDetail.class); 260 | 261 | final boolean retVal = debugClient.onRenderProcessGone(webView, detail); 262 | verifyLogger().onRenderProcessGone(webView, detail, retVal); 263 | verifyWrappedClient().onRenderProcessGone(webView, detail); 264 | } 265 | 266 | @Test 267 | public void onSafeBrowsingHit() { 268 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 269 | final SafeBrowsingResponse callback = Mockito.mock(SafeBrowsingResponse.class); 270 | final int threatType = -1; 271 | 272 | debugClient.onSafeBrowsingHit(webView, request, threatType, callback); 273 | 274 | verifyLogger().onSafeBrowsingHit(webView, request, threatType, callback); 275 | verifyWrappedClient().onSafeBrowsingHit(webView, request, threatType, callback); 276 | } 277 | 278 | private WebViewClient verifyWrappedClient() { 279 | return Mockito.verify(wrappedClient, Mockito.times(1)); 280 | } 281 | 282 | private DebugWebViewClientLogger verifyLogger() { 283 | return Mockito.verify(logger, Mockito.times(1)); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /LICENSE-2.0.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Apache License, Version 2.0 - The Apache Software Foundation 10 | 11 | 12 |

13 | Apache License
14 | Version 2.0, January 2004
15 | http://www.apache.org/licenses/ 16 |

17 |

18 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 19 |

20 |

1. Definitions.

21 |

22 | "License" shall mean the terms and conditions for use, reproduction, 23 | and distribution as defined by Sections 1 through 9 of this document. 24 |

25 |

26 | "Licensor" shall mean the copyright owner or entity authorized by 27 | the copyright owner that is granting the License. 28 |

29 |

30 | "Legal Entity" shall mean the union of the acting entity and all 31 | other entities that control, are controlled by, or are under common 32 | control with that entity. For the purposes of this definition, 33 | "control" means (i) the power, direct or indirect, to cause the 34 | direction or management of such entity, whether by contract or 35 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 36 | outstanding shares, or (iii) beneficial ownership of such entity. 37 |

38 |

39 | "You" (or "Your") shall mean an individual or Legal Entity 40 | exercising permissions granted by this License. 41 |

42 |

43 | "Source" form shall mean the preferred form for making modifications, 44 | including but not limited to software source code, documentation 45 | source, and configuration files. 46 |

47 |

48 | "Object" form shall mean any form resulting from mechanical 49 | transformation or translation of a Source form, including but 50 | not limited to compiled object code, generated documentation, 51 | and conversions to other media types. 52 |

53 |

54 | "Work" shall mean the work of authorship, whether in Source or 55 | Object form, made available under the License, as indicated by a 56 | copyright notice that is included in or attached to the work 57 | (an example is provided in the Appendix below). 58 |

59 |

60 | "Derivative Works" shall mean any work, whether in Source or Object 61 | form, that is based on (or derived from) the Work and for which the 62 | editorial revisions, annotations, elaborations, or other modifications 63 | represent, as a whole, an original work of authorship. For the purposes 64 | of this License, Derivative Works shall not include works that remain 65 | separable from, or merely link (or bind by name) to the interfaces of, 66 | the Work and Derivative Works thereof. 67 |

68 |

69 | "Contribution" shall mean any work of authorship, including 70 | the original version of the Work and any modifications or additions 71 | to that Work or Derivative Works thereof, that is intentionally 72 | submitted to Licensor for inclusion in the Work by the copyright owner 73 | or by an individual or Legal Entity authorized to submit on behalf of 74 | the copyright owner. For the purposes of this definition, "submitted" 75 | means any form of electronic, verbal, or written communication sent 76 | to the Licensor or its representatives, including but not limited to 77 | communication on electronic mailing lists, source code control systems, 78 | and issue tracking systems that are managed by, or on behalf of, the 79 | Licensor for the purpose of discussing and improving the Work, but 80 | excluding communication that is conspicuously marked or otherwise 81 | designated in writing by the copyright owner as "Not a Contribution." 82 |

83 |

84 | "Contributor" shall mean Licensor and any individual or Legal Entity 85 | on behalf of whom a Contribution has been received by Licensor and 86 | subsequently incorporated within the Work. 87 |

88 |

2. Grant of Copyright License. 89 | Subject to the terms and conditions of 90 | this License, each Contributor hereby grants to You a perpetual, 91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 92 | copyright license to reproduce, prepare Derivative Works of, 93 | publicly display, publicly perform, sublicense, and distribute the 94 | Work and such Derivative Works in Source or Object form. 95 |

96 |

3. Grant of Patent License. 97 | Subject to the terms and conditions of 98 | this License, each Contributor hereby grants to You a perpetual, 99 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 100 | (except as stated in this section) patent license to make, have made, 101 | use, offer to sell, sell, import, and otherwise transfer the Work, 102 | where such license applies only to those patent claims licensable 103 | by such Contributor that are necessarily infringed by their 104 | Contribution(s) alone or by combination of their Contribution(s) 105 | with the Work to which such Contribution(s) was submitted. If You 106 | institute patent litigation against any entity (including a 107 | cross-claim or counterclaim in a lawsuit) alleging that the Work 108 | or a Contribution incorporated within the Work constitutes direct 109 | or contributory patent infringement, then any patent licenses 110 | granted to You under this License for that Work shall terminate 111 | as of the date such litigation is filed. 112 |

113 |

4. Redistribution. 114 | You may reproduce and distribute copies of the 115 | Work or Derivative Works thereof in any medium, with or without 116 | modifications, and in Source or Object form, provided that You 117 | meet the following conditions: 118 |

119 |
    120 |
  1. You must give any other recipients of the Work or 121 | Derivative Works a copy of this License; and 122 |

  2. 123 | 124 |
  3. You must cause any modified files to carry prominent notices 125 | stating that You changed the files; and 126 |

  4. 127 | 128 |
  5. You must retain, in the Source form of any Derivative Works 129 | that You distribute, all copyright, patent, trademark, and 130 | attribution notices from the Source form of the Work, 131 | excluding those notices that do not pertain to any part of 132 | the Derivative Works; and 133 |

  6. 134 | 135 |
  7. If the Work includes a "NOTICE" text file as part of its 136 | distribution, then any Derivative Works that You distribute must 137 | include a readable copy of the attribution notices contained 138 | within such NOTICE file, excluding those notices that do not 139 | pertain to any part of the Derivative Works, in at least one 140 | of the following places: within a NOTICE text file distributed 141 | as part of the Derivative Works; within the Source form or 142 | documentation, if provided along with the Derivative Works; or, 143 | within a display generated by the Derivative Works, if and 144 | wherever such third-party notices normally appear. The contents 145 | of the NOTICE file are for informational purposes only and 146 | do not modify the License. You may add Your own attribution 147 | notices within Derivative Works that You distribute, alongside 148 | or as an addendum to the NOTICE text from the Work, provided 149 | that such additional attribution notices cannot be construed 150 | as modifying the License. 151 |
  8. 152 |
153 | You may add Your own copyright statement to Your modifications and 154 | may provide additional or different license terms and conditions 155 | for use, reproduction, or distribution of Your modifications, or 156 | for any such Derivative Works as a whole, provided Your use, 157 | reproduction, and distribution of the Work otherwise complies with 158 | the conditions stated in this License. 159 | 160 |

5. Submission of Contributions. 161 | Unless You explicitly state otherwise, 162 | any Contribution intentionally submitted for inclusion in the Work 163 | by You to the Licensor shall be under the terms and conditions of 164 | this License, without any additional terms or conditions. 165 | Notwithstanding the above, nothing herein shall supersede or modify 166 | the terms of any separate license agreement you may have executed 167 | with Licensor regarding such Contributions. 168 |

169 |

6. Trademarks. 170 | This License does not grant permission to use the trade 171 | names, trademarks, service marks, or product names of the Licensor, 172 | except as required for reasonable and customary use in describing the 173 | origin of the Work and reproducing the content of the NOTICE file. 174 |

175 |

7. Disclaimer of Warranty. 176 | Unless required by applicable law or 177 | agreed to in writing, Licensor provides the Work (and each 178 | Contributor provides its Contributions) on an "AS IS" BASIS, 179 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 180 | implied, including, without limitation, any warranties or conditions 181 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 182 | PARTICULAR PURPOSE. You are solely responsible for determining the 183 | appropriateness of using or redistributing the Work and assume any 184 | risks associated with Your exercise of permissions under this License. 185 |

186 |

8. Limitation of Liability. 187 | In no event and under no legal theory, 188 | whether in tort (including negligence), contract, or otherwise, 189 | unless required by applicable law (such as deliberate and grossly 190 | negligent acts) or agreed to in writing, shall any Contributor be 191 | liable to You for damages, including any direct, indirect, special, 192 | incidental, or consequential damages of any character arising as a 193 | result of this License or out of the use or inability to use the 194 | Work (including but not limited to damages for loss of goodwill, 195 | work stoppage, computer failure or malfunction, or any and all 196 | other commercial damages or losses), even if such Contributor 197 | has been advised of the possibility of such damages. 198 |

199 |

9. Accepting Warranty or Additional Liability. 200 | While redistributing 201 | the Work or Derivative Works thereof, You may choose to offer, 202 | and charge a fee for, acceptance of support, warranty, indemnity, 203 | or other liability obligations and/or rights consistent with this 204 | License. However, in accepting such obligations, You may act only 205 | on Your own behalf and on Your sole responsibility, not on behalf 206 | of any other Contributor, and only if You agree to indemnify, 207 | defend, and hold each Contributor harmless for any liability 208 | incurred by, or claims asserted against, such Contributor by reason 209 | of your accepting any such warranty or additional liability. 210 |

211 |

212 | END OF TERMS AND CONDITIONS 213 |

214 | 215 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/alt236/webviewdebug/webviewclient/DebugWebViewClientLogger.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Bitmap; 5 | import android.net.Uri; 6 | import android.net.http.SslError; 7 | import android.os.Build; 8 | import android.os.Message; 9 | import android.view.InputEvent; 10 | import android.view.KeyEvent; 11 | import android.webkit.ClientCertRequest; 12 | import android.webkit.HttpAuthHandler; 13 | import android.webkit.RenderProcessGoneDetail; 14 | import android.webkit.SafeBrowsingResponse; 15 | import android.webkit.SslErrorHandler; 16 | import android.webkit.WebResourceError; 17 | import android.webkit.WebResourceRequest; 18 | import android.webkit.WebResourceResponse; 19 | import android.webkit.WebView; 20 | 21 | import java.util.Locale; 22 | 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.RequiresApi; 25 | import androidx.annotation.VisibleForTesting; 26 | import uk.co.alt236.webviewdebug.BuildConfig; 27 | 28 | @SuppressWarnings("WeakerAccess") 29 | public class DebugWebViewClientLogger implements LogControl { 30 | private static final Locale LOCALE = Locale.US; 31 | private static final String IN = "--->"; 32 | private static final String OUT = "<---"; 33 | private static final String SPACE = " "; 34 | private static final String DEFAULT_TAG = BuildConfig.DEFAULT_LOG_TAG; 35 | 36 | private final LogEngine logger; 37 | private boolean loggingEnabled; 38 | private boolean logKeyEventsEnabled; 39 | 40 | public DebugWebViewClientLogger() { 41 | this(DEFAULT_TAG); 42 | } 43 | 44 | public DebugWebViewClientLogger(@NonNull final String tag) { 45 | this(new LogEngine(tag)); 46 | } 47 | 48 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 49 | protected DebugWebViewClientLogger(@NonNull final LogEngine logEngine) { 50 | this.logger = logEngine; 51 | } 52 | 53 | @RequiresApi(api = Build.VERSION_CODES.M) 54 | public void onReceivedError(final WebView view, final WebResourceRequest request, final WebResourceError error) { 55 | if (loggingEnabled) { 56 | final Uri url = request.getUrl(); 57 | final String method = request.getMethod(); 58 | final int code = error.getErrorCode(); 59 | 60 | logger.logError(String.format(LOCALE, "%s onReceivedError() 1/3 CALL : %d %s %s", SPACE, code, method, url)); 61 | logger.logError(String.format(LOCALE, "%s onReceivedError() 2/3 REQ HEADERS: %s", SPACE, request.getRequestHeaders())); 62 | logger.logError(String.format(LOCALE, "%s onReceivedError() 3/3 ERR DESC : %s", SPACE, error.getDescription())); 63 | } 64 | } 65 | 66 | public void onReceivedError(final WebView view, final int errorCode, final String description, final String failingUrl) { 67 | if (loggingEnabled) { 68 | logger.logError(String.format(LOCALE, "%s onReceivedError() 1/2 CALL: %d %s", SPACE, errorCode, failingUrl)); 69 | logger.logError(String.format(LOCALE, "%s onReceivedError() 2/2 ERR : %s", SPACE, description)); 70 | } 71 | } 72 | 73 | @RequiresApi(api = Build.VERSION_CODES.M) 74 | public void onReceivedHttpError(final WebView view, final WebResourceRequest request, final WebResourceResponse errorResponse) { 75 | if (loggingEnabled) { 76 | final Uri url = request.getUrl(); 77 | final int code = errorResponse.getStatusCode(); 78 | final String method = request.getMethod(); 79 | 80 | logger.logError(String.format(LOCALE, "%s onReceivedHttpError() 1/4 CALL : %d %s %s", SPACE, code, method, url)); 81 | logger.logError(String.format(LOCALE, "%s onReceivedHttpError() 2/4 REQ HEADERS: %s", SPACE, request.getRequestHeaders())); 82 | logger.logError(String.format(LOCALE, "%s onReceivedHttpError() 3/4 ERR DESC : %s", SPACE, errorResponse.getReasonPhrase())); 83 | logger.logError(String.format(LOCALE, "%s onReceivedHttpError() 4/4 ERR HEADERS: %s", SPACE, errorResponse.getResponseHeaders())); 84 | } 85 | } 86 | 87 | public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) { 88 | if (loggingEnabled) { 89 | logger.logError(String.format(LOCALE, "%s onReceivedSslError() ERR: %s", SPACE, error)); 90 | } 91 | } 92 | 93 | @RequiresApi(api = Build.VERSION_CODES.N) 94 | public void shouldOverrideUrlLoading(WebView view, WebResourceRequest request, boolean retVal) { 95 | if (loggingEnabled) { 96 | final Uri url = request.getUrl(); 97 | final String method = request.getMethod(); 98 | final boolean redirect = request.isRedirect(); 99 | final boolean mainframe = request.isForMainFrame(); 100 | final boolean gesture = request.hasGesture(); 101 | 102 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 1/4 CALL : %s %s", SPACE, method, url)); 103 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=%s, forMainFrame=%s, hasGesture=%s", SPACE, redirect, mainframe, gesture)); 104 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 3/4 REQ HEADERS: %s", SPACE, request.getRequestHeaders())); 105 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 4/4 OVERRIDE : %s", SPACE, retVal)); 106 | } 107 | } 108 | 109 | public void shouldOverrideUrlLoading(WebView view, String url, boolean retVal) { 110 | if (loggingEnabled) { 111 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 1/2 CALL : %s", SPACE, url)); 112 | logger.log(String.format(LOCALE, "%s shouldOverrideUrlLoading() 2/2 OVERRIDE: %s", SPACE, retVal)); 113 | } 114 | } 115 | 116 | public void onLoadResource(WebView view, String url) { 117 | if (loggingEnabled) { 118 | logger.log(String.format(LOCALE, "%s onLoadResource() %s", SPACE, url)); 119 | } 120 | } 121 | 122 | @TargetApi(Build.VERSION_CODES.M) 123 | public void onPageCommitVisible(WebView view, String url) { 124 | if (loggingEnabled) { 125 | logger.log(String.format(LOCALE, "%s onPageCommitVisible() %s", SPACE, url)); 126 | } 127 | } 128 | 129 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 130 | public void shouldInterceptRequest(WebView view, String url, WebResourceResponse retVal) { 131 | if (loggingEnabled) { 132 | final String result = retVal == null ? "false" : StringUtils.toString(retVal); 133 | 134 | logger.log(String.format(LOCALE, "%s shouldInterceptRequest() 1/2 CALL : %s", SPACE, url)); 135 | logger.log(String.format(LOCALE, "%s shouldInterceptRequest() 2/2 OVERRIDE: %s", SPACE, url, result)); 136 | } 137 | } 138 | 139 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 140 | public void shouldInterceptRequest(WebView view, WebResourceRequest request, final WebResourceResponse retVal) { 141 | if (loggingEnabled) { 142 | final Uri url = request.getUrl(); 143 | final String method = request.getMethod(); 144 | final String result = retVal == null ? "false" : StringUtils.toString(retVal); 145 | 146 | logger.log(String.format(LOCALE, "%s shouldInterceptRequest() 1/3 CALL : %s %s", SPACE, method, url)); 147 | logger.log(String.format(LOCALE, "%s shouldInterceptRequest() 2/3 REQ HEADERS: %s", SPACE, request.getRequestHeaders())); 148 | logger.log(String.format(LOCALE, "%s shouldInterceptRequest() 3/3 INTERCEPT : %s", SPACE, result)); 149 | } 150 | } 151 | 152 | public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) { 153 | if (loggingEnabled) { 154 | logger.logError(String.format(LOCALE, "%s onTooManyRedirects()", SPACE)); 155 | } 156 | } 157 | 158 | public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { 159 | if (loggingEnabled) { 160 | logger.logSecurity(String.format(LOCALE, "%s onReceivedHttpAuthRequest() %s %s %s", SPACE, host, realm, handler)); 161 | } 162 | } 163 | 164 | public void onPageStarted(WebView view, String url, Bitmap facIcon) { 165 | if (loggingEnabled) { 166 | logger.log(String.format(LOCALE, "%s onPageStarted() %s", IN, url)); 167 | } 168 | } 169 | 170 | public void onPageFinished(WebView view, String url) { 171 | if (loggingEnabled) { 172 | logger.log(String.format(LOCALE, "%s onPageFinished() %s", OUT, url)); 173 | } 174 | } 175 | 176 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 177 | public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { 178 | if (loggingEnabled) { 179 | logger.logSecurity(String.format(LOCALE, "%s onReceivedClientCertRequest() %s", SPACE, StringUtils.toString(request))); 180 | } 181 | } 182 | 183 | public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { 184 | if (loggingEnabled) { 185 | logger.log(String.format(LOCALE, "%s onFormResubmission()", SPACE)); 186 | } 187 | } 188 | 189 | public void doUpdateVisitedHistory(final WebView view, final String url, final boolean isReload) { 190 | if (loggingEnabled) { 191 | logger.log(String.format(LOCALE, "%s doUpdateVisitedHistory() %s, isReload: %s", SPACE, url, isReload)); 192 | } 193 | } 194 | 195 | public void shouldOverrideKeyEvent(final WebView view, final KeyEvent event, boolean retVal) { 196 | if (loggingEnabled && logKeyEventsEnabled) { 197 | logger.logKeyEvent(String.format(LOCALE, "%s shouldOverrideKeyEvent() 1/2 EVENT : %s", SPACE, event)); 198 | logger.logKeyEvent(String.format(LOCALE, "%s shouldOverrideKeyEvent() 2/2 OVERRIDE: %s", SPACE, retVal)); 199 | } 200 | } 201 | 202 | public void onUnhandledInputEvent(final WebView view, final InputEvent event) { 203 | if (loggingEnabled && logKeyEventsEnabled) { 204 | logger.logKeyEvent(String.format(LOCALE, "%s onUnhandledInputEvent() %s", SPACE, event)); 205 | } 206 | } 207 | 208 | public void onUnhandledKeyEvent(final WebView view, final KeyEvent event) { 209 | if (loggingEnabled && logKeyEventsEnabled) { 210 | logger.logKeyEvent(String.format(LOCALE, "%s onUnhandledKeyEvent() %s", SPACE, event)); 211 | } 212 | } 213 | 214 | public void onScaleChanged(final WebView view, final float oldScale, final float newScale) { 215 | if (loggingEnabled) { 216 | logger.log(String.format(LOCALE, "%s onScaleChanged() old: %s, new: %s", SPACE, oldScale, newScale)); 217 | } 218 | } 219 | 220 | public void onReceivedLoginRequest(final WebView view, final String realm, final String account, final String args) { 221 | if (loggingEnabled) { 222 | logger.logSecurity(String.format(LOCALE, "%s onReceivedLoginRequest() %s, %s, %s", SPACE, realm, account, args)); 223 | } 224 | } 225 | 226 | @RequiresApi(api = Build.VERSION_CODES.O) 227 | public void onRenderProcessGone(final WebView view, final RenderProcessGoneDetail detail, boolean retVal) { 228 | if (loggingEnabled) { 229 | logger.log(String.format(LOCALE, "%s onRenderProcessGone() 1/2 DETAIL: %s", SPACE, detail)); 230 | logger.log(String.format(LOCALE, "%s onRenderProcessGone() 2/2 RESULT: %s", SPACE, retVal)); 231 | } 232 | } 233 | 234 | @RequiresApi(api = Build.VERSION_CODES.O_MR1) 235 | public void onSafeBrowsingHit(final WebView view, final WebResourceRequest request, final int threatType, final SafeBrowsingResponse callback) { 236 | if (loggingEnabled) { 237 | final Uri url = request.getUrl(); 238 | final String method = request.getMethod(); 239 | final String threat = StringUtils.resolveThreatType(threatType); 240 | 241 | logger.log(String.format(LOCALE, "%s onSafeBrowsingHit() 1/3 CALL : %s %s", SPACE, method, url)); 242 | logger.log(String.format(LOCALE, "%s onSafeBrowsingHit() 2/3 REQ HEADERS: %s", SPACE, request.getRequestHeaders())); 243 | logger.log(String.format(LOCALE, "%s onSafeBrowsingHit() 3/3 THREAT : %s", SPACE, threat)); 244 | } 245 | } 246 | 247 | @Override 248 | public boolean isLoggingEnabled() { 249 | return loggingEnabled; 250 | } 251 | 252 | @Override 253 | public void setLoggingEnabled(final boolean enabled) { 254 | this.loggingEnabled = enabled; 255 | } 256 | 257 | @Override 258 | public boolean isLogKeyEventsEnabled() { 259 | return logKeyEventsEnabled; 260 | } 261 | 262 | @Override 263 | public void setLogKeyEventsEnabled(final boolean enabled) { 264 | this.logKeyEventsEnabled = enabled; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /library/src/test/java/uk/co/alt236/webviewdebug/webviewclient/DebugWebViewClientLoggerTest.java: -------------------------------------------------------------------------------- 1 | package uk.co.alt236.webviewdebug.webviewclient; 2 | 3 | import android.graphics.Bitmap; 4 | import android.net.http.SslError; 5 | import android.os.Build; 6 | import android.os.Message; 7 | import android.view.InputEvent; 8 | import android.view.KeyEvent; 9 | import android.webkit.ClientCertRequest; 10 | import android.webkit.HttpAuthHandler; 11 | import android.webkit.RenderProcessGoneDetail; 12 | import android.webkit.SafeBrowsingResponse; 13 | import android.webkit.SslErrorHandler; 14 | import android.webkit.WebResourceError; 15 | import android.webkit.WebResourceRequest; 16 | import android.webkit.WebResourceResponse; 17 | import android.webkit.WebView; 18 | 19 | import org.junit.After; 20 | import org.junit.Before; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.mockito.Mockito; 24 | import org.robolectric.RobolectricTestRunner; 25 | import org.robolectric.annotation.Config; 26 | 27 | import static junit.framework.Assert.assertFalse; 28 | import static junit.framework.Assert.assertTrue; 29 | 30 | @RunWith(RobolectricTestRunner.class) 31 | @Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.O) 32 | public class DebugWebViewClientLoggerTest { 33 | 34 | private WebView webView; 35 | private LogEngine logEngine; 36 | private DebugWebViewClientLogger logger; 37 | 38 | @Before 39 | public void setUp() { 40 | webView = Mockito.mock(WebView.class); 41 | logEngine = Mockito.mock(LogEngine.class); 42 | logger = new DebugWebViewClientLogger(logEngine); 43 | } 44 | 45 | @After 46 | public void tearDown() { 47 | webView = null; 48 | logEngine = null; 49 | logger = null; 50 | } 51 | 52 | @Test 53 | public void onReceivedError() { 54 | final int code = 500; 55 | final String message = "foo"; 56 | final String url = "bar"; 57 | 58 | logger.setLoggingEnabled(false); 59 | logger.onReceivedError(webView, code, message, url); 60 | verifyLogNotCalled(); 61 | 62 | logger.setLoggingEnabled(true); 63 | logger.onReceivedError(webView, code, message, url); 64 | verifyLogEngine().logError(Mockito.anyString()); 65 | } 66 | 67 | @Test 68 | public void onReceivedError_api23() { 69 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 70 | final WebResourceError error = Mockito.mock(WebResourceError.class); 71 | 72 | logger.setLoggingEnabled(false); 73 | logger.onReceivedError(webView, request, error); 74 | verifyLogNotCalled(); 75 | 76 | logger.setLoggingEnabled(true); 77 | logger.onReceivedError(webView, request, error); 78 | verifyLogEngine().logError(Mockito.anyString()); 79 | } 80 | 81 | @Test 82 | public void onReceivedHttpError() { 83 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 84 | final WebResourceResponse response = Mockito.mock(WebResourceResponse.class); 85 | 86 | logger.setLoggingEnabled(false); 87 | logger.onReceivedHttpError(webView, request, response); 88 | verifyLogNotCalled(); 89 | 90 | logger.setLoggingEnabled(true); 91 | logger.onReceivedHttpError(webView, request, response); 92 | verifyLogEngine().logError(Mockito.anyString()); 93 | } 94 | 95 | @Test 96 | public void onReceivedSslError() { 97 | final SslErrorHandler errorHandler = Mockito.mock(SslErrorHandler.class); 98 | final SslError sslError = Mockito.mock(SslError.class); 99 | 100 | logger.setLoggingEnabled(false); 101 | logger.onReceivedSslError(webView, errorHandler, sslError); 102 | verifyLogNotCalled(); 103 | 104 | logger.setLoggingEnabled(true); 105 | logger.onReceivedSslError(webView, errorHandler, sslError); 106 | verifyLogEngine().logError(Mockito.anyString()); 107 | } 108 | 109 | @Test 110 | public void shouldOverrideUrlLoading() { 111 | final String url = "foo"; 112 | 113 | logger.setLoggingEnabled(false); 114 | logger.shouldOverrideUrlLoading(webView, url, false); 115 | verifyLogNotCalled(); 116 | 117 | logger.setLoggingEnabled(true); 118 | logger.shouldOverrideUrlLoading(webView, url, false); 119 | verifyLogEngine().log(Mockito.anyString()); 120 | } 121 | 122 | @Test 123 | public void shouldOverrideUrlLoading1() { 124 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 125 | 126 | logger.setLoggingEnabled(false); 127 | logger.shouldOverrideUrlLoading(webView, request, false); 128 | verifyLogNotCalled(); 129 | 130 | logger.setLoggingEnabled(true); 131 | logger.shouldOverrideUrlLoading(webView, request, false); 132 | verifyLogEngine().log(Mockito.anyString()); 133 | } 134 | 135 | @Test 136 | public void onLoadResource() { 137 | final String url = "foo"; 138 | 139 | logger.setLoggingEnabled(false); 140 | logger.onLoadResource(webView, url); 141 | verifyLogNotCalled(); 142 | 143 | logger.setLoggingEnabled(true); 144 | logger.onLoadResource(webView, url); 145 | verifyLogEngine().log(Mockito.anyString()); 146 | } 147 | 148 | @Test 149 | public void onPageCommitVisible() { 150 | final String url = "foo"; 151 | 152 | logger.setLoggingEnabled(false); 153 | logger.onPageCommitVisible(webView, url); 154 | verifyLogNotCalled(); 155 | 156 | logger.setLoggingEnabled(true); 157 | logger.onPageCommitVisible(webView, url); 158 | verifyLogEngine().log(Mockito.anyString()); 159 | } 160 | 161 | @Test 162 | public void shouldInterceptRequest() { 163 | final String url = "foo"; 164 | final WebResourceResponse response = Mockito.mock(WebResourceResponse.class); 165 | 166 | logger.setLoggingEnabled(false); 167 | logger.shouldInterceptRequest(webView, url, response); 168 | verifyLogNotCalled(); 169 | 170 | logger.setLoggingEnabled(true); 171 | logger.shouldInterceptRequest(webView, url, response); 172 | verifyLogEngine().log(Mockito.anyString()); 173 | } 174 | 175 | @Test 176 | public void shouldInterceptRequest_api21() { 177 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 178 | final WebResourceResponse response = Mockito.mock(WebResourceResponse.class); 179 | 180 | logger.setLoggingEnabled(false); 181 | logger.shouldInterceptRequest(webView, request, response); 182 | verifyLogNotCalled(); 183 | 184 | logger.setLoggingEnabled(true); 185 | logger.shouldInterceptRequest(webView, request, response); 186 | verifyLogEngine().log(Mockito.anyString()); 187 | } 188 | 189 | @Test 190 | public void onTooManyRedirects() { 191 | final Message cancelMsg = Mockito.mock(Message.class); 192 | final Message continueMsg = Mockito.mock(Message.class); 193 | 194 | logger.setLoggingEnabled(false); 195 | logger.onTooManyRedirects(webView, cancelMsg, continueMsg); 196 | verifyLogNotCalled(); 197 | 198 | logger.setLoggingEnabled(true); 199 | logger.onTooManyRedirects(webView, cancelMsg, continueMsg); 200 | verifyLogEngine().logError(Mockito.anyString()); 201 | } 202 | 203 | @Test 204 | public void onReceivedHttpAuthRequest() { 205 | final HttpAuthHandler handler = Mockito.mock(HttpAuthHandler.class); 206 | final String host = "foo"; 207 | final String realm = "bar"; 208 | 209 | logger.setLoggingEnabled(false); 210 | logger.onReceivedHttpAuthRequest(webView, handler, host, realm); 211 | verifyLogNotCalled(); 212 | 213 | logger.setLoggingEnabled(true); 214 | logger.onReceivedHttpAuthRequest(webView, handler, host, realm); 215 | verifyLogEngine().logSecurity(Mockito.anyString()); 216 | } 217 | 218 | @Test 219 | public void onPageStarted() { 220 | final String url = "foo"; 221 | final Bitmap bitmap = Mockito.mock(Bitmap.class); 222 | 223 | logger.setLoggingEnabled(false); 224 | logger.onPageStarted(webView, url, bitmap); 225 | verifyLogNotCalled(); 226 | 227 | logger.setLoggingEnabled(true); 228 | logger.onPageStarted(webView, url, bitmap); 229 | verifyLogEngine().log(Mockito.anyString()); 230 | } 231 | 232 | @Test 233 | public void onPageFinished() { 234 | final String url = "foo"; 235 | 236 | logger.setLoggingEnabled(false); 237 | logger.onPageFinished(webView, url); 238 | verifyLogNotCalled(); 239 | 240 | logger.setLoggingEnabled(true); 241 | logger.onPageFinished(webView, url); 242 | verifyLogEngine().log(Mockito.anyString()); 243 | } 244 | 245 | @Test 246 | public void onReceivedClientCertRequest() { 247 | final ClientCertRequest request = Mockito.mock(ClientCertRequest.class); 248 | 249 | logger.setLoggingEnabled(false); 250 | logger.onReceivedClientCertRequest(webView, request); 251 | verifyLogNotCalled(); 252 | 253 | logger.setLoggingEnabled(true); 254 | logger.onReceivedClientCertRequest(webView, request); 255 | verifyLogEngine().logSecurity(Mockito.anyString()); 256 | } 257 | 258 | @Test 259 | public void onFormResubmission() { 260 | final Message dontResend = Mockito.mock(Message.class); 261 | final Message resend = Mockito.mock(Message.class); 262 | 263 | logger.setLoggingEnabled(false); 264 | logger.onFormResubmission(webView, dontResend, resend); 265 | verifyLogNotCalled(); 266 | 267 | logger.setLoggingEnabled(true); 268 | logger.onFormResubmission(webView, dontResend, resend); 269 | verifyLogEngine().log(Mockito.anyString()); 270 | } 271 | 272 | @Test 273 | public void doUpdateVisitedHistory() { 274 | final String url = "foo"; 275 | final boolean reload = true; 276 | 277 | logger.setLoggingEnabled(false); 278 | logger.doUpdateVisitedHistory(webView, url, reload); 279 | verifyLogNotCalled(); 280 | 281 | logger.setLoggingEnabled(true); 282 | logger.doUpdateVisitedHistory(webView, url, reload); 283 | verifyLogEngine().log(Mockito.anyString()); 284 | } 285 | 286 | @Test 287 | public void shouldOverrideKeyEvent() { 288 | final KeyEvent keyEvent = Mockito.mock(KeyEvent.class); 289 | 290 | logger.setLoggingEnabled(false); 291 | logger.setLogKeyEventsEnabled(false); 292 | logger.shouldOverrideKeyEvent(webView, keyEvent, false); 293 | verifyLogNotCalled(); 294 | 295 | logger.setLoggingEnabled(false); 296 | logger.setLogKeyEventsEnabled(true); 297 | logger.shouldOverrideKeyEvent(webView, keyEvent, false); 298 | verifyLogNotCalled(); 299 | 300 | logger.setLoggingEnabled(true); 301 | logger.setLogKeyEventsEnabled(false); 302 | logger.shouldOverrideKeyEvent(webView, keyEvent, false); 303 | verifyLogNotCalled(); 304 | 305 | logger.setLoggingEnabled(true); 306 | logger.setLogKeyEventsEnabled(true); 307 | logger.shouldOverrideKeyEvent(webView, keyEvent, false); 308 | verifyLogEngine().logKeyEvent(Mockito.anyString()); 309 | } 310 | 311 | @Test 312 | public void onUnhandledInputEvent() { 313 | final InputEvent inputEvent = Mockito.mock(InputEvent.class); 314 | 315 | logger.setLoggingEnabled(false); 316 | logger.setLogKeyEventsEnabled(false); 317 | logger.onUnhandledInputEvent(webView, inputEvent); 318 | verifyLogNotCalled(); 319 | 320 | logger.setLoggingEnabled(false); 321 | logger.setLogKeyEventsEnabled(true); 322 | logger.onUnhandledInputEvent(webView, inputEvent); 323 | verifyLogNotCalled(); 324 | 325 | logger.setLoggingEnabled(true); 326 | logger.setLogKeyEventsEnabled(false); 327 | logger.onUnhandledInputEvent(webView, inputEvent); 328 | verifyLogNotCalled(); 329 | 330 | logger.setLoggingEnabled(true); 331 | logger.setLogKeyEventsEnabled(true); 332 | logger.onUnhandledInputEvent(webView, inputEvent); 333 | verifyLogEngine().logKeyEvent(Mockito.anyString()); 334 | } 335 | 336 | @Test 337 | public void onUnhandledKeyEvent() { 338 | final KeyEvent keyEvent = Mockito.mock(KeyEvent.class); 339 | 340 | logger.setLoggingEnabled(false); 341 | logger.setLogKeyEventsEnabled(false); 342 | logger.onUnhandledKeyEvent(webView, keyEvent); 343 | verifyLogNotCalled(); 344 | 345 | logger.setLoggingEnabled(false); 346 | logger.setLogKeyEventsEnabled(true); 347 | logger.onUnhandledKeyEvent(webView, keyEvent); 348 | verifyLogNotCalled(); 349 | 350 | logger.setLoggingEnabled(true); 351 | logger.setLogKeyEventsEnabled(false); 352 | logger.onUnhandledKeyEvent(webView, keyEvent); 353 | verifyLogNotCalled(); 354 | 355 | logger.setLoggingEnabled(true); 356 | logger.setLogKeyEventsEnabled(true); 357 | logger.onUnhandledKeyEvent(webView, keyEvent); 358 | verifyLogEngine().logKeyEvent(Mockito.anyString()); 359 | } 360 | 361 | @Test 362 | public void onScaleChanged() { 363 | final float oldScale = 1.0f; 364 | final float newScale = 2.0f; 365 | 366 | logger.setLoggingEnabled(false); 367 | logger.onScaleChanged(webView, oldScale, newScale); 368 | verifyLogNotCalled(); 369 | 370 | logger.setLoggingEnabled(true); 371 | logger.onScaleChanged(webView, oldScale, newScale); 372 | verifyLogEngine().log(Mockito.anyString()); 373 | } 374 | 375 | @Test 376 | public void onReceivedLoginRequest() { 377 | final String realm = "realm"; 378 | final String account = "account"; 379 | final String args = "args"; 380 | 381 | logger.setLoggingEnabled(false); 382 | logger.onReceivedLoginRequest(webView, realm, account, args); 383 | verifyLogNotCalled(); 384 | 385 | logger.setLoggingEnabled(true); 386 | logger.onReceivedLoginRequest(webView, realm, account, args); 387 | verifyLogEngine().logSecurity(Mockito.anyString()); 388 | } 389 | 390 | @Test 391 | public void onRenderProcessGone() { 392 | final RenderProcessGoneDetail detail = Mockito.mock(RenderProcessGoneDetail.class); 393 | 394 | logger.setLoggingEnabled(false); 395 | logger.onRenderProcessGone(webView, detail, false); 396 | verifyLogNotCalled(); 397 | 398 | logger.setLoggingEnabled(true); 399 | logger.onRenderProcessGone(webView, detail, false); 400 | verifyLogEngine().log(Mockito.anyString()); 401 | } 402 | 403 | @Test 404 | public void onSafeBrowsingHit() { 405 | final WebResourceRequest request = Mockito.mock(WebResourceRequest.class); 406 | final SafeBrowsingResponse callback = Mockito.mock(SafeBrowsingResponse.class); 407 | final int threatType = -1; 408 | 409 | logger.setLoggingEnabled(false); 410 | logger.onSafeBrowsingHit(webView, request, threatType, callback); 411 | verifyLogNotCalled(); 412 | 413 | logger.setLoggingEnabled(true); 414 | logger.onSafeBrowsingHit(webView, request, threatType, callback); 415 | verifyLogEngine().log(Mockito.anyString()); 416 | } 417 | 418 | @Test 419 | public void testGettersAndSetters() { 420 | assertFalse(logger.isLoggingEnabled()); 421 | logger.setLoggingEnabled(true); 422 | assertTrue(logger.isLoggingEnabled()); 423 | 424 | assertFalse(logger.isLogKeyEventsEnabled()); 425 | logger.setLogKeyEventsEnabled(true); 426 | assertTrue(logger.isLogKeyEventsEnabled()); 427 | } 428 | 429 | private void verifyLogNotCalled() { 430 | Mockito.verify(logEngine, Mockito.never()).log(Mockito.anyString()); 431 | Mockito.verify(logEngine, Mockito.never()).logError(Mockito.anyString()); 432 | Mockito.verify(logEngine, Mockito.never()).logSecurity(Mockito.anyString()); 433 | Mockito.verify(logEngine, Mockito.never()).logKeyEvent(Mockito.anyString()); 434 | } 435 | 436 | private LogEngine verifyLogEngine() { 437 | return Mockito.verify(logEngine, Mockito.atLeastOnce()); 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WEBVIEW DEBUG 2 | [![Android Arsenal]( https://img.shields.io/badge/Android%20Arsenal-webviewdebug-green.svg?style=flat )]( https://android-arsenal.com/details/3/6436 ) 3 | 4 | Provides a logging wrapper around a WebViewClient, in order to figure out what is going on. 5 | 6 | This happens by creating a `DebugWebViewClient` which logs events and passes them to an enclosed `WebViewClient`. 7 | 8 | ## Warning 9 | The `DebugWebViewClient` is implementing all `WebViewClient` up to API 26. If your `WebViewClient` is implementing a method that the `DebugWebViewClient` does not, and that method is critical for your business logic, then your app will probably not work properly. 10 | 11 | When a `DebugWebViewClient` is initialised, it will print in log a list of all methods that are declared in the passed `WebViewClient` class and any parents and are NOT overridden. 12 | 13 | For as long as your app is does not need any of the listed, non-overridden, methods, then there won't be a problem. 14 | 15 | ## Getting the Library 16 | 17 | gradle 18 | ```groovy 19 | repositories { 20 | maven { 21 | url "https://dl.bintray.com/alt236/maven" 22 | } 23 | } 24 | 25 | dependencies { 26 | compile 'uk.co.alt236:webviewdebug:1.0.0' 27 | } 28 | ``` 29 | 30 | ## Usage 31 | 32 | Output in logcat uses this tag: `DebugWVClient`. 33 | 34 | ### Debugging a WebViewClient 35 | ###### 1. Fast way if you already have a WebViewClient 36 | If you already have a `WebViewClient` implementation, wrap it with `DebugWebViewClient` before assigning it to the WebView. 37 | 38 | ```java 39 | final DebugWebViewClient debugWebViewClient = new DebugWebViewClient(new MyCustomWebViewClient()); 40 | debugWebViewClient.setLoggingEnabled(true); 41 | webView.setWebViewClient(debugWebViewClient); 42 | ``` 43 | 44 | ###### 2. You have a custom WebViewClient but want more control 45 | You can use `DebugWebViewClientLogger` to log things as needed in your own `WebViewClient`. 46 | 47 | Make sure you pass the parameters and any return values of your own `WebViewClient` to the equivalent methods of the `DebugWebViewClientLogger`. 48 | 49 | ###### 3. You don't have a WebViewClient but you want to know what is going on 50 | Just instantiate and assign a `DebugWebViewClient`to the WebView. 51 | 52 | ```java 53 | final DebugWebViewClient debugWebViewClient = new DebugWebViewClient(); 54 | debugWebViewClient.setLoggingEnabled(true); 55 | webView.setWebViewClient(debugWebViewClient); 56 | ``` 57 | 58 | ### Controlling output 59 | Both `DebugWebViewClient` and `DebugWebViewClientLogger` implemetn `LogControl` which contains the following signatures: 60 | 61 | * `isLoggingEnabled()`: Check if logging is globally enabled 62 | * `setLoggingEnabled(boolean)`: Enable or disable logging 63 | * `isLogKeyEventsEnabled()`: Check if logging of `KeyEvent` related methods is enabled 64 | * `setLogKeyEventsEnabled(boolean)`: Enable or disable logging of `KeyEvent` related methods is enabled 65 | 66 | `KeyEvent` related methods have more granularity due to privacy concerns, as all keystrokes will be logged. 67 | 68 | `setLoggingEnabled(boolean)` is a global switch which overrides `setLogKeyEventsEnabled(boolean)` 69 | 70 | ## Sample output 71 | ``` 72 | D/DebugWVClient: All methods implemented :) 73 | I/DebugWVClient: ---> onPageStarted() http://www.google.com/ 74 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET http://www.google.com/ 75 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8, Upgrade-Insecure-Requests=1} 76 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 77 | I/DebugWVClient: onLoadResource() http://www.google.com/ 78 | I/DebugWVClient: ---> onPageStarted() http://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg 79 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET http://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg 80 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=true, forMainFrame=true, hasGesture=false 81 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 82 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 83 | I/DebugWVClient: ---> onPageStarted() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl 84 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl 85 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=true, forMainFrame=true, hasGesture=false 86 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 87 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 88 | I/DebugWVClient: doUpdateVisitedHistory() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl, isReload: false 89 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.co.uk/images/hpp/gsa_super_g-64.gif 90 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl} 91 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 92 | I/DebugWVClient: onLoadResource() https://www.google.co.uk/images/hpp/gsa_super_g-64.gif 93 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.co.uk/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png 94 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl} 95 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 96 | I/DebugWVClient: onLoadResource() https://www.google.co.uk/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png 97 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://ssl.gstatic.com/gb/images/qi2_00ed8ca1.png 98 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl} 99 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 100 | I/DebugWVClient: onLoadResource() https://ssl.gstatic.com/gb/images/qi2_00ed8ca1.png 101 | I/DebugWVClient: onPageCommitVisible() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl 102 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.co.uk/images/nav_logo242_hr.webp 103 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl} 104 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 105 | I/DebugWVClient: onLoadResource() https://www.google.co.uk/images/nav_logo242_hr.webp 106 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.co.uk/images/branding/product/1x/gsa_android_144dp.png 107 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl} 108 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 109 | I/DebugWVClient: <--- onPageFinished() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=sGrWWdSvNq_38AfK37roAg&gws_rd=ssl 110 | I/DebugWVClient: onLoadResource() https://www.google.co.uk/images/branding/product/1x/gsa_android_144dp.png 111 | I/DebugWVClient: All methods implemented :) 112 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET http://www.google.com/ 113 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8, Upgrade-Insecure-Requests=1} 114 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 115 | I/DebugWVClient: ---> onPageStarted() http://www.google.com/ 116 | I/DebugWVClient: onLoadResource() http://www.google.com/ 117 | I/DebugWVClient: ---> onPageStarted() http://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg 118 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET http://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg 119 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=true, forMainFrame=true, hasGesture=false 120 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 121 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 122 | I/DebugWVClient: ---> onPageStarted() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl 123 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl 124 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=true, forMainFrame=true, hasGesture=false 125 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 126 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 127 | I/DebugWVClient: doUpdateVisitedHistory() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl, isReload: false 128 | I/DebugWVClient: onPageCommitVisible() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl 129 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.co.uk/images/branding/product/1x/gsa_android_144dp.png 130 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl} 131 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 132 | I/DebugWVClient: <--- onPageFinished() https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl 133 | I/DebugWVClient: onLoadResource() https://www.google.co.uk/images/branding/product/1x/gsa_android_144dp.png 134 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET https://www.google.com/url?q=https://store.google.com/gb/%3Fhl%3Den-GB%26countryRedirect%3Dtrue%26utm_source%3Dhpp%26utm_medium%3Dgoogle_oo%26utm_campaign%3DGS100077&source=hpp&id=19003843&ct=3&usg=AFQjCNGBVJRo84BJtfajQmUdhNeb8iYuSQ&sa=X&ved=0ahUKEwjbxISSgNrWAhUKJ8AKHetwDJ0Q8IcBCAk 135 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=false, forMainFrame=true, hasGesture=true 136 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 137 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 138 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.google.com/url?q=https://store.google.com/gb/%3Fhl%3Den-GB%26countryRedirect%3Dtrue%26utm_source%3Dhpp%26utm_medium%3Dgoogle_oo%26utm_campaign%3DGS100077&source=hpp&id=19003843&ct=3&usg=AFQjCNGBVJRo84BJtfajQmUdhNeb8iYuSQ&sa=X&ved=0ahUKEwjbxISSgNrWAhUKJ8AKHetwDJ0Q8IcBCAk 139 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Referer=https://www.google.co.uk/?gfe_rd=cr&dcr=0&ei=AGvWWePPO6_38AfK37roAg&gws_rd=ssl, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8, Upgrade-Insecure-Requests=1} 140 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 141 | I/DebugWVClient: ---> onPageStarted() https://www.google.com/url?q=https://store.google.com/gb/%3Fhl%3Den-GB%26countryRedirect%3Dtrue%26utm_source%3Dhpp%26utm_medium%3Dgoogle_oo%26utm_campaign%3DGS100077&source=hpp&id=19003843&ct=3&usg=AFQjCNGBVJRo84BJtfajQmUdhNeb8iYuSQ&sa=X&ved=0ahUKEwjbxISSgNrWAhUKJ8AKHetwDJ0Q8IcBCAk 142 | I/DebugWVClient: onLoadResource() https://www.google.com/url?q=https://store.google.com/gb/%3Fhl%3Den-GB%26countryRedirect%3Dtrue%26utm_source%3Dhpp%26utm_medium%3Dgoogle_oo%26utm_campaign%3DGS100077&source=hpp&id=19003843&ct=3&usg=AFQjCNGBVJRo84BJtfajQmUdhNeb8iYuSQ&sa=X&ved=0ahUKEwjbxISSgNrWAhUKJ8AKHetwDJ0Q8IcBCAk 143 | I/DebugWVClient: ---> onPageStarted() https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077 144 | I/DebugWVClient: shouldOverrideUrlLoading() 1/4 CALL : GET https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077 145 | I/DebugWVClient: shouldOverrideUrlLoading() 2/4 CALL INFO : redirect=true, forMainFrame=true, hasGesture=false 146 | I/DebugWVClient: shouldOverrideUrlLoading() 3/4 REQ HEADERS: null 147 | I/DebugWVClient: shouldOverrideUrlLoading() 4/4 OVERRIDE : false 148 | I/DebugWVClient: doUpdateVisitedHistory() https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077, isReload: false 149 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.googletagmanager.com/ns.html?id=GTM-MX89MJ 150 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8, Upgrade-Insecure-Requests=1} 151 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 152 | I/DebugWVClient: onLoadResource() https://www.googletagmanager.com/ns.html?id=GTM-MX89MJ 153 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://fonts.gstatic.com/s/productsans/v9/HYvgU2fE2nRJvZ5JFAumwRampu5_7CjHW5spxoeN3Vs.woff2 154 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Origin=https://store.google.com, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077, Accept=*/*} 155 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 156 | I/DebugWVClient: onLoadResource() https://fonts.gstatic.com/s/productsans/v9/HYvgU2fE2nRJvZ5JFAumwRampu5_7CjHW5spxoeN3Vs.woff2 157 | I/DebugWVClient: onPageCommitVisible() https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077 158 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://fonts.gstatic.com/s/productsans/v9/N0c8y_dasvG2CzM7uYqPLshHwsiXhsDb0smKjAA7Bek.woff2 159 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Origin=https://store.google.com, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077, Accept=*/*} 160 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 161 | I/DebugWVClient: onLoadResource() https://fonts.gstatic.com/s/productsans/v9/N0c8y_dasvG2CzM7uYqPLshHwsiXhsDb0smKjAA7Bek.woff2 162 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://lh3.googleusercontent.com/bxB6wR8V43WB9bMVZ0ILjriCFgCT-MNn2Mz9wPxlH1PyaWbCgBsV-EAPzbyRSfxHRNE=w96-h96 163 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 164 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 165 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://lh3.googleusercontent.com/QJ8E3sNKLviIyxol6UNAjnwmAvlta6fzl94f2Hxqj1vnbvB9LyXKfcT1XatulWFgkbvm=w96-h96 166 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 167 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 168 | I/DebugWVClient: onLoadResource() https://lh3.googleusercontent.com/bxB6wR8V43WB9bMVZ0ILjriCFgCT-MNn2Mz9wPxlH1PyaWbCgBsV-EAPzbyRSfxHRNE=w96-h96 169 | I/DebugWVClient: onLoadResource() https://lh3.googleusercontent.com/QJ8E3sNKLviIyxol6UNAjnwmAvlta6fzl94f2Hxqj1vnbvB9LyXKfcT1XatulWFgkbvm=w96-h96 170 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://lh3.googleusercontent.com/k3ZARl_vWPzBgKaFIL279g2_IQRSWbvCK-eQn52APeZavVDw7__iMRZ9h5Tn9YdKc4s=w96-h96 171 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 172 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 173 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://lh3.googleusercontent.com/JB70a3qUIFB2OiIfgNc7qB69N2N3m68oX1XHTRSuaJWNXJY5ITm2m62lqQ_qD5NDcpU=w96-h96 174 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 175 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 176 | I/DebugWVClient: onLoadResource() https://lh3.googleusercontent.com/k3ZARl_vWPzBgKaFIL279g2_IQRSWbvCK-eQn52APeZavVDw7__iMRZ9h5Tn9YdKc4s=w96-h96 177 | I/DebugWVClient: onLoadResource() https://lh3.googleusercontent.com/JB70a3qUIFB2OiIfgNc7qB69N2N3m68oX1XHTRSuaJWNXJY5ITm2m62lqQ_qD5NDcpU=w96-h96 178 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 179 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 180 | I/DebugWVClient: onLoadResource() https://lh3.googleusercontent.com/GlrKLWut6gWN52kyEtFK85r0ER-paG3TdSDfEGqsNfeYW0gcgLUW0ARYyassu3Y-pxs=w96-h96 181 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://fonts.gstatic.com/s/productsans/v9/N0c8y_dasvG2CzM7uYqPLtCODO6R-QMzjsZRstdx6VU.woff2 182 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Origin=https://store.google.com, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077, Accept=*/*} 183 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 184 | I/DebugWVClient: onLoadResource() https://fonts.gstatic.com/s/productsans/v9/N0c8y_dasvG2CzM7uYqPLtCODO6R-QMzjsZRstdx6VU.woff2 185 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.gstatic.com/images/icons/material/system/2x/arrow_forward_googblue_24dp.png 186 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 187 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 188 | I/DebugWVClient: onLoadResource() https://www.gstatic.com/images/icons/material/system/2x/arrow_forward_googblue_24dp.png 189 | I/DebugWVClient: doUpdateVisitedHistory() https://www.googletagmanager.com/ns.html?id=GTM-MX89MJ, isReload: false 190 | I/DebugWVClient: <--- onPageFinished() https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077 191 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png 192 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 193 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 194 | I/DebugWVClient: onLoadResource() https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png 195 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png 196 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 197 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 198 | I/DebugWVClient: onLoadResource() https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png 199 | I/DebugWVClient: shouldInterceptRequest() 1/3 CALL : GET https://www.gstatic.com/images/branding/product/1x/googleg_96dp.png 200 | I/DebugWVClient: shouldInterceptRequest() 2/3 REQ HEADERS: {User-Agent=Mozilla/5.0 (Linux; Android 8.0.0; Android SDK built for x86 Build/OSR1.170720.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/58.0.3029.125 Mobile Safari/537.36, Accept=image/webp,image/*,*/*;q=0.8, Referer=https://store.google.com/gb/?hl=en-GB&countryRedirect=true&utm_source=hpp&utm_medium=google_oo&utm_campaign=GS100077} 201 | I/DebugWVClient: shouldInterceptRequest() 3/3 INTERCEPT : false 202 | I/DebugWVClient: onLoadResource() https://www.gstatic.com/images/branding/product/1x/googleg_96dp.png 203 | 204 | ``` 205 | 206 | ## License 207 | Copyright (C) 2017 Alexandros Schillings 208 | 209 | Licensed under the Apache License, Version 2.0 (the "License"); 210 | you may not use this file except in compliance with the License. 211 | You may obtain a copy of the License at 212 | 213 | http://www.apache.org/licenses/LICENSE-2.0 214 | 215 | Unless required by applicable law or agreed to in writing, software 216 | distributed under the License is distributed on an "AS IS" BASIS, 217 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 218 | See the License for the specific language governing permissions and 219 | limitations under the License. 220 | --------------------------------------------------------------------------------