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