Track URL loading by tab.
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / views / NestedScrollWebView.java
1 /*
2  * Copyright © 2019 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 package com.stoutner.privacybrowser.views;
21
22 import android.content.Context;
23 import android.util.AttributeSet;
24 import android.view.MotionEvent;
25 import android.webkit.WebView;
26
27 import androidx.annotation.NonNull;
28 import androidx.core.view.NestedScrollingChild2;
29 import androidx.core.view.NestedScrollingChildHelper;
30 import androidx.core.view.ViewCompat;
31
32 import java.util.ArrayList;
33 import java.util.Date;
34
35 // NestedScrollWebView extends WebView to handle nested scrolls (scrolling the app bar off the screen).
36 public class NestedScrollWebView extends WebView implements NestedScrollingChild2 {
37     // These constants identify the blocklists.
38     public final static int BLOCKED_REQUESTS = 0;
39     public final static int EASY_LIST_BLOCKED_REQUESTS = 1;
40     public final static int EASY_PRIVACY_BLOCKED_REQUESTS = 2;
41     public final static int FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS = 3;
42     public final static int FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS = 4;
43     public final static int ULTRA_PRIVACY_BLOCKED_REQUESTS = 5;
44     public final static int THIRD_PARTY_BLOCKED_REQUESTS = 6;
45
46     // Keep a copy of the WebView fragment ID.
47     private long webViewFragmentId;
48
49     // Track if domain settings are applied to this nested scroll WebView and, if so, the database ID.
50     private boolean domainSettingsApplied;
51     private int domainSettingsDatabaseId;
52
53     // Keep track of when the domain name changes so that domain settings can be reapplied.  This should never be null.
54     private String currentDomainName = "";
55
56     // Track the resource requests.
57     private ArrayList<String[]> resourceRequests = new ArrayList<>();
58     private int blockedRequests;
59     private int easyListBlockedRequests;
60     private int easyPrivacyBlockedRequests;
61     private int fanboysAnnoyanceListBlockedRequests;
62     private int fanboysSocialBlockingListBlockedRequests;
63     private int ultraPrivacyBlockedRequests;
64     private int thirdPartyBlockedRequests;
65
66     // The pinned SSL certificate variables.
67     private boolean hasPinnedSslCertificate;
68     private String pinnedSslIssuedToCName;
69     private String pinnedSslIssuedToOName;
70     private String pinnedSslIssuedToUName;
71     private String pinnedSslIssuedByCName;
72     private String pinnedSslIssuedByOName;
73     private String pinnedSslIssuedByUName;
74     private Date pinnedSslStartDate;
75     private Date pinnedSslEndDate;
76
77     // The current IP addresses variables.
78     private boolean hasCurrentIpAddresses;
79     private String currentIpAddresses;
80
81     // The pinned IP addresses variables.
82     private boolean hasPinnedIpAddresses;
83     private String pinnedIpAddresses;
84
85     // 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.
86     private boolean ignorePinnedDomainInformation;
87
88     // The nested scrolling child helper is used throughout the class.
89     private NestedScrollingChildHelper nestedScrollingChildHelper;
90
91     // The previous Y position needs to be tracked between motion events.
92     private int previousYPosition;
93
94
95
96     // The basic constructor.
97     public NestedScrollWebView(Context context) {
98         // Roll up to the next constructor.
99         this(context, null);
100     }
101
102     // The intermediate constructor.
103     public NestedScrollWebView(Context context, AttributeSet attributeSet) {
104         // Roll up to the next constructor.
105         this(context, attributeSet, android.R.attr.webViewStyle);
106     }
107
108     // The full constructor.
109     public NestedScrollWebView(Context context, AttributeSet attributeSet, int defaultStyle) {
110         // Run the default commands.
111         super(context, attributeSet, defaultStyle);
112
113         // Initialize the nested scrolling child helper.
114         nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
115
116         // Enable nested scrolling by default.
117         nestedScrollingChildHelper.setNestedScrollingEnabled(true);
118     }
119
120
121
122     // WebView Fragment ID.
123     public void setWebViewFragmentId(long webViewFragmentId) {
124         // Store the WebView fragment ID.
125         this.webViewFragmentId = webViewFragmentId;
126     }
127
128     public long getWebViewFragmentId() {
129         // Return the WebView fragment ID.
130         return webViewFragmentId;
131     }
132
133
134     // Domain settings.
135     public void setDomainSettingsApplied(boolean applied) {
136         // Store the domain settings applied status.
137         domainSettingsApplied = applied;
138     }
139
140     public boolean getDomainSettingsApplied() {
141         // Return the domain settings applied status.
142         return domainSettingsApplied;
143     }
144
145
146     // Domain settings database ID.
147     public void setDomainSettingsDatabaseId(int databaseId) {
148         // Store the domain settings database ID.
149         domainSettingsDatabaseId = databaseId;
150     }
151
152     public int getDomainSettingsDatabaseId() {
153         // Return the domain settings database ID.
154         return domainSettingsDatabaseId;
155     }
156
157
158     // Current domain name.  To function well when called, the domain name should never be allowed to be null.
159     public void setCurrentDomainName(@NonNull String domainName) {
160         // Store the current domain name.
161         currentDomainName = domainName;
162     }
163
164     public void resetCurrentDomainName() {
165         // Reset the current domain name.
166         currentDomainName = "";
167     }
168
169     public String getCurrentDomainName() {
170         // Return the current domain name.
171         return currentDomainName;
172     }
173
174
175     // Resource requests.
176     public void addResourceRequest(String[] resourceRequest) {
177         // Add the resource request to the list.
178         resourceRequests.add(resourceRequest);
179     }
180
181     public ArrayList<String[]> getResourceRequests() {
182         // Return the list of resource requests.
183         return resourceRequests;
184     }
185
186     public void clearResourceRequests() {
187         // Clear the resource requests.
188         resourceRequests.clear();
189     }
190
191
192     // Resource request counters.
193     public void resetRequestsCount(int list) {
194         // Run the command on the indicated list.
195         switch (list) {
196             case BLOCKED_REQUESTS:
197                 // Reset the blocked requests count.
198                 blockedRequests = 0;
199                 break;
200
201             case EASY_LIST_BLOCKED_REQUESTS:
202                 // Reset the EasyList blocked requests count.
203                 easyListBlockedRequests = 0;
204                 break;
205
206             case EASY_PRIVACY_BLOCKED_REQUESTS:
207                 // Reset the EasyPrivacy blocked requests count.
208                 easyPrivacyBlockedRequests = 0;
209                 break;
210
211             case FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS:
212                 // Reset the Fanboy's Annoyance List blocked requests count.
213                 fanboysAnnoyanceListBlockedRequests = 0;
214                 break;
215
216             case FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS:
217                 // Reset the Fanboy's Social Blocking List blocked requests count.
218                 fanboysSocialBlockingListBlockedRequests = 0;
219                 break;
220
221             case ULTRA_PRIVACY_BLOCKED_REQUESTS:
222                 // Reset the UltraPrivacy blocked requests count.
223                 ultraPrivacyBlockedRequests = 0;
224                 break;
225
226             case THIRD_PARTY_BLOCKED_REQUESTS:
227                 // Reset the Third Party blocked requests count.
228                 thirdPartyBlockedRequests = 0;
229                 break;
230         }
231     }
232
233     public void incrementRequestsCount(int list) {
234         // Run the command on the indicated list.
235         switch (list) {
236             case BLOCKED_REQUESTS:
237                 // Increment the blocked requests count.
238                 blockedRequests++;
239                 break;
240
241             case EASY_LIST_BLOCKED_REQUESTS:
242                 // Increment the EasyList blocked requests count.
243                 easyListBlockedRequests++;
244                 break;
245
246             case EASY_PRIVACY_BLOCKED_REQUESTS:
247                 // Increment the EasyPrivacy blocked requests count.
248                 easyPrivacyBlockedRequests++;
249                 break;
250
251             case FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS:
252                 // Increment the Fanboy's Annoyance List blocked requests count.
253                 fanboysAnnoyanceListBlockedRequests++;
254                 break;
255
256             case FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS:
257                 // Increment the Fanboy's Social Blocking List blocked requests count.
258                 fanboysSocialBlockingListBlockedRequests++;
259                 break;
260
261             case ULTRA_PRIVACY_BLOCKED_REQUESTS:
262                 // Increment the UltraPrivacy blocked requests count.
263                 ultraPrivacyBlockedRequests++;
264                 break;
265
266             case THIRD_PARTY_BLOCKED_REQUESTS:
267                 // Increment the Third Party blocked requests count.
268                 thirdPartyBlockedRequests++;
269                 break;
270         }
271     }
272
273     public int getRequestsCount(int list) {
274         // Run the command on the indicated list.
275         switch (list) {
276             case BLOCKED_REQUESTS:
277                 // Return the blocked requests count.
278                 return blockedRequests;
279
280             case EASY_LIST_BLOCKED_REQUESTS:
281                 // Return the EasyList blocked requests count.
282                 return easyListBlockedRequests;
283
284             case EASY_PRIVACY_BLOCKED_REQUESTS:
285                 // Return the EasyPrivacy blocked requests count.
286                 return easyPrivacyBlockedRequests;
287
288             case FANBOYS_ANNOYANCE_LIST_BLOCKED_REQUESTS:
289                 // Return the Fanboy's Annoyance List blocked requests count.
290                 return fanboysAnnoyanceListBlockedRequests;
291
292             case FANBOYS_SOCIAL_BLOCKING_LIST_BLOCKED_REQUESTS:
293                 // Return the Fanboy's Social Blocking List blocked requests count.
294                 return fanboysSocialBlockingListBlockedRequests;
295
296             case ULTRA_PRIVACY_BLOCKED_REQUESTS:
297                 // Return the UltraPrivacy blocked requests count.
298                 return ultraPrivacyBlockedRequests;
299
300             case THIRD_PARTY_BLOCKED_REQUESTS:
301                 // Return the Third Party blocked requests count.
302                 return thirdPartyBlockedRequests;
303
304             default:
305                 // Return 0.  This should never end up being called.
306                 return 0;
307         }
308     }
309
310
311     // Pinned SSL certificates.
312     public boolean hasPinnedSslCertificate() {
313         // Return the status of the pinned SSL certificate.
314         return hasPinnedSslCertificate;
315     }
316
317     public void setPinnedSslCertificate(String issuedToCName, String issuedToOName, String issuedToUName, String issuedByCName, String issuedByOName, String issuedByUName, Date startDate, Date endDate) {
318         // Store the pinned SSL certificate information.
319         pinnedSslIssuedToCName = issuedToCName;
320         pinnedSslIssuedToOName = issuedToOName;
321         pinnedSslIssuedToUName = issuedToUName;
322         pinnedSslIssuedByCName = issuedByCName;
323         pinnedSslIssuedByOName = issuedByOName;
324         pinnedSslIssuedByUName = issuedByUName;
325         pinnedSslStartDate = startDate;
326         pinnedSslEndDate = endDate;
327
328         // Set the pinned SSL certificate tracker.
329         hasPinnedSslCertificate = true;
330     }
331
332     public ArrayList<Object> getPinnedSslCertificate() {
333         // Initialize an array list.
334         ArrayList<Object> arrayList = new ArrayList<>();
335
336         // Create the SSL certificate string array.
337         String[] sslCertificateStringArray = new String[] {pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName};
338
339         // Create the SSL certificate date array.
340         Date[] sslCertificateDateArray = new Date[] {pinnedSslStartDate, pinnedSslEndDate};
341
342         // Add the arrays to the array list.
343         arrayList.add(sslCertificateStringArray);
344         arrayList.add(sslCertificateDateArray);
345
346         // Return the pinned SSL certificate array list.
347         return arrayList;
348     }
349
350     public void clearPinnedSslCertificate() {
351         // Clear the pinned SSL certificate.
352         pinnedSslIssuedToCName = null;
353         pinnedSslIssuedToOName = null;
354         pinnedSslIssuedToUName = null;
355         pinnedSslIssuedByCName = null;
356         pinnedSslIssuedByOName = null;
357         pinnedSslIssuedByUName = null;
358         pinnedSslStartDate = null;
359         pinnedSslEndDate = null;
360
361         // Clear the pinned SSL certificate tracker.
362         hasPinnedSslCertificate = false;
363     }
364
365
366     // Current IP addresses.
367     public boolean hasCurrentIpAddresses() {
368         // Return the status of the current IP addresses.
369         return hasCurrentIpAddresses;
370     }
371
372     public void setCurrentIpAddresses(String ipAddresses) {
373         // Store the current IP addresses.
374         currentIpAddresses = ipAddresses;
375
376         // Set the current IP addresses tracker.
377         hasCurrentIpAddresses = true;
378     }
379
380     public String getCurrentIpAddresses() {
381         // Return the current IP addresses.
382         return currentIpAddresses;
383     }
384
385     public void clearCurrentIpAddresses() {
386         // Clear the current IP addresses.
387         currentIpAddresses = null;
388
389         // Clear the current IP addresses tracker.
390         hasCurrentIpAddresses = false;
391     }
392
393
394     // Pinned IP addresses.
395     public boolean hasPinnedIpAddresses() {
396         // Return the status of the pinned IP addresses.
397         return hasPinnedIpAddresses;
398     }
399
400     public void setPinnedIpAddresses(String ipAddresses) {
401         // Store the pinned IP addresses.
402         pinnedIpAddresses = ipAddresses;
403
404         // Set the pinned IP addresses tracker.
405         hasPinnedIpAddresses = true;
406     }
407
408     public String getPinnedIpAddresses() {
409         // Return the pinned IP addresses.
410         return pinnedIpAddresses;
411     }
412
413     public void clearPinnedIpAddresses() {
414         // Clear the pinned IP addresses.
415         pinnedIpAddresses = null;
416
417         // Clear the pinned IP addresses tracker.
418         hasPinnedIpAddresses = false;
419     }
420
421
422     // Ignore pinned information.  The syntax looks better as written, even if it is always inverted.
423     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
424     public boolean ignorePinnedDomainInformation() {
425         // Return the status of the ignore pinned domain information tracker.
426         return ignorePinnedDomainInformation;
427     }
428
429     public void setIgnorePinnedDomainInformation(boolean status) {
430         // Set the status of the ignore pinned domain information tracker.
431         ignorePinnedDomainInformation = status;
432     }
433
434
435
436     @Override
437     public boolean onTouchEvent(MotionEvent motionEvent) {
438         // Initialize a tracker to return if this motion event is handled.
439         boolean motionEventHandled;
440
441         // Run the commands for the given motion event action.
442         switch (motionEvent.getAction()) {
443             case MotionEvent.ACTION_DOWN:
444                 // Start nested scrolling along the vertical axis.  `ViewCompat` must be used until the minimum API >= 21.
445                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
446
447                 // Save the current Y position.  Action down will not be called again until a new motion starts.
448                 previousYPosition = (int) motionEvent.getY();
449
450                 // Run the default commands.
451                 motionEventHandled = super.onTouchEvent(motionEvent);
452                 break;
453
454             case MotionEvent.ACTION_MOVE:
455                 // Get the current Y position.
456                 int currentYMotionPosition = (int) motionEvent.getY();
457
458                 // Calculate the pre-scroll delta Y.
459                 int preScrollDeltaY = previousYPosition - currentYMotionPosition;
460
461                 // Initialize a variable to track how much of the scroll is consumed.
462                 int[] consumedScroll = new int[2];
463
464                 // Initialize a variable to track the offset in the window.
465                 int[] offsetInWindow = new int[2];
466
467                 // Get the WebView Y position.
468                 int webViewYPosition = getScrollY();
469
470                 // Set the scroll delta Y to initially be the same as the pre-scroll delta Y.
471                 int scrollDeltaY = preScrollDeltaY;
472
473                 // Dispatch the nested pre-school.  This scrolls the app bar if it needs it.  `offsetInWindow` will be returned with an updated value.
474                 if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
475                     // Update the scroll delta Y if some of it was consumed.
476                     scrollDeltaY = preScrollDeltaY - consumedScroll[1];
477                 }
478
479                 // Check to see if the WebView is at the top and and the scroll action is downward.
480                 if ((webViewYPosition == 0) && (scrollDeltaY < 0)) {  // Swipe to refresh is being engaged.
481                     // Stop the nested scroll so that swipe to refresh has complete control.
482                     stopNestedScroll();
483                 } else {  // Swipe to refresh is not being engaged.
484                     // Start the nested scroll so that the app bar can scroll off the screen.
485                     startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
486
487                     // 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.
488                     dispatchNestedScroll(0, scrollDeltaY, 0, 0, offsetInWindow);
489
490                     // Store the current Y position for use in the next action move.
491                     previousYPosition = previousYPosition - scrollDeltaY;
492                 }
493
494                 // Run the default commands.
495                 motionEventHandled = super.onTouchEvent(motionEvent);
496                 break;
497
498
499             default:
500                 // Stop nested scrolling.
501                 stopNestedScroll();
502
503                 // Run the default commands.
504                 motionEventHandled = super.onTouchEvent(motionEvent);
505         }
506
507         // Perform a click.  This is required by the Android accessibility guidelines.
508         performClick();
509
510         // Return the status of the motion event.
511         return motionEventHandled;
512     }
513
514     // The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
515     @Override
516     public boolean performClick() {
517         return super.performClick();
518     }
519
520
521     // Method from NestedScrollingChild.
522     @Override
523     public void setNestedScrollingEnabled(boolean status) {
524         // Set the status of the nested scrolling.
525         nestedScrollingChildHelper.setNestedScrollingEnabled(status);
526     }
527
528     // Method from NestedScrollingChild.
529     @Override
530     public boolean isNestedScrollingEnabled() {
531         // Return the status of nested scrolling.
532         return nestedScrollingChildHelper.isNestedScrollingEnabled();
533     }
534
535
536     // Method from NestedScrollingChild.
537     @Override
538     public boolean startNestedScroll(int axes) {
539         // Start a nested scroll along the indicated axes.
540         return nestedScrollingChildHelper.startNestedScroll(axes);
541     }
542
543     // Method from NestedScrollingChild2.
544     @Override
545     public boolean startNestedScroll(int axes, int type) {
546         // Start a nested scroll along the indicated axes for the given type of input which caused the scroll event.
547         return nestedScrollingChildHelper.startNestedScroll(axes, type);
548     }
549
550
551     // Method from NestedScrollingChild.
552     @Override
553     public void stopNestedScroll() {
554         // Stop the nested scroll.
555         nestedScrollingChildHelper.stopNestedScroll();
556     }
557
558     // Method from NestedScrollingChild2.
559     @Override
560     public void stopNestedScroll(int type) {
561         // Stop the nested scroll of the given type of input which caused the scroll event.
562         nestedScrollingChildHelper.stopNestedScroll(type);
563     }
564
565
566     // Method from NestedScrollingChild.
567     @Override
568     public boolean hasNestedScrollingParent() {
569         // Return the status of the nested scrolling parent.
570         return nestedScrollingChildHelper.hasNestedScrollingParent();
571     }
572
573     // Method from NestedScrollingChild2.
574     @Override
575     public boolean hasNestedScrollingParent(int type) {
576         // return the status of the nested scrolling parent for the given type of input which caused the scroll event.
577         return nestedScrollingChildHelper.hasNestedScrollingParent(type);
578     }
579
580
581     // Method from NestedScrollingChild.
582     @Override
583     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow) {
584         // Dispatch a nested pre-scroll with the specified deltas, which lets a parent to consume some of the scroll if desired.
585         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow);
586     }
587
588     // Method from NestedScrollingChild2.
589     @Override
590     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow, int type) {
591         // 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.
592         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, type);
593     }
594
595
596     // Method from NestedScrollingChild.
597     @Override
598     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow) {
599         // Dispatch a nested scroll with the specified deltas.
600         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow);
601     }
602
603     // Method from NestedScrollingChild2.
604     @Override
605     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow, int type) {
606         // Dispatch a nested scroll with the specified deltas for the given type of input which caused the scroll event.
607         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow, type);
608     }
609
610
611     // Method from NestedScrollingChild.
612     @Override
613     public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
614         // Dispatch a nested pre-fling with the specified velocity, which lets a parent consume the fling if desired.
615         return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
616     }
617
618     // Method from NestedScrollingChild.
619     @Override
620     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
621         // Dispatch a nested fling with the specified velocity.
622         return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
623     }
624 }