| OLD | NEW | 
| (Empty) |  | 
 |     1 /* | 
 |     2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 
 |     3  * Copyright (C) 2006-2016 Eyeo GmbH | 
 |     4  * | 
 |     5  * Adblock Plus is free software: you can redistribute it and/or modify | 
 |     6  * it under the terms of the GNU General Public License version 3 as | 
 |     7  * published by the Free Software Foundation. | 
 |     8  * | 
 |     9  * Adblock Plus is distributed in the hope that it will be useful, | 
 |    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
 |    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
 |    12  * GNU General Public License for more details. | 
 |    13  * | 
 |    14  * You should have received a copy of the GNU General Public License | 
 |    15  * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>. | 
 |    16  */ | 
 |    17  | 
 |    18 package org.adblockplus.android; | 
 |    19  | 
 |    20 import org.adblockplus.libadblockplus.AppInfo; | 
 |    21 import org.adblockplus.libadblockplus.Filter; | 
 |    22 import org.adblockplus.libadblockplus.FilterEngine; | 
 |    23 import org.adblockplus.libadblockplus.JsEngine; | 
 |    24 import org.adblockplus.libadblockplus.Subscription; | 
 |    25  | 
 |    26 import android.annotation.TargetApi; | 
 |    27 import android.content.Context; | 
 |    28 import android.content.pm.PackageInfo; | 
 |    29 import android.content.pm.PackageManager; | 
 |    30 import android.graphics.Bitmap; | 
 |    31 import android.graphics.Canvas; | 
 |    32 import android.graphics.Color; | 
 |    33 import android.net.http.SslError; | 
 |    34 import android.os.Build; | 
 |    35 import android.os.Handler; | 
 |    36 import android.os.Message; | 
 |    37 import android.util.AttributeSet; | 
 |    38 import android.util.Log; | 
 |    39 import android.view.KeyEvent; | 
 |    40 import android.view.View; | 
 |    41 import android.webkit.ConsoleMessage; | 
 |    42 import android.webkit.GeolocationPermissions; | 
 |    43 import android.webkit.HttpAuthHandler; | 
 |    44 import android.webkit.JsPromptResult; | 
 |    45 import android.webkit.JsResult; | 
 |    46 import android.webkit.SslErrorHandler; | 
 |    47 import android.webkit.ValueCallback; | 
 |    48 import android.webkit.WebChromeClient; | 
 |    49 import android.webkit.WebResourceRequest;  // makes android min version to be 21 | 
 |    50 import android.webkit.WebResourceResponse; | 
 |    51 import android.webkit.WebStorage; | 
 |    52 import android.webkit.WebView; | 
 |    53 import android.webkit.JavascriptInterface; // makes android min version to be 17 | 
 |    54 import android.webkit.WebViewClient; | 
 |    55  | 
 |    56 import java.io.IOException; | 
 |    57 import java.lang.reflect.Proxy; | 
 |    58 import java.net.URISyntaxException; | 
 |    59 import java.util.Collections; | 
 |    60 import java.util.HashMap; | 
 |    61 import java.util.List; | 
 |    62 import java.util.Locale; | 
 |    63 import java.util.Map; | 
 |    64 import java.util.concurrent.CountDownLatch; | 
 |    65 import java.util.concurrent.atomic.AtomicBoolean; | 
 |    66 import java.util.regex.Pattern; | 
 |    67  | 
 |    68 /** | 
 |    69  * WebView with ad blocking | 
 |    70  */ | 
 |    71 public class AdblockWebView extends WebView | 
 |    72 { | 
 |    73   private static final String TAG = Utils.getTag(AdblockWebView.class); | 
 |    74  | 
 |    75   private volatile boolean addDomListener = true; | 
 |    76  | 
 |    77   /** | 
 |    78    * Warning: do not rename (used in injected JS by method name) | 
 |    79    * @param value set if one need to set DOM listener | 
 |    80    */ | 
 |    81   @JavascriptInterface | 
 |    82   public void setAddDomListener(boolean value) | 
 |    83   { | 
 |    84     d("addDomListener=" + value); | 
 |    85     this.addDomListener = value; | 
 |    86   } | 
 |    87  | 
 |    88   @JavascriptInterface | 
 |    89   public boolean getAddDomListener() | 
 |    90   { | 
 |    91     return addDomListener; | 
 |    92   } | 
 |    93  | 
 |    94   private boolean adBlockEnabled = true; | 
 |    95  | 
 |    96   public boolean isAdBlockEnabled() | 
 |    97   { | 
 |    98     return adBlockEnabled; | 
 |    99   } | 
 |   100  | 
 |   101   private void applyAdblockEnabled() | 
 |   102   { | 
 |   103     super.setWebViewClient(adBlockEnabled ? intWebViewClient : extWebViewClient)
      ; | 
 |   104     super.setWebChromeClient(adBlockEnabled ? intWebChromeClient : extWebChromeC
      lient); | 
 |   105   } | 
 |   106  | 
 |   107   public void setAdBlockEnabled(boolean adBlockEnabled) | 
 |   108   { | 
 |   109     this.adBlockEnabled = adBlockEnabled; | 
 |   110     applyAdblockEnabled(); | 
 |   111   } | 
 |   112  | 
 |   113   private WebChromeClient extWebChromeClient; | 
 |   114  | 
 |   115   @Override | 
 |   116   public void setWebChromeClient(WebChromeClient client) | 
 |   117   { | 
 |   118     extWebChromeClient = client; | 
 |   119     applyAdblockEnabled(); | 
 |   120   } | 
 |   121  | 
 |   122   private boolean debugMode; | 
 |   123  | 
 |   124   public boolean isDebugMode() | 
 |   125   { | 
 |   126     return debugMode; | 
 |   127   } | 
 |   128  | 
 |   129   /** | 
 |   130    * Set to true to see debug log output int AdblockWebView and JS console | 
 |   131    * @param debugMode is debug mode | 
 |   132    */ | 
 |   133   public void setDebugMode(boolean debugMode) | 
 |   134   { | 
 |   135     this.debugMode = debugMode; | 
 |   136   } | 
 |   137  | 
 |   138   private void d(String message) | 
 |   139   { | 
 |   140     if (debugMode) | 
 |   141     { | 
 |   142       Log.d(TAG, message); | 
 |   143     } | 
 |   144   } | 
 |   145  | 
 |   146   private void w(String message) | 
 |   147   { | 
 |   148     if (debugMode) | 
 |   149     { | 
 |   150       Log.w(TAG, message); | 
 |   151     } | 
 |   152   } | 
 |   153  | 
 |   154   private void e(String message, Throwable t) | 
 |   155   { | 
 |   156     Log.e(TAG, message, t); | 
 |   157   } | 
 |   158  | 
 |   159   private void e(String message) | 
 |   160   { | 
 |   161     Log.e(TAG, message); | 
 |   162   } | 
 |   163  | 
 |   164   private static final String BRIDGE_TOKEN = "{{BRIDGE}}"; | 
 |   165   private static final String DEBUG_TOKEN = "{{DEBUG}}"; | 
 |   166   private static final String HIDE_TOKEN = "{{HIDE}}"; | 
 |   167   private static final String BRIDGE = "jsBridge"; | 
 |   168  | 
 |   169   private String readScriptFile(String filename) throws IOException | 
 |   170   { | 
 |   171     return Utils | 
 |   172       .readAssetAsString(getContext(), filename) | 
 |   173       .replace(BRIDGE_TOKEN, BRIDGE) | 
 |   174       .replace(DEBUG_TOKEN, (debugMode ? "" : "//")); | 
 |   175   } | 
 |   176  | 
 |   177   private void runScript(String script) | 
 |   178   { | 
 |   179     d("runScript started"); | 
 |   180     if (Build.VERSION.SDK_INT >= 19) | 
 |   181     { | 
 |   182       evaluateJavascript(script, null); | 
 |   183     } | 
 |   184     else | 
 |   185     { | 
 |   186       loadUrl("javascript:" + script); | 
 |   187     } | 
 |   188     d("runScript finished"); | 
 |   189   } | 
 |   190  | 
 |   191   private Subscription acceptableAdsSubscription; | 
 |   192  | 
 |   193   private boolean acceptableAdsEnabled = true; | 
 |   194  | 
 |   195   public boolean isAcceptableAdsEnabled() | 
 |   196   { | 
 |   197     return acceptableAdsEnabled; | 
 |   198   } | 
 |   199  | 
 |   200   /** | 
 |   201    * Enable or disable Acceptable Ads | 
 |   202    * @param enabled enabled | 
 |   203    */ | 
 |   204   public void setAcceptableAdsEnabled(boolean enabled) | 
 |   205   { | 
 |   206     this.acceptableAdsEnabled = enabled; | 
 |   207  | 
 |   208     if (filterEngine != null) | 
 |   209     { | 
 |   210       applyAcceptableAds(); | 
 |   211     } | 
 |   212   } | 
 |   213  | 
 |   214   private final static String EXCEPTIONS_URL = "subscriptions_exceptionsurl"; | 
 |   215  | 
 |   216   private void applyAcceptableAds() | 
 |   217   { | 
 |   218     if (acceptableAdsEnabled) | 
 |   219     { | 
 |   220       if (acceptableAdsSubscription == null) | 
 |   221       { | 
 |   222         String url = filterEngine.getPref(EXCEPTIONS_URL).toString(); | 
 |   223         if (url == null) | 
 |   224         { | 
 |   225           w("no AA subscription url"); | 
 |   226           return; | 
 |   227         } | 
 |   228  | 
 |   229         acceptableAdsSubscription = filterEngine.getSubscription(url); | 
 |   230         acceptableAdsSubscription.addToList(); | 
 |   231         d("AA subscription added (" + url + ")"); | 
 |   232       } | 
 |   233     } | 
 |   234     else | 
 |   235     { | 
 |   236       if (acceptableAdsSubscription != null) | 
 |   237       { | 
 |   238         removeAcceptableAdsSubscription(); | 
 |   239       } | 
 |   240     } | 
 |   241   } | 
 |   242  | 
 |   243   private void removeAcceptableAdsSubscription() | 
 |   244   { | 
 |   245     acceptableAdsSubscription.removeFromList(); | 
 |   246     acceptableAdsSubscription = null; | 
 |   247     d("AA subscription removed"); | 
 |   248   } | 
 |   249  | 
 |   250   private boolean disposeFilterEngine; | 
 |   251  | 
 |   252   private JsEngine jsEngine; | 
 |   253  | 
 |   254   public JsEngine getJsEngine() | 
 |   255   { | 
 |   256     return jsEngine; | 
 |   257   } | 
 |   258  | 
 |   259   private FilterEngine filterEngine; | 
 |   260  | 
 |   261   public FilterEngine getFilterEngine() | 
 |   262   { | 
 |   263     return filterEngine; | 
 |   264   } | 
 |   265  | 
 |   266   private Integer loadError; | 
 |   267  | 
 |   268   /** | 
 |   269    * Set external filter engine. A new (internal) is created automatically if no
      t set | 
 |   270    * Don't forget to invoke {@link #dispose()} if not using external filter engi
      ne | 
 |   271    * @param newFilterEngine external filter engine | 
 |   272    */ | 
 |   273   public void setFilterEngine(FilterEngine newFilterEngine) | 
 |   274   { | 
 |   275     if (filterEngine != null && newFilterEngine != null && newFilterEngine == fi
      lterEngine) | 
 |   276     { | 
 |   277       return; | 
 |   278     } | 
 |   279  | 
 |   280     if (filterEngine != null) | 
 |   281     { | 
 |   282       if (acceptableAdsEnabled && acceptableAdsSubscription != null) | 
 |   283       { | 
 |   284         removeAcceptableAdsSubscription(); | 
 |   285       } | 
 |   286  | 
 |   287       if (disposeFilterEngine) | 
 |   288       { | 
 |   289         filterEngine.dispose(); | 
 |   290       } | 
 |   291     } | 
 |   292  | 
 |   293     filterEngine = newFilterEngine; | 
 |   294     disposeFilterEngine = false; | 
 |   295  | 
 |   296     if (filterEngine != null) | 
 |   297     { | 
 |   298       applyAcceptableAds(); | 
 |   299  | 
 |   300       if (jsEngine != null) | 
 |   301       { | 
 |   302         jsEngine.dispose(); | 
 |   303       } | 
 |   304     } | 
 |   305  | 
 |   306     jsEngine = null; | 
 |   307   } | 
 |   308  | 
 |   309   private WebChromeClient intWebChromeClient = new WebChromeClient() | 
 |   310   { | 
 |   311     @Override | 
 |   312     public void onReceivedTitle(WebView view, String title) | 
 |   313     { | 
 |   314       if (extWebChromeClient != null) | 
 |   315       { | 
 |   316         extWebChromeClient.onReceivedTitle(view, title); | 
 |   317       } | 
 |   318     } | 
 |   319  | 
 |   320     @Override | 
 |   321     public void onReceivedIcon(WebView view, Bitmap icon) | 
 |   322     { | 
 |   323       if (extWebChromeClient != null) | 
 |   324       { | 
 |   325         extWebChromeClient.onReceivedIcon(view, icon); | 
 |   326       } | 
 |   327     } | 
 |   328  | 
 |   329     @Override | 
 |   330     public void onReceivedTouchIconUrl(WebView view, String url, boolean precomp
      osed) | 
 |   331     { | 
 |   332       if (extWebChromeClient != null) | 
 |   333       { | 
 |   334         extWebChromeClient.onReceivedTouchIconUrl(view, url, precomposed); | 
 |   335       } | 
 |   336     } | 
 |   337  | 
 |   338     @Override | 
 |   339     public void onShowCustomView(View view, CustomViewCallback callback) | 
 |   340     { | 
 |   341       if (extWebChromeClient != null) | 
 |   342       { | 
 |   343         extWebChromeClient.onShowCustomView(view, callback); | 
 |   344       } | 
 |   345     } | 
 |   346  | 
 |   347     @Override | 
 |   348     public void onShowCustomView(View view, int requestedOrientation, CustomView
      Callback callback) | 
 |   349     { | 
 |   350       if (extWebChromeClient != null) | 
 |   351       { | 
 |   352         extWebChromeClient.onShowCustomView(view, requestedOrientation, callback
      ); | 
 |   353       } | 
 |   354     } | 
 |   355  | 
 |   356     @Override | 
 |   357     public void onHideCustomView() | 
 |   358     { | 
 |   359       if (extWebChromeClient != null) | 
 |   360       { | 
 |   361         extWebChromeClient.onHideCustomView(); | 
 |   362       } | 
 |   363     } | 
 |   364  | 
 |   365     @Override | 
 |   366     public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUser
      Gesture, | 
 |   367                                   Message resultMsg) | 
 |   368     { | 
 |   369       if (extWebChromeClient != null) | 
 |   370       { | 
 |   371         return extWebChromeClient.onCreateWindow(view, isDialog, isUserGesture, 
      resultMsg); | 
 |   372       } | 
 |   373       else | 
 |   374       { | 
 |   375         return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg); | 
 |   376       } | 
 |   377     } | 
 |   378  | 
 |   379     @Override | 
 |   380     public void onRequestFocus(WebView view) | 
 |   381     { | 
 |   382       if (extWebChromeClient != null) | 
 |   383       { | 
 |   384         extWebChromeClient.onRequestFocus(view); | 
 |   385       } | 
 |   386     } | 
 |   387  | 
 |   388     @Override | 
 |   389     public void onCloseWindow(WebView window) | 
 |   390     { | 
 |   391       if (extWebChromeClient != null) | 
 |   392       { | 
 |   393         extWebChromeClient.onCloseWindow(window); | 
 |   394       } | 
 |   395     } | 
 |   396  | 
 |   397     @Override | 
 |   398     public boolean onJsAlert(WebView view, String url, String message, JsResult 
      result) | 
 |   399     { | 
 |   400       if (extWebChromeClient != null) | 
 |   401       { | 
 |   402         return extWebChromeClient.onJsAlert(view, url, message, result); | 
 |   403       } | 
 |   404       else | 
 |   405       { | 
 |   406         return super.onJsAlert(view, url, message, result); | 
 |   407       } | 
 |   408     } | 
 |   409  | 
 |   410     @Override | 
 |   411     public boolean onJsConfirm(WebView view, String url, String message, JsResul
      t result) | 
 |   412     { | 
 |   413       if (extWebChromeClient != null) | 
 |   414       { | 
 |   415         return extWebChromeClient.onJsConfirm(view, url, message, result); | 
 |   416       } | 
 |   417       else | 
 |   418       { | 
 |   419         return super.onJsConfirm(view, url, message, result); | 
 |   420       } | 
 |   421     } | 
 |   422  | 
 |   423     @Override | 
 |   424     public boolean onJsPrompt(WebView view, String url, String message, String d
      efaultValue, | 
 |   425                               JsPromptResult result) | 
 |   426     { | 
 |   427       if (extWebChromeClient != null) | 
 |   428       { | 
 |   429         return extWebChromeClient.onJsPrompt(view, url, message, defaultValue, r
      esult); | 
 |   430       } | 
 |   431       else | 
 |   432       { | 
 |   433         return super.onJsPrompt(view, url, message, defaultValue, result); | 
 |   434       } | 
 |   435     } | 
 |   436  | 
 |   437     @Override | 
 |   438     public boolean onJsBeforeUnload(WebView view, String url, String message, Js
      Result result) | 
 |   439     { | 
 |   440       if (extWebChromeClient != null) | 
 |   441       { | 
 |   442         return extWebChromeClient.onJsBeforeUnload(view, url, message, result); | 
 |   443       } | 
 |   444       else | 
 |   445       { | 
 |   446         return super.onJsBeforeUnload(view, url, message, result); | 
 |   447       } | 
 |   448     } | 
 |   449  | 
 |   450     @Override | 
 |   451     public void onExceededDatabaseQuota(String url, String databaseIdentifier, l
      ong quota, | 
 |   452                                         long estimatedDatabaseSize, long totalQu
      ota, | 
 |   453                                         WebStorage.QuotaUpdater quotaUpdater) | 
 |   454     { | 
 |   455       if (extWebChromeClient != null) | 
 |   456       { | 
 |   457         extWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quot
      a, | 
 |   458           estimatedDatabaseSize, totalQuota, quotaUpdater); | 
 |   459       } | 
 |   460       else | 
 |   461       { | 
 |   462         super.onExceededDatabaseQuota(url, databaseIdentifier, quota, | 
 |   463           estimatedDatabaseSize, totalQuota, quotaUpdater); | 
 |   464       } | 
 |   465     } | 
 |   466  | 
 |   467     @Override | 
 |   468     public void onReachedMaxAppCacheSize(long requiredStorage, long quota, | 
 |   469                                          WebStorage.QuotaUpdater quotaUpdater) | 
 |   470     { | 
 |   471       if (extWebChromeClient != null) | 
 |   472       { | 
 |   473         extWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quot
      aUpdater); | 
 |   474       } | 
 |   475       else | 
 |   476       { | 
 |   477         super.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); | 
 |   478       } | 
 |   479     } | 
 |   480  | 
 |   481     @Override | 
 |   482     public void onGeolocationPermissionsShowPrompt(String origin, | 
 |   483                                                    GeolocationPermissions.Callba
      ck callback) | 
 |   484     { | 
 |   485       if (extWebChromeClient != null) | 
 |   486       { | 
 |   487         extWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); | 
 |   488       } | 
 |   489       else | 
 |   490       { | 
 |   491         super.onGeolocationPermissionsShowPrompt(origin, callback); | 
 |   492       } | 
 |   493     } | 
 |   494  | 
 |   495     @Override | 
 |   496     public void onGeolocationPermissionsHidePrompt() | 
 |   497     { | 
 |   498       if (extWebChromeClient != null) | 
 |   499       { | 
 |   500         extWebChromeClient.onGeolocationPermissionsHidePrompt(); | 
 |   501       } | 
 |   502       else | 
 |   503       { | 
 |   504         super.onGeolocationPermissionsHidePrompt(); | 
 |   505       } | 
 |   506     } | 
 |   507  | 
 |   508     @Override | 
 |   509     public boolean onJsTimeout() | 
 |   510     { | 
 |   511       if (extWebChromeClient != null) | 
 |   512       { | 
 |   513         return extWebChromeClient.onJsTimeout(); | 
 |   514       } | 
 |   515       else | 
 |   516       { | 
 |   517         return super.onJsTimeout(); | 
 |   518       } | 
 |   519     } | 
 |   520  | 
 |   521     @Override | 
 |   522     public void onConsoleMessage(String message, int lineNumber, String sourceID
      ) | 
 |   523     { | 
 |   524       if (extWebChromeClient != null) | 
 |   525       { | 
 |   526         extWebChromeClient.onConsoleMessage(message, lineNumber, sourceID); | 
 |   527       } | 
 |   528       else | 
 |   529       { | 
 |   530         super.onConsoleMessage(message, lineNumber, sourceID); | 
 |   531       } | 
 |   532     } | 
 |   533  | 
 |   534     @Override | 
 |   535     public boolean onConsoleMessage(ConsoleMessage consoleMessage) | 
 |   536     { | 
 |   537       d("JS: level=" + consoleMessage.messageLevel() | 
 |   538         + ", message=\"" + consoleMessage.message() + "\"" | 
 |   539         + ", line=" + consoleMessage.lineNumber()); | 
 |   540  | 
 |   541       if (extWebChromeClient != null) | 
 |   542       { | 
 |   543         return extWebChromeClient.onConsoleMessage(consoleMessage); | 
 |   544       } | 
 |   545       else | 
 |   546       { | 
 |   547         return super.onConsoleMessage(consoleMessage); | 
 |   548       } | 
 |   549     } | 
 |   550  | 
 |   551     @Override | 
 |   552     public Bitmap getDefaultVideoPoster() | 
 |   553     { | 
 |   554       if (extWebChromeClient != null) | 
 |   555       { | 
 |   556         return extWebChromeClient.getDefaultVideoPoster(); | 
 |   557       } | 
 |   558       else | 
 |   559       { | 
 |   560         return super.getDefaultVideoPoster(); | 
 |   561       } | 
 |   562     } | 
 |   563  | 
 |   564     @Override | 
 |   565     public View getVideoLoadingProgressView() | 
 |   566     { | 
 |   567       if (extWebChromeClient != null) | 
 |   568       { | 
 |   569         return extWebChromeClient.getVideoLoadingProgressView(); | 
 |   570       } | 
 |   571       else | 
 |   572       { | 
 |   573         return super.getVideoLoadingProgressView(); | 
 |   574       } | 
 |   575     } | 
 |   576  | 
 |   577     @Override | 
 |   578     public void getVisitedHistory(ValueCallback<String[]> callback) | 
 |   579     { | 
 |   580       if (extWebChromeClient != null) | 
 |   581       { | 
 |   582         extWebChromeClient.getVisitedHistory(callback); | 
 |   583       } | 
 |   584       else | 
 |   585       { | 
 |   586         super.getVisitedHistory(callback); | 
 |   587       } | 
 |   588     } | 
 |   589  | 
 |   590     @Override | 
 |   591     public void onProgressChanged(WebView view, int newProgress) | 
 |   592     { | 
 |   593       d("Loading progress=" + newProgress + "%"); | 
 |   594  | 
 |   595       // addDomListener is changed to 'false' in `setAddDomListener` invoked fro
      m injected JS | 
 |   596       if (getAddDomListener() && loadError == null && injectJs != null) | 
 |   597       { | 
 |   598         d("Injecting script"); | 
 |   599         runScript(injectJs); | 
 |   600  | 
 |   601         if (allowDraw && loading) | 
 |   602         { | 
 |   603           startPreventDrawing(); | 
 |   604         } | 
 |   605       } | 
 |   606  | 
 |   607       if (extWebChromeClient != null) | 
 |   608       { | 
 |   609         extWebChromeClient.onProgressChanged(view, newProgress); | 
 |   610       } | 
 |   611     } | 
 |   612   }; | 
 |   613  | 
 |   614   /** | 
 |   615    * Default (in some conditions) start redraw delay after DOM modified with inj
      ected JS (millis) | 
 |   616    */ | 
 |   617   public static final int ALLOW_DRAW_DELAY = 200; | 
 |   618   /* | 
 |   619      The value could be different for devices and completely unclear why we need
       it and | 
 |   620      how to measure actual value | 
 |   621   */ | 
 |   622  | 
 |   623   private int allowDrawDelay = ALLOW_DRAW_DELAY; | 
 |   624  | 
 |   625   public int getAllowDrawDelay() | 
 |   626   { | 
 |   627     return allowDrawDelay; | 
 |   628   } | 
 |   629  | 
 |   630   /** | 
 |   631    * Set start redraw delay after DOM modified with injected JS | 
 |   632    * (used to prevent flickering after 'DOM ready') | 
 |   633    * @param allowDrawDelay delay (in millis) | 
 |   634    */ | 
 |   635   public void setAllowDrawDelay(int allowDrawDelay) | 
 |   636   { | 
 |   637     if (allowDrawDelay < 0) | 
 |   638       throw new IllegalArgumentException("Negative value is not allowed"); | 
 |   639  | 
 |   640     this.allowDrawDelay = allowDrawDelay; | 
 |   641   } | 
 |   642  | 
 |   643   private WebViewClient extWebViewClient; | 
 |   644  | 
 |   645   @Override | 
 |   646   public void setWebViewClient(WebViewClient client) | 
 |   647   { | 
 |   648     extWebViewClient = client; | 
 |   649     applyAdblockEnabled(); | 
 |   650   } | 
 |   651  | 
 |   652   private static final Pattern RE_JS = Pattern.compile("\\.js$", Pattern.CASE_IN
      SENSITIVE); | 
 |   653   private static final Pattern RE_CSS = Pattern.compile("\\.css$", Pattern.CASE_
      INSENSITIVE); | 
 |   654   private static final Pattern RE_IMAGE = Pattern.compile("\\.(?:gif|png|jpe?g|b
      mp|ico)$", Pattern.CASE_INSENSITIVE); | 
 |   655   private static final Pattern RE_FONT = Pattern.compile("\\.(?:ttf|woff)$", Pat
      tern.CASE_INSENSITIVE); | 
 |   656   private static final Pattern RE_HTML = Pattern.compile("\\.html?$", Pattern.CA
      SE_INSENSITIVE); | 
 |   657  | 
 |   658   private WebViewClient intWebViewClient; | 
 |   659  | 
 |   660   /** | 
 |   661    * WebViewClient for API pre 21 | 
 |   662    * (does not have Referers information) | 
 |   663    */ | 
 |   664   class AdblockWebViewClient extends WebViewClient | 
 |   665   { | 
 |   666     @Override | 
 |   667     public boolean shouldOverrideUrlLoading(WebView view, String url) | 
 |   668     { | 
 |   669       if (extWebViewClient != null) | 
 |   670       { | 
 |   671         return extWebViewClient.shouldOverrideUrlLoading(view, url); | 
 |   672       } | 
 |   673       else | 
 |   674       { | 
 |   675         return super.shouldOverrideUrlLoading(view, url); | 
 |   676       } | 
 |   677     } | 
 |   678  | 
 |   679     @Override | 
 |   680     public void onPageStarted(WebView view, String url, Bitmap favicon) | 
 |   681     { | 
 |   682       if (loading) | 
 |   683       { | 
 |   684         stopAbpLoading(); | 
 |   685       } | 
 |   686  | 
 |   687       startAbpLoading(url); | 
 |   688  | 
 |   689       if (extWebViewClient != null) | 
 |   690       { | 
 |   691         extWebViewClient.onPageStarted(view, url, favicon); | 
 |   692       } | 
 |   693       else | 
 |   694       { | 
 |   695         super.onPageStarted(view, url, favicon); | 
 |   696       } | 
 |   697     } | 
 |   698  | 
 |   699     @Override | 
 |   700     public void onPageFinished(WebView view, String url) | 
 |   701     { | 
 |   702       loading = false; | 
 |   703       if (extWebViewClient != null) | 
 |   704       { | 
 |   705         extWebViewClient.onPageFinished(view, url); | 
 |   706       } | 
 |   707       else | 
 |   708       { | 
 |   709         super.onPageFinished(view, url); | 
 |   710       } | 
 |   711     } | 
 |   712  | 
 |   713     @Override | 
 |   714     public void onLoadResource(WebView view, String url) | 
 |   715     { | 
 |   716       if (extWebViewClient != null) | 
 |   717       { | 
 |   718         extWebViewClient.onLoadResource(view, url); | 
 |   719       } | 
 |   720       else | 
 |   721       { | 
 |   722         super.onLoadResource(view, url); | 
 |   723       } | 
 |   724     } | 
 |   725  | 
 |   726     @Override | 
 |   727     public void onTooManyRedirects(WebView view, Message cancelMsg, Message cont
      inueMsg) | 
 |   728     { | 
 |   729       if (extWebViewClient != null) | 
 |   730       { | 
 |   731         extWebViewClient.onTooManyRedirects(view, cancelMsg, continueMsg); | 
 |   732       } | 
 |   733       else | 
 |   734       { | 
 |   735         super.onTooManyRedirects(view, cancelMsg, continueMsg); | 
 |   736       } | 
 |   737     } | 
 |   738  | 
 |   739     @Override | 
 |   740     public void onReceivedError(WebView view, int errorCode, String description,
       String failingUrl) | 
 |   741     { | 
 |   742       e("Load error:" + | 
 |   743         " code=" + errorCode + | 
 |   744         " with description=" + description + | 
 |   745         " for url=" + failingUrl); | 
 |   746       loadError = errorCode; | 
 |   747  | 
 |   748       stopAbpLoading(); | 
 |   749  | 
 |   750       if (extWebViewClient != null) | 
 |   751       { | 
 |   752         extWebViewClient.onReceivedError(view, errorCode, description, failingUr
      l); | 
 |   753       } | 
 |   754       else | 
 |   755       { | 
 |   756         super.onReceivedError(view, errorCode, description, failingUrl); | 
 |   757       } | 
 |   758     } | 
 |   759  | 
 |   760     @Override | 
 |   761     public void onFormResubmission(WebView view, Message dontResend, Message res
      end) | 
 |   762     { | 
 |   763       if (extWebViewClient != null) | 
 |   764       { | 
 |   765         extWebViewClient.onFormResubmission(view, dontResend, resend); | 
 |   766       } | 
 |   767       else | 
 |   768       { | 
 |   769         super.onFormResubmission(view, dontResend, resend); | 
 |   770       } | 
 |   771     } | 
 |   772  | 
 |   773     @Override | 
 |   774     public void doUpdateVisitedHistory(WebView view, String url, boolean isReloa
      d) | 
 |   775     { | 
 |   776       if (extWebViewClient != null) | 
 |   777       { | 
 |   778         extWebViewClient.doUpdateVisitedHistory(view, url, isReload); | 
 |   779       } | 
 |   780       else | 
 |   781       { | 
 |   782         super.doUpdateVisitedHistory(view, url, isReload); | 
 |   783       } | 
 |   784     } | 
 |   785  | 
 |   786     @Override | 
 |   787     public void onReceivedSslError(WebView view, SslErrorHandler handler, SslErr
      or error) | 
 |   788     { | 
 |   789       if (extWebViewClient != null) | 
 |   790       { | 
 |   791         extWebViewClient.onReceivedSslError(view, handler, error); | 
 |   792       } | 
 |   793       else | 
 |   794       { | 
 |   795         super.onReceivedSslError(view, handler, error); | 
 |   796       } | 
 |   797     } | 
 |   798  | 
 |   799     @Override | 
 |   800     public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler,
       String host, String realm) | 
 |   801     { | 
 |   802       if (extWebViewClient != null) | 
 |   803       { | 
 |   804         extWebViewClient.onReceivedHttpAuthRequest(view, handler, host, realm); | 
 |   805       } | 
 |   806       else | 
 |   807       { | 
 |   808         super.onReceivedHttpAuthRequest(view, handler, host, realm); | 
 |   809       } | 
 |   810     } | 
 |   811  | 
 |   812     @Override | 
 |   813     public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) | 
 |   814     { | 
 |   815       if (extWebViewClient != null) | 
 |   816       { | 
 |   817         return extWebViewClient.shouldOverrideKeyEvent(view, event); | 
 |   818       } | 
 |   819       else | 
 |   820       { | 
 |   821         return super.shouldOverrideKeyEvent(view, event); | 
 |   822       } | 
 |   823     } | 
 |   824  | 
 |   825     @Override | 
 |   826     public void onUnhandledKeyEvent(WebView view, KeyEvent event) | 
 |   827     { | 
 |   828       if (extWebViewClient != null) | 
 |   829       { | 
 |   830         extWebViewClient.onUnhandledKeyEvent(view, event); | 
 |   831       } | 
 |   832       else | 
 |   833       { | 
 |   834         super.onUnhandledKeyEvent(view, event); | 
 |   835       } | 
 |   836     } | 
 |   837  | 
 |   838     @Override | 
 |   839     public void onScaleChanged(WebView view, float oldScale, float newScale) | 
 |   840     { | 
 |   841       if (extWebViewClient != null) | 
 |   842       { | 
 |   843         extWebViewClient.onScaleChanged(view, oldScale, newScale); | 
 |   844       } | 
 |   845       else | 
 |   846       { | 
 |   847         super.onScaleChanged(view, oldScale, newScale); | 
 |   848       } | 
 |   849     } | 
 |   850  | 
 |   851     @Override | 
 |   852     public void onReceivedLoginRequest(WebView view, String realm, String accoun
      t, String args) | 
 |   853     { | 
 |   854       if (extWebViewClient != null) | 
 |   855       { | 
 |   856         extWebViewClient.onReceivedLoginRequest(view, realm, account, args); | 
 |   857       } | 
 |   858       else | 
 |   859       { | 
 |   860         super.onReceivedLoginRequest(view, realm, account, args); | 
 |   861       } | 
 |   862     } | 
 |   863  | 
 |   864     protected WebResourceResponse shouldInterceptRequest( | 
 |   865       WebView webview, String url, boolean isMainFrame, String[] documentUrls) | 
 |   866     { | 
 |   867       // if dispose() was invoke, but the page is still loading then just let it
       go | 
 |   868       if (filterEngine == null) | 
 |   869       { | 
 |   870         e("FilterEngine already disposed, allow loading"); | 
 |   871  | 
 |   872         // Allow loading by returning null | 
 |   873         return null; | 
 |   874       } | 
 |   875  | 
 |   876       if (isMainFrame) | 
 |   877       { | 
 |   878         // never blocking main frame requests, just subrequests | 
 |   879         w(url + " is main frame, allow loading"); | 
 |   880  | 
 |   881         // allow loading by returning null | 
 |   882         return null; | 
 |   883       } | 
 |   884  | 
 |   885       if (filterEngine.isDocumentWhitelisted(url, documentUrls)) | 
 |   886       { | 
 |   887         w(url + " document is whitelisted, allow loading"); | 
 |   888  | 
 |   889         // allow loading by returning null | 
 |   890         return null; | 
 |   891       } | 
 |   892  | 
 |   893       // determine the content | 
 |   894       FilterEngine.ContentType contentType; | 
 |   895       if (RE_JS.matcher(url).find()) | 
 |   896       { | 
 |   897         contentType = FilterEngine.ContentType.SCRIPT; | 
 |   898       } | 
 |   899       else if (RE_CSS.matcher(url).find()) | 
 |   900       { | 
 |   901         contentType = FilterEngine.ContentType.STYLESHEET; | 
 |   902       } | 
 |   903       else if (RE_IMAGE.matcher(url).find()) | 
 |   904       { | 
 |   905         contentType = FilterEngine.ContentType.IMAGE; | 
 |   906       } | 
 |   907       else if (RE_FONT.matcher(url).find()) | 
 |   908       { | 
 |   909         contentType = FilterEngine.ContentType.FONT; | 
 |   910       } | 
 |   911       else if (RE_HTML.matcher(url).find()) | 
 |   912       { | 
 |   913         contentType = FilterEngine.ContentType.SUBDOCUMENT; | 
 |   914       } | 
 |   915       else | 
 |   916       { | 
 |   917         contentType = FilterEngine.ContentType.OTHER; | 
 |   918       } | 
 |   919  | 
 |   920       // check if we should block | 
 |   921       Filter filter = filterEngine.matches(url, contentType, documentUrls); | 
 |   922       if (filter != null) | 
 |   923       { | 
 |   924         if (filter.getType().equals(Filter.Type.BLOCKING)) | 
 |   925         { | 
 |   926           w("Blocked loading " + url + " due to filter \"" + filter.toString() +
       "\""); | 
 |   927  | 
 |   928           // If we should block, return empty response which results in 'errorLo
      ading' callback | 
 |   929           return new WebResourceResponse("text/plain", "UTF-8", null); | 
 |   930         } | 
 |   931         else | 
 |   932         { | 
 |   933           w(url + "is not blocked due to filter \"" + filter.toString() + "\" of
       type " + filter.getType()); | 
 |   934         } | 
 |   935       } | 
 |   936       else | 
 |   937       { | 
 |   938         d("No filter found for " + url); | 
 |   939       } | 
 |   940  | 
 |   941       d("Allowed loading " + url); | 
 |   942  | 
 |   943       // continue by returning null | 
 |   944       return null; | 
 |   945     } | 
 |   946  | 
 |   947     @Override | 
 |   948     public WebResourceResponse shouldInterceptRequest(WebView view, String url) | 
 |   949     { | 
 |   950       // if dispose() was invoke, but the page is still loading then just let it
       go | 
 |   951       if (filterEngine == null) | 
 |   952       { | 
 |   953         e("FilterEngine already disposed"); | 
 |   954         return null; | 
 |   955       } | 
 |   956  | 
 |   957       /* | 
 |   958         we use hack - injecting Proxy instance instead of IoThreadClient and int
      ercepting | 
 |   959         `isForMainFrame` argument value, see `initIoThreadClient()`. | 
 |   960  | 
 |   961         it's expected to be not `null` as first invocation goes to | 
 |   962         https://android.googlesource.com/platform/external/chromium_org/+/androi
      d-4.4_r1/android_webview/java/src/org/chromium/android_webview/AwContents.java#2
      35 | 
 |   963         and we intercept it by injecting our proxy instead of `IoThreadClientImp
      l` | 
 |   964         and then it goes here | 
 |   965         https://android.googlesource.com/platform/external/chromium_org/+/androi
      d-4.4_r1/android_webview/java/src/org/chromium/android_webview/AwContents.java#2
      42 | 
 |   966       */ | 
 |   967       Boolean isMainFrame = IoThreadClientInvocationHandler.isMainFrame; | 
 |   968       if (isMainFrame == null) | 
 |   969       { | 
 |   970         // the only reason for having null here is probably it failed to inject 
      IoThreadClient proxy | 
 |   971         isMainFrame = false; | 
 |   972       } | 
 |   973  | 
 |   974       // sadly we don't have request headers | 
 |   975       // (and referer until android-21 and new callback with request (instead of
       just url) argument) | 
 |   976       String[] referers = null; | 
 |   977  | 
 |   978       return shouldInterceptRequest(view, url, isMainFrame, referers); | 
 |   979     } | 
 |   980   } | 
 |   981  | 
 |   982   private void clearReferers() | 
 |   983   { | 
 |   984     d("Clearing referers"); | 
 |   985     url2Referer.clear(); | 
 |   986   } | 
 |   987  | 
 |   988   /** | 
 |   989    * WebViewClient for API 21 and newer | 
 |   990    * (has Referer since it overrides `shouldInterceptRequest(..., request)` with
       referer) | 
 |   991    */ | 
 |   992   class AdblockWebViewClient21 extends AdblockWebViewClient | 
 |   993   { | 
 |   994     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | 
 |   995     @Override | 
 |   996     public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceR
      equest request) | 
 |   997     { | 
 |   998       // here we just trying to fill url -> referer map | 
 |   999       // blocking/allowing loading will happen in `shouldInterceptRequest(WebVie
      w,String)` | 
 |  1000       String url = request.getUrl().toString(); | 
 |  1001       String referer = request.getRequestHeaders().get("Referer"); | 
 |  1002       String[] referers; | 
 |  1003  | 
 |  1004       if (referer != null) | 
 |  1005       { | 
 |  1006         d("Header referer for " + url + " is " + referer); | 
 |  1007         url2Referer.put(url, referer); | 
 |  1008  | 
 |  1009         referers = new String[] | 
 |  1010         { | 
 |  1011           referer | 
 |  1012         }; | 
 |  1013       } | 
 |  1014       else | 
 |  1015       { | 
 |  1016         w("No referer header for " + url); | 
 |  1017         referers = EMPTY_ARRAY; | 
 |  1018       } | 
 |  1019  | 
 |  1020       return shouldInterceptRequest(view, url, request.isForMainFrame(), referer
      s); | 
 |  1021     } | 
 |  1022   } | 
 |  1023  | 
 |  1024   private Map<String, String> url2Referer = Collections.synchronizedMap(new Hash
      Map<String, String>()); | 
 |  1025  | 
 |  1026   private void initAbp() | 
 |  1027   { | 
 |  1028     addJavascriptInterface(this, BRIDGE); | 
 |  1029     initClients(); | 
 |  1030  | 
 |  1031     if (Build.VERSION.SDK_INT < 21) | 
 |  1032     { | 
 |  1033       initIoThreadClient(); | 
 |  1034     } | 
 |  1035   } | 
 |  1036  | 
 |  1037   private synchronized void initIoThreadClient() | 
 |  1038   { | 
 |  1039     final Object awContents = ReflectionUtils.extractProperty(this, new String[] | 
 |  1040       { | 
 |  1041         "mProvider", | 
 |  1042         "mAwContents" | 
 |  1043       }); | 
 |  1044  | 
 |  1045     final String ioThreadClientProperty = "mIoThreadClient"; | 
 |  1046     final Object originalClient = ReflectionUtils.extractProperty( | 
 |  1047       awContents, | 
 |  1048       new String[] | 
 |  1049       { | 
 |  1050         ioThreadClientProperty | 
 |  1051       }); | 
 |  1052  | 
 |  1053     // avoid injecting twice (already injected Proxy instance has another class 
      name) | 
 |  1054     if (!originalClient.getClass().getSimpleName().startsWith("$Proxy")) | 
 |  1055     { | 
 |  1056       Object proxyClient = Proxy.newProxyInstance( | 
 |  1057         originalClient.getClass().getClassLoader(), | 
 |  1058         originalClient.getClass().getInterfaces(), | 
 |  1059         new IoThreadClientInvocationHandler(originalClient)); | 
 |  1060  | 
 |  1061       // inject proxy instead of original client | 
 |  1062       boolean injected = ReflectionUtils.injectProperty(awContents, ioThreadClie
      ntProperty, proxyClient); | 
 |  1063       if (injected) | 
 |  1064       { | 
 |  1065         Integer mNativeAwContents = (Integer) ReflectionUtils.extractProperty(aw
      Contents, "mNativeAwContents"); | 
 |  1066         Object mWebContentsDelegate = ReflectionUtils.extractProperty(awContents
      , "mWebContentsDelegate"); | 
 |  1067         Object mContentsClientBridge = ReflectionUtils.extractProperty(awContent
      s, "mContentsClientBridge"); | 
 |  1068         Object mInterceptNavigationDelegate = ReflectionUtils.extractProperty(aw
      Contents, "mInterceptNavigationDelegate"); | 
 |  1069  | 
 |  1070         boolean invoked = ReflectionUtils.invokeMethod(awContents, "nativeSetJav
      aPeers", new Object[] | 
 |  1071           { | 
 |  1072             mNativeAwContents, awContents, mWebContentsDelegate, | 
 |  1073             mContentsClientBridge, proxyClient, mInterceptNavigationDelegate | 
 |  1074           }); | 
 |  1075         if (!invoked) | 
 |  1076         { | 
 |  1077           e("Failed to inject IoThreadClient proxy"); | 
 |  1078         } | 
 |  1079       } | 
 |  1080     } | 
 |  1081   } | 
 |  1082  | 
 |  1083   private void initClients() | 
 |  1084   { | 
 |  1085     if (Build.VERSION.SDK_INT >= 21) | 
 |  1086     { | 
 |  1087       intWebViewClient = new AdblockWebViewClient21(); | 
 |  1088     } | 
 |  1089     else | 
 |  1090     { | 
 |  1091       intWebViewClient = new AdblockWebViewClient(); | 
 |  1092     } | 
 |  1093     applyAdblockEnabled(); | 
 |  1094   } | 
 |  1095  | 
 |  1096   /** | 
 |  1097    * Build app info using Android package information | 
 |  1098    * @param context context | 
 |  1099    * @param developmentBuild if it's dev build | 
 |  1100    * @return app info required to build JsEngine | 
 |  1101    */ | 
 |  1102   public static AppInfo buildAppInfo(final Context context, boolean developmentB
      uild) | 
 |  1103   { | 
 |  1104     String version = "0"; | 
 |  1105     try | 
 |  1106     { | 
 |  1107       final PackageInfo info = context.getPackageManager().getPackageInfo(contex
      t.getPackageName(), 0); | 
 |  1108       version = info.versionName; | 
 |  1109       if (developmentBuild) | 
 |  1110         version += "." + info.versionCode; | 
 |  1111     } | 
 |  1112     catch (final PackageManager.NameNotFoundException e) | 
 |  1113     { | 
 |  1114       Log.e(TAG, "Failed to get the application version number", e); | 
 |  1115     } | 
 |  1116     final String sdkVersion = String.valueOf(Build.VERSION.SDK_INT); | 
 |  1117     final String locale = Locale.getDefault().toString().replace('_', '-'); | 
 |  1118  | 
 |  1119     return AppInfo.builder() | 
 |  1120       .setVersion(version) | 
 |  1121       .setApplicationVersion(sdkVersion) | 
 |  1122       .setLocale(locale) | 
 |  1123       .setDevelopmentBuild(developmentBuild) | 
 |  1124       .build(); | 
 |  1125   } | 
 |  1126  | 
 |  1127   /** | 
 |  1128    * Build JsEngine required to build FilterEngine | 
 |  1129    * @param context context | 
 |  1130    * @param developmentBuild if it's dev build | 
 |  1131    * @return JsEngine | 
 |  1132    */ | 
 |  1133   public static JsEngine buildJsEngine(Context context, boolean developmentBuild
      ) | 
 |  1134   { | 
 |  1135     JsEngine jsEngine = new JsEngine(buildAppInfo(context, developmentBuild)); | 
 |  1136     jsEngine.setDefaultFileSystem(context.getCacheDir().getAbsolutePath()); | 
 |  1137     jsEngine.setWebRequest(new AndroidWebRequest(true)); // 'true' because we ne
      ed element hiding | 
 |  1138     return jsEngine; | 
 |  1139   } | 
 |  1140  | 
 |  1141   private void createFilterEngine() | 
 |  1142   { | 
 |  1143     w("Creating FilterEngine"); | 
 |  1144     jsEngine = buildJsEngine(getContext(), debugMode); | 
 |  1145     filterEngine = new FilterEngine(jsEngine); | 
 |  1146     applyAcceptableAds(); | 
 |  1147     d("FilterEngine created"); | 
 |  1148   } | 
 |  1149  | 
 |  1150   private String url; | 
 |  1151   private String domain; | 
 |  1152   private String injectJs; | 
 |  1153   private CountDownLatch elemHideLatch; | 
 |  1154   private String elemHideSelectorsString; | 
 |  1155   private Object elemHideThreadLockObject = new Object(); | 
 |  1156   private ElemHideThread elemHideThread; | 
 |  1157  | 
 |  1158   private static final String[] EMPTY_ARRAY = new String[] {}; | 
 |  1159  | 
 |  1160   private class ElemHideThread extends Thread | 
 |  1161   { | 
 |  1162     private String selectorsString; | 
 |  1163     private CountDownLatch finishedLatch; | 
 |  1164     private AtomicBoolean isCancelled; | 
 |  1165  | 
 |  1166     public ElemHideThread(CountDownLatch finishedLatch) | 
 |  1167     { | 
 |  1168       this.finishedLatch = finishedLatch; | 
 |  1169       isCancelled = new AtomicBoolean(false); | 
 |  1170     } | 
 |  1171  | 
 |  1172     @Override | 
 |  1173     public void run() | 
 |  1174     { | 
 |  1175       try | 
 |  1176       { | 
 |  1177         if (filterEngine == null) | 
 |  1178         { | 
 |  1179           w("FilterEngine already disposed"); | 
 |  1180           selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; | 
 |  1181         } | 
 |  1182         else | 
 |  1183         { | 
 |  1184           String[] referers = new String[] | 
 |  1185           { | 
 |  1186             url | 
 |  1187           }; | 
 |  1188  | 
 |  1189           d("Check whitelisting for " + url); | 
 |  1190  | 
 |  1191           if (filterEngine.isDocumentWhitelisted(url, referers)) | 
 |  1192           { | 
 |  1193             w("Whitelisted " + url + " for document"); | 
 |  1194             selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; | 
 |  1195           } | 
 |  1196           else if (filterEngine.isElemhideWhitelisted(url, referers)) | 
 |  1197           { | 
 |  1198             w("Whitelisted " + url + " for elemhide"); | 
 |  1199             selectorsString = EMPTY_ELEMHIDE_ARRAY_STRING; | 
 |  1200           } | 
 |  1201           else | 
 |  1202           { | 
 |  1203             List<Subscription> subscriptions = filterEngine.getListedSubscriptio
      ns(); | 
 |  1204             d("Listed subscriptions: " + subscriptions.size()); | 
 |  1205             if (debugMode) | 
 |  1206             { | 
 |  1207               for (Subscription eachSubscription : subscriptions) | 
 |  1208               { | 
 |  1209                 d("Subscribed to " + eachSubscription); | 
 |  1210               } | 
 |  1211             } | 
 |  1212  | 
 |  1213             d("Requesting elemhide selectors from FilterEngine for " + domain + 
      " in " + this); | 
 |  1214             List<String> selectors = filterEngine.getElementHidingSelectors(doma
      in); | 
 |  1215             d("Finished requesting elemhide selectors, got " + selectors.size() 
      + " in " + this); | 
 |  1216             selectorsString = Utils.stringListToJsonArray(selectors); | 
 |  1217           } | 
 |  1218         } | 
 |  1219       } | 
 |  1220       finally | 
 |  1221       { | 
 |  1222         if (!isCancelled.get()) | 
 |  1223         { | 
 |  1224           finish(selectorsString); | 
 |  1225         } | 
 |  1226         else | 
 |  1227         { | 
 |  1228           w("This thread is cancelled, exiting silently " + this); | 
 |  1229         } | 
 |  1230       } | 
 |  1231     } | 
 |  1232  | 
 |  1233     private void onFinished() | 
 |  1234     { | 
 |  1235       finishedLatch.countDown(); | 
 |  1236       synchronized (finishedRunnableLockObject) | 
 |  1237       { | 
 |  1238         if (finishedRunnable != null) | 
 |  1239         { | 
 |  1240           finishedRunnable.run(); | 
 |  1241         } | 
 |  1242       } | 
 |  1243     } | 
 |  1244  | 
 |  1245     private void finish(String result) | 
 |  1246     { | 
 |  1247       d("Setting elemhide string " + result.length() + " bytes"); | 
 |  1248       elemHideSelectorsString = result; | 
 |  1249       onFinished(); | 
 |  1250     } | 
 |  1251  | 
 |  1252     private Object finishedRunnableLockObject = new Object(); | 
 |  1253     private Runnable finishedRunnable; | 
 |  1254  | 
 |  1255     public void setFinishedRunnable(Runnable runnable) | 
 |  1256     { | 
 |  1257       synchronized (finishedRunnableLockObject) | 
 |  1258       { | 
 |  1259         this.finishedRunnable = runnable; | 
 |  1260       } | 
 |  1261     } | 
 |  1262  | 
 |  1263     public void cancel() | 
 |  1264     { | 
 |  1265       w("Cancelling elemhide thread " + this); | 
 |  1266       isCancelled.set(true); | 
 |  1267  | 
 |  1268       finish(EMPTY_ELEMHIDE_ARRAY_STRING); | 
 |  1269     } | 
 |  1270   } | 
 |  1271  | 
 |  1272   private Runnable elemHideThreadFinishedRunnable = new Runnable() | 
 |  1273   { | 
 |  1274     @Override | 
 |  1275     public void run() | 
 |  1276     { | 
 |  1277       synchronized (elemHideThreadLockObject) | 
 |  1278       { | 
 |  1279         w("elemHideThread set to null"); | 
 |  1280         elemHideThread = null; | 
 |  1281       } | 
 |  1282     } | 
 |  1283   }; | 
 |  1284  | 
 |  1285   private boolean loading; | 
 |  1286  | 
 |  1287   private void initAbpLoading() | 
 |  1288   { | 
 |  1289     getSettings().setJavaScriptEnabled(true); | 
 |  1290     buildInjectJs(); | 
 |  1291  | 
 |  1292     if (filterEngine == null) | 
 |  1293     { | 
 |  1294       createFilterEngine(); | 
 |  1295       disposeFilterEngine = true; | 
 |  1296     } | 
 |  1297   } | 
 |  1298  | 
 |  1299   private void startAbpLoading(String newUrl) | 
 |  1300   { | 
 |  1301     d("Start loading " + newUrl); | 
 |  1302  | 
 |  1303     loading = true; | 
 |  1304     addDomListener = true; | 
 |  1305     elementsHidden = false; | 
 |  1306     loadError = null; | 
 |  1307     url = newUrl; | 
 |  1308  | 
 |  1309     if (url != null) | 
 |  1310     { | 
 |  1311       try | 
 |  1312       { | 
 |  1313         domain = Utils.getDomain(url); | 
 |  1314       } | 
 |  1315       catch (URISyntaxException e) | 
 |  1316       { | 
 |  1317         domain = null; | 
 |  1318         e("Failed to extract domain for " + url); | 
 |  1319       } | 
 |  1320  | 
 |  1321       elemHideLatch = new CountDownLatch(1); | 
 |  1322       elemHideThread = new ElemHideThread(elemHideLatch); | 
 |  1323       elemHideThread.setFinishedRunnable(elemHideThreadFinishedRunnable); | 
 |  1324       elemHideThread.start(); | 
 |  1325     } | 
 |  1326     else | 
 |  1327     { | 
 |  1328       elemHideLatch = null; | 
 |  1329     } | 
 |  1330   } | 
 |  1331  | 
 |  1332   private void buildInjectJs() | 
 |  1333   { | 
 |  1334     try | 
 |  1335     { | 
 |  1336       if (injectJs == null) | 
 |  1337       { | 
 |  1338         injectJs = readScriptFile("inject.js").replace(HIDE_TOKEN, readScriptFil
      e("css.js")); | 
 |  1339       } | 
 |  1340     } | 
 |  1341     catch (IOException e) | 
 |  1342     { | 
 |  1343       e("Failed to read script", e); | 
 |  1344     } | 
 |  1345   } | 
 |  1346  | 
 |  1347   @Override | 
 |  1348   public void goBack() | 
 |  1349   { | 
 |  1350     if (loading) | 
 |  1351     { | 
 |  1352       stopAbpLoading(); | 
 |  1353     } | 
 |  1354  | 
 |  1355     super.goBack(); | 
 |  1356   } | 
 |  1357  | 
 |  1358   @Override | 
 |  1359   public void goForward() | 
 |  1360   { | 
 |  1361     if (loading) | 
 |  1362     { | 
 |  1363       stopAbpLoading(); | 
 |  1364     } | 
 |  1365  | 
 |  1366     super.goForward(); | 
 |  1367   } | 
 |  1368  | 
 |  1369   @Override | 
 |  1370   public void reload() | 
 |  1371   { | 
 |  1372     initAbpLoading(); | 
 |  1373  | 
 |  1374     if (loading) | 
 |  1375     { | 
 |  1376       stopAbpLoading(); | 
 |  1377     } | 
 |  1378  | 
 |  1379     super.reload(); | 
 |  1380   } | 
 |  1381  | 
 |  1382   @Override | 
 |  1383   public void loadUrl(String url) | 
 |  1384   { | 
 |  1385     initAbpLoading(); | 
 |  1386  | 
 |  1387     if (loading) | 
 |  1388     { | 
 |  1389       stopAbpLoading(); | 
 |  1390     } | 
 |  1391  | 
 |  1392     super.loadUrl(url); | 
 |  1393   } | 
 |  1394  | 
 |  1395   @Override | 
 |  1396   public void loadUrl(String url, Map<String, String> additionalHttpHeaders) | 
 |  1397   { | 
 |  1398     initAbpLoading(); | 
 |  1399  | 
 |  1400     if (loading) | 
 |  1401     { | 
 |  1402       stopAbpLoading(); | 
 |  1403     } | 
 |  1404  | 
 |  1405     super.loadUrl(url, additionalHttpHeaders); | 
 |  1406   } | 
 |  1407  | 
 |  1408   @Override | 
 |  1409   public void loadData(String data, String mimeType, String encoding) | 
 |  1410   { | 
 |  1411     initAbpLoading(); | 
 |  1412  | 
 |  1413     if (loading) | 
 |  1414     { | 
 |  1415       stopAbpLoading(); | 
 |  1416     } | 
 |  1417  | 
 |  1418     super.loadData(data, mimeType, encoding); | 
 |  1419   } | 
 |  1420  | 
 |  1421   @Override | 
 |  1422   public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, 
      String encoding, | 
 |  1423                                   String historyUrl) | 
 |  1424   { | 
 |  1425     initAbpLoading(); | 
 |  1426  | 
 |  1427     if (loading) | 
 |  1428     { | 
 |  1429       stopAbpLoading(); | 
 |  1430     } | 
 |  1431  | 
 |  1432     super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); | 
 |  1433   } | 
 |  1434  | 
 |  1435   @Override | 
 |  1436   public void stopLoading() | 
 |  1437   { | 
 |  1438     stopAbpLoading(); | 
 |  1439     super.stopLoading(); | 
 |  1440   } | 
 |  1441  | 
 |  1442   private void stopAbpLoading() | 
 |  1443   { | 
 |  1444     d("Stop abp loading"); | 
 |  1445  | 
 |  1446     loading = false; | 
 |  1447     stopPreventDrawing(); | 
 |  1448     clearReferers(); | 
 |  1449  | 
 |  1450     synchronized (elemHideThreadLockObject) | 
 |  1451     { | 
 |  1452       if (elemHideThread != null) | 
 |  1453       { | 
 |  1454         elemHideThread.cancel(); | 
 |  1455       } | 
 |  1456     } | 
 |  1457   } | 
 |  1458  | 
 |  1459   private volatile boolean elementsHidden = false; | 
 |  1460  | 
 |  1461   // warning: do not rename (used in injected JS by method name) | 
 |  1462   @JavascriptInterface | 
 |  1463   public void setElementsHidden(boolean value) | 
 |  1464   { | 
 |  1465     // invoked with 'true' by JS callback when DOM is loaded | 
 |  1466     elementsHidden = value; | 
 |  1467  | 
 |  1468     // fired on worker thread, but needs to be invoked on main thread | 
 |  1469     if (value) | 
 |  1470     { | 
 |  1471 //     handler.post(allowDrawRunnable); | 
 |  1472 //     should work, but it's not working: | 
 |  1473 //     the user can see element visible even though it was hidden on dom event | 
 |  1474  | 
 |  1475       if (allowDrawDelay > 0) | 
 |  1476       { | 
 |  1477         d("Scheduled 'allow drawing' invocation in " + allowDrawDelay + " ms"); | 
 |  1478       } | 
 |  1479       handler.postDelayed(allowDrawRunnable, allowDrawDelay); | 
 |  1480     } | 
 |  1481   } | 
 |  1482  | 
 |  1483   // warning: do not rename (used in injected JS by method name) | 
 |  1484   @JavascriptInterface | 
 |  1485   public boolean isElementsHidden() | 
 |  1486   { | 
 |  1487     return elementsHidden; | 
 |  1488   } | 
 |  1489  | 
 |  1490   @Override | 
 |  1491   public void onPause() | 
 |  1492   { | 
 |  1493     handler.removeCallbacks(allowDrawRunnable); | 
 |  1494     super.onPause(); | 
 |  1495   } | 
 |  1496  | 
 |  1497   public AdblockWebView(Context context) | 
 |  1498   { | 
 |  1499     super(context); | 
 |  1500     initAbp(); | 
 |  1501   } | 
 |  1502  | 
 |  1503   public AdblockWebView(Context context, AttributeSet attrs) | 
 |  1504   { | 
 |  1505     super(context, attrs); | 
 |  1506     initAbp(); | 
 |  1507   } | 
 |  1508  | 
 |  1509   public AdblockWebView(Context context, AttributeSet attrs, int defStyle) | 
 |  1510   { | 
 |  1511     super(context, attrs, defStyle); | 
 |  1512     initAbp(); | 
 |  1513   } | 
 |  1514  | 
 |  1515   // used to prevent user see flickering for elements to hide | 
 |  1516   // for some reason it's rendered even if element is hidden on 'dom ready' even
      t | 
 |  1517   private volatile boolean allowDraw = true; | 
 |  1518  | 
 |  1519   @Override | 
 |  1520   protected void onDraw(Canvas canvas) | 
 |  1521   { | 
 |  1522     if (allowDraw) | 
 |  1523     { | 
 |  1524       super.onDraw(canvas); | 
 |  1525     } | 
 |  1526     else | 
 |  1527     { | 
 |  1528       w("Prevent drawing"); | 
 |  1529       drawEmptyPage(canvas); | 
 |  1530     } | 
 |  1531   } | 
 |  1532  | 
 |  1533   private void drawEmptyPage(Canvas canvas) | 
 |  1534   { | 
 |  1535     // assuming default color is WHITE | 
 |  1536     canvas.drawColor(Color.WHITE); | 
 |  1537   } | 
 |  1538  | 
 |  1539   private Handler handler = new Handler(); | 
 |  1540  | 
 |  1541   protected void startPreventDrawing() | 
 |  1542   { | 
 |  1543     w("Start prevent drawing"); | 
 |  1544  | 
 |  1545     allowDraw = false; | 
 |  1546   } | 
 |  1547  | 
 |  1548   protected void stopPreventDrawing() | 
 |  1549   { | 
 |  1550     d("Stop prevent drawing, invalidating"); | 
 |  1551  | 
 |  1552     allowDraw = true; | 
 |  1553     invalidate(); | 
 |  1554   } | 
 |  1555  | 
 |  1556   private Runnable allowDrawRunnable = new Runnable() | 
 |  1557   { | 
 |  1558     @Override | 
 |  1559     public void run() | 
 |  1560     { | 
 |  1561       stopPreventDrawing(); | 
 |  1562     } | 
 |  1563   }; | 
 |  1564  | 
 |  1565   private static final String EMPTY_ELEMHIDE_ARRAY_STRING = "[]"; | 
 |  1566  | 
 |  1567   // warning: do not rename (used in injected JS by method name) | 
 |  1568   @JavascriptInterface | 
 |  1569   public String getElemhideSelectors() | 
 |  1570   { | 
 |  1571     if (elemHideLatch == null) | 
 |  1572     { | 
 |  1573       return EMPTY_ELEMHIDE_ARRAY_STRING; | 
 |  1574     } | 
 |  1575     else | 
 |  1576     { | 
 |  1577       try | 
 |  1578       { | 
 |  1579         // elemhide selectors list getting is started in startAbpLoad() in backg
      round thread | 
 |  1580         d("Waiting for elemhide selectors to be ready"); | 
 |  1581         elemHideLatch.await(); | 
 |  1582         d("Elemhide selectors ready, " + elemHideSelectorsString.length() + " by
      tes"); | 
 |  1583  | 
 |  1584         clearReferers(); | 
 |  1585  | 
 |  1586         return elemHideSelectorsString; | 
 |  1587       } | 
 |  1588       catch (InterruptedException e) | 
 |  1589       { | 
 |  1590         w("Interrupted, returning empty selectors list"); | 
 |  1591         return EMPTY_ELEMHIDE_ARRAY_STRING; | 
 |  1592       } | 
 |  1593     } | 
 |  1594   } | 
 |  1595  | 
 |  1596   private void doDispose() | 
 |  1597   { | 
 |  1598     w("Disposing jsEngine"); | 
 |  1599     jsEngine.dispose(); | 
 |  1600     jsEngine = null; | 
 |  1601  | 
 |  1602     w("Disposing filterEngine"); | 
 |  1603     filterEngine.dispose(); | 
 |  1604     filterEngine = null; | 
 |  1605  | 
 |  1606     disposeFilterEngine = false; | 
 |  1607   } | 
 |  1608  | 
 |  1609   public void dispose() | 
 |  1610   { | 
 |  1611     d("Dispose invoked"); | 
 |  1612  | 
 |  1613     removeJavascriptInterface(BRIDGE); | 
 |  1614  | 
 |  1615     if (disposeFilterEngine) | 
 |  1616     { | 
 |  1617       synchronized (elemHideThreadLockObject) | 
 |  1618       { | 
 |  1619         if (elemHideThread != null) | 
 |  1620         { | 
 |  1621           w("Busy with elemhide selectors, delayed disposing scheduled"); | 
 |  1622           elemHideThread.setFinishedRunnable(new Runnable() | 
 |  1623           { | 
 |  1624             @Override | 
 |  1625             public void run() | 
 |  1626             { | 
 |  1627               doDispose(); | 
 |  1628             } | 
 |  1629           }); | 
 |  1630         } | 
 |  1631         else | 
 |  1632         { | 
 |  1633           doDispose(); | 
 |  1634         } | 
 |  1635       } | 
 |  1636     } | 
 |  1637   } | 
 |  1638 } | 
| OLD | NEW |