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

Powered by Google App Engine
This is Rietveld