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