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