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

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

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

Powered by Google App Engine
This is Rietveld