]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/views/NestedScrollWebView.java
Fix a crash when two threads try to update the resource requests at the same time...
[PrivacyBrowserAndroid.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 EASY_LIST = 1;
50     public final static int EASY_PRIVACY = 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 ULTRA_PRIVACY = 5;
54     public final static int THIRD_PARTY_REQUESTS = 6;
55
56     // Keep a copy of the WebView fragment ID.
57     private long webViewFragmentId;
58
59     // Store the handlers.
60     private SslErrorHandler sslErrorHandler;
61     private HttpAuthHandler httpAuthHandler;
62
63     // Track if domain settings are applied to this nested scroll WebView and, if so, the database ID.
64     private boolean domainSettingsApplied;
65     private int domainSettingsDatabaseId;
66
67     // Keep track of when the domain name changes so that domain settings can be reapplied.  This should never be null.
68     private String currentDomainName = "";
69
70     // Track the status of first-party cookies.
71     private boolean acceptFirstPartyCookies;
72
73     // Track the domain settings JavaScript status.  This can be removed once night mode does not require JavaScript.
74     private boolean domainSettingsJavaScriptEnabled;
75
76     // Track the resource requests.
77     private List<String[]> resourceRequests = Collections.synchronizedList(new ArrayList<>());  // Using a synchronized list makes adding resource requests thread safe.
78     private boolean easyListEnabled;
79     private boolean easyPrivacyEnabled;
80     private boolean fanboysAnnoyanceListEnabled;
81     private boolean fanboysSocialBlockingListEnabled;
82     private boolean ultraPrivacyEnabled;
83     private boolean blockAllThirdPartyRequests;
84     private int blockedRequests;
85     private int easyListBlockedRequests;
86     private int easyPrivacyBlockedRequests;
87     private int fanboysAnnoyanceListBlockedRequests;
88     private int fanboysSocialBlockingListBlockedRequests;
89     private int ultraPrivacyBlockedRequests;
90     private int thirdPartyBlockedRequests;
91
92     // The pinned SSL certificate variables.
93     private boolean hasPinnedSslCertificate;
94     private String pinnedSslIssuedToCName;
95     private String pinnedSslIssuedToOName;
96     private String pinnedSslIssuedToUName;
97     private String pinnedSslIssuedByCName;
98     private String pinnedSslIssuedByOName;
99     private String pinnedSslIssuedByUName;
100     private Date pinnedSslStartDate;
101     private Date pinnedSslEndDate;
102
103     // The current IP addresses variables.
104     private boolean hasCurrentIpAddresses;
105     private String currentIpAddresses;
106
107     // The pinned IP addresses variables.
108     private boolean hasPinnedIpAddresses;
109     private String pinnedIpAddresses;
110
111     // 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.
112     private boolean ignorePinnedDomainInformation;
113
114     // Track navigation of history.
115     private boolean navigatingHistory;
116
117     // The default or favorite icon.
118     private Bitmap favoriteOrDefaultIcon;
119
120     // Track night mode.
121     private boolean nightMode;
122
123     // Track swipe to refresh.
124     private boolean swipeToRefresh;
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 domain name.  To function well when called, the domain name should never be allowed to be null.
231     public void setCurrentDomainName(@NonNull String domainName) {
232         // Store the current domain name.
233         currentDomainName = domainName;
234     }
235
236     public void resetCurrentDomainName() {
237         // Reset the current domain name.
238         currentDomainName = "";
239     }
240
241     public String getCurrentDomainName() {
242         // Return the current domain name.
243         return currentDomainName;
244     }
245
246
247     // First-party cookies.
248     public void setAcceptFirstPartyCookies(boolean status) {
249         // Store the accept first-party cookies status.
250         acceptFirstPartyCookies = status;
251     }
252
253     public boolean getAcceptFirstPartyCookies() {
254         // Return the accept first-party cookies status.
255         return acceptFirstPartyCookies;
256     }
257
258
259     // Domain settings JavaScript enabled.  This can be removed once night mode does not require JavaScript.
260     public void setDomainSettingsJavaScriptEnabled(boolean status) {
261         // Store the domain settings JavaScript status.
262         domainSettingsJavaScriptEnabled = status;
263     }
264
265     public boolean getDomainSettingsJavaScriptEnabled() {
266         // Return the domain settings JavaScript status.
267         return domainSettingsJavaScriptEnabled;
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 EASY_LIST:
293                 // Update the status of the blocklist.
294                 easyListEnabled = status;
295                 break;
296
297             case EASY_PRIVACY:
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 ULTRA_PRIVACY:
313                 // Update the status of the blocklist.
314                 ultraPrivacyEnabled = status;
315                 break;
316
317             case THIRD_PARTY_REQUESTS:
318                 // Update the status of the blocklist.
319                 blockAllThirdPartyRequests = status;
320                 break;
321         }
322     }
323
324     public boolean isBlocklistEnabled(int blocklist) {
325         // Get the status of the indicated blocklist.
326         switch (blocklist) {
327             case EASY_LIST:
328                 // Return the status of the blocklist.
329                 return easyListEnabled;
330
331             case EASY_PRIVACY:
332                 // Return the status of the blocklist.
333                 return easyPrivacyEnabled;
334
335             case FANBOYS_ANNOYANCE_LIST:
336                 // Return the status of the blocklist.
337                 return fanboysAnnoyanceListEnabled;
338
339             case FANBOYS_SOCIAL_BLOCKING_LIST:
340                 // Return the status of the blocklist.
341                 return fanboysSocialBlockingListEnabled;
342
343             case ULTRA_PRIVACY:
344                 // Return the status of the blocklist.
345                 return ultraPrivacyEnabled;
346
347             case THIRD_PARTY_REQUESTS:
348                 // Return the status of the blocklist.
349                 return blockAllThirdPartyRequests;
350
351             default:
352                 // The default value is required but should never be used.
353                 return false;
354         }
355     }
356
357
358     // Resource request counters.
359     public void resetRequestsCounters() {
360         // Reset all the resource request counters.
361         blockedRequests = 0;
362         easyListBlockedRequests = 0;
363         easyPrivacyBlockedRequests = 0;
364         fanboysAnnoyanceListBlockedRequests = 0;
365         fanboysSocialBlockingListBlockedRequests = 0;
366         ultraPrivacyBlockedRequests = 0;
367         thirdPartyBlockedRequests = 0;
368     }
369
370     public void incrementRequestsCount(int blocklist) {
371         // Increment the count of the indicated blocklist.
372         switch (blocklist) {
373             case BLOCKED_REQUESTS:
374                 // Increment the blocked requests count.
375                 blockedRequests++;
376                 break;
377
378             case EASY_LIST:
379                 // Increment the EasyList blocked requests count.
380                 easyListBlockedRequests++;
381                 break;
382
383             case EASY_PRIVACY:
384                 // Increment the EasyPrivacy blocked requests count.
385                 easyPrivacyBlockedRequests++;
386                 break;
387
388             case FANBOYS_ANNOYANCE_LIST:
389                 // Increment the Fanboy's Annoyance List blocked requests count.
390                 fanboysAnnoyanceListBlockedRequests++;
391                 break;
392
393             case FANBOYS_SOCIAL_BLOCKING_LIST:
394                 // Increment the Fanboy's Social Blocking List blocked requests count.
395                 fanboysSocialBlockingListBlockedRequests++;
396                 break;
397
398             case ULTRA_PRIVACY:
399                 // Increment the UltraPrivacy blocked requests count.
400                 ultraPrivacyBlockedRequests++;
401                 break;
402
403             case THIRD_PARTY_REQUESTS:
404                 // Increment the Third Party blocked requests count.
405                 thirdPartyBlockedRequests++;
406                 break;
407         }
408     }
409
410     public int getRequestsCount(int blocklist) {
411         // Get the count of the indicated blocklist.
412         switch (blocklist) {
413             case BLOCKED_REQUESTS:
414                 // Return the blocked requests count.
415                 return blockedRequests;
416
417             case EASY_LIST:
418                 // Return the EasyList blocked requests count.
419                 return easyListBlockedRequests;
420
421             case EASY_PRIVACY:
422                 // Return the EasyPrivacy blocked requests count.
423                 return easyPrivacyBlockedRequests;
424
425             case FANBOYS_ANNOYANCE_LIST:
426                 // Return the Fanboy's Annoyance List blocked requests count.
427                 return fanboysAnnoyanceListBlockedRequests;
428
429             case FANBOYS_SOCIAL_BLOCKING_LIST:
430                 // Return the Fanboy's Social Blocking List blocked requests count.
431                 return fanboysSocialBlockingListBlockedRequests;
432
433             case ULTRA_PRIVACY:
434                 // Return the UltraPrivacy blocked requests count.
435                 return ultraPrivacyBlockedRequests;
436
437             case THIRD_PARTY_REQUESTS:
438                 // Return the Third Party blocked requests count.
439                 return thirdPartyBlockedRequests;
440
441             default:
442                 // Return 0.  This should never end up being called.
443                 return 0;
444         }
445     }
446
447
448     // Pinned SSL certificates.
449     public boolean hasPinnedSslCertificate() {
450         // Return the status of the pinned SSL certificate.
451         return hasPinnedSslCertificate;
452     }
453
454     public void setPinnedSslCertificate(String issuedToCName, String issuedToOName, String issuedToUName, String issuedByCName, String issuedByOName, String issuedByUName, Date startDate, Date endDate) {
455         // Store the pinned SSL certificate information.
456         pinnedSslIssuedToCName = issuedToCName;
457         pinnedSslIssuedToOName = issuedToOName;
458         pinnedSslIssuedToUName = issuedToUName;
459         pinnedSslIssuedByCName = issuedByCName;
460         pinnedSslIssuedByOName = issuedByOName;
461         pinnedSslIssuedByUName = issuedByUName;
462         pinnedSslStartDate = startDate;
463         pinnedSslEndDate = endDate;
464
465         // Set the pinned SSL certificate tracker.
466         hasPinnedSslCertificate = true;
467     }
468
469     public ArrayList<Object> getPinnedSslCertificate() {
470         // Initialize an array list.
471         ArrayList<Object> arrayList = new ArrayList<>();
472
473         // Create the SSL certificate string array.
474         String[] sslCertificateStringArray = new String[] {pinnedSslIssuedToCName, pinnedSslIssuedToOName, pinnedSslIssuedToUName, pinnedSslIssuedByCName, pinnedSslIssuedByOName, pinnedSslIssuedByUName};
475
476         // Create the SSL certificate date array.
477         Date[] sslCertificateDateArray = new Date[] {pinnedSslStartDate, pinnedSslEndDate};
478
479         // Add the arrays to the array list.
480         arrayList.add(sslCertificateStringArray);
481         arrayList.add(sslCertificateDateArray);
482
483         // Return the pinned SSL certificate array list.
484         return arrayList;
485     }
486
487     public void clearPinnedSslCertificate() {
488         // Clear the pinned SSL certificate.
489         pinnedSslIssuedToCName = null;
490         pinnedSslIssuedToOName = null;
491         pinnedSslIssuedToUName = null;
492         pinnedSslIssuedByCName = null;
493         pinnedSslIssuedByOName = null;
494         pinnedSslIssuedByUName = null;
495         pinnedSslStartDate = null;
496         pinnedSslEndDate = null;
497
498         // Clear the pinned SSL certificate tracker.
499         hasPinnedSslCertificate = false;
500     }
501
502
503     // Current IP addresses.
504     public boolean hasCurrentIpAddresses() {
505         // Return the status of the current IP addresses.
506         return hasCurrentIpAddresses;
507     }
508
509     public void setCurrentIpAddresses(String ipAddresses) {
510         // Store the current IP addresses.
511         currentIpAddresses = ipAddresses;
512
513         // Set the current IP addresses tracker.
514         hasCurrentIpAddresses = true;
515     }
516
517     public String getCurrentIpAddresses() {
518         // Return the current IP addresses.
519         return currentIpAddresses;
520     }
521
522     public void clearCurrentIpAddresses() {
523         // Clear the current IP addresses.
524         currentIpAddresses = null;
525
526         // Clear the current IP addresses tracker.
527         hasCurrentIpAddresses = false;
528     }
529
530
531     // Pinned IP addresses.
532     public boolean hasPinnedIpAddresses() {
533         // Return the status of the pinned IP addresses.
534         return hasPinnedIpAddresses;
535     }
536
537     public void setPinnedIpAddresses(String ipAddresses) {
538         // Store the pinned IP addresses.
539         pinnedIpAddresses = ipAddresses;
540
541         // Set the pinned IP addresses tracker.
542         hasPinnedIpAddresses = true;
543     }
544
545     public String getPinnedIpAddresses() {
546         // Return the pinned IP addresses.
547         return pinnedIpAddresses;
548     }
549
550     public void clearPinnedIpAddresses() {
551         // Clear the pinned IP addresses.
552         pinnedIpAddresses = null;
553
554         // Clear the pinned IP addresses tracker.
555         hasPinnedIpAddresses = false;
556     }
557
558
559     // Ignore pinned information.
560     public void setIgnorePinnedDomainInformation(boolean status) {
561         // Set the status of the ignore pinned domain information tracker.
562         ignorePinnedDomainInformation = status;
563     }
564
565     // The syntax looks better as written, even if it is always inverted.
566     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
567     public boolean ignorePinnedDomainInformation() {
568         // Return the status of the ignore pinned domain information tracker.
569         return ignorePinnedDomainInformation;
570     }
571
572
573     // Navigating history.
574     public void setNavigatingHistory(boolean status) {
575         // Set the status of navigating history.
576         navigatingHistory = status;
577     }
578
579     public boolean getNavigatingHistory() {
580         // Return the status of navigating history.
581         return navigatingHistory;
582     }
583
584
585     // Favorite or default icon.
586     public void initializeFavoriteIcon() {
587         // Get the default favorite icon drawable.  `ContextCompat` must be used until API >= 21.
588         Drawable favoriteIconDrawable = ContextCompat.getDrawable(getContext(), R.drawable.world);
589
590         // Cast the favorite icon drawable to a bitmap drawable.
591         BitmapDrawable favoriteIconBitmapDrawable = (BitmapDrawable) favoriteIconDrawable;
592
593         // Remove the incorrect warning below that the favorite icon bitmap drawable might be null.
594         assert favoriteIconBitmapDrawable != null;
595
596         // Store the default icon bitmap.
597         favoriteOrDefaultIcon = favoriteIconBitmapDrawable.getBitmap();
598     }
599
600     public void setFavoriteOrDefaultIcon(Bitmap icon) {
601         // Scale the favorite icon bitmap down if it is larger than 256 x 256.  Filtering uses bilinear interpolation.
602         if ((icon.getHeight() > 256) || (icon.getWidth() > 256)) {
603             favoriteOrDefaultIcon = Bitmap.createScaledBitmap(icon, 256, 256, true);
604         } else {
605             // Store the icon as presented.
606             favoriteOrDefaultIcon = icon;
607         }
608     }
609
610     public Bitmap getFavoriteOrDefaultIcon() {
611         // Return the favorite or default icon.
612         return favoriteOrDefaultIcon;
613     }
614
615
616     // Night mode.
617     public void setNightMode(boolean status) {
618         // Store the night mode status.
619         nightMode = status;
620     }
621
622     public boolean getNightMode() {
623         // Return the night mode status.
624         return nightMode;
625     }
626
627
628     // Swipe to refresh.
629     public void setSwipeToRefresh(boolean status) {
630         // Store the swipe to refresh status.
631         swipeToRefresh = status;
632     }
633
634     public boolean getSwipeToRefresh() {
635         // Return the swipe to refresh status.
636         return swipeToRefresh;
637     }
638
639
640     // Scroll range.
641     public int getHorizontalScrollRange() {
642         // Return the horizontal scroll range.
643         return computeHorizontalScrollRange();
644     }
645
646     public int getVerticalScrollRange() {
647         // Return the vertical scroll range.
648         return computeVerticalScrollRange();
649     }
650
651
652
653     @Override
654     public boolean onTouchEvent(MotionEvent motionEvent) {
655         // Initialize a tracker to return if this motion event is handled.
656         boolean motionEventHandled;
657
658         // Run the commands for the given motion event action.
659         switch (motionEvent.getAction()) {
660             case MotionEvent.ACTION_DOWN:
661                 // Start nested scrolling along the vertical axis.  `ViewCompat` must be used until the minimum API >= 21.
662                 startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
663
664                 // Save the current Y position.  Action down will not be called again until a new motion starts.
665                 previousYPosition = (int) motionEvent.getY();
666
667                 // Run the default commands.
668                 motionEventHandled = super.onTouchEvent(motionEvent);
669                 break;
670
671             case MotionEvent.ACTION_MOVE:
672                 // Get the current Y position.
673                 int currentYMotionPosition = (int) motionEvent.getY();
674
675                 // Calculate the pre-scroll delta Y.
676                 int preScrollDeltaY = previousYPosition - currentYMotionPosition;
677
678                 // Initialize a variable to track how much of the scroll is consumed.
679                 int[] consumedScroll = new int[2];
680
681                 // Initialize a variable to track the offset in the window.
682                 int[] offsetInWindow = new int[2];
683
684                 // Get the WebView Y position.
685                 int webViewYPosition = getScrollY();
686
687                 // Set the scroll delta Y to initially be the same as the pre-scroll delta Y.
688                 int scrollDeltaY = preScrollDeltaY;
689
690                 // Dispatch the nested pre-school.  This scrolls the app bar if it needs it.  `offsetInWindow` will be returned with an updated value.
691                 if (dispatchNestedPreScroll(0, preScrollDeltaY, consumedScroll, offsetInWindow)) {
692                     // Update the scroll delta Y if some of it was consumed.
693                     // 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.
694                     scrollDeltaY = preScrollDeltaY - consumedScroll[1];
695                 }
696
697                 // Check to see if the WebView is at the top and and the scroll action is downward.
698                 if ((webViewYPosition == 0) && (scrollDeltaY < 0)) {  // Swipe to refresh is being engaged.
699                     // 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.
700                     stopNestedScroll();
701                 } else {  // Swipe to refresh is not being engaged.
702                     // Start the nested scroll so that the app bar can scroll off the screen.
703                     startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
704
705                     // 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.
706                     dispatchNestedScroll(0, scrollDeltaY, 0, 0, offsetInWindow);
707
708                     // Store the current Y position for use in the next action move.
709                     previousYPosition = previousYPosition - scrollDeltaY;
710                 }
711
712                 // Run the default commands.
713                 motionEventHandled = super.onTouchEvent(motionEvent);
714                 break;
715
716
717             default:
718                 // Stop nested scrolling.
719                 stopNestedScroll();
720
721                 // Run the default commands.
722                 motionEventHandled = super.onTouchEvent(motionEvent);
723         }
724
725         // Perform a click.  This is required by the Android accessibility guidelines.
726         performClick();
727
728         // Return the status of the motion event.
729         return motionEventHandled;
730     }
731
732     // The Android accessibility guidelines require overriding `performClick()` and calling it from `onTouchEvent()`.
733     @Override
734     public boolean performClick() {
735         return super.performClick();
736     }
737
738
739     // Method from NestedScrollingChild.
740     @Override
741     public void setNestedScrollingEnabled(boolean status) {
742         // Set the status of the nested scrolling.
743         nestedScrollingChildHelper.setNestedScrollingEnabled(status);
744     }
745
746     // Method from NestedScrollingChild.
747     @Override
748     public boolean isNestedScrollingEnabled() {
749         // Return the status of nested scrolling.
750         return nestedScrollingChildHelper.isNestedScrollingEnabled();
751     }
752
753
754     // Method from NestedScrollingChild.
755     @Override
756     public boolean startNestedScroll(int axes) {
757         // Start a nested scroll along the indicated axes.
758         return nestedScrollingChildHelper.startNestedScroll(axes);
759     }
760
761     // Method from NestedScrollingChild2.
762     @Override
763     public boolean startNestedScroll(int axes, int type) {
764         // Start a nested scroll along the indicated axes for the given type of input which caused the scroll event.
765         return nestedScrollingChildHelper.startNestedScroll(axes, type);
766     }
767
768
769     // Method from NestedScrollingChild.
770     @Override
771     public void stopNestedScroll() {
772         // Stop the nested scroll.
773         nestedScrollingChildHelper.stopNestedScroll();
774     }
775
776     // Method from NestedScrollingChild2.
777     @Override
778     public void stopNestedScroll(int type) {
779         // Stop the nested scroll of the given type of input which caused the scroll event.
780         nestedScrollingChildHelper.stopNestedScroll(type);
781     }
782
783
784     // Method from NestedScrollingChild.
785     @Override
786     public boolean hasNestedScrollingParent() {
787         // Return the status of the nested scrolling parent.
788         return nestedScrollingChildHelper.hasNestedScrollingParent();
789     }
790
791     // Method from NestedScrollingChild2.
792     @Override
793     public boolean hasNestedScrollingParent(int type) {
794         // return the status of the nested scrolling parent for the given type of input which caused the scroll event.
795         return nestedScrollingChildHelper.hasNestedScrollingParent(type);
796     }
797
798
799     // Method from NestedScrollingChild.
800     @Override
801     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow) {
802         // Dispatch a nested pre-scroll with the specified deltas, which lets a parent to consume some of the scroll if desired.
803         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow);
804     }
805
806     // Method from NestedScrollingChild2.
807     @Override
808     public boolean dispatchNestedPreScroll(int deltaX, int deltaY, int[] consumed, int[] offsetInWindow, int type) {
809         // 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.
810         return nestedScrollingChildHelper.dispatchNestedPreScroll(deltaX, deltaY, consumed, offsetInWindow, type);
811     }
812
813
814     // Method from NestedScrollingChild.
815     @Override
816     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow) {
817         // Dispatch a nested scroll with the specified deltas.
818         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow);
819     }
820
821     // Method from NestedScrollingChild2.
822     @Override
823     public boolean dispatchNestedScroll(int deltaXConsumed, int deltaYConsumed, int deltaXUnconsumed, int deltaYUnconsumed, int[] offsetInWindow, int type) {
824         // Dispatch a nested scroll with the specified deltas for the given type of input which caused the scroll event.
825         return nestedScrollingChildHelper.dispatchNestedScroll(deltaXConsumed, deltaYConsumed, deltaXUnconsumed, deltaYUnconsumed, offsetInWindow, type);
826     }
827
828
829     // Method from NestedScrollingChild.
830     @Override
831     public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
832         // Dispatch a nested pre-fling with the specified velocity, which lets a parent consume the fling if desired.
833         return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
834     }
835
836     // Method from NestedScrollingChild.
837     @Override
838     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
839         // Dispatch a nested fling with the specified velocity.
840         return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
841     }
842 }