| Index: libadblockplus-android-webview/src/org/adblockplus/android/AdblockWebView.java |
| diff --git a/libadblockplus-android-webview/src/org/adblockplus/android/AdblockWebView.java b/libadblockplus-android-webview/src/org/adblockplus/android/AdblockWebView.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..d52d0018ee01aef6886bf0cf1a1f839eb5604075 |
| --- /dev/null |
| +++ b/libadblockplus-android-webview/src/org/adblockplus/android/AdblockWebView.java |
| @@ -0,0 +1,1641 @@ |
| +/* |
| + * This file is part of Adblock Plus <https://adblockplus.org/>, |
| + * Copyright (C) 2006-2016 Eyeo GmbH |
| + * |
| + * Adblock Plus is free software: you can redistribute it and/or modify |
| + * it under the terms of the GNU General Public License version 3 as |
| + * published by the Free Software Foundation. |
| + * |
| + * Adblock Plus is distributed in the hope that it will be useful, |
| + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| + * GNU General Public License for more details. |
| + * |
| + * You should have received a copy of the GNU General Public License |
| + * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
| + */ |
| + |
| +package org.adblockplus.android; |
| + |
| +import org.adblockplus.libadblockplus.AppInfo; |
| +import org.adblockplus.libadblockplus.Filter; |
| +import org.adblockplus.libadblockplus.FilterEngine; |
| +import org.adblockplus.libadblockplus.JsEngine; |
| +import org.adblockplus.libadblockplus.Subscription; |
| + |
| +import android.annotation.TargetApi; |
| +import android.content.Context; |
| +import android.content.pm.PackageInfo; |
| +import android.content.pm.PackageManager; |
| +import android.graphics.Bitmap; |
| +import android.graphics.Canvas; |
| +import android.graphics.Color; |
| +import android.net.http.SslError; |
| +import android.os.Build; |
| +import android.os.Handler; |
| +import android.os.Message; |
| +import android.util.AttributeSet; |
| +import android.util.Log; |
| +import android.view.KeyEvent; |
| +import android.view.View; |
| +import android.webkit.ConsoleMessage; |
| +import android.webkit.GeolocationPermissions; |
| +import android.webkit.HttpAuthHandler; |
| +import android.webkit.JsPromptResult; |
| +import android.webkit.JsResult; |
| +import android.webkit.SslErrorHandler; |
| +import android.webkit.ValueCallback; |
| +import android.webkit.WebChromeClient; |
| +import android.webkit.WebResourceRequest; // makes android min version to be 21 |
| +import android.webkit.WebResourceResponse; |
| +import android.webkit.WebStorage; |
| +import android.webkit.WebView; |
| +import android.webkit.JavascriptInterface; // makes android min version to be 17 |
| +import android.webkit.WebViewClient; |
| + |
| +import java.io.IOException; |
| +import java.lang.reflect.Proxy; |
| +import java.util.Collections; |
| +import java.util.HashMap; |
| +import java.util.List; |
| +import java.util.Locale; |
| +import java.util.Map; |
| +import java.util.concurrent.CountDownLatch; |
| +import java.util.concurrent.atomic.AtomicBoolean; |
| +import java.util.regex.Pattern; |
| + |
| +/** |
| + * WebView with ad blocking |
| + */ |
| +public class AdblockWebView extends WebView |
| +{ |
| + private static final String TAG = Utils.getTag(AdblockWebView.class); |
| + |
| + private volatile boolean addDomListener = true; |
| + |
| + /** |
| + * Warning: do not rename (used in injected JS by method name) |
| + * @param value set if one need to set DOM listener |
| + */ |
| + @JavascriptInterface |
| + public void setAddDomListener(boolean value) |
| + { |
| + d("addDomListener=" + value); |
| + this.addDomListener = value; |
| + } |
| + |
| + @JavascriptInterface |
| + public boolean getAddDomListener() |
| + { |
| + return addDomListener; |
| + } |
| + |
| + private boolean adBlockEnabled = true; |
| + |
| + public boolean isAdBlockEnabled() |
| + { |
| + return adBlockEnabled; |
| + } |
| + |
| + private void applyAdblockEnabled() |
| + { |
| + super.setWebViewClient(adBlockEnabled ? intWebViewClient : extWebViewClient); |
| + super.setWebChromeClient(adBlockEnabled ? intWebChromeClient : extWebChromeClient); |
| + } |
| + |
| + public void setAdBlockEnabled(boolean adBlockEnabled) |
| + { |
| + this.adBlockEnabled = adBlockEnabled; |
| + applyAdblockEnabled(); |
| + } |
| + |
| + private WebChromeClient extWebChromeClient; |
| + |
| + @Override |
| + public void setWebChromeClient(WebChromeClient client) |
| + { |
| + extWebChromeClient = client; |
| + applyAdblockEnabled(); |
| + } |
| + |
| + private boolean debugMode; |
| + |
| + public boolean isDebugMode() |
| + { |
| + return debugMode; |
| + } |
| + |
| + /** |
| + * Set to true to see debug log output int AdblockWebView and JS console |
| + * @param debugMode is debug mode |
| + */ |
| + public void setDebugMode(boolean debugMode) |
| + { |
| + this.debugMode = debugMode; |
| + } |
| + |
| + private void d(String message) |
| + { |
| + if (debugMode) |
| + { |
| + Log.d(TAG, message); |
| + } |
| + } |
| + |
| + private void w(String message) |
| + { |
| + if (debugMode) |
| + { |
| + Log.w(TAG, message); |
| + } |
| + } |
| + |
| + private void e(String message, Throwable t) |
| + { |
| + Log.e(TAG, message, t); |
| + } |
| + |
| + private void e(String message) |
| + { |
| + Log.e(TAG, message); |
| + } |
| + |
| + private static final String BRIDGE_TOKEN = "{{BRIDGE}}"; |
| + private static final String DEBUG_TOKEN = "{{DEBUG}}"; |
| + private static final String HIDE_TOKEN = "{{HIDE}}"; |
| + private static final String BRIDGE = "jsBridge"; |
| + |
| + private String readScriptFile(String filename) throws IOException |
| + { |
| + return Utils |
| + .readAssetAsString(getContext(), filename) |
| + .replace(BRIDGE_TOKEN, BRIDGE) |
| + .replace(DEBUG_TOKEN, (debugMode ? "" : "//")); |
| + } |
| + |
| + private void runScript(String script) |
| + { |
| + d("runScript started"); |
| + if (Build.VERSION.SDK_INT >= 19) |
| + { |
| + evaluateJavascript(script, null); |
| + } |
| + else |
| + { |
| + loadUrl("javascript:" + script); |
| + } |
| + d("runScript finished"); |
| + } |
| + |
| + private Subscription acceptableAdsSubscription; |
| + |
| + private boolean acceptableAdsEnabled = true; |
| + |
| + public boolean isAcceptableAdsEnabled() |
| + { |
| + return acceptableAdsEnabled; |
| + } |
| + |
| + /** |
| + * Enable or disable Acceptable Ads |
| + * @param enabled enabled |
| + */ |
| + public void setAcceptableAdsEnabled(boolean enabled) |
| + { |
| + this.acceptableAdsEnabled = enabled; |
| + |
| + if (filterEngine != null) |
| + { |
| + applyAcceptableAds(); |
| + } |
| + } |
| + |
| + private final static String EXCEPTIONS_URL = "subscriptions_exceptionsurl"; |
| + |
| + private void applyAcceptableAds() |
| + { |
| + if (acceptableAdsEnabled) |
| + { |
| + if (acceptableAdsSubscription == null) |
| + { |
| + String url = filterEngine.getPref(EXCEPTIONS_URL).toString(); |
| + if (url == null) |
| + { |
| + w("no AA subscription url"); |
| + return; |
| + } |
| + |
| + acceptableAdsSubscription = filterEngine.getSubscription(url); |
| + acceptableAdsSubscription.addToList(); |
| + d("AA subscription added (" + url + ")"); |
| + } |
| + } |
| + else |
| + { |
| + if (acceptableAdsSubscription != null) |
| + { |
| + removeAcceptableAdsSubscription(); |
| + } |
| + } |
| + } |
| + |
| + private void removeAcceptableAdsSubscription() |
| + { |
| + acceptableAdsSubscription.removeFromList(); |
| + acceptableAdsSubscription = null; |
| + d("AA subscription removed"); |
| + } |
| + |
| + private boolean disposeFilterEngine; |
| + |
| + private JsEngine jsEngine; |
| + |
| + public JsEngine getJsEngine() |
| + { |
| + return jsEngine; |
| + } |
| + |
| + private FilterEngine filterEngine; |
| + |
| + public FilterEngine getFilterEngine() |
| + { |
| + return filterEngine; |
| + } |
| + |
| + private Integer loadError; |
| + |
| + /** |
| + * Set external filter engine. A new (internal) is created automatically if not set |
| + * Don't forget to invoke {@link #dispose()} if not using external filter engine |
| + * @param newFilterEngine external filter engine |
| + */ |
| + public void setFilterEngine(FilterEngine newFilterEngine) |
| + { |
| + if (filterEngine != null && newFilterEngine != null && newFilterEngine == filterEngine) |
| + { |
| + return; |
| + } |
| + |
| + if (filterEngine != null) |
| + { |
| + if (acceptableAdsEnabled && acceptableAdsSubscription != null) |
| + { |
| + removeAcceptableAdsSubscription(); |
| + } |
| + |
| + if (disposeFilterEngine) |
| + { |
| + filterEngine.dispose(); |
| + } |
| + } |
| + |
| + filterEngine = newFilterEngine; |
| + disposeFilterEngine = false; |
| + |
| + if (filterEngine != null) |
| + { |
| + applyAcceptableAds(); |
| + |
| + if (jsEngine != null) |
| + { |
| + jsEngine.dispose(); |
| + } |
| + } |
| + |
| + jsEngine = null; |
| + } |
| + |
| + private WebChromeClient intWebChromeClient = new WebChromeClient() |
| + { |
| + @Override |
| + public void onReceivedTitle(WebView view, String title) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onReceivedTitle(view, title); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedIcon(WebView view, Bitmap icon) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onReceivedIcon(view, icon); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); |
| + } |
| + } |
| + |
| + @Override |
| + public void onShowCustomView(View view, CustomViewCallback callback) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onShowCustomView(view, callback); |
| + } |
| + } |
| + |
| + @Override |
| + public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onShowCustomView(view, requestedOrientation, callback); |
| + } |
| + } |
| + |
| + @Override |
| + public void onHideCustomView() |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onHideCustomView(); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, |
| + Message resultMsg) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, resultMsg); |
| + } |
| + else |
| + { |
| + return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg); |
| + } |
| + } |
| + |
| + @Override |
| + public void onRequestFocus(WebView view) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onRequestFocus(view); |
| + } |
| + } |
| + |
| + @Override |
| + public void onCloseWindow(WebView window) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onCloseWindow(window); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onJsAlert(WebView view, String url, String message, JsResult result) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onJsAlert(view, url, message, result); |
| + } |
| + else |
| + { |
| + return super.onJsAlert(view, url, message, result); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onJsConfirm(WebView view, String url, String message, JsResult result) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onJsConfirm(view, url, message, result); |
| + } |
| + else |
| + { |
| + return super.onJsConfirm(view, url, message, result); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, |
| + JsPromptResult result) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onJsPrompt(view, url, message, defaultValue, result); |
| + } |
| + else |
| + { |
| + return super.onJsPrompt(view, url, message, defaultValue, result); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onJsBeforeUnload(view, url, message, result); |
| + } |
| + else |
| + { |
| + return super.onJsBeforeUnload(view, url, message, result); |
| + } |
| + } |
| + |
| + @Override |
| + public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, |
| + long estimatedDatabaseSize, long totalQuota, |
| + WebStorage.QuotaUpdater quotaUpdater) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, |
| + estimatedDatabaseSize, totalQuota, quotaUpdater); |
| + } |
| + else |
| + { |
| + super.onExceededDatabaseQuota(url, databaseIdentifier, quota, |
| + estimatedDatabaseSize, totalQuota, quotaUpdater); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReachedMaxAppCacheSize(long requiredStorage, long quota, |
| + WebStorage.QuotaUpdater quotaUpdater) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); |
| + } |
| + else |
| + { |
| + super.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); |
| + } |
| + } |
| + |
| + @Override |
| + public void onGeolocationPermissionsShowPrompt(String origin, |
| + GeolocationPermissions.Callback callback) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); |
| + } |
| + else |
| + { |
| + super.onGeolocationPermissionsShowPrompt(origin, callback); |
| + } |
| + } |
| + |
| + @Override |
| + public void onGeolocationPermissionsHidePrompt() |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onGeolocationPermissionsHidePrompt(); |
| + } |
| + else |
| + { |
| + super.onGeolocationPermissionsHidePrompt(); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onJsTimeout() |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onJsTimeout(); |
| + } |
| + else |
| + { |
| + return super.onJsTimeout(); |
| + } |
| + } |
| + |
| + @Override |
| + public void onConsoleMessage(String message, int lineNumber, String sourceID) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); |
| + } |
| + else |
| + { |
| + super.onConsoleMessage(message, lineNumber, sourceID); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean onConsoleMessage(ConsoleMessage consoleMessage) |
| + { |
| + d("JS: level=" + consoleMessage.messageLevel() |
| + + ", message=\"" + consoleMessage.message() + "\"" |
| + + ", line=" + consoleMessage.lineNumber()); |
| + |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.onConsoleMessage(consoleMessage); |
| + } |
| + else |
| + { |
| + return super.onConsoleMessage(consoleMessage); |
| + } |
| + } |
| + |
| + @Override |
| + public Bitmap getDefaultVideoPoster() |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.getDefaultVideoPoster(); |
| + } |
| + else |
| + { |
| + return super.getDefaultVideoPoster(); |
| + } |
| + } |
| + |
| + @Override |
| + public View getVideoLoadingProgressView() |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + return extWebChromeClient.getVideoLoadingProgressView(); |
| + } |
| + else |
| + { |
| + return super.getVideoLoadingProgressView(); |
| + } |
| + } |
| + |
| + @Override |
| + public void getVisitedHistory(ValueCallback<String[]> callback) |
| + { |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.getVisitedHistory(callback); |
| + } |
| + else |
| + { |
| + super.getVisitedHistory(callback); |
| + } |
| + } |
| + |
| + @Override |
| + public void onProgressChanged(WebView view, int newProgress) |
| + { |
| + d("Loading progress=" + newProgress + "%"); |
| + |
| + // addDomListener is changed to 'false' in `setAddDomListener` invoked from injected JS |
| + if (getAddDomListener() && loadError == null && injectJs != null) |
| + { |
| + d("Injecting script"); |
| + runScript(injectJs); |
| + |
| + if (allowDraw && loading) |
| + { |
| + startPreventDrawing(); |
| + } |
| + } |
| + |
| + if (extWebChromeClient != null) |
| + { |
| + extWebChromeClient.onProgressChanged(view, newProgress); |
| + } |
| + } |
| + }; |
| + |
| + /** |
| + * Default (in some conditions) start redraw delay after DOM modified with injected JS (millis) |
| + */ |
| + public static final int ALLOW_DRAW_DELAY = 200; |
| + /* |
| + The value could be different for devices and completely unclear why we need it and |
| + how to measure actual value |
| + */ |
| + |
| + private int allowDrawDelay = ALLOW_DRAW_DELAY; |
| + |
| + public int getAllowDrawDelay() |
| + { |
| + return allowDrawDelay; |
| + } |
| + |
| + /** |
| + * Set start redraw delay after DOM modified with injected JS |
| + * (used to prevent flickering after 'DOM ready') |
| + * @param allowDrawDelay delay (in millis) |
| + */ |
| + public void setAllowDrawDelay(int allowDrawDelay) |
| + { |
| + if (allowDrawDelay < 0) |
| + throw new IllegalArgumentException("Negative value is not allowed"); |
| + |
| + this.allowDrawDelay = allowDrawDelay; |
| + } |
| + |
| + private WebViewClient extWebViewClient; |
| + |
| + @Override |
| + public void setWebViewClient(WebViewClient client) |
| + { |
| + extWebViewClient = client; |
| + applyAdblockEnabled(); |
| + } |
| + |
| + private static final Pattern RE_JS = Pattern.compile("\\.js$", Pattern.CASE_INSENSITIVE); |
| + private static final Pattern RE_CSS = Pattern.compile("\\.css$", Pattern.CASE_INSENSITIVE); |
| + private static final Pattern RE_IMAGE = Pattern.compile("\\.(?:gif|png|jpe?g|bmp|ico)$", Pattern.CASE_INSENSITIVE); |
| + private static final Pattern RE_FONT = Pattern.compile("\\.(?:ttf|woff)$", Pattern.CASE_INSENSITIVE); |
| + private static final Pattern RE_HTML = Pattern.compile("\\.html?$", Pattern.CASE_INSENSITIVE); |
| + |
| + private WebViewClient intWebViewClient; |
| + |
| + /** |
| + * WebViewClient for API pre 21 |
| + * (does not have Referers information) |
| + */ |
| + class AdblockWebViewClient extends WebViewClient |
| + { |
| + @Override |
| + public boolean shouldOverrideUrlLoading(WebView view, String url) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + return extWebViewClient.shouldOverrideUrlLoading(view, url); |
| + } |
| + else |
| + { |
| + return super.shouldOverrideUrlLoading(view, url); |
| + } |
| + } |
| + |
| + @Override |
| + public void onPageStarted(WebView view, String url, Bitmap favicon) |
| + { |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + startAbpLoading(url); |
| + |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onPageStarted(view, url, favicon); |
| + } |
| + else |
| + { |
| + super.onPageStarted(view, url, favicon); |
| + } |
| + } |
| + |
| + @Override |
| + public void onPageFinished(WebView view, String url) |
| + { |
| + loading = false; |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onPageFinished(view, url); |
| + } |
| + else |
| + { |
| + super.onPageFinished(view, url); |
| + } |
| + } |
| + |
| + @Override |
| + public void onLoadResource(WebView view, String url) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onLoadResource(view, url); |
| + } |
| + else |
| + { |
| + super.onLoadResource(view, url); |
| + } |
| + } |
| + |
| + @Override |
| + public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onTooManyRedirects(view, cancelMsg, continueMsg); |
| + } |
| + else |
| + { |
| + super.onTooManyRedirects(view, cancelMsg, continueMsg); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) |
| + { |
| + e("Load error:" + |
| + " code=" + errorCode + |
| + " with description=" + description + |
| + " for url=" + failingUrl); |
| + loadError = errorCode; |
| + |
| + stopAbpLoading(); |
| + |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onReceivedError(view, errorCode, description, failingUrl); |
| + } |
| + else |
| + { |
| + super.onReceivedError(view, errorCode, description, failingUrl); |
| + } |
| + } |
| + |
| + @Override |
| + public void onFormResubmission(WebView view, Message dontResend, Message resend) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onFormResubmission(view, dontResend, resend); |
| + } |
| + else |
| + { |
| + super.onFormResubmission(view, dontResend, resend); |
| + } |
| + } |
| + |
| + @Override |
| + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.doUpdateVisitedHistory(view, url, isReload); |
| + } |
| + else |
| + { |
| + super.doUpdateVisitedHistory(view, url, isReload); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onReceivedSslError(view, handler, error); |
| + } |
| + else |
| + { |
| + super.onReceivedSslError(view, handler, error); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onReceivedHttpAuthRequest(view, handler, host, realm); |
| + } |
| + else |
| + { |
| + super.onReceivedHttpAuthRequest(view, handler, host, realm); |
| + } |
| + } |
| + |
| + @Override |
| + public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + return extWebViewClient.shouldOverrideKeyEvent(view, event); |
| + } |
| + else |
| + { |
| + return super.shouldOverrideKeyEvent(view, event); |
| + } |
| + } |
| + |
| + @Override |
| + public void onUnhandledKeyEvent(WebView view, KeyEvent event) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onUnhandledKeyEvent(view, event); |
| + } |
| + else |
| + { |
| + super.onUnhandledKeyEvent(view, event); |
| + } |
| + } |
| + |
| + @Override |
| + public void onScaleChanged(WebView view, float oldScale, float newScale) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onScaleChanged(view, oldScale, newScale); |
| + } |
| + else |
| + { |
| + super.onScaleChanged(view, oldScale, newScale); |
| + } |
| + } |
| + |
| + @Override |
| + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) |
| + { |
| + if (extWebViewClient != null) |
| + { |
| + extWebViewClient.onReceivedLoginRequest(view, realm, account, args); |
| + } |
| + else |
| + { |
| + super.onReceivedLoginRequest(view, realm, account, args); |
| + } |
| + } |
| + |
| + protected WebResourceResponse shouldInterceptRequest( |
| + WebView webview, String url, boolean isMainFrame, String[] documentUrls) |
| + { |
| + // if dispose() was invoke, but the page is still loading then just let it go |
| + if (filterEngine == null) |
| + { |
| + e("FilterEngine already disposed, allow loading"); |
| + |
| + // Allow loading by returning null |
| + return null; |
| + } |
| + |
| + if (isMainFrame) |
| + { |
| + // never blocking main frame requests, just subrequests |
| + w(url + " is main frame, allow loading"); |
| + |
| + // allow loading by returning null |
| + return null; |
| + } |
| + |
| + if (filterEngine.isDocumentWhitelisted(url, documentUrls)) |
| + { |
| + w(url + " document is whitelisted, allow loading"); |
| + |
| + // allow loading by returning null |
| + return null; |
| + } |
| + |
| + // determine the content |
| + FilterEngine.ContentType contentType; |
| + if (RE_JS.matcher(url).find()) |
| + { |
| + contentType = FilterEngine.ContentType.SCRIPT; |
| + } |
| + else if (RE_CSS.matcher(url).find()) |
| + { |
| + contentType = FilterEngine.ContentType.STYLESHEET; |
| + } |
| + else if (RE_IMAGE.matcher(url).find()) |
| + { |
| + contentType = FilterEngine.ContentType.IMAGE; |
| + } |
| + else if (RE_FONT.matcher(url).find()) |
| + { |
| + contentType = FilterEngine.ContentType.FONT; |
| + } |
| + else if (RE_HTML.matcher(url).find()) |
| + { |
| + contentType = FilterEngine.ContentType.SUBDOCUMENT; |
| + } |
| + else |
| + { |
| + contentType = FilterEngine.ContentType.OTHER; |
| + } |
| + |
| + // check if we should block |
| + Filter filter = filterEngine.matches(url, contentType, documentUrls); |
| + if (filter != null) |
| + { |
| + if (filter.getType().equals(Filter.Type.BLOCKING)) |
| + { |
| + w("Blocked loading " + url + " due to filter \"" + filter.toString() + "\""); |
| + |
| + // If we should block, return empty response which results in 'errorLoading' callback |
| + return new WebResourceResponse("text/plain", "UTF-8", null); |
| + } |
| + else |
| + { |
| + w(url + "is not blocked due to filter \"" + filter.toString() + "\" of type " + filter.getType()); |
| + } |
| + } |
| + else |
| + { |
| + d("No filter found for " + url); |
| + } |
| + |
| + d("Allowed loading " + url); |
| + |
| + // continue by returning null |
| + return null; |
| + } |
| + |
| + @Override |
| + public WebResourceResponse shouldInterceptRequest(WebView view, String url) |
| + { |
| + // if dispose() was invoke, but the page is still loading then just let it go |
| + if (filterEngine == null) |
| + { |
| + e("FilterEngine already disposed"); |
| + return null; |
| + } |
| + |
| + /* |
| + we use hack - injecting Proxy instance instead of IoThreadClient and intercepting |
| + `isForMainFrame` argument value, see `initIoThreadClient()`. |
| + |
| + it's expected to be not `null` as first invocation goes to |
| + https://android.googlesource.com/platform/external/chromium_org/+/android-4.4_r1/android_webview/java/src/org/chromium/android_webview/AwContents.java#235 |
| + and we intercept it by injecting our proxy instead of `IoThreadClientImpl` |
| + and then it goes here |
| + https://android.googlesource.com/platform/external/chromium_org/+/android-4.4_r1/android_webview/java/src/org/chromium/android_webview/AwContents.java#242 |
| + */ |
| + Boolean isMainFrame = IoThreadClientInvocationHandler.isMainFrame; |
| + if (isMainFrame == null) |
| + { |
| + // the only reason for having null here is probably it failed to inject IoThreadClient proxy |
| + isMainFrame = false; |
| + } |
| + |
| + // sadly we don't have request headers |
| + // (and referer until android-21 and new callback with request (instead of just url) argument) |
| + String[] referers = null; |
| + |
| + return shouldInterceptRequest(view, url, isMainFrame, referers); |
| + } |
| + } |
| + |
| + private void clearReferers() |
| + { |
| + d("Clearing referers"); |
| + url2Referer.clear(); |
| + } |
| + |
| + /** |
| + * WebViewClient for API 21 and newer |
| + * (has Referer since it overrides `shouldInterceptRequest(..., request)` with referer) |
| + */ |
| + class AdblockWebViewClient21 extends AdblockWebViewClient |
| + { |
| + @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| + @Override |
| + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) |
| + { |
| + // here we just trying to fill url -> referer map |
| + // blocking/allowing loading will happen in `shouldInterceptRequest(WebView,String)` |
| + String url = request.getUrl().toString(); |
| + String referer = request.getRequestHeaders().get("Referer"); |
| + String[] referers; |
| + |
| + if (referer != null) |
| + { |
| + d("Header referer for " + url + " is " + referer); |
| + url2Referer.put(url, referer); |
| + |
| + referers = new String[] |
| + { |
| + referer |
| + }; |
| + } |
| + else |
| + { |
| + w("No referer header for " + url); |
| + referers = EMPTY_ARRAY; |
| + } |
| + |
| + return shouldInterceptRequest(view, url, request.isForMainFrame(), referers); |
| + } |
| + } |
| + |
| + private Map<String, String> url2Referer = Collections.synchronizedMap(new HashMap<String, String>()); |
| + |
| + private void initAbp() |
| + { |
| + addJavascriptInterface(this, BRIDGE); |
| + initClients(); |
| + |
| + if (Build.VERSION.SDK_INT < 21) |
| + { |
| + initIoThreadClient(); |
| + } |
| + } |
| + |
| + private synchronized void initIoThreadClient() |
| + { |
| + final Object awContents = ReflectionUtils.extractProperty(this, new String[] |
| + { |
| + "mProvider", |
| + "mAwContents" |
| + }); |
| + |
| + final String ioThreadClientProperty = "mIoThreadClient"; |
| + final Object originalClient = ReflectionUtils.extractProperty( |
| + awContents, |
| + new String[] |
| + { |
| + ioThreadClientProperty |
| + }); |
| + |
| + // avoid injecting twice (already injected Proxy instance has another class name) |
| + if (!originalClient.getClass().getSimpleName().startsWith("$Proxy")) |
| + { |
| + Object proxyClient = Proxy.newProxyInstance( |
| + originalClient.getClass().getClassLoader(), |
| + originalClient.getClass().getInterfaces(), |
| + new IoThreadClientInvocationHandler(originalClient)); |
| + |
| + // inject proxy instead of original client |
| + boolean injected = ReflectionUtils.injectProperty(awContents, ioThreadClientProperty, proxyClient); |
| + if (injected) |
| + { |
| + Integer mNativeAwContents = (Integer) ReflectionUtils.extractProperty(awContents, "mNativeAwContents"); |
| + Object mWebContentsDelegate = ReflectionUtils.extractProperty(awContents, "mWebContentsDelegate"); |
| + Object mContentsClientBridge = ReflectionUtils.extractProperty(awContents, "mContentsClientBridge"); |
| + Object mInterceptNavigationDelegate = ReflectionUtils.extractProperty(awContents, "mInterceptNavigationDelegate"); |
| + |
| + boolean invoked = ReflectionUtils.invokeMethod(awContents, "nativeSetJavaPeers", new Object[] |
| + { |
| + mNativeAwContents, awContents, mWebContentsDelegate, |
| + mContentsClientBridge, proxyClient, mInterceptNavigationDelegate |
| + }); |
| + if (!invoked) |
| + { |
| + e("Failed to inject IoThreadClient proxy"); |
| + } |
| + } |
| + } |
| + } |
| + |
| + private void initClients() |
| + { |
| + if (Build.VERSION.SDK_INT >= 21) |
| + { |
| + intWebViewClient = new AdblockWebViewClient21(); |
| + } |
| + else |
| + { |
| + intWebViewClient = new AdblockWebViewClient(); |
| + } |
| + applyAdblockEnabled(); |
| + } |
| + |
| + /** |
| + * Build app info using Android package information |
| + * @param context context |
| + * @param developmentBuild if it's dev build |
| + * @return app info required to build JsEngine |
| + */ |
| + public static AppInfo buildAppInfo(final Context context, boolean developmentBuild) |
| + { |
| + String version = "0"; |
| + try |
| + { |
| + final PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); |
| + version = info.versionName; |
| + if (developmentBuild) |
| + version += "." + info.versionCode; |
| + } |
| + catch (final PackageManager.NameNotFoundException e) |
| + { |
| + Log.e(TAG, "Failed to get the application version number", e); |
| + } |
| + final String sdkVersion = String.valueOf(Build.VERSION.SDK_INT); |
| + final String locale = Locale.getDefault().toString().replace('_', '-'); |
| + |
| + return AppInfo.builder() |
| + .setVersion(version) |
| + .setApplicationVersion(sdkVersion) |
| + .setLocale(locale) |
| + .setDevelopmentBuild(developmentBuild) |
| + .build(); |
| + } |
| + |
| + /** |
| + * Build JsEngine required to build FilterEngine |
| + * @param context context |
| + * @param developmentBuild if it's dev build |
| + * @return JsEngine |
| + */ |
| + public static JsEngine buildJsEngine(Context context, boolean developmentBuild) |
| + { |
| + JsEngine jsEngine = new JsEngine(buildAppInfo(context, developmentBuild)); |
| + jsEngine.setDefaultFileSystem(context.getCacheDir().getAbsolutePath()); |
| + jsEngine.setWebRequest(new AndroidWebRequest(true)); // 'true' because we need element hiding |
| + return jsEngine; |
| + } |
| + |
| + private void createFilterEngine() |
| + { |
| + w("Creating FilterEngine"); |
| + jsEngine = buildJsEngine(getContext(), debugMode); |
| + filterEngine = new FilterEngine(jsEngine); |
| + applyAcceptableAds(); |
| + d("FilterEngine created"); |
| + } |
| + |
| + private String url; |
| + private String domain; |
| + private String injectJs; |
| + private CountDownLatch elemHideLatch; |
| + private String elemHideSelectorsString; |
| + private Object elemHideThreadLockObject = new Object(); |
| + private ElemHideThread elemHideThread; |
| + |
| + private static final String[] EMPTY_ARRAY = new String[] {}; |
| + |
| + private class ElemHideThread extends Thread |
| + { |
| + private String selectorsString; |
| + private CountDownLatch finishedLatch; |
| + private AtomicBoolean isCancelled; |
| + |
| + public ElemHideThread(CountDownLatch finishedLatch) |
| + { |
| + this.finishedLatch = finishedLatch; |
| + isCancelled = new AtomicBoolean(false); |
| + } |
| + |
| + @Override |
| + public void run() |
| + { |
| + try |
| + { |
| + if (filterEngine == null) |
| + { |
| + w("FilterEngine already disposed"); |
| + selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; |
| + } |
| + else |
| + { |
| + String[] referers = new String[] |
| + { |
| + url |
| + }; |
| + |
| + d("Check whitelisting for " + url); |
| + |
| + if (filterEngine.isDocumentWhitelisted(url, referers)) |
| + { |
| + w("Whitelisted " + url + " for document"); |
| + selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; |
| + } |
| + else if (filterEngine.isElemhideWhitelisted(url, referers)) |
| + { |
| + w("Whitelisted " + url + " for elemhide"); |
| + selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; |
| + } |
| + else |
| + { |
| + List<Subscription> subscriptions = filterEngine.getListedSubscriptions(); |
| + d("Listed subscriptions: " + subscriptions.size()); |
| + if (debugMode) |
| + { |
| + for (Subscription eachSubscription : subscriptions) |
| + { |
| + d("Subscribed to " + eachSubscription); |
| + } |
| + } |
| + |
| + d("Requesting elemhide selectors from FilterEngine for " + domain + " in " + this); |
| + List<String> selectors = filterEngine.getElementHidingSelectors(domain); |
| + d("Finished requesting elemhide selectors, got " + selectors.size() + " in " + this); |
| + selectorsString = Utils.stringListToJsonArray(selectors); |
| + } |
| + } |
| + } |
| + finally |
| + { |
| + if (!isCancelled.get()) |
| + { |
| + finish(selectorsString); |
| + } |
| + else |
| + { |
| + w("This thread is cancelled, exiting silently " + this); |
| + } |
| + } |
| + } |
| + |
| + private void onFinished() |
| + { |
| + finishedLatch.countDown(); |
| + synchronized (finishedRunnableLockObject) |
| + { |
| + if (finishedRunnable != null) |
| + { |
| + finishedRunnable.run(); |
| + } |
| + } |
| + } |
| + |
| + private void finish(String result) |
| + { |
| + d("Setting elemhide string " + result.length() + " bytes"); |
| + elemHideSelectorsString = result; |
| + onFinished(); |
| + } |
| + |
| + private Object finishedRunnableLockObject = new Object(); |
| + private Runnable finishedRunnable; |
| + |
| + public void setFinishedRunnable(Runnable runnable) |
| + { |
| + synchronized (finishedRunnableLockObject) |
| + { |
| + this.finishedRunnable = runnable; |
| + } |
| + } |
| + |
| + public void cancel() |
| + { |
| + w("Cancelling elemhide thread " + this); |
| + isCancelled.set(true); |
| + |
| + finish(EMPTY_ELEMHIDE_ARRAY_STRING); |
| + } |
| + } |
| + |
| + private Runnable elemHideThreadFinishedRunnable = new Runnable() |
| + { |
| + @Override |
| + public void run() |
| + { |
| + synchronized (elemHideThreadLockObject) |
| + { |
| + w("elemHideThread set to null"); |
| + elemHideThread = null; |
| + } |
| + } |
| + }; |
| + |
| + private boolean loading; |
| + |
| + private void initAbpLoading() |
| + { |
| + getSettings().setJavaScriptEnabled(true); |
| + buildInjectJs(); |
| + |
| + if (filterEngine == null) |
| + { |
| + createFilterEngine(); |
| + disposeFilterEngine = true; |
| + } |
| + } |
| + |
| + private void startAbpLoading(String newUrl) |
| + { |
| + d("Start loading " + newUrl); |
| + |
| + loading = true; |
| + addDomListener = true; |
| + elementsHidden = false; |
| + loadError = null; |
| + url = newUrl; |
| + |
| + if (url != null) |
| + { |
| + try |
| + { |
| + domain = filterEngine.getHostFromURL(url); |
| + if (domain == null) |
| + { |
| + throw new RuntimeException("Failed to extract domain from " + url); |
| + } |
| + |
| + d("Extracted domain " + domain + " from " + url); |
| + } |
| + catch (Throwable t) { |
| + e("Failed to extract domain from " + url, t); |
| + } |
| + |
| + elemHideLatch = new CountDownLatch(1); |
| + elemHideThread = new ElemHideThread(elemHideLatch); |
| + elemHideThread.setFinishedRunnable(elemHideThreadFinishedRunnable); |
| + elemHideThread.start(); |
| + } |
| + else |
| + { |
| + elemHideLatch = null; |
| + } |
| + } |
| + |
| + private void buildInjectJs() |
| + { |
| + try |
| + { |
| + if (injectJs == null) |
| + { |
| + injectJs = readScriptFile("inject.js").replace(HIDE_TOKEN, readScriptFile("css.js")); |
| + } |
| + } |
| + catch (IOException e) |
| + { |
| + e("Failed to read script", e); |
| + } |
| + } |
| + |
| + @Override |
| + public void goBack() |
| + { |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.goBack(); |
| + } |
| + |
| + @Override |
| + public void goForward() |
| + { |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.goForward(); |
| + } |
| + |
| + @Override |
| + public void reload() |
| + { |
| + initAbpLoading(); |
| + |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.reload(); |
| + } |
| + |
| + @Override |
| + public void loadUrl(String url) |
| + { |
| + initAbpLoading(); |
| + |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.loadUrl(url); |
| + } |
| + |
| + @Override |
| + public void loadUrl(String url, Map<String, String> additionalHttpHeaders) |
| + { |
| + initAbpLoading(); |
| + |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.loadUrl(url, additionalHttpHeaders); |
| + } |
| + |
| + @Override |
| + public void loadData(String data, String mimeType, String encoding) |
| + { |
| + initAbpLoading(); |
| + |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.loadData(data, mimeType, encoding); |
| + } |
| + |
| + @Override |
| + public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, |
| + String historyUrl) |
| + { |
| + initAbpLoading(); |
| + |
| + if (loading) |
| + { |
| + stopAbpLoading(); |
| + } |
| + |
| + super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); |
| + } |
| + |
| + @Override |
| + public void stopLoading() |
| + { |
| + stopAbpLoading(); |
| + super.stopLoading(); |
| + } |
| + |
| + private void stopAbpLoading() |
| + { |
| + d("Stop abp loading"); |
| + |
| + loading = false; |
| + stopPreventDrawing(); |
| + clearReferers(); |
| + |
| + synchronized (elemHideThreadLockObject) |
| + { |
| + if (elemHideThread != null) |
| + { |
| + elemHideThread.cancel(); |
| + } |
| + } |
| + } |
| + |
| + private volatile boolean elementsHidden = false; |
| + |
| + // warning: do not rename (used in injected JS by method name) |
| + @JavascriptInterface |
| + public void setElementsHidden(boolean value) |
| + { |
| + // invoked with 'true' by JS callback when DOM is loaded |
| + elementsHidden = value; |
| + |
| + // fired on worker thread, but needs to be invoked on main thread |
| + if (value) |
| + { |
| +// handler.post(allowDrawRunnable); |
| +// should work, but it's not working: |
| +// the user can see element visible even though it was hidden on dom event |
| + |
| + if (allowDrawDelay > 0) |
| + { |
| + d("Scheduled 'allow drawing' invocation in " + allowDrawDelay + " ms"); |
| + } |
| + handler.postDelayed(allowDrawRunnable, allowDrawDelay); |
| + } |
| + } |
| + |
| + // warning: do not rename (used in injected JS by method name) |
| + @JavascriptInterface |
| + public boolean isElementsHidden() |
| + { |
| + return elementsHidden; |
| + } |
| + |
| + @Override |
| + public void onPause() |
| + { |
| + handler.removeCallbacks(allowDrawRunnable); |
| + super.onPause(); |
| + } |
| + |
| + public AdblockWebView(Context context) |
| + { |
| + super(context); |
| + initAbp(); |
| + } |
| + |
| + public AdblockWebView(Context context, AttributeSet attrs) |
| + { |
| + super(context, attrs); |
| + initAbp(); |
| + } |
| + |
| + public AdblockWebView(Context context, AttributeSet attrs, int defStyle) |
| + { |
| + super(context, attrs, defStyle); |
| + initAbp(); |
| + } |
| + |
| + // used to prevent user see flickering for elements to hide |
| + // for some reason it's rendered even if element is hidden on 'dom ready' event |
| + private volatile boolean allowDraw = true; |
| + |
| + @Override |
| + protected void onDraw(Canvas canvas) |
| + { |
| + if (allowDraw) |
| + { |
| + super.onDraw(canvas); |
| + } |
| + else |
| + { |
| + w("Prevent drawing"); |
| + drawEmptyPage(canvas); |
| + } |
| + } |
| + |
| + private void drawEmptyPage(Canvas canvas) |
| + { |
| + // assuming default color is WHITE |
| + canvas.drawColor(Color.WHITE); |
| + } |
| + |
| + private Handler handler = new Handler(); |
| + |
| + protected void startPreventDrawing() |
| + { |
| + w("Start prevent drawing"); |
| + |
| + allowDraw = false; |
| + } |
| + |
| + protected void stopPreventDrawing() |
| + { |
| + d("Stop prevent drawing, invalidating"); |
| + |
| + allowDraw = true; |
| + invalidate(); |
| + } |
| + |
| + private Runnable allowDrawRunnable = new Runnable() |
| + { |
| + @Override |
| + public void run() |
| + { |
| + stopPreventDrawing(); |
| + } |
| + }; |
| + |
| + private static final String EMPTY_ELEMHIDE_ARRAY_STRING = "[]"; |
| + |
| + // warning: do not rename (used in injected JS by method name) |
| + @JavascriptInterface |
| + public String getElemhideSelectors() |
| + { |
| + if (elemHideLatch == null) |
| + { |
| + return EMPTY_ELEMHIDE_ARRAY_STRING; |
| + } |
| + else |
| + { |
| + try |
| + { |
| + // elemhide selectors list getting is started in startAbpLoad() in background thread |
| + d("Waiting for elemhide selectors to be ready"); |
| + elemHideLatch.await(); |
| + d("Elemhide selectors ready, " + elemHideSelectorsString.length() + " bytes"); |
| + |
| + clearReferers(); |
| + |
| + return elemHideSelectorsString; |
| + } |
| + catch (InterruptedException e) |
| + { |
| + w("Interrupted, returning empty selectors list"); |
| + return EMPTY_ELEMHIDE_ARRAY_STRING; |
| + } |
| + } |
| + } |
| + |
| + private void doDispose() |
| + { |
| + w("Disposing jsEngine"); |
| + jsEngine.dispose(); |
| + jsEngine = null; |
| + |
| + w("Disposing filterEngine"); |
| + filterEngine.dispose(); |
| + filterEngine = null; |
| + |
| + disposeFilterEngine = false; |
| + } |
| + |
| + public void dispose() |
| + { |
| + d("Dispose invoked"); |
| + |
| + removeJavascriptInterface(BRIDGE); |
| + |
| + if (disposeFilterEngine) |
| + { |
| + synchronized (elemHideThreadLockObject) |
| + { |
| + if (elemHideThread != null) |
| + { |
| + w("Busy with elemhide selectors, delayed disposing scheduled"); |
| + elemHideThread.setFinishedRunnable(new Runnable() |
| + { |
| + @Override |
| + public void run() |
| + { |
| + doDispose(); |
| + } |
| + }); |
| + } |
| + else |
| + { |
| + doDispose(); |
| + } |
| + } |
| + } |
| + } |
| +} |