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: get 'isMainFrame' for android-pre-21 and #4518 fix Created Oct. 14, 2016, 12:34 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.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 }
OLDNEW

Powered by Google App Engine
This is Rietveld