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

Powered by Google App Engine
This is Rietveld