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