21ab2c64b19504f4169e4358b5bdebe73b0c7db2
[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.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.HttpAuthHandler;
29 import android.webkit.SslErrorHandler;
30 import android.webkit.WebView;
31
32 import androidx.annotation.NonNull;
33 import androidx.core.content.ContextCompat;
34 import androidx.core.view.NestedScrollingChild2;
35 import androidx.core.view.NestedScrollingChildHelper;
36 import androidx.core.view.ViewCompat;
37
38 import com.stoutner.privacybrowser.R;
39
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.Date;
43 import java.util.List;
44
45 // NestedScrollWebView extends WebView to handle nested scrolls (scrolling the app bar off the screen).
46 public class NestedScrollWebView extends WebView implements NestedScrollingChild2 {
47     // These constants identify the blocklists.
48     public final static int BLOCKED_REQUESTS = 0;
49     public final static int EASYLIST = 1;
50     public final static int EASYPRIVACY = 2;
51     public final static int FANBOYS_ANNOYANCE_LIST = 3;
52     public final static int FANBOYS_SOCIAL_BLOCKING_LIST = 4;
53     public final static int ULTRALIST = 5;
54     public final static int ULTRAPRIVACY = 6;
55     public final static int THIRD_PARTY_REQUESTS = 7;
56
57     // Keep a copy of the WebView fragment ID.
58     private long webViewFragmentId;
59
60     // Store the handlers.
61     private SslErrorHandler sslErrorHandler;
62     private HttpAuthHandler httpAuthHandler;
63
64     // Track if domain settings are applied to this nested scroll WebView and, if so, the database ID.
65     private boolean domainSettingsApplied;
66     private int domainSettingsDatabaseId;
67
68     // Keep track of the current URL.  This is used to not block resource requests to the main URL.
69     private String currentUrl;
70
71     // Keep track of when the domain name changes so that domain settings can be reapplied.  This should never be null.
72     private String currentDomainName = "";
73
74     // Track the status of first-party cookies.
75     private boolean acceptFirstPartyCookies;
76
77     // Track the domain settings JavaScript status.  This can be removed once night mode does not require JavaScript.
78     private boolean domainSettingsJavaScriptEnabled;
79
80     // Track the resource requests.
81     private List<String[]> resourceRequests = Collections.synchronizedList(new ArrayList<>());  // Using a synchronized list makes adding resource requests thread safe.
82     private boolean easyListEnabled;
83     private boolean easyPrivacyEnabled;
84     private boolean fanboysAnnoyanceListEnabled;
85     private boolean fanboysSocialBlockingListEnabled;
86     private boolean ultraListEnabled;
87     private boolean ultraPrivacyEnabled;
88     private boolean blockAllThirdPartyRequests;
89     private int blockedRequests;
90     private int easyListBlockedRequests;
91     private int easyPrivacyBlockedRequests;
92     private int fanboysAnnoyanceListBlockedRequests;
93     private int fanboysSocialBlockingListBlockedRequests;
94     private int ultraListBlockedRequests;
95     private int ultraPrivacyBlockedRequests;
96     private int thirdPartyBlockedRequests;
97
98     // The pinned SSL certificate variables.
99     private boolean hasPinnedSslCertificate;
100     private String pinnedSslIssuedToCName;
101     private String pinnedSslIssuedToOName;
102     private String pinnedSslIssuedToUName;
103     private String pinnedSslIssuedByCName;
104     private String pinnedSslIssuedByOName;
105     private String pinnedSslIssuedByUName;
106     private Date pinnedSslStartDate;
107     private Date pinnedSslEndDate;
108
109     // The current IP addresses variables.
110     private boolean hasCurrentIpAddresses;
111     private String currentIpAddresses;
112
113     // The pinned IP addresses variables.
114     private boolean hasPinnedIpAddresses;
115     private String pinnedIpAddresses;
116
117     // 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.
118     private boolean ignorePinnedDomainInformation;
119
120     // The default or favorite icon.
121     private Bitmap favoriteOrDefaultIcon;
122
123     // Track night mode.
124     private boolean nightMode;
125
126     // Track swipe to refresh.
127     private boolean swipeToRefresh;
128
129     // Track a URL waiting for a proxy.
130     private String waitingForProxyUrlString = "";
131
132     // The nested scrolling child helper is used throughout the class.
133     private NestedScrollingChildHelper nestedScrollingChildHelper;
134
135     // The previous Y position needs to be tracked between motion events.
136     private int previousYPosition;
137
138
139
140     // The basic constructor.
141     public NestedScrollWebView(Context context) {
142         // Roll up to the next constructor.
143         this(context, null);
144     }
145
146     // The intermediate constructor.
147     public NestedScrollWebView(Context context, AttributeSet attributeSet) {
148         // Roll up to the next constructor.
149         this(context, attributeSet, android.R.attr.webViewStyle);
150     }
151
152     // The full constructor.
153     public NestedScrollWebView(Context context, AttributeSet attributeSet, int defaultStyle) {
154         // Run the default commands.
155         super(context, attributeSet, defaultStyle);
156
157         // Initialize the nested scrolling child helper.
158         nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
159
160         // Enable nested scrolling by default.
161         nestedScrollingChildHelper.setNestedScrollingEnabled(true);
162     }
163
164
165
166     // WebView Fragment ID.
167     public void setWebViewFragmentId(long webViewFragmentId) {
168         // Store the WebView fragment ID.
169         this.webViewFragmentId = webViewFragmentId;
170     }
171
172     public long getWebViewFragmentId() {
173         // Return the WebView fragment ID.
174         return webViewFragmentId;
175     }
176
177
178     // SSL error handler.
179     public void setSslErrorHandler(SslErrorHandler sslErrorHandler) {
180         // Store the current SSL error handler.
181         this.sslErrorHandler = sslErrorHandler;
182     }
183
184     public SslErrorHandler getSslErrorHandler() {
185         // Return the current SSL error handler.
186         return sslErrorHandler;
187     }
188
189     public void resetSslErrorHandler() {
190         // Reset the current SSL error handler.
191         sslErrorHandler = null;
192     }
193
194
195     // HTTP authentication handler.
196     public void setHttpAuthHandler(HttpAuthHandler httpAuthHandler) {
197         // Store the current HTTP authentication handler.
198         this.httpAuthHandler = httpAuthHandler;
199     }
200
201     public HttpAuthHandler getHttpAuthHandler() {
202         // Return the current HTTP authentication handler.
203         return httpAuthHandler;
204     }
205
206     public void resetHttpAuthHandler() {
207         // Reset the current HTTP authentication handler.
208         httpAuthHandler = null;
209     }
210
211
212     // Domain settings.
213     public void setDomainSettingsApplied(boolean applied) {
214         // Store the domain settings applied status.
215         domainSettingsApplied = applied;
216     }
217
218     public boolean getDomainSettingsApplied() {
219         // Return the domain settings applied status.
220         return domainSettingsApplied;
221     }
222
223
224     // Domain settings database ID.
225     public void setDomainSettingsDatabaseId(int databaseId) {
226         // Store the domain settings database ID.
227         domainSettingsDatabaseId = databaseId;
228     }
229
230     public int getDomainSettingsDatabaseId() {
231         // Return the domain settings database ID.
232         return domainSettingsDatabaseId;
233     }
234
235
236     // Current URL.
237     public void setCurrentUrl(String url) {
238         // Store the current URL.
239         currentUrl = url;
240     }
241
242     public String getCurrentUrl() {
243         // Return the current URL.
244         return currentUrl;
245     }
246
247
248     // Current domain name.  To function well when called, the domain name should never be allowed to be null.
249     public void setCurrentDomainName(@NonNull String domainName) {
250         // Store the current domain name.
251         currentDomainName = domainName;
252     }
253
254     public void resetCurrentDomainName() {
255         // Reset the current domain name.
256         currentDomainName = "";
257     }
258
259     public String getCurrentDomainName() {
260         // Return the current domain name.
261         return currentDomainName;
262     }
263
264
265     // First-party cookies.
266     public void setAcceptFirstPartyCookies(boolean status) {
267         // Store the accept first-party cookies status.
268         acceptFirstPartyCookies = status;
269     }
270
271     public boolean getAcceptFirstPartyCookies() {
272         // Return the accept first-party cookies status.
273         return acceptFirstPartyCookies;
274     }
275
276
277     // Domain settings JavaScript enabled.  This can be removed once night mode does not require JavaScript.
278     public void setDomainSettingsJavaScriptEnabled(boolean status) {
279         // Store the domain settings JavaScript status.
280         domainSettingsJavaScriptEnabled = status;
281     }
282
283     public boolean getDomainSettingsJavaScriptEnabled() {
284         // Return the domain settings JavaScript status.
285         return domainSettingsJavaScriptEnabled;
286     }
287
288
289     // Resource requests.
290     public void addResourceRequest(String[] resourceRequest) {
291         // Add the resource request to the list.
292         resourceRequests.add(resourceRequest);
293     }
294
295     public List<String[]> getResourceRequests() {
296         // Return the list of resource requests as an array list.
297         return resourceRequests;
298     }
299
300     public void clearResourceRequests() {
301         // Clear the resource requests.
302         resourceRequests.clear();
303     }
304
305
306     // Blocklists.
307     public void enableBlocklist(int blocklist, boolean status) {
308         // Update the status of the indicated blocklist.
309         switch (blocklist) {
310             case EASYLIST:
311                 // Update the status of the blocklist.
312                 easyListEnabled = status;
313                 break;
314
315             case EASYPRIVACY:
316                 // Update the status of the blocklist.
317                 easyPrivacyEnabled = status;
318                 break;
319
320             case FANBOYS_ANNOYANCE_LIST:
321                 // Update the status of the blocklist.
322                 fanboysAnnoyanceListEnabled = status;
323                 break;
324
325             case FANBOYS_SOCIAL_BLOCKING_LIST:
326                 // Update the status of the blocklist.
327                 fanboysSocialBlockingListEnabled = status;
328                 break;
329
330             case ULTRALIST:
331                 // Update the status of the blocklist.
332                 ultraListEnabled = status;
333                 break;
334
335             case ULTRAPRIVACY:
336                 // Update the status of the blocklist.
337                 ultraPrivacyEnabled = status;
338                 break;
339
340             case THIRD_PARTY_REQUESTS:
341                 // Update the status of the blocklist.
342                 blockAllThirdPartyRequests = status;
343                 break;
344         }
345     }
346
347     public boolean isBlocklistEnabled(int blocklist) {
348         // Get the status of the indicated blocklist.
349         switch (blocklist) {
350             case EASYLIST:
351                 // Return the status of the blocklist.
352                 return easyListEnabled;
353
354             case EASYPRIVACY:
355                 // Return the status of the blocklist.
356                 return easyPrivacyEnabled;
357
358             case FANBOYS_ANNOYANCE_LIST:
359                 // Return the status of the blocklist.
360                 return fanboysAnnoyanceListEnabled;
361
362             case FANBOYS_SOCIAL_BLOCKING_LIST:
363                 // Return the status of the blocklist.
364                 return fanboysSocialBlockingListEnabled;
365
366             case ULTRALIST:
367                 // Return the status of the blocklist.
368                 return ultraListEnabled;
369
370             case ULTRAPRIVACY:
371                 // Return the status of the blocklist.
372                 return ultraPrivacyEnabled;
373
374             case THIRD_PARTY_REQUESTS:
375                 // Return the status of the blocklist.
376                 return blockAllThirdPartyRequests;
377
378             default:
379                 // The default value is required but should never be used.
380                 return false;
381         }
382     }
383
384
385     // Resource request counters.
386     public void resetRequestsCounters() {
387         // Reset all the resource request counters.
388         blockedRequests = 0;
389         easyListBlockedRequests = 0;
390         easyPrivacyBlockedRequests = 0;
391         fanboysAnnoyanceListBlockedRequests = 0;
392         fanboysSocialBlockingListBlockedRequests = 0;
393         ultraListBlockedRequests = 0;
394         ultraPrivacyBlockedRequests = 0;
395         thirdPartyBlockedRequests = 0;
396     }
397
398     public void incrementRequestsCount(int blocklist) {
399         // Increment the count of the indicated blocklist.
400         switch (blocklist) {
401             case BLOCKED_REQUESTS:
402                 // Increment the blocked requests count.
403                 blockedRequests++;
404                 break;
405
406             case EASYLIST:
407                 // Increment the EasyList blocked requests count.
408                 easyListBlockedRequests++;
409                 break;
410
411             case EASYPRIVACY:
412                 // Increment the EasyPrivacy blocked requests count.
413                 easyPrivacyBlockedRequests++;
414                 break;
415
416             case FANBOYS_ANNOYANCE_LIST:
417                 // Increment the Fanboy's Annoyance List blocked requests count.
418                 fanboysAnnoyanceListBlockedRequests++;
419                 break;
420
421             case FANBOYS_SOCIAL_BLOCKING_LIST:
422                 // Increment the Fanboy's Social Blocking List blocked requests count.
423                 fanboysSocialBlockingListBlockedRequests++;
424                 break;
425
426             case ULTRALIST:
427                 // Increment the UltraList blocked requests count.
428                 ultraListBlockedRequests++;
429                 break;
430
431             case ULTRAPRIVACY:
432                 // Increment the UltraPrivacy blocked requests count.
433                 ultraPrivacyBlockedRequests++;
434                 break;
435
436             case THIRD_PARTY_REQUESTS:
437                 // Increment the Third Party blocked requests count.
438                 thirdPartyBlockedRequests++;
439                 break;
440         }
441     }
442
443     public int getRequestsCount(int blocklist) {
444         // Get the count of the indicated blocklist.
445         switch (blocklist) {
446             case BLOCKED_REQUESTS:
447                 // Return the blocked requests count.
448                 return blockedRequests;
449
450             case EASYLIST:
451                 // Return the EasyList blocked requests count.
452                 return easyListBlockedRequests;
453
454             case EASYPRIVACY:
455                 // Return the EasyPrivacy blocked requests count.
456                 return easyPrivacyBlockedRequests;
457
458             case FANBOYS_ANNOYANCE_LIST:
459                 // Return the Fanboy's Annoyance List blocked requests count.
460                 return fanboysAnnoyanceListBlockedRequests;
461
462             case FANBOYS_SOCIAL_BLOCKING_LIST:
463                 // Return the Fanboy's Social Blocking List blocked requests count.
464                 return fanboysSocialBlockingListBlockedRequests;
465
466             case ULTRALIST:
467                 // Return the UltraList blocked requests count.
468                 return ultraListBlockedRequests;
469
470             case ULTRAPRIVACY:
471                 // Return the UltraPrivacy blocked requests count.
472                 return ultraPrivacyBlockedRequests;
473
474             case THIRD_PARTY_REQUESTS:
475                 // Return the Third Party blocked requests count.
476                 return thirdPartyBlockedRequests;
477
478             default:
479                 // Return 0.  This should never end up being called.
480                 return 0;
481         }
482     }
483
484
485     // Pinned SSL certificates.
486     public boolean hasPinnedSslCertificate() {
487         // Return the status of the pinned SSL certificate.
488         return hasPinnedSslCertificate;
489     }
490
491     public void setPinnedSslCertificate(String issuedToCName, String issuedToOName, String issuedToUName, String issuedByCName, String issuedByOName, String issuedByUName, Date startDate, Date endDate) {
492         // Store the pinned SSL certificate information.
493         pinnedSslIssuedToCName = issuedToCName;
494         pinnedSslIssuedToOName = issuedToOName;
495         pinnedSslIssuedToUName = issuedToUName;
496         pinnedSslIssuedByCName = issuedByCName;
497         pinnedSslIssuedByOName = issuedByOName;
498         pinnedSslIssuedByUName = issuedByUName;
499         pinnedSslStartDate = startDate;
500         pinnedSslEndDate = endDate;
501
502         // Set the pinned SSL certificate tracker.
503         hasPinnedSslCertificate = true;
504     }
505
506     public ArrayList<Object> getPinnedSslCertificate() {
507         // Initialize an array list.
508         ArrayList<Object> arrayList = new ArrayList<>();
509
510         // Create the SSL certificate string array.
511         String[] sslCertificateStringArray = new String[] {pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName};
512
513         // Create the SSL certificate date array.
514         Date[] sslCertificateDateArray = new Date[] {pinnedSslStartDate, pinnedSslEndDate};
515
516         // Add the arrays to the array list.
517         arrayList.add(sslCertificateStringArray);
518         arrayList.add(sslCertificateDateArray);
519
520         // Return the pinned SSL certificate array list.
521         return arrayList;
522     }
523
524     public void clearPinnedSslCertificate() {
525         // Clear the pinned SSL certificate.
526         pinnedSslIssuedToCName = null;
527         pinnedSslIssuedToOName = null;
528         pinnedSslIssuedToUName = null;
529         pinnedSslIssuedByCName = null;
530         pinnedSslIssuedByOName = null;
531         pinnedSslIssuedByUName = null;
532         pinnedSslStartDate = null;
533         pinnedSslEndDate = null;
534
535         // Clear the pinned SSL certificate tracker.
536         hasPinnedSslCertificate = false;
537     }
538
539
540     // Current IP addresses.
541     public boolean hasCurrentIpAddresses() {
542         // Return the status of the current IP addresses.
543         return hasCurrentIpAddresses;
544     }
545
546     public void setCurrentIpAddresses(String ipAddresses) {
547         // Store the current IP addresses.
548         currentIpAddresses = ipAddresses;
549
550         // Set the current IP addresses tracker.
551         hasCurrentIpAddresses = true;
552     }
553
554     public String getCurrentIpAddresses() {
555         // Return the current IP addresses.
556         return currentIpAddresses;
557     }
558
559     public void clearCurrentIpAddresses() {
560         // Clear the current IP addresses.
561         currentIpAddresses = null;
562
563         // Clear the current IP addresses tracker.
564         hasCurrentIpAddresses = false;
565     }
566
567
568     // Pinned IP addresses.
569     public boolean hasPinnedIpAddresses() {
570         // Return the status of the pinned IP addresses.
571         return hasPinnedIpAddresses;
572     }
573
574     public void setPinnedIpAddresses(String ipAddresses) {
575         // Store the pinned IP addresses.
576         pinnedIpAddresses = ipAddresses;
577
578         // Set the pinned IP addresses tracker.
579         hasPinnedIpAddresses = true;
580     }
581
582     public String getPinnedIpAddresses() {
583         // Return the pinned IP addresses.
584         return pinnedIpAddresses;
585     }
586
587     public void clearPinnedIpAddresses() {
588         // Clear the pinned IP addresses.
589         pinnedIpAddresses = null;
590
591         // Clear the pinned IP addresses tracker.
592         hasPinnedIpAddresses = false;
593     }
594
595
596     // Ignore pinned information.
597     public void setIgnorePinnedDomainInformation(boolean status) {
598         // Set the status of the ignore pinned domain information tracker.
599         ignorePinnedDomainInformation = status;
600     }
601
602     // The syntax looks better as written, even if it is always inverted.
603     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
604     public boolean ignorePinnedDomainInformation() {
605         // Return the status of the ignore pinned domain information tracker.
606         return ignorePinnedDomainInformation;
607     }
608
609
610     // Favorite or default icon.
611     public void initializeFavoriteIcon() {
612         // Get the default favorite icon drawable.  `ContextCompat` must be used until API >= 21.
613         Drawable favoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
614
615         // Cast the favorite icon drawable to a bitmap drawable.
616         BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
617
618         // Remove the incorrect warning below that the favorite icon bitmap drawable might be null.
619         assert favoriteIconBitmapDrawable != null;
620
621         // Store the default icon bitmap.
622         favoriteOrDefaultIcon = favoriteIconBitmapDrawable.getBitmap();
623     }
624
625     public void setFavoriteOrDefaultIcon(Bitmap icon) {
626         // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
627         if ((icon.getHeight() > 256) || (icon.getWidth() > 256)) {
628             favoriteOrDefaultIcon = Bitmap.createScaledBitmap(icon, 256, 256, true);
629         } else {
630             // Store the icon as presented.
631             favoriteOrDefaultIcon = icon;
632         }
633     }
634
635     public Bitmap getFavoriteOrDefaultIcon() {
636         // Return the favorite or default icon.
637         return favoriteOrDefaultIcon;
638     }
639
640
641     // Night mode.
642     public void setNightMode(boolean status) {
643         // Store the night mode status.
644         nightMode = status;
645     }
646
647     public boolean getNightMode() {
648         // Return the night mode status.
649         return nightMode;
650     }
651
652
653     // Swipe to refresh.
654     public void setSwipeToRefresh(boolean status) {
655         // Store the swipe to refresh status.
656         swipeToRefresh = status;
657     }
658
659     public boolean getSwipeToRefresh() {
660         // Return the swipe to refresh status.
661         return swipeToRefresh;
662     }
663
664
665     // Waiting for proxy.
666     public void setWaitingForProxyUrlString(String urlString) {
667         // Store the waiting for proxy URL string.
668         waitingForProxyUrlString = urlString;
669     }
670
671     public String getWaitingForProxyUrlString() {
672         // Return the waiting for proxy URL string.
673         return waitingForProxyUrlString;
674     }
675
676     public void resetWaitingForProxyUrlString() {
677         // Clear the waiting for proxy URL string.
678         waitingForProxyUrlString = "";
679     }
680
681     // Scroll range.
682     public int getHorizontalScrollRange() {
683         // Return the horizontal scroll range.
684         return computeHorizontalScrollRange();
685     }
686
687     public int getVerticalScrollRange() {
688         // Return the vertical scroll range.
689         return computeVerticalScrollRange();
690     }
691
692
693
694     @Override
695     public boolean onTouchEvent(MotionEvent motionEvent) {
696         // Initialize a tracker to return if this motion event is handled.
697         boolean motionEventHandled;
698
699         // Run the commands for the given motion event action.
700         switch (motionEvent.getAction()) {
701             case MotionEvent.ACTION_DOWN:
702                 // Start nested scrolling along the vertical axis.  `ViewCompat` must be used until the minimum API >= 21.
703                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
704
705                 // Save the current Y position.  Action down will not be called again until a new motion starts.
706                 previousYPosition = (int) motionEvent.getY();
707
708                 // Run the default commands.
709                 motionEventHandled = super.onTouchEvent(motionEvent);
710                 break;
711
712             case MotionEvent.ACTION_MOVE:
713                 // Get the current Y position.
714                 int currentYMotionPosition = (int) motionEvent.getY();
715
716                 // Calculate the pre-scroll delta Y.
717                 int preScrollDeltaY = previousYPosition - currentYMotionPosition;
718
719                 // Initialize a variable to track how much of the scroll is consumed.
720                 int[] consumedScroll = new int[2];
721
722                 // Initialize a variable to track the offset in the window.
723                 int[] offsetInWindow = new int[2];
724
725                 // Get the WebView Y position.
726                 int webViewYPosition = getScrollY();
727
728                 // Set the scroll delta Y to initially be the same as the pre-scroll delta Y.
729                 int scrollDeltaY = preScrollDeltaY;
730
731                 // Dispatch the nested pre-school.  This scrolls the app bar if it needs it.  `offsetInWindow` will be returned with an updated value.
732                 if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
733                     // Update the scroll delta Y if some of it was consumed.
734                     // 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.
735                     scrollDeltaY = preScrollDeltaY - consumedScroll[1];
736                 }
737
738                 // Check to see if the WebView is at the top and and the scroll action is downward.
739                 if ((webViewYPosition == 0) && (scrollDeltaY < 0)) {  // Swipe to refresh is being engaged.
740                     // 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.
741                     stopNestedScroll();
742                 } else {  // Swipe to refresh is not being engaged.
743                     // Start the nested scroll so that the app bar can scroll off the screen.
744                     startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
745
746                     // 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.
747                     dispatchNestedScroll(0, scrollDeltaY, 0, 0, offsetInWindow);
748
749                     // Store the current Y position for use in the next action move.
750                     previousYPosition = previousYPosition - scrollDeltaY;
751                 }
752
753                 // Run the default commands.
754                 motionEventHandled = super.onTouchEvent(motionEvent);
755                 break;
756
757
758             default:
759                 // Stop nested scrolling.
760                 stopNestedScroll();
761
762                 // Run the default commands.
763                 motionEventHandled = super.onTouchEvent(motionEvent);
764         }
765
766         // Perform a click.  This is required by the Android accessibility guidelines.
767         performClick();
768
769         // Return the status of the motion event.
770         return motionEventHandled;
771     }
772
773     // The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
774     @Override
775     public boolean performClick() {
776         return super.performClick();
777     }
778
779
780     // Method from NestedScrollingChild.
781     @Override
782     public void setNestedScrollingEnabled(boolean status) {
783         // Set the status of the nested scrolling.
784         nestedScrollingChildHelper.setNestedScrollingEnabled(status);
785     }
786
787     // Method from NestedScrollingChild.
788     @Override
789     public boolean isNestedScrollingEnabled() {
790         // Return the status of nested scrolling.
791         return nestedScrollingChildHelper.isNestedScrollingEnabled();
792     }
793
794
795     // Method from NestedScrollingChild.
796     @Override
797     public boolean startNestedScroll(int axes) {
798         // Start a nested scroll along the indicated axes.
799         return nestedScrollingChildHelper.startNestedScroll(axes);
800     }
801
802     // Method from NestedScrollingChild2.
803     @Override
804     public boolean startNestedScroll(int axes, int type) {
805         // Start a nested scroll along the indicated axes for the given type of input which caused the scroll event.
806         return nestedScrollingChildHelper.startNestedScroll(axes, type);
807     }
808
809
810     // Method from NestedScrollingChild.
811     @Override
812     public void stopNestedScroll() {
813         // Stop the nested scroll.
814         nestedScrollingChildHelper.stopNestedScroll();
815     }
816
817     // Method from NestedScrollingChild2.
818     @Override
819     public void stopNestedScroll(int type) {
820         // Stop the nested scroll of the given type of input which caused the scroll event.
821         nestedScrollingChildHelper.stopNestedScroll(type);
822     }
823
824
825     // Method from NestedScrollingChild.
826     @Override
827     public boolean hasNestedScrollingParent() {
828         // Return the status of the nested scrolling parent.
829         return nestedScrollingChildHelper.hasNestedScrollingParent();
830     }
831
832     // Method from NestedScrollingChild2.
833     @Override
834     public boolean hasNestedScrollingParent(int type) {
835         // return the status of the nested scrolling parent for the given type of input which caused the scroll event.
836         return nestedScrollingChildHelper.hasNestedScrollingParent(type);
837     }
838
839
840     // Method from NestedScrollingChild.
841     @Override
842     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow) {
843         // Dispatch a nested pre-scroll with the specified deltas, which lets a parent to consume some of the scroll if desired.
844         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow);
845     }
846
847     // Method from NestedScrollingChild2.
848     @Override
849     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow, int type) {
850         // 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.
851         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, type);
852     }
853
854
855     // Method from NestedScrollingChild.
856     @Override
857     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow) {
858         // Dispatch a nested scroll with the specified deltas.
859         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow);
860     }
861
862     // Method from NestedScrollingChild2.
863     @Override
864     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow, int type) {
865         // Dispatch a nested scroll with the specified deltas for the given type of input which caused the scroll event.
866         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow, type);
867     }
868
869
870     // Method from NestedScrollingChild.
871     @Override
872     public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
873         // Dispatch a nested pre-fling with the specified velocity, which lets a parent consume the fling if desired.
874         return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
875     }
876
877     // Method from NestedScrollingChild.
878     @Override
879     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
880         // Dispatch a nested fling with the specified velocity.
881         return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
882     }
883 }