1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.geckoview;
7 
8 import org.mozilla.gecko.GeckoAppShell;
9 import org.mozilla.gecko.annotation.WrapForJNI;
10 import org.mozilla.gecko.mozglue.JNIObject;
11 import org.mozilla.gecko.util.GeckoBundle;
12 import org.mozilla.gecko.util.ThreadUtils;
13 
14 import android.app.UiModeManager;
15 import android.content.Context;
16 import android.content.res.Configuration;
17 import android.graphics.Rect;
18 import android.os.SystemClock;
19 import androidx.annotation.AnyThread;
20 import androidx.annotation.NonNull;
21 import androidx.annotation.UiThread;
22 import androidx.annotation.IntDef;
23 import android.util.Log;
24 import android.util.Pair;
25 import android.view.MotionEvent;
26 import android.view.InputDevice;
27 
28 import java.lang.annotation.Retention;
29 import java.lang.annotation.RetentionPolicy;
30 
31 import java.util.ArrayList;
32 
33 @UiThread
34 public class PanZoomController {
35     private static final String LOGTAG = "GeckoNPZC";
36     private static final int EVENT_SOURCE_SCROLL = 0;
37     private static final int EVENT_SOURCE_MOTION = 1;
38     private static final int EVENT_SOURCE_MOUSE = 2;
39     private static Boolean sTreatMouseAsTouch = null;
40 
41     private final GeckoSession mSession;
42     private final Rect mTempRect = new Rect();
43     private boolean mAttached;
44     private float mPointerScrollFactor = 64.0f;
45     private long mLastDownTime;
46 
47     @Retention(RetentionPolicy.SOURCE)
48     @IntDef({SCROLL_BEHAVIOR_SMOOTH, SCROLL_BEHAVIOR_AUTO})
49     /* package */ @interface ScrollBehaviorType {}
50 
51     /**
52      * Specifies smooth scrolling which animates content to the desired scroll position.
53      */
54     public static final int SCROLL_BEHAVIOR_SMOOTH = 0;
55     /**
56      * Specifies auto scrolling which jumps content to the desired scroll position.
57      */
58     public static final int SCROLL_BEHAVIOR_AUTO = 1;
59 
60     @Retention(RetentionPolicy.SOURCE)
61     @IntDef({INPUT_RESULT_UNHANDLED, INPUT_RESULT_HANDLED, INPUT_RESULT_HANDLED_CONTENT, INPUT_RESULT_IGNORED})
62     /* package */ @interface InputResult {}
63 
64     /**
65      * Specifies that an input event was not handled by the PanZoomController for a panning
66      * or zooming operation. The event may have been handled by Web content or
67      * internally (e.g. text selection).
68      */
69     @WrapForJNI
70     public static final int INPUT_RESULT_UNHANDLED = 0;
71 
72     /**
73      * Specifies that an input event was handled by the PanZoomController for a
74      * panning or zooming operation, but likely not by any touch event listeners in Web content.
75      */
76     @WrapForJNI
77     public static final int INPUT_RESULT_HANDLED = 1;
78 
79     /**
80      * Specifies that an input event was handled by the PanZoomController and passed on
81      * to touch event listeners in Web content.
82      */
83     @WrapForJNI
84     public static final int INPUT_RESULT_HANDLED_CONTENT = 2;
85 
86     /**
87      * Specifies that an input event was consumed by a PanZoomController internally and
88      * browsers should do nothing in response to the event.
89      */
90     @WrapForJNI
91     public static final int INPUT_RESULT_IGNORED = 3;
92 
93     @Retention(RetentionPolicy.SOURCE)
94     @IntDef(flag = true,
95             value = { SCROLLABLE_FLAG_NONE, SCROLLABLE_FLAG_TOP, SCROLLABLE_FLAG_RIGHT,
96                       SCROLLABLE_FLAG_BOTTOM, SCROLLABLE_FLAG_LEFT })
97     /* package */ @interface ScrollableDirections {}
98     /**
99      * Represents which directions can be scrolled in the scroll container where
100      * an input event was handled.
101      * This value is only useful in the case of {@link PanZoomController#INPUT_RESULT_HANDLED}.
102      */
103     /* The container cannot be scrolled. */
104     @WrapForJNI
105     public static final int SCROLLABLE_FLAG_NONE = 0;
106     /* The container cannot be scrolled to top */
107     @WrapForJNI
108     public static final int SCROLLABLE_FLAG_TOP = 1 << 0;
109     /* The container cannot be scrolled to right */
110     @WrapForJNI
111     public static final int SCROLLABLE_FLAG_RIGHT = 1 << 1;
112     /* The container cannot be scrolled to bottom */
113     @WrapForJNI
114     public static final int SCROLLABLE_FLAG_BOTTOM = 1 << 2;
115     /* The container cannot be scrolled to left */
116     @WrapForJNI
117     public static final int SCROLLABLE_FLAG_LEFT = 1 << 3;
118 
119     @Retention(RetentionPolicy.SOURCE)
120     @IntDef(flag = true,
121             value = { OVERSCROLL_FLAG_NONE, OVERSCROLL_FLAG_HORIZONTAL, OVERSCROLL_FLAG_VERTICAL })
122     /* package */ @interface OverscrollDirections {}
123     /**
124      * Represents which directions can be over-scrolled in the scroll container where
125      * an input event was handled.
126      * This value is only useful in the case of {@link PanZoomController#INPUT_RESULT_HANDLED}.
127      */
128     /* the container cannot be over-scrolled. */
129     @WrapForJNI
130     public static final int OVERSCROLL_FLAG_NONE = 0;
131     /* the container can be over-scrolled horizontally. */
132     @WrapForJNI
133     public static final int OVERSCROLL_FLAG_HORIZONTAL = 1 << 0;
134     /* the container can be over-scrolled vertically. */
135     @WrapForJNI
136     public static final int OVERSCROLL_FLAG_VERTICAL = 1 << 1;
137 
138     /**
139      * Represents how a {@link MotionEvent} was handled in Gecko.
140      * This value can be used by browser apps to implement features like pull-to-refresh. Failing
141      * to account this value might break some websites expectations about touch events.
142      *
143      * For example, a {@link PanZoomController.InputResultDetail#handledResult} value of
144      * {@link PanZoomController#INPUT_RESULT_HANDLED} and
145      * {@link PanZoomController.InputResultDetail#overscrollDirections} of
146      * {@link PanZoomController#OVERSCROLL_FLAG_NONE} indicates that the event was consumed for a panning
147      * or zooming operation and that the website does not expect the browser to react to the touch
148      * event (say, by triggering the pull-to-refresh feature) even though the scroll container reached
149      * to the edge.
150      */
151     @WrapForJNI
152     public static class InputResultDetail {
InputResultDetail(final @InputResult int handledResult, final @ScrollableDirections int scrollableDirections, final @OverscrollDirections int overscrollDirections)153         protected InputResultDetail(final @InputResult int handledResult,
154                                     final @ScrollableDirections int scrollableDirections,
155                                     final @OverscrollDirections int overscrollDirections) {
156             mHandledResult = handledResult;
157             mScrollableDirections = scrollableDirections;
158             mOverscrollDirections = overscrollDirections;
159         }
160 
161         /**
162          * @return One of the {@link #INPUT_RESULT_UNHANDLED INPUT_RESULT_*} indicating how
163          * the event was handled.
164          */
165         @AnyThread
handledResult()166         public @InputResult int handledResult() {
167             return mHandledResult;
168         }
169         /**
170          * @return an OR-ed value of {@link #SCROLLABLE_FLAG_NONE SCROLLABLE_FLAG_*} indicating which
171          * directions can be scrollable.
172          */
173         @AnyThread
scrollableDirections()174         public @ScrollableDirections int scrollableDirections() {
175             return mScrollableDirections;
176         }
177         /**
178          * @return an OR-ed value of {@link #OVERSCROLL_FLAG_NONE OVERSCROLL_FLAG_*} indicating which
179          * directions can be over-scrollable.
180          */
181         @AnyThread
overscrollDirections()182         public @OverscrollDirections int overscrollDirections() {
183             return mOverscrollDirections;
184         }
185 
186         private final @InputResult int mHandledResult;
187         private final @ScrollableDirections int mScrollableDirections;
188         private final @OverscrollDirections int mOverscrollDirections;
189     }
190 
191     private SynthesizedEventState mPointerState;
192 
193     private ArrayList<Pair<Integer, MotionEvent>> mQueuedEvents;
194 
195     private boolean mSynthesizedEvent = false;
196 
197     @WrapForJNI
198     private static class MotionEventData {
199         public final int action;
200         public final int actionIndex;
201         public final long time;
202         public final int metaState;
203         public final int pointerId[];
204         public final int historySize;
205         public final long historicalTime[];
206         public final float historicalX[];
207         public final float historicalY[];
208         public final float historicalOrientation[];
209         public final float historicalPressure[];
210         public final float historicalToolMajor[];
211         public final float historicalToolMinor[];
212         public final float x[];
213         public final float y[];
214         public final float orientation[];
215         public final float pressure[];
216         public final float toolMajor[];
217         public final float toolMinor[];
218 
MotionEventData(final MotionEvent event)219         public MotionEventData(final MotionEvent event) {
220             final int count = event.getPointerCount();
221             action = event.getActionMasked();
222             actionIndex = event.getActionIndex();
223             time = event.getEventTime();
224             metaState = event.getMetaState();
225             historySize = event.getHistorySize();
226             historicalTime = new long[historySize];
227             historicalX = new float[historySize * count];
228             historicalY = new float[historySize * count];
229             historicalOrientation = new float[historySize * count];
230             historicalPressure = new float[historySize * count];
231             historicalToolMajor = new float[historySize * count];
232             historicalToolMinor = new float[historySize * count];
233             pointerId = new int[count];
234             x = new float[count];
235             y = new float[count];
236             orientation = new float[count];
237             pressure = new float[count];
238             toolMajor = new float[count];
239             toolMinor = new float[count];
240 
241 
242             for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
243                 historicalTime[historyIndex] = event.getHistoricalEventTime(historyIndex);
244             }
245 
246             final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
247             for (int i = 0; i < count; i++) {
248                 pointerId[i] = event.getPointerId(i);
249 
250                 for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
251                     event.getHistoricalPointerCoords(i, historyIndex, coords);
252 
253                     final int historicalI = historyIndex * count + i;
254                     historicalX[historicalI] = coords.x;
255                     historicalY[historicalI] = coords.y;
256 
257                     historicalOrientation[historicalI] = coords.orientation;
258                     historicalPressure[historicalI] = coords.pressure;
259 
260                     // If we are converting to CSS pixels, we should adjust the radii as well.
261                     historicalToolMajor[historicalI] = coords.toolMajor;
262                     historicalToolMinor[historicalI] = coords.toolMinor;
263                 }
264 
265                 event.getPointerCoords(i, coords);
266 
267                 x[i] = coords.x;
268                 y[i] = coords.y;
269 
270                 orientation[i] = coords.orientation;
271                 pressure[i] = coords.pressure;
272 
273                 // If we are converting to CSS pixels, we should adjust the radii as well.
274                 toolMajor[i] = coords.toolMajor;
275                 toolMinor[i] = coords.toolMinor;
276             }
277         }
278     }
279 
280     /* package */ final class NativeProvider extends JNIObject {
281         @Override // JNIObject
disposeNative()282         protected void disposeNative() {
283             // Disposal happens in native code.
284             throw new UnsupportedOperationException();
285         }
286 
287         @WrapForJNI(calledFrom = "ui")
handleMotionEvent( MotionEventData eventData, float screenX, float screenY, GeckoResult<InputResultDetail> result)288         private native void handleMotionEvent(
289                MotionEventData eventData, float screenX, float screenY,
290                GeckoResult<InputResultDetail> result);
291 
292         @WrapForJNI(calledFrom = "ui")
handleScrollEvent( long time, int metaState, float x, float y, float hScroll, float vScroll)293         private native @InputResult int handleScrollEvent(
294                 long time, int metaState,
295                 float x, float y,
296                 float hScroll, float vScroll);
297 
298         @WrapForJNI(calledFrom = "ui")
handleMouseEvent( int action, long time, int metaState, float x, float y, int buttons)299         private native @InputResult int handleMouseEvent(
300                 int action, long time, int metaState,
301                 float x, float y, int buttons);
302 
303         @WrapForJNI(stubName = "SetIsLongpressEnabled") // Called from test thread.
nativeSetIsLongpressEnabled(boolean isLongpressEnabled)304         private native void nativeSetIsLongpressEnabled(boolean isLongpressEnabled);
305 
306         @WrapForJNI(calledFrom = "ui")
synthesizeNativeTouchPoint(final int pointerId, final int eventType, final int clientX, final int clientY, final double pressure, final int orientation)307         private void synthesizeNativeTouchPoint(final int pointerId, final int eventType,
308                                                 final int clientX, final int clientY,
309                                                 final double pressure, final int orientation) {
310             if (pointerId == PointerInfo.RESERVED_MOUSE_POINTER_ID) {
311                 throw new IllegalArgumentException("Pointer ID reserved for mouse");
312             }
313             synthesizeNativePointer(InputDevice.SOURCE_TOUCHSCREEN, pointerId,
314                     eventType, clientX, clientY, pressure, orientation, 0);
315         }
316 
317         @WrapForJNI(calledFrom = "ui")
synthesizeNativeMouseEvent(final int eventType, final int clientX, final int clientY, final int button)318         private void synthesizeNativeMouseEvent(final int eventType, final int clientX,
319                                                 final int clientY, final int button) {
320             synthesizeNativePointer(InputDevice.SOURCE_MOUSE,
321                     PointerInfo.RESERVED_MOUSE_POINTER_ID,
322                     eventType, clientX, clientY, 0, 0, button);
323         }
324 
325         @WrapForJNI(calledFrom = "ui")
setAttached(final boolean attached)326         private void setAttached(final boolean attached) {
327             if (attached) {
328                 mAttached = true;
329                 flushEventQueue();
330             } else if (mAttached) {
331                 mAttached = false;
332                 enableEventQueue();
333             }
334         }
335     }
336 
337     /* package */ final NativeProvider mNative = new NativeProvider();
338 
handleMotionEvent(final MotionEvent event)339     private void handleMotionEvent(final MotionEvent event) {
340         handleMotionEvent(event, null);
341     }
342 
handleMotionEvent(final MotionEvent event, final GeckoResult<InputResultDetail> result)343     private void handleMotionEvent(final MotionEvent event, final GeckoResult<InputResultDetail> result) {
344         if (!mAttached) {
345             mQueuedEvents.add(new Pair<>(EVENT_SOURCE_MOTION, event));
346             if (result != null) {
347                 result.complete(
348                     new InputResultDetail(INPUT_RESULT_HANDLED, SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE));
349             }
350             return;
351         }
352 
353         final int action = event.getActionMasked();
354 
355         if (action == MotionEvent.ACTION_DOWN) {
356             mLastDownTime = event.getDownTime();
357         } else if (mLastDownTime != event.getDownTime()) {
358             if (result != null) {
359                 result.complete(
360                     new InputResultDetail(INPUT_RESULT_UNHANDLED, SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE));
361             }
362             return;
363         }
364 
365         final float screenX = event.getRawX() - event.getX();
366         final float screenY = event.getRawY() - event.getY();
367 
368         // Take this opportunity to update screen origin of session. This gets
369         // dispatched to the gecko thread, so we also pass the new screen x/y directly to apz.
370         // If this is a synthesized touch, the screen offset is bogus so ignore it.
371         if (!mSynthesizedEvent) {
372             mSession.onScreenOriginChanged((int)screenX, (int)screenY);
373         }
374 
375         final MotionEventData data = new MotionEventData(event);
376         mNative.handleMotionEvent(data, screenX, screenY, result);
377     }
378 
handleScrollEvent(final MotionEvent event)379     private @InputResult int handleScrollEvent(final MotionEvent event) {
380         if (!mAttached) {
381             mQueuedEvents.add(new Pair<>(EVENT_SOURCE_SCROLL, event));
382             return INPUT_RESULT_HANDLED;
383         }
384 
385         final int count = event.getPointerCount();
386 
387         if (count <= 0) {
388             return INPUT_RESULT_UNHANDLED;
389         }
390 
391         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
392         event.getPointerCoords(0, coords);
393 
394         // Translate surface origin to client origin for scroll events.
395         mSession.getSurfaceBounds(mTempRect);
396         final float x = coords.x - mTempRect.left;
397         final float y = coords.y - mTempRect.top;
398 
399         final float hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL) *
400                               mPointerScrollFactor;
401         final float vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL) *
402                               mPointerScrollFactor;
403 
404         return mNative.handleScrollEvent(event.getEventTime(), event.getMetaState(), x, y,
405                                          hScroll, vScroll);
406     }
407 
handleMouseEvent(final MotionEvent event)408     private @InputResult int handleMouseEvent(final MotionEvent event) {
409         if (!mAttached) {
410             mQueuedEvents.add(new Pair<>(EVENT_SOURCE_MOUSE, event));
411             return INPUT_RESULT_UNHANDLED;
412         }
413 
414         final int count = event.getPointerCount();
415 
416         if (count <= 0) {
417             return INPUT_RESULT_UNHANDLED;
418         }
419 
420         final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
421         event.getPointerCoords(0, coords);
422 
423         // Translate surface origin to client origin for mouse events.
424         mSession.getSurfaceBounds(mTempRect);
425         final float x = coords.x - mTempRect.left;
426         final float y = coords.y - mTempRect.top;
427 
428         return mNative.handleMouseEvent(event.getActionMasked(), event.getEventTime(),
429                                         event.getMetaState(), x, y, event.getButtonState());
430     }
431 
PanZoomController(final GeckoSession session)432     protected PanZoomController(final GeckoSession session) {
433         mSession = session;
434         enableEventQueue();
435     }
436 
treatMouseAsTouch()437     private boolean treatMouseAsTouch() {
438         if (sTreatMouseAsTouch == null) {
439             final Context c = GeckoAppShell.getApplicationContext();
440             if (c == null) {
441                 // This might happen if the GeckoRuntime has not been initialized yet.
442                 return false;
443             }
444             final UiModeManager m = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE);
445             // on TV devices, treat mouse as touch. everywhere else, don't
446             sTreatMouseAsTouch = (m.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION);
447         }
448 
449         return sTreatMouseAsTouch;
450     }
451 
452     /**
453      * Set the current scroll factor. The scroll factor is the maximum scroll amount that
454      * one scroll event may generate, in device pixels.
455      *
456      * @param factor Scroll factor.
457      */
setScrollFactor(final float factor)458     public void setScrollFactor(final float factor) {
459         ThreadUtils.assertOnUiThread();
460         mPointerScrollFactor = factor;
461     }
462 
463     /**
464      * Get the current scroll factor.
465      *
466      * @return Scroll factor.
467      */
getScrollFactor()468     public float getScrollFactor() {
469         ThreadUtils.assertOnUiThread();
470         return mPointerScrollFactor;
471     }
472 
473     /**
474      * This is a workaround for touch pad on Android app by Chrome OS.
475      * Android app on Chrome OS fires weird motion event by two finger scroll.
476      * See https://crbug.com/704051
477      */
mayTouchpadScroll(final @NonNull MotionEvent event)478     private boolean mayTouchpadScroll(final @NonNull MotionEvent event) {
479         final int action = event.getActionMasked();
480         return event.getButtonState() == 0 &&
481                (action == MotionEvent.ACTION_DOWN ||
482                 (mLastDownTime == event.getDownTime() &&
483                  (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_UP)));
484     }
485 
486     /**
487      * Process a touch event through the pan-zoom controller. Treat any mouse events as
488      * "touch" rather than as "mouse". Pointer coordinates should be relative to the
489      * display surface.
490      *
491      * @param event MotionEvent to process.
492      */
onTouchEvent(final @NonNull MotionEvent event)493     public void onTouchEvent(final @NonNull MotionEvent event) {
494         ThreadUtils.assertOnUiThread();
495 
496         if (!treatMouseAsTouch() && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE && !mayTouchpadScroll(event)) {
497             handleMouseEvent(event);
498             return;
499         }
500         handleMotionEvent(event);
501     }
502 
503     /**
504      * Process a touch event through the pan-zoom controller. Treat any mouse events as
505      * "touch" rather than as "mouse". Pointer coordinates should be relative to the
506      * display surface.
507      *
508      * NOTE: It is highly recommended to only call this with ACTION_DOWN or in otherwise
509      * limited capacity. Returning a GeckoResult for every touch event will generate
510      * a lot of allocations and unnecessary GC pressure. Instead, prefer to call
511      * {@link #onTouchEvent(MotionEvent)}.
512      *
513      * @param event MotionEvent to process.
514      * @return A GeckoResult resolving to {@link PanZoomController.InputResultDetail}).
515      */
onTouchEventForDetailResult(final @NonNull MotionEvent event)516     public @NonNull GeckoResult<InputResultDetail> onTouchEventForDetailResult(final @NonNull MotionEvent event) {
517         ThreadUtils.assertOnUiThread();
518 
519         if (!treatMouseAsTouch() && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE && !mayTouchpadScroll(event)) {
520             return GeckoResult.fromValue(
521                 new InputResultDetail(handleMouseEvent(event), SCROLLABLE_FLAG_NONE, OVERSCROLL_FLAG_NONE));
522         }
523 
524         final GeckoResult<InputResultDetail> result = new GeckoResult<>();
525         handleMotionEvent(event, result);
526         return result;
527     }
528 
529     /**
530      * Process a touch event through the pan-zoom controller. Treat any mouse events as
531      * "mouse" rather than as "touch". Pointer coordinates should be relative to the
532      * display surface.
533      *
534      * @param event MotionEvent to process.
535      */
onMouseEvent(final @NonNull MotionEvent event)536     public void onMouseEvent(final @NonNull MotionEvent event) {
537         ThreadUtils.assertOnUiThread();
538 
539         if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) {
540             return;
541         }
542         handleMotionEvent(event);
543     }
544 
545     @Override
finalize()546     protected void finalize() throws Throwable {
547         mNative.setAttached(false);
548     }
549 
550     /**
551      * Process a non-touch motion event through the pan-zoom controller. Currently, hover
552      * and scroll events are supported. Pointer coordinates should be relative to the
553      * display surface.
554      *
555      * @param event MotionEvent to process.
556      */
onMotionEvent(final @NonNull MotionEvent event)557     public void onMotionEvent(final @NonNull MotionEvent event) {
558         ThreadUtils.assertOnUiThread();
559 
560         final int action = event.getActionMasked();
561         if (action == MotionEvent.ACTION_SCROLL) {
562             if (event.getDownTime() >= mLastDownTime) {
563                 mLastDownTime = event.getDownTime();
564             } else if ((InputDevice.getDevice(event.getDeviceId()) != null) &&
565                        (InputDevice.getDevice(event.getDeviceId()).getSources() &
566                         InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
567                 return;
568             }
569             handleScrollEvent(event);
570         } else if ((action == MotionEvent.ACTION_HOVER_MOVE) ||
571                    (action == MotionEvent.ACTION_HOVER_ENTER) ||
572                    (action == MotionEvent.ACTION_HOVER_EXIT)) {
573             handleMouseEvent(event);
574         }
575     }
576 
enableEventQueue()577     private void enableEventQueue() {
578         if (mQueuedEvents != null) {
579             throw new IllegalStateException("Already have an event queue");
580         }
581         mQueuedEvents = new ArrayList<>();
582     }
583 
flushEventQueue()584     private void flushEventQueue() {
585         if (mQueuedEvents == null) {
586             return;
587         }
588 
589         final ArrayList<Pair<Integer, MotionEvent>> events = mQueuedEvents;
590         mQueuedEvents = null;
591         for (final Pair<Integer, MotionEvent> pair : events) {
592             switch (pair.first) {
593                 case EVENT_SOURCE_MOTION:
594                     handleMotionEvent(pair.second);
595                     break;
596                 case EVENT_SOURCE_SCROLL:
597                     handleScrollEvent(pair.second);
598                     break;
599                 case EVENT_SOURCE_MOUSE:
600                     handleMouseEvent(pair.second);
601                     break;
602             }
603         }
604     }
605 
606     /**
607      * Set whether Gecko should generate long-press events.
608      *
609      * @param isLongpressEnabled True if Gecko should generate long-press events.
610      */
setIsLongpressEnabled(final boolean isLongpressEnabled)611     public void setIsLongpressEnabled(final boolean isLongpressEnabled) {
612         ThreadUtils.assertOnUiThread();
613 
614         if (mAttached) {
615             mNative.nativeSetIsLongpressEnabled(isLongpressEnabled);
616         }
617     }
618 
619     private static class PointerInfo {
620         // We reserve one pointer ID for the mouse, so that tests don't have
621         // to worry about tracking pointer IDs if they just want to test mouse
622         // event synthesization. If somebody tries to use this ID for a
623         // synthesized touch event we'll throw an exception.
624         public static final int RESERVED_MOUSE_POINTER_ID = 100000;
625 
626         public int pointerId;
627         public int source;
628         public int surfaceX;
629         public int surfaceY;
630         public double pressure;
631         public int orientation;
632         public int buttonState;
633 
getCoords()634         public MotionEvent.PointerCoords getCoords() {
635             final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
636             coords.orientation = orientation;
637             coords.pressure = (float)pressure;
638             coords.x = surfaceX;
639             coords.y = surfaceY;
640             return coords;
641         }
642     }
643 
644     private static class SynthesizedEventState {
645         public final ArrayList<PointerInfo> pointers;
646         public long downTime;
647 
SynthesizedEventState()648         SynthesizedEventState() {
649             pointers = new ArrayList<PointerInfo>();
650         }
651 
getPointerIndex(final int pointerId)652         int getPointerIndex(final int pointerId) {
653             for (int i = 0; i < pointers.size(); i++) {
654                 if (pointers.get(i).pointerId == pointerId) {
655                     return i;
656                 }
657             }
658             return -1;
659         }
660 
addPointer(final int pointerId, final int source)661         int addPointer(final int pointerId, final int source) {
662             final PointerInfo info = new PointerInfo();
663             info.pointerId = pointerId;
664             info.source = source;
665             pointers.add(info);
666             return pointers.size() - 1;
667         }
668 
getPointerCount(final int source)669         int getPointerCount(final int source) {
670             int count = 0;
671             for (int i = 0; i < pointers.size(); i++) {
672                 if (pointers.get(i).source == source) {
673                     count++;
674                 }
675             }
676             return count;
677         }
678 
getPointerButtonState(final int source)679         int getPointerButtonState(final int source) {
680             for (int i = 0; i < pointers.size(); i++) {
681                 if (pointers.get(i).source == source) {
682                     return pointers.get(i).buttonState;
683                 }
684             }
685             return 0;
686         }
687 
getPointerProperties(final int source)688         MotionEvent.PointerProperties[] getPointerProperties(final int source) {
689             final MotionEvent.PointerProperties[] props =
690                     new MotionEvent.PointerProperties[getPointerCount(source)];
691             int index = 0;
692             for (int i = 0; i < pointers.size(); i++) {
693                 if (pointers.get(i).source == source) {
694                     final MotionEvent.PointerProperties p = new MotionEvent.PointerProperties();
695                     p.id = pointers.get(i).pointerId;
696                     switch (source) {
697                         case InputDevice.SOURCE_TOUCHSCREEN:
698                             p.toolType = MotionEvent.TOOL_TYPE_FINGER;
699                             break;
700                         case InputDevice.SOURCE_MOUSE:
701                             p.toolType = MotionEvent.TOOL_TYPE_MOUSE;
702                             break;
703                     }
704                     props[index++] = p;
705                 }
706             }
707             return props;
708         }
709 
getPointerCoords(final int source)710         MotionEvent.PointerCoords[] getPointerCoords(final int source) {
711             final MotionEvent.PointerCoords[] coords =
712                     new MotionEvent.PointerCoords[getPointerCount(source)];
713             int index = 0;
714             for (int i = 0; i < pointers.size(); i++) {
715                 if (pointers.get(i).source == source) {
716                     coords[index++] = pointers.get(i).getCoords();
717                 }
718             }
719             return coords;
720         }
721     }
722 
synthesizeNativePointer(final int source, final int pointerId, final int originalEventType, final int clientX, final int clientY, final double pressure, final int orientation, final int button)723     private void synthesizeNativePointer(final int source, final int pointerId,
724                                          final int originalEventType,
725                                          final int clientX, final int clientY,
726                                          final double pressure, final int orientation,
727                                          final int button) {
728         if (mPointerState == null) {
729             mPointerState = new SynthesizedEventState();
730         }
731 
732         // Find the pointer if it already exists
733         int pointerIndex = mPointerState.getPointerIndex(pointerId);
734 
735         // Event-specific handling
736         int eventType = originalEventType;
737         switch (originalEventType) {
738             case MotionEvent.ACTION_POINTER_UP:
739                 if (pointerIndex < 0) {
740                     Log.w(LOGTAG, "Pointer-up for invalid pointer");
741                     return;
742                 }
743                 if (mPointerState.pointers.size() == 1) {
744                     // Last pointer is going up
745                     eventType = MotionEvent.ACTION_UP;
746                 }
747                 break;
748             case MotionEvent.ACTION_CANCEL:
749                 if (pointerIndex < 0) {
750                     Log.w(LOGTAG, "Pointer-cancel for invalid pointer");
751                     return;
752                 }
753                 break;
754             case MotionEvent.ACTION_POINTER_DOWN:
755                 if (pointerIndex < 0) {
756                     // Adding a new pointer
757                     pointerIndex = mPointerState.addPointer(pointerId, source);
758                     if (pointerIndex == 0) {
759                         // first pointer
760                         eventType = MotionEvent.ACTION_DOWN;
761                         mPointerState.downTime = SystemClock.uptimeMillis();
762                     }
763                 } else {
764                     // We're moving an existing pointer
765                     eventType = MotionEvent.ACTION_MOVE;
766                 }
767                 break;
768             case MotionEvent.ACTION_HOVER_MOVE:
769                 if (pointerIndex < 0) {
770                     // Mouse-move a pointer without it going "down". However
771                     // in order to send the right MotionEvent without a lot of
772                     // duplicated code, we add the pointer to mPointerState,
773                     // and then remove it at the bottom of this function.
774                     pointerIndex = mPointerState.addPointer(pointerId, source);
775                 } else {
776                     // We're moving an existing mouse pointer that went down.
777                     eventType = MotionEvent.ACTION_MOVE;
778                 }
779                 break;
780         }
781 
782         // Translate client origin to surface origin.
783         mSession.getSurfaceBounds(mTempRect);
784         final int surfaceX = clientX + mTempRect.left;
785         final int surfaceY = clientY + mTempRect.top;
786 
787         // Update the pointer with the new info
788         final PointerInfo info = mPointerState.pointers.get(pointerIndex);
789         info.surfaceX = surfaceX;
790         info.surfaceY = surfaceY;
791         info.pressure = pressure;
792         info.orientation = orientation;
793         if (source == InputDevice.SOURCE_MOUSE) {
794             if (eventType == MotionEvent.ACTION_DOWN ||
795                 eventType == MotionEvent.ACTION_MOVE) {
796                 info.buttonState |= button;
797             } else if (eventType == MotionEvent.ACTION_UP) {
798                 info.buttonState &= button;
799             }
800         }
801 
802         // Dispatch the event
803         int action = 0;
804         if (eventType == MotionEvent.ACTION_POINTER_DOWN ||
805             eventType == MotionEvent.ACTION_POINTER_UP) {
806             // for pointer-down and pointer-up events we need to add the
807             // index of the relevant pointer.
808             action = (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
809             action &= MotionEvent.ACTION_POINTER_INDEX_MASK;
810         }
811         action |= (eventType & MotionEvent.ACTION_MASK);
812         final MotionEvent event = MotionEvent.obtain(
813             /*downTime*/ mPointerState.downTime,
814             /*eventTime*/ SystemClock.uptimeMillis(),
815             /*action*/ action,
816             /*pointerCount*/ mPointerState.getPointerCount(source),
817             /*pointerProperties*/ mPointerState.getPointerProperties(source),
818             /*pointerCoords*/ mPointerState.getPointerCoords(source),
819             /*metaState*/ 0,
820             /*buttonState*/ mPointerState.getPointerButtonState(source),
821             /*xPrecision*/ 0,
822             /*yPrecision*/ 0,
823             /*deviceId*/ 0,
824             /*edgeFlags*/ 0,
825             /*source*/ source,
826             /*flags*/ 0);
827 
828         mSynthesizedEvent = true;
829         onTouchEvent(event);
830         mSynthesizedEvent = false;
831 
832         // Forget about removed pointers
833         if (eventType == MotionEvent.ACTION_POINTER_UP ||
834             eventType == MotionEvent.ACTION_UP ||
835             eventType == MotionEvent.ACTION_CANCEL ||
836             eventType == MotionEvent.ACTION_HOVER_MOVE) {
837             mPointerState.pointers.remove(pointerIndex);
838         }
839     }
840 
841     /**
842      * Scroll the document body by an offset from the current scroll position.
843      * Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
844      *
845      * @param width {@link ScreenLength} offset to scroll along X axis.
846      * @param height {@link ScreenLength} offset to scroll along Y axis.
847      */
848     @UiThread
scrollBy(final @NonNull ScreenLength width, final @NonNull ScreenLength height)849     public void scrollBy(final @NonNull ScreenLength width, final @NonNull ScreenLength height) {
850         scrollBy(width, height, SCROLL_BEHAVIOR_SMOOTH);
851     }
852 
853     /**
854      * Scroll the document body by an offset from the current scroll position.
855      *
856      * @param width {@link ScreenLength} offset to scroll along X axis.
857      * @param height {@link ScreenLength} offset to scroll along Y axis.
858      * @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link #SCROLL_BEHAVIOR_AUTO},
859      *                 that specifies how to scroll the content.
860      */
861     @UiThread
scrollBy(final @NonNull ScreenLength width, final @NonNull ScreenLength height, final @ScrollBehaviorType int behavior)862     public void scrollBy(final @NonNull ScreenLength width, final @NonNull ScreenLength height,
863                          final @ScrollBehaviorType int behavior) {
864         final GeckoBundle msg = buildScrollMessage(width, height, behavior);
865         mSession.getEventDispatcher().dispatch("GeckoView:ScrollBy", msg);
866     }
867 
868     /**
869      * Scroll the document body to an absolute  position.
870      * Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
871      *
872      * @param width {@link ScreenLength} position to scroll along X axis.
873      * @param height {@link ScreenLength} position to scroll along Y axis.
874      */
875     @UiThread
scrollTo(final @NonNull ScreenLength width, final @NonNull ScreenLength height)876     public void scrollTo(final @NonNull ScreenLength width, final @NonNull ScreenLength height) {
877         scrollTo(width, height, SCROLL_BEHAVIOR_SMOOTH);
878     }
879 
880     /**
881      * Scroll the document body to an absolute position.
882      *
883      * @param width {@link ScreenLength} position to scroll along X axis.
884      * @param height {@link ScreenLength} position to scroll along Y axis.
885      * @param behavior ScrollBehaviorType One of {@link #SCROLL_BEHAVIOR_SMOOTH}, {@link #SCROLL_BEHAVIOR_AUTO},
886      *                 that specifies how to scroll the content.
887      */
888     @UiThread
scrollTo(final @NonNull ScreenLength width, final @NonNull ScreenLength height, final @ScrollBehaviorType int behavior)889     public void scrollTo(final @NonNull ScreenLength width, final @NonNull ScreenLength height,
890                          final @ScrollBehaviorType int behavior) {
891         final GeckoBundle msg = buildScrollMessage(width, height, behavior);
892         mSession.getEventDispatcher().dispatch("GeckoView:ScrollTo", msg);
893     }
894 
895     /**
896      * Scroll to the top left corner of the screen.
897      * Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
898      */
899     @UiThread
scrollToTop()900     public void scrollToTop() {
901         scrollTo(ScreenLength.zero(), ScreenLength.top(), SCROLL_BEHAVIOR_SMOOTH);
902     }
903 
904     /**
905      * Scroll to the bottom left corner of the screen.
906      * Uses {@link #SCROLL_BEHAVIOR_SMOOTH}.
907      */
908     @UiThread
scrollToBottom()909     public void scrollToBottom() {
910         scrollTo(ScreenLength.zero(), ScreenLength.bottom(), SCROLL_BEHAVIOR_SMOOTH);
911     }
912 
buildScrollMessage(final @NonNull ScreenLength width, final @NonNull ScreenLength height, final @ScrollBehaviorType int behavior)913     private GeckoBundle buildScrollMessage(final @NonNull ScreenLength width,
914                                            final @NonNull ScreenLength height,
915                                            final @ScrollBehaviorType int behavior) {
916         final GeckoBundle msg = new GeckoBundle();
917         msg.putDouble("widthValue", width.getValue());
918         msg.putInt("widthType", width.getType());
919         msg.putDouble("heightValue", height.getValue());
920         msg.putInt("heightType", height.getType());
921         msg.putInt("behavior", behavior);
922         return msg;
923     }
924 }
925