Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: libadblockplus-android-webview/src/org/adblockplus/android/AdblockWebView.java

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

Powered by Google App Engine
This is Rietveld