| Index: libadblockplus-android/src/org/adblockplus/android/AdblockWebView.java | 
| diff --git a/libadblockplus-android/src/org/adblockplus/android/AdblockWebView.java b/libadblockplus-android/src/org/adblockplus/android/AdblockWebView.java | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..f96e112df628a6d546721a95727acb5e6fa04938 | 
| --- /dev/null | 
| +++ b/libadblockplus-android/src/org/adblockplus/android/AdblockWebView.java | 
| @@ -0,0 +1,1356 @@ | 
| +/* | 
| + * 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.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.WebBackForwardList; | 
| +import android.webkit.WebChromeClient; | 
| +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.net.URISyntaxException; | 
| +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) | 
| +  { | 
| +    this.addDomListener = value; | 
| +  } | 
| + | 
| +  @JavascriptInterface | 
| +  public boolean getAddDomListener() | 
| +  { | 
| +    return addDomListener; | 
| +  } | 
| + | 
| +  private WebChromeClient extWebChromeClient; | 
| + | 
| +  @Override | 
| +  public void setWebChromeClient(WebChromeClient client) | 
| +  { | 
| +    extWebChromeClient = client; | 
| +  } | 
| + | 
| +  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) | 
| +  { | 
| +    if (Build.VERSION.SDK_INT >= 19) | 
| +    { | 
| +      evaluateJavascript(script, null); | 
| +    } | 
| +    else | 
| +    { | 
| +      loadUrl("javascript:" + script); | 
| +    } | 
| +  } | 
| + | 
| +  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) | 
| +      { | 
| +        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 && disposeFilterEngine) | 
| +    { | 
| +      filterEngine.dispose(); | 
| +    } | 
| + | 
| +    filterEngine = newFilterEngine; | 
| +    disposeFilterEngine = false; | 
| + | 
| +    if (newFilterEngine != null && 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) | 
| +    { | 
| +      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) | 
| +      { | 
| +        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; | 
| +  } | 
| + | 
| +  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 = new 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) | 
| +    { | 
| +      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); | 
| +      } | 
| +    } | 
| + | 
| +    @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; | 
| +      } | 
| + | 
| +      // Determine the content | 
| +      FilterEngine.ContentType contentType = null; | 
| +      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 ... we sadly do not have the referrer chain here, | 
| +      // might also be hard to get as we do not have HTTP headers here | 
| +      Filter filter = filterEngine.matches(url, contentType, new String[0]); | 
| +      if (filter != null && filter.getType().equals(Filter.Type.BLOCKING)) | 
| +      { | 
| +        w("Blocked loading " + url); | 
| +        // If we should block, return empty response which results in a 404 | 
| +        return new WebResourceResponse("text/plain", "UTF-8", null); | 
| +      } | 
| + | 
| +      d("Allowed loading " + url); | 
| + | 
| +      // Otherwise, continue by returning null | 
| +      return null; | 
| +    } | 
| +  }; | 
| + | 
| +  private void initAbp() | 
| +  { | 
| +    addJavascriptInterface(this, BRIDGE); | 
| + | 
| +    super.setWebChromeClient(intWebChromeClient); | 
| +    super.setWebViewClient(intWebViewClient); | 
| +  } | 
| + | 
| +  /** | 
| +   * 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 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 | 
| +        { | 
| +          if (filterEngine.isDocumentWhitelisted(url, null) || | 
| +            filterEngine.isElemhideWhitelisted(url, null)) | 
| +          { | 
| +            d(url + " whitelisted"); | 
| +            selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; | 
| +          } | 
| +          else | 
| +          { | 
| +            d("Requesting elemhide selectors from FilterEngine for " + domain); | 
| +            List<String> selectors = filterEngine.getElementHidingSelectors(domain); | 
| +            d("Finished requesting elemhide selectors, got " + selectors.size()); | 
| +            selectorsString = Utils.stringListToJsonArray(selectors); | 
| +          } | 
| +        } | 
| +      } | 
| +      finally | 
| +      { | 
| +        if (!isCancelled.get()) | 
| +        { | 
| +          finish(selectorsString); | 
| +        } | 
| +      } | 
| +    } | 
| + | 
| +    private void onFinished() | 
| +    { | 
| +      finishedLatch.countDown(); | 
| +      synchronized (finishedRunnableLockObject) | 
| +      { | 
| +        if (finishedRunnable != null) | 
| +        { | 
| +          finishedRunnable.run(); | 
| +        } | 
| +      } | 
| +    } | 
| + | 
| +    private void finish(String result) | 
| +    { | 
| +      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"); | 
| +      isCancelled.set(true); | 
| + | 
| +      finish(EMPTY_ELEMHIDE_ARRAY_STRING); | 
| +    } | 
| +  } | 
| + | 
| +  private Runnable elemHideThreadFinishedRunnable = new Runnable() | 
| +  { | 
| +    @Override | 
| +    public void run() | 
| +    { | 
| +      synchronized (elemHideThreadLockObject) | 
| +      { | 
| +        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 = Utils.getDomain(url); | 
| +      } | 
| +      catch (URISyntaxException e) | 
| +      { | 
| +        domain = null; | 
| +        e("Failed to extract domain for " + url); | 
| +      } | 
| + | 
| +      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 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() | 
| +  { | 
| +    loading = false; | 
| +    stopPreventDrawing(); | 
| + | 
| +    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"); | 
| +        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(); | 
| +        } | 
| +      } | 
| +    } | 
| +  } | 
| +} | 
|  |