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 resource requests.
63 private ArrayList<String[]> resourceRequests = new ArrayList<>();
64 private boolean easyListEnabled;
65 private boolean easyPrivacyEnabled;
66 private boolean fanboysAnnoyanceListEnabled;
67 private boolean fanboysSocialBlockingListEnabled;
68 private boolean ultraPrivacyEnabled;
69 private boolean blockAllThirdPartyRequests;
70 private int blockedRequests;
71 private int easyListBlockedRequests;
72 private int easyPrivacyBlockedRequests;
73 private int fanboysAnnoyanceListBlockedRequests;
74 private int fanboysSocialBlockingListBlockedRequests;
75 private int ultraPrivacyBlockedRequests;
76 private int thirdPartyBlockedRequests;
78 // The pinned SSL certificate variables.
79 private boolean hasPinnedSslCertificate;
80 private String pinnedSslIssuedToCName;
81 private String pinnedSslIssuedToOName;
82 private String pinnedSslIssuedToUName;
83 private String pinnedSslIssuedByCName;
84 private String pinnedSslIssuedByOName;
85 private String pinnedSslIssuedByUName;
86 private Date pinnedSslStartDate;
87 private Date pinnedSslEndDate;
89 // The current IP addresses variables.
90 private boolean hasCurrentIpAddresses;
91 private String currentIpAddresses;
93 // The pinned IP addresses variables.
94 private boolean hasPinnedIpAddresses;
95 private String pinnedIpAddresses;
97 // 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.
98 private boolean ignorePinnedDomainInformation;
100 // The default or favorite icon.
101 Bitmap favoriteOrDefaultIcon;
104 private boolean nightMode;
106 // Track swipe to refresh.
107 private boolean swipeToRefresh;
109 // The nested scrolling child helper is used throughout the class.
110 private NestedScrollingChildHelper nestedScrollingChildHelper;
112 // The previous Y position needs to be tracked between motion events.
113 private int previousYPosition;
117 // The basic constructor.
118 public NestedScrollWebView(Context context) {
119 // Roll up to the next constructor.
123 // The intermediate constructor.
124 public NestedScrollWebView(Context context, AttributeSet attributeSet) {
125 // Roll up to the next constructor.
126 this(context, attributeSet, android.R.attr.webViewStyle);
129 // The full constructor.
130 public NestedScrollWebView(Context context, AttributeSet attributeSet, int defaultStyle) {
131 // Run the default commands.
132 super(context, attributeSet, defaultStyle);
134 // Initialize the nested scrolling child helper.
135 nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
137 // Enable nested scrolling by default.
138 nestedScrollingChildHelper.setNestedScrollingEnabled(true);
143 // WebView Fragment ID.
144 public void setWebViewFragmentId(long webViewFragmentId) {
145 // Store the WebView fragment ID.
146 this.webViewFragmentId = webViewFragmentId;
149 public long getWebViewFragmentId() {
150 // Return the WebView fragment ID.
151 return webViewFragmentId;
156 public void setDomainSettingsApplied(boolean applied) {
157 // Store the domain settings applied status.
158 domainSettingsApplied = applied;
161 public boolean getDomainSettingsApplied() {
162 // Return the domain settings applied status.
163 return domainSettingsApplied;
167 // Domain settings database ID.
168 public void setDomainSettingsDatabaseId(int databaseId) {
169 // Store the domain settings database ID.
170 domainSettingsDatabaseId = databaseId;
173 public int getDomainSettingsDatabaseId() {
174 // Return the domain settings database ID.
175 return domainSettingsDatabaseId;
179 // Current domain name. To function well when called, the domain name should never be allowed to be null.
180 public void setCurrentDomainName(@NonNull String domainName) {
181 // Store the current domain name.
182 currentDomainName = domainName;
185 public void resetCurrentDomainName() {
186 // Reset the current domain name.
187 currentDomainName = "";
190 public String getCurrentDomainName() {
191 // Return the current domain name.
192 return currentDomainName;
196 // Resource requests.
197 public void addResourceRequest(String[] resourceRequest) {
198 // Add the resource request to the list.
199 resourceRequests.add(resourceRequest);
202 public ArrayList<String[]> getResourceRequests() {
203 // Return the list of resource requests.
204 return resourceRequests;
207 public void clearResourceRequests() {
208 // Clear the resource requests.
209 resourceRequests.clear();
214 public void enableBlocklist(int blocklist, boolean status) {
215 // Update the status of the indicated blocklist.
218 // Update the status of the blocklist.
219 easyListEnabled = status;
223 // Update the status of the blocklist.
224 easyPrivacyEnabled = status;
227 case FANBOYS_ANNOYANCE_LIST:
228 // Update the status of the blocklist.
229 fanboysAnnoyanceListEnabled = status;
232 case FANBOYS_SOCIAL_BLOCKING_LIST:
233 // Update the status of the blocklist.
234 fanboysSocialBlockingListEnabled = status;
238 // Update the status of the blocklist.
239 ultraPrivacyEnabled = status;
242 case THIRD_PARTY_REQUESTS:
243 // Update the status of the blocklist.
244 blockAllThirdPartyRequests = status;
249 public boolean isBlocklistEnabled(int blocklist) {
250 // Get the status of the indicated blocklist.
253 // Return the status of the blocklist.
254 return easyListEnabled;
257 // Return the status of the blocklist.
258 return easyPrivacyEnabled;
260 case FANBOYS_ANNOYANCE_LIST:
261 // Return the status of the blocklist.
262 return fanboysAnnoyanceListEnabled;
264 case FANBOYS_SOCIAL_BLOCKING_LIST:
265 // Return the status of the blocklist.
266 return fanboysSocialBlockingListEnabled;
269 // Return the status of the blocklist.
270 return ultraPrivacyEnabled;
272 case THIRD_PARTY_REQUESTS:
273 // Return the status of the blocklist.
274 return blockAllThirdPartyRequests;
277 // The default value is required but should never be used.
283 // Resource request counters.
284 public void resetRequestsCounters() {
285 // Reset all the resource request counters.
287 easyListBlockedRequests = 0;
288 easyPrivacyBlockedRequests = 0;
289 fanboysAnnoyanceListBlockedRequests = 0;
290 fanboysSocialBlockingListBlockedRequests = 0;
291 ultraPrivacyBlockedRequests = 0;
292 thirdPartyBlockedRequests = 0;
295 public void incrementRequestsCount(int blocklist) {
296 // Increment the count of the indicated blocklist.
298 case BLOCKED_REQUESTS:
299 // Increment the blocked requests count.
304 // Increment the EasyList blocked requests count.
305 easyListBlockedRequests++;
309 // Increment the EasyPrivacy blocked requests count.
310 easyPrivacyBlockedRequests++;
313 case FANBOYS_ANNOYANCE_LIST:
314 // Increment the Fanboy's Annoyance List blocked requests count.
315 fanboysAnnoyanceListBlockedRequests++;
318 case FANBOYS_SOCIAL_BLOCKING_LIST:
319 // Increment the Fanboy's Social Blocking List blocked requests count.
320 fanboysSocialBlockingListBlockedRequests++;
324 // Increment the UltraPrivacy blocked requests count.
325 ultraPrivacyBlockedRequests++;
328 case THIRD_PARTY_REQUESTS:
329 // Increment the Third Party blocked requests count.
330 thirdPartyBlockedRequests++;
335 public int getRequestsCount(int blocklist) {
336 // Get the count of the indicated blocklist.
338 case BLOCKED_REQUESTS:
339 // Return the blocked requests count.
340 return blockedRequests;
343 // Return the EasyList blocked requests count.
344 return easyListBlockedRequests;
347 // Return the EasyPrivacy blocked requests count.
348 return easyPrivacyBlockedRequests;
350 case FANBOYS_ANNOYANCE_LIST:
351 // Return the Fanboy's Annoyance List blocked requests count.
352 return fanboysAnnoyanceListBlockedRequests;
354 case FANBOYS_SOCIAL_BLOCKING_LIST:
355 // Return the Fanboy's Social Blocking List blocked requests count.
356 return fanboysSocialBlockingListBlockedRequests;
359 // Return the UltraPrivacy blocked requests count.
360 return ultraPrivacyBlockedRequests;
362 case THIRD_PARTY_REQUESTS:
363 // Return the Third Party blocked requests count.
364 return thirdPartyBlockedRequests;
367 // Return 0. This should never end up being called.
373 // Pinned SSL certificates.
374 public boolean hasPinnedSslCertificate() {
375 // Return the status of the pinned SSL certificate.
376 return hasPinnedSslCertificate;
379 public void setPinnedSslCertificate(String issuedToCName, String issuedToOName, String issuedToUName, String issuedByCName, String issuedByOName, String issuedByUName, Date startDate, Date endDate) {
380 // Store the pinned SSL certificate information.
381 pinnedSslIssuedToCName = issuedToCName;
382 pinnedSslIssuedToOName = issuedToOName;
383 pinnedSslIssuedToUName = issuedToUName;
384 pinnedSslIssuedByCName = issuedByCName;
385 pinnedSslIssuedByOName = issuedByOName;
386 pinnedSslIssuedByUName = issuedByUName;
387 pinnedSslStartDate = startDate;
388 pinnedSslEndDate = endDate;
390 // Set the pinned SSL certificate tracker.
391 hasPinnedSslCertificate = true;
394 public ArrayList<Object> getPinnedSslCertificate() {
395 // Initialize an array list.
396 ArrayList<Object> arrayList = new ArrayList<>();
398 // Create the SSL certificate string array.
399 String[] sslCertificateStringArray = new String[] {pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName};
401 // Create the SSL certificate date array.
402 Date[] sslCertificateDateArray = new Date[] {pinnedSslStartDate, pinnedSslEndDate};
404 // Add the arrays to the array list.
405 arrayList.add(sslCertificateStringArray);
406 arrayList.add(sslCertificateDateArray);
408 // Return the pinned SSL certificate array list.
412 public void clearPinnedSslCertificate() {
413 // Clear the pinned SSL certificate.
414 pinnedSslIssuedToCName = null;
415 pinnedSslIssuedToOName = null;
416 pinnedSslIssuedToUName = null;
417 pinnedSslIssuedByCName = null;
418 pinnedSslIssuedByOName = null;
419 pinnedSslIssuedByUName = null;
420 pinnedSslStartDate = null;
421 pinnedSslEndDate = null;
423 // Clear the pinned SSL certificate tracker.
424 hasPinnedSslCertificate = false;
428 // Current IP addresses.
429 public boolean hasCurrentIpAddresses() {
430 // Return the status of the current IP addresses.
431 return hasCurrentIpAddresses;
434 public void setCurrentIpAddresses(String ipAddresses) {
435 // Store the current IP addresses.
436 currentIpAddresses = ipAddresses;
438 // Set the current IP addresses tracker.
439 hasCurrentIpAddresses = true;
442 public String getCurrentIpAddresses() {
443 // Return the current IP addresses.
444 return currentIpAddresses;
447 public void clearCurrentIpAddresses() {
448 // Clear the current IP addresses.
449 currentIpAddresses = null;
451 // Clear the current IP addresses tracker.
452 hasCurrentIpAddresses = false;
456 // Pinned IP addresses.
457 public boolean hasPinnedIpAddresses() {
458 // Return the status of the pinned IP addresses.
459 return hasPinnedIpAddresses;
462 public void setPinnedIpAddresses(String ipAddresses) {
463 // Store the pinned IP addresses.
464 pinnedIpAddresses = ipAddresses;
466 // Set the pinned IP addresses tracker.
467 hasPinnedIpAddresses = true;
470 public String getPinnedIpAddresses() {
471 // Return the pinned IP addresses.
472 return pinnedIpAddresses;
475 public void clearPinnedIpAddresses() {
476 // Clear the pinned IP addresses.
477 pinnedIpAddresses = null;
479 // Clear the pinned IP addresses tracker.
480 hasPinnedIpAddresses = false;
484 // Ignore pinned information. The syntax looks better as written, even if it is always inverted.
485 @SuppressWarnings("BooleanMethodIsAlwaysInverted")
486 public boolean ignorePinnedDomainInformation() {
487 // Return the status of the ignore pinned domain information tracker.
488 return ignorePinnedDomainInformation;
491 public void setIgnorePinnedDomainInformation(boolean status) {
492 // Set the status of the ignore pinned domain information tracker.
493 ignorePinnedDomainInformation = status;
497 // Favorite or default icon.
498 public void initializeFavoriteIcon() {
499 // Get the default favorite icon drawable. `ContextCompat` must be used until API >= 21.
500 Drawable favoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
502 // Cast the favorite icon drawable to a bitmap drawable.
503 BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
505 // Remove the incorrect warning below that the favorite icon bitmap drawable might be null.
506 assert favoriteIconBitmapDrawable != null;
508 // Store the default icon bitmap.
509 favoriteOrDefaultIcon = favoriteIconBitmapDrawable.getBitmap();
512 public void setFavoriteOrDefaultIcon(Bitmap icon) {
513 // Scale the favorite icon bitmap down if it is larger than 256 x 256. Filtering uses bilinear interpolation.
514 if ((icon.getHeight() > 256) || (icon.getWidth() > 256)) {
515 favoriteOrDefaultIcon = Bitmap.createScaledBitmap(icon, 256, 256, true);
517 // Store the icon as presented.
518 favoriteOrDefaultIcon = icon;
522 public Bitmap getFavoriteOrDefaultIcon() {
523 // Return the favorite or default icon.
524 return favoriteOrDefaultIcon;
529 public void setNightMode(boolean status) {
530 // Store the night mode status.
534 public boolean getNightMode() {
535 // Return the night mode status.
541 public void setSwipeToRefresh(boolean status) {
542 // Store the swipe to refresh status.
543 swipeToRefresh = status;
546 public boolean getSwipeToRefresh() {
547 // Return the swipe to refresh status.
548 return swipeToRefresh;
554 public boolean onTouchEvent(MotionEvent motionEvent) {
555 // Initialize a tracker to return if this motion event is handled.
556 boolean motionEventHandled;
558 // Run the commands for the given motion event action.
559 switch (motionEvent.getAction()) {
560 case MotionEvent.ACTION_DOWN:
561 // Start nested scrolling along the vertical axis. `ViewCompat` must be used until the minimum API >= 21.
562 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
564 // Save the current Y position. Action down will not be called again until a new motion starts.
565 previousYPosition = (int) motionEvent.getY();
567 // Run the default commands.
568 motionEventHandled = super.onTouchEvent(motionEvent);
571 case MotionEvent.ACTION_MOVE:
572 // Get the current Y position.
573 int currentYMotionPosition = (int) motionEvent.getY();
575 // Calculate the pre-scroll delta Y.
576 int preScrollDeltaY = previousYPosition - currentYMotionPosition;
578 // Initialize a variable to track how much of the scroll is consumed.
579 int[] consumedScroll = new int[2];
581 // Initialize a variable to track the offset in the window.
582 int[] offsetInWindow = new int[2];
584 // Get the WebView Y position.
585 int webViewYPosition = getScrollY();
587 // Set the scroll delta Y to initially be the same as the pre-scroll delta Y.
588 int scrollDeltaY = preScrollDeltaY;
590 // Dispatch the nested pre-school. This scrolls the app bar if it needs it. `offsetInWindow` will be returned with an updated value.
591 if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
592 // Update the scroll delta Y if some of it was consumed.
593 // 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.
594 scrollDeltaY = preScrollDeltaY - consumedScroll[1];
597 // Check to see if the WebView is at the top and and the scroll action is downward.
598 if ((webViewYPosition == 0) && (scrollDeltaY < 0)) { // Swipe to refresh is being engaged.
599 // 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.
601 } else { // Swipe to refresh is not being engaged.
602 // Start the nested scroll so that the app bar can scroll off the screen.
603 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
605 // 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.
606 dispatchNestedScroll(0, scrollDeltaY, 0, 0, offsetInWindow);
608 // Store the current Y position for use in the next action move.
609 previousYPosition = previousYPosition - scrollDeltaY;
612 // Run the default commands.
613 motionEventHandled = super.onTouchEvent(motionEvent);
618 // Stop nested scrolling.
621 // Run the default commands.
622 motionEventHandled = super.onTouchEvent(motionEvent);
625 // Perform a click. This is required by the Android accessibility guidelines.
628 // Return the status of the motion event.
629 return motionEventHandled;
632 // The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
634 public boolean performClick() {
635 return super.performClick();
639 // Method from NestedScrollingChild.
641 public void setNestedScrollingEnabled(boolean status) {
642 // Set the status of the nested scrolling.
643 nestedScrollingChildHelper.setNestedScrollingEnabled(status);
646 // Method from NestedScrollingChild.
648 public boolean isNestedScrollingEnabled() {
649 // Return the status of nested scrolling.
650 return nestedScrollingChildHelper.isNestedScrollingEnabled();
654 // Method from NestedScrollingChild.
656 public boolean startNestedScroll(int axes) {
657 // Start a nested scroll along the indicated axes.
658 return nestedScrollingChildHelper.startNestedScroll(axes);
661 // Method from NestedScrollingChild2.
663 public boolean startNestedScroll(int axes, int type) {
664 // Start a nested scroll along the indicated axes for the given type of input which caused the scroll event.
665 return nestedScrollingChildHelper.startNestedScroll(axes, type);
669 // Method from NestedScrollingChild.
671 public void stopNestedScroll() {
672 // Stop the nested scroll.
673 nestedScrollingChildHelper.stopNestedScroll();
676 // Method from NestedScrollingChild2.
678 public void stopNestedScroll(int type) {
679 // Stop the nested scroll of the given type of input which caused the scroll event.
680 nestedScrollingChildHelper.stopNestedScroll(type);
684 // Method from NestedScrollingChild.
686 public boolean hasNestedScrollingParent() {
687 // Return the status of the nested scrolling parent.
688 return nestedScrollingChildHelper.hasNestedScrollingParent();
691 // Method from NestedScrollingChild2.
693 public boolean hasNestedScrollingParent(int type) {
694 // return the status of the nested scrolling parent for the given type of input which caused the scroll event.
695 return nestedScrollingChildHelper.hasNestedScrollingParent(type);
699 // Method from NestedScrollingChild.
701 public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow) {
702 // Dispatch a nested pre-scroll with the specified deltas, which lets a parent to consume some of the scroll if desired.
703 return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow);
706 // Method from NestedScrollingChild2.
708 public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow, int type) {
709 // 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.
710 return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, type);
714 // Method from NestedScrollingChild.
716 public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow) {
717 // Dispatch a nested scroll with the specified deltas.
718 return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow);
721 // Method from NestedScrollingChild2.
723 public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow, int type) {
724 // Dispatch a nested scroll with the specified deltas for the given type of input which caused the scroll event.
725 return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow, type);
729 // Method from NestedScrollingChild.
731 public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
732 // Dispatch a nested pre-fling with the specified velocity, which lets a parent consume the fling if desired.
733 return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
736 // Method from NestedScrollingChild.
738 public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
739 // Dispatch a nested fling with the specified velocity.
740 return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);