2 * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
6 * Privacy Browser is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * Privacy Browser is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with Privacy Browser. If not, see <http://www.gnu.org/licenses/>.
20 package com.stoutner.privacybrowser.views;
22 import android.content.Context;
23 import android.graphics.Bitmap;
24 import android.graphics.drawable.BitmapDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.util.AttributeSet;
27 import android.view.MotionEvent;
28 import android.webkit.WebView;
30 import androidx.annotation.NonNull;
31 import androidx.core.content.ContextCompat;
32 import androidx.core.view.NestedScrollingChild2;
33 import androidx.core.view.NestedScrollingChildHelper;
34 import androidx.core.view.ViewCompat;
36 import com.stoutner.privacybrowser.R;
38 import java.util.ArrayList;
39 import java.util.Date;
41 // NestedScrollWebView extends WebView to handle nested scrolls (scrolling the app bar off the screen).
42 public class NestedScrollWebView extends WebView implements NestedScrollingChild2 {
43 // These constants identify the blocklists.
44 public final static int BLOCKED_REQUESTS = 0;
45 public final static int EASY_LIST = 1;
46 public final static int EASY_PRIVACY = 2;
47 public final static int FANBOYS_ANNOYANCE_LIST = 3;
48 public final static int FANBOYS_SOCIAL_BLOCKING_LIST = 4;
49 public final static int ULTRA_PRIVACY = 5;
50 public final static int THIRD_PARTY_REQUESTS = 6;
52 // Keep a copy of the WebView fragment ID.
53 private long webViewFragmentId;
55 // Track if domain settings are applied to this nested scroll WebView and, if so, the database ID.
56 private boolean domainSettingsApplied;
57 private int domainSettingsDatabaseId;
59 // Keep track of when the domain name changes so that domain settings can be reapplied. This should never be null.
60 private String currentDomainName = "";
62 // Track the status of first-party cookies.
63 private boolean acceptFirstPartyCookies;
65 // Track the resource requests.
66 private ArrayList<String[]> resourceRequests = new ArrayList<>();
67 private boolean easyListEnabled;
68 private boolean easyPrivacyEnabled;
69 private boolean fanboysAnnoyanceListEnabled;
70 private boolean fanboysSocialBlockingListEnabled;
71 private boolean ultraPrivacyEnabled;
72 private boolean blockAllThirdPartyRequests;
73 private int blockedRequests;
74 private int easyListBlockedRequests;
75 private int easyPrivacyBlockedRequests;
76 private int fanboysAnnoyanceListBlockedRequests;
77 private int fanboysSocialBlockingListBlockedRequests;
78 private int ultraPrivacyBlockedRequests;
79 private int thirdPartyBlockedRequests;
81 // The pinned SSL certificate variables.
82 private boolean hasPinnedSslCertificate;
83 private String pinnedSslIssuedToCName;
84 private String pinnedSslIssuedToOName;
85 private String pinnedSslIssuedToUName;
86 private String pinnedSslIssuedByCName;
87 private String pinnedSslIssuedByOName;
88 private String pinnedSslIssuedByUName;
89 private Date pinnedSslStartDate;
90 private Date pinnedSslEndDate;
92 // The current IP addresses variables.
93 private boolean hasCurrentIpAddresses;
94 private String currentIpAddresses;
96 // The pinned IP addresses variables.
97 private boolean hasPinnedIpAddresses;
98 private String pinnedIpAddresses;
100 // The ignore pinned domain information tracker. This is set when a user proceeds past a pinned mismatch dialog to prevent the dialog from showing again until after the domain changes.
101 private boolean ignorePinnedDomainInformation;
103 // The default or favorite icon.
104 Bitmap favoriteOrDefaultIcon;
107 private boolean nightMode;
109 // Track swipe to refresh.
110 private boolean swipeToRefresh;
112 // The nested scrolling child helper is used throughout the class.
113 private NestedScrollingChildHelper nestedScrollingChildHelper;
115 // The previous Y position needs to be tracked between motion events.
116 private int previousYPosition;
120 // The basic constructor.
121 public NestedScrollWebView(Context context) {
122 // Roll up to the next constructor.
126 // The intermediate constructor.
127 public NestedScrollWebView(Context context, AttributeSet attributeSet) {
128 // Roll up to the next constructor.
129 this(context, attributeSet, android.R.attr.webViewStyle);
132 // The full constructor.
133 public NestedScrollWebView(Context context, AttributeSet attributeSet, int defaultStyle) {
134 // Run the default commands.
135 super(context, attributeSet, defaultStyle);
137 // Initialize the nested scrolling child helper.
138 nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
140 // Enable nested scrolling by default.
141 nestedScrollingChildHelper.setNestedScrollingEnabled(true);
146 // WebView Fragment ID.
147 public void setWebViewFragmentId(long webViewFragmentId) {
148 // Store the WebView fragment ID.
149 this.webViewFragmentId = webViewFragmentId;
152 public long getWebViewFragmentId() {
153 // Return the WebView fragment ID.
154 return webViewFragmentId;
159 public void setDomainSettingsApplied(boolean applied) {
160 // Store the domain settings applied status.
161 domainSettingsApplied = applied;
164 public boolean getDomainSettingsApplied() {
165 // Return the domain settings applied status.
166 return domainSettingsApplied;
170 // Domain settings database ID.
171 public void setDomainSettingsDatabaseId(int databaseId) {
172 // Store the domain settings database ID.
173 domainSettingsDatabaseId = databaseId;
176 public int getDomainSettingsDatabaseId() {
177 // Return the domain settings database ID.
178 return domainSettingsDatabaseId;
182 // Current domain name. To function well when called, the domain name should never be allowed to be null.
183 public void setCurrentDomainName(@NonNull String domainName) {
184 // Store the current domain name.
185 currentDomainName = domainName;
188 public void resetCurrentDomainName() {
189 // Reset the current domain name.
190 currentDomainName = "";
193 public String getCurrentDomainName() {
194 // Return the current domain name.
195 return currentDomainName;
199 // First-party cookies.
200 public void setAcceptFirstPartyCookies(boolean status) {
201 // Store the accept first-party cookies status.
202 acceptFirstPartyCookies = status;
205 public boolean getAcceptFirstPartyCookies() {
206 // Return the accept first-party cookies status.
207 return acceptFirstPartyCookies;
211 // Resource requests.
212 public void addResourceRequest(String[] resourceRequest) {
213 // Add the resource request to the list.
214 resourceRequests.add(resourceRequest);
217 public ArrayList<String[]> getResourceRequests() {
218 // Return the list of resource requests.
219 return resourceRequests;
222 public void clearResourceRequests() {
223 // Clear the resource requests.
224 resourceRequests.clear();
229 public void enableBlocklist(int blocklist, boolean status) {
230 // Update the status of the indicated blocklist.
233 // Update the status of the blocklist.
234 easyListEnabled = status;
238 // Update the status of the blocklist.
239 easyPrivacyEnabled = status;
242 case FANBOYS_ANNOYANCE_LIST:
243 // Update the status of the blocklist.
244 fanboysAnnoyanceListEnabled = status;
247 case FANBOYS_SOCIAL_BLOCKING_LIST:
248 // Update the status of the blocklist.
249 fanboysSocialBlockingListEnabled = status;
253 // Update the status of the blocklist.
254 ultraPrivacyEnabled = status;
257 case THIRD_PARTY_REQUESTS:
258 // Update the status of the blocklist.
259 blockAllThirdPartyRequests = status;
264 public boolean isBlocklistEnabled(int blocklist) {
265 // Get the status of the indicated blocklist.
268 // Return the status of the blocklist.
269 return easyListEnabled;
272 // Return the status of the blocklist.
273 return easyPrivacyEnabled;
275 case FANBOYS_ANNOYANCE_LIST:
276 // Return the status of the blocklist.
277 return fanboysAnnoyanceListEnabled;
279 case FANBOYS_SOCIAL_BLOCKING_LIST:
280 // Return the status of the blocklist.
281 return fanboysSocialBlockingListEnabled;
284 // Return the status of the blocklist.
285 return ultraPrivacyEnabled;
287 case THIRD_PARTY_REQUESTS:
288 // Return the status of the blocklist.
289 return blockAllThirdPartyRequests;
292 // The default value is required but should never be used.
298 // Resource request counters.
299 public void resetRequestsCounters() {
300 // Reset all the resource request counters.
302 easyListBlockedRequests = 0;
303 easyPrivacyBlockedRequests = 0;
304 fanboysAnnoyanceListBlockedRequests = 0;
305 fanboysSocialBlockingListBlockedRequests = 0;
306 ultraPrivacyBlockedRequests = 0;
307 thirdPartyBlockedRequests = 0;
310 public void incrementRequestsCount(int blocklist) {
311 // Increment the count of the indicated blocklist.
313 case BLOCKED_REQUESTS:
314 // Increment the blocked requests count.
319 // Increment the EasyList blocked requests count.
320 easyListBlockedRequests++;
324 // Increment the EasyPrivacy blocked requests count.
325 easyPrivacyBlockedRequests++;
328 case FANBOYS_ANNOYANCE_LIST:
329 // Increment the Fanboy's Annoyance List blocked requests count.
330 fanboysAnnoyanceListBlockedRequests++;
333 case FANBOYS_SOCIAL_BLOCKING_LIST:
334 // Increment the Fanboy's Social Blocking List blocked requests count.
335 fanboysSocialBlockingListBlockedRequests++;
339 // Increment the UltraPrivacy blocked requests count.
340 ultraPrivacyBlockedRequests++;
343 case THIRD_PARTY_REQUESTS:
344 // Increment the Third Party blocked requests count.
345 thirdPartyBlockedRequests++;
350 public int getRequestsCount(int blocklist) {
351 // Get the count of the indicated blocklist.
353 case BLOCKED_REQUESTS:
354 // Return the blocked requests count.
355 return blockedRequests;
358 // Return the EasyList blocked requests count.
359 return easyListBlockedRequests;
362 // Return the EasyPrivacy blocked requests count.
363 return easyPrivacyBlockedRequests;
365 case FANBOYS_ANNOYANCE_LIST:
366 // Return the Fanboy's Annoyance List blocked requests count.
367 return fanboysAnnoyanceListBlockedRequests;
369 case FANBOYS_SOCIAL_BLOCKING_LIST:
370 // Return the Fanboy's Social Blocking List blocked requests count.
371 return fanboysSocialBlockingListBlockedRequests;
374 // Return the UltraPrivacy blocked requests count.
375 return ultraPrivacyBlockedRequests;
377 case THIRD_PARTY_REQUESTS:
378 // Return the Third Party blocked requests count.
379 return thirdPartyBlockedRequests;
382 // Return 0. This should never end up being called.
388 // Pinned SSL certificates.
389 public boolean hasPinnedSslCertificate() {
390 // Return the status of the pinned SSL certificate.
391 return hasPinnedSslCertificate;
394 public void setPinnedSslCertificate(String issuedToCName, String issuedToOName, String issuedToUName, String issuedByCName, String issuedByOName, String issuedByUName, Date startDate, Date endDate) {
395 // Store the pinned SSL certificate information.
396 pinnedSslIssuedToCName = issuedToCName;
397 pinnedSslIssuedToOName = issuedToOName;
398 pinnedSslIssuedToUName = issuedToUName;
399 pinnedSslIssuedByCName = issuedByCName;
400 pinnedSslIssuedByOName = issuedByOName;
401 pinnedSslIssuedByUName = issuedByUName;
402 pinnedSslStartDate = startDate;
403 pinnedSslEndDate = endDate;
405 // Set the pinned SSL certificate tracker.
406 hasPinnedSslCertificate = true;
409 public ArrayList<Object> getPinnedSslCertificate() {
410 // Initialize an array list.
411 ArrayList<Object> arrayList = new ArrayList<>();
413 // Create the SSL certificate string array.
414 String[] sslCertificateStringArray = new String[] {pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName};
416 // Create the SSL certificate date array.
417 Date[] sslCertificateDateArray = new Date[] {pinnedSslStartDate, pinnedSslEndDate};
419 // Add the arrays to the array list.
420 arrayList.add(sslCertificateStringArray);
421 arrayList.add(sslCertificateDateArray);
423 // Return the pinned SSL certificate array list.
427 public void clearPinnedSslCertificate() {
428 // Clear the pinned SSL certificate.
429 pinnedSslIssuedToCName = null;
430 pinnedSslIssuedToOName = null;
431 pinnedSslIssuedToUName = null;
432 pinnedSslIssuedByCName = null;
433 pinnedSslIssuedByOName = null;
434 pinnedSslIssuedByUName = null;
435 pinnedSslStartDate = null;
436 pinnedSslEndDate = null;
438 // Clear the pinned SSL certificate tracker.
439 hasPinnedSslCertificate = false;
443 // Current IP addresses.
444 public boolean hasCurrentIpAddresses() {
445 // Return the status of the current IP addresses.
446 return hasCurrentIpAddresses;
449 public void setCurrentIpAddresses(String ipAddresses) {
450 // Store the current IP addresses.
451 currentIpAddresses = ipAddresses;
453 // Set the current IP addresses tracker.
454 hasCurrentIpAddresses = true;
457 public String getCurrentIpAddresses() {
458 // Return the current IP addresses.
459 return currentIpAddresses;
462 public void clearCurrentIpAddresses() {
463 // Clear the current IP addresses.
464 currentIpAddresses = null;
466 // Clear the current IP addresses tracker.
467 hasCurrentIpAddresses = false;
471 // Pinned IP addresses.
472 public boolean hasPinnedIpAddresses() {
473 // Return the status of the pinned IP addresses.
474 return hasPinnedIpAddresses;
477 public void setPinnedIpAddresses(String ipAddresses) {
478 // Store the pinned IP addresses.
479 pinnedIpAddresses = ipAddresses;
481 // Set the pinned IP addresses tracker.
482 hasPinnedIpAddresses = true;
485 public String getPinnedIpAddresses() {
486 // Return the pinned IP addresses.
487 return pinnedIpAddresses;
490 public void clearPinnedIpAddresses() {
491 // Clear the pinned IP addresses.
492 pinnedIpAddresses = null;
494 // Clear the pinned IP addresses tracker.
495 hasPinnedIpAddresses = false;
499 // Ignore pinned information. The syntax looks better as written, even if it is always inverted.
500 @SuppressWarnings("BooleanMethodIsAlwaysInverted")
501 public boolean ignorePinnedDomainInformation() {
502 // Return the status of the ignore pinned domain information tracker.
503 return ignorePinnedDomainInformation;
506 public void setIgnorePinnedDomainInformation(boolean status) {
507 // Set the status of the ignore pinned domain information tracker.
508 ignorePinnedDomainInformation = status;
512 // Favorite or default icon.
513 public void initializeFavoriteIcon() {
514 // Get the default favorite icon drawable. `ContextCompat` must be used until API >= 21.
515 Drawable favoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
517 // Cast the favorite icon drawable to a bitmap drawable.
518 BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
520 // Remove the incorrect warning below that the favorite icon bitmap drawable might be null.
521 assert favoriteIconBitmapDrawable != null;
523 // Store the default icon bitmap.
524 favoriteOrDefaultIcon = favoriteIconBitmapDrawable.getBitmap();
527 public void setFavoriteOrDefaultIcon(Bitmap icon) {
528 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
529 if ((icon.getHeight() > 256) || (icon.getWidth() > 256)) {
530 favoriteOrDefaultIcon = Bitmap.createScaledBitmap(icon, 256, 256, true);
532 // Store the icon as presented.
533 favoriteOrDefaultIcon = icon;
537 public Bitmap getFavoriteOrDefaultIcon() {
538 // Return the favorite or default icon.
539 return favoriteOrDefaultIcon;
544 public void setNightMode(boolean status) {
545 // Store the night mode status.
549 public boolean getNightMode() {
550 // Return the night mode status.
556 public void setSwipeToRefresh(boolean status) {
557 // Store the swipe to refresh status.
558 swipeToRefresh = status;
561 public boolean getSwipeToRefresh() {
562 // Return the swipe to refresh status.
563 return swipeToRefresh;
569 public boolean onTouchEvent(MotionEvent motionEvent) {
570 // Initialize a tracker to return if this motion event is handled.
571 boolean motionEventHandled;
573 // Run the commands for the given motion event action.
574 switch (motionEvent.getAction()) {
575 case MotionEvent.ACTION_DOWN:
576 // Start nested scrolling along the vertical axis. `ViewCompat` must be used until the minimum API >= 21.
577 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
579 // Save the current Y position. Action down will not be called again until a new motion starts.
580 previousYPosition = (int) motionEvent.getY();
582 // Run the default commands.
583 motionEventHandled = super.onTouchEvent(motionEvent);
586 case MotionEvent.ACTION_MOVE:
587 // Get the current Y position.
588 int currentYMotionPosition = (int) motionEvent.getY();
590 // Calculate the pre-scroll delta Y.
591 int preScrollDeltaY = previousYPosition - currentYMotionPosition;
593 // Initialize a variable to track how much of the scroll is consumed.
594 int[] consumedScroll = new int[2];
596 // Initialize a variable to track the offset in the window.
597 int[] offsetInWindow = new int[2];
599 // Get the WebView Y position.
600 int webViewYPosition = getScrollY();
602 // Set the scroll delta Y to initially be the same as the pre-scroll delta Y.
603 int scrollDeltaY = preScrollDeltaY;
605 // Dispatch the nested pre-school. This scrolls the app bar if it needs it. `offsetInWindow` will be returned with an updated value.
606 if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
607 // Update the scroll delta Y if some of it was consumed.
608 // There is currently a bug in Android where if scrolling up at a certain slow speed the input can lock the pre scroll and continue to consume it after the app bar is fully displayed.
609 scrollDeltaY = preScrollDeltaY - consumedScroll[1];
612 // Check to see if the WebView is at the top and and the scroll action is downward.
613 if ((webViewYPosition == 0) && (scrollDeltaY < 0)) { // Swipe to refresh is being engaged.
614 // Stop the nested scroll so that swipe to refresh has complete control. This way releasing the scroll to refresh circle doesn't scroll the WebView at the same time.
616 } else { // Swipe to refresh is not being engaged.
617 // Start the nested scroll so that the app bar can scroll off the screen.
618 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
620 // Dispatch the nested scroll. This scrolls the WebView. The delta Y unconsumed normally controls the swipe refresh layout, but that is handled with the `if` statement above.
621 dispatchNestedScroll(0, scrollDeltaY, 0, 0, offsetInWindow);
623 // Store the current Y position for use in the next action move.
624 previousYPosition = previousYPosition - scrollDeltaY;
627 // Run the default commands.
628 motionEventHandled = super.onTouchEvent(motionEvent);
633 // Stop nested scrolling.
636 // Run the default commands.
637 motionEventHandled = super.onTouchEvent(motionEvent);
640 // Perform a click. This is required by the Android accessibility guidelines.
643 // Return the status of the motion event.
644 return motionEventHandled;
647 // The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
649 public boolean performClick() {
650 return super.performClick();
654 // Method from NestedScrollingChild.
656 public void setNestedScrollingEnabled(boolean status) {
657 // Set the status of the nested scrolling.
658 nestedScrollingChildHelper.setNestedScrollingEnabled(status);
661 // Method from NestedScrollingChild.
663 public boolean isNestedScrollingEnabled() {
664 // Return the status of nested scrolling.
665 return nestedScrollingChildHelper.isNestedScrollingEnabled();
669 // Method from NestedScrollingChild.
671 public boolean startNestedScroll(int axes) {
672 // Start a nested scroll along the indicated axes.
673 return nestedScrollingChildHelper.startNestedScroll(axes);
676 // Method from NestedScrollingChild2.
678 public boolean startNestedScroll(int axes, int type) {
679 // Start a nested scroll along the indicated axes for the given type of input which caused the scroll event.
680 return nestedScrollingChildHelper.startNestedScroll(axes, type);
684 // Method from NestedScrollingChild.
686 public void stopNestedScroll() {
687 // Stop the nested scroll.
688 nestedScrollingChildHelper.stopNestedScroll();
691 // Method from NestedScrollingChild2.
693 public void stopNestedScroll(int type) {
694 // Stop the nested scroll of the given type of input which caused the scroll event.
695 nestedScrollingChildHelper.stopNestedScroll(type);
699 // Method from NestedScrollingChild.
701 public boolean hasNestedScrollingParent() {
702 // Return the status of the nested scrolling parent.
703 return nestedScrollingChildHelper.hasNestedScrollingParent();
706 // Method from NestedScrollingChild2.
708 public boolean hasNestedScrollingParent(int type) {
709 // return the status of the nested scrolling parent for the given type of input which caused the scroll event.
710 return nestedScrollingChildHelper.hasNestedScrollingParent(type);
714 // Method from NestedScrollingChild.
716 public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow) {
717 // Dispatch a nested pre-scroll with the specified deltas, which lets a parent to consume some of the scroll if desired.
718 return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow);
721 // Method from NestedScrollingChild2.
723 public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow, int type) {
724 // Dispatch a nested pre-scroll with the specified deltas for the given type of input which caused the scroll event, which lets a parent to consume some of the scroll if desired.
725 return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, type);
729 // Method from NestedScrollingChild.
731 public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow) {
732 // Dispatch a nested scroll with the specified deltas.
733 return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow);
736 // Method from NestedScrollingChild2.
738 public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow, int type) {
739 // Dispatch a nested scroll with the specified deltas for the given type of input which caused the scroll event.
740 return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow, type);
744 // Method from NestedScrollingChild.
746 public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
747 // Dispatch a nested pre-fling with the specified velocity, which lets a parent consume the fling if desired.
748 return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
751 // Method from NestedScrollingChild.
753 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
754 // Dispatch a nested fling with the specified velocity.
755 return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);