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