diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java index de60256fccd40..f3a75a991b309 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java @@ -1,9 +1,13 @@ package io.flutter.embedding.android; +import android.annotation.TargetApi; +import android.content.Context; import android.graphics.Matrix; import android.os.Build; +import android.util.TypedValue; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.ViewConfiguration; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -80,6 +84,10 @@ public class AndroidTouchProcessor { private static final int POINTER_DATA_FIELD_COUNT = 35; @VisibleForTesting static final int BYTES_PER_FIELD = 8; + // Default if context is null, chosen to ensure reasonable speed scrolling. + @VisibleForTesting static final int DEFAULT_VERTICAL_SCROLL_FACTOR = 48; + @VisibleForTesting static final int DEFAULT_HORIZONTAL_SCROLL_FACTOR = 48; + // This value must match the value in framework's platform_view.dart. // This flag indicates whether the original Android pointer events were batched together. private static final int POINTER_DATA_FLAG_BATCHED = 1; @@ -93,6 +101,9 @@ public class AndroidTouchProcessor { private final Map ongoingPans = new HashMap<>(); + // Only used on api 25 and below to avoid requerying display metrics. + private int cachedVerticalScrollFactor; + /** * Constructs an {@code AndroidTouchProcessor} that will send touch event data to the Flutter * execution context represented by the given {@link FlutterRenderer}. @@ -181,9 +192,10 @@ public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transfor * wheel movements, etc. * * @param event The generic motion event being processed. + * @param context For use by ViewConfiguration.get(context) to scale input. * @return True if the event was handled. */ - public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + public boolean onGenericMotionEvent(@NonNull MotionEvent event, @NonNull Context context) { // Method isFromSource is only available in API 18+ (Jelly Bean MR2) // Mouse hover support is not implemented for API < 18. boolean isPointerEvent = @@ -192,7 +204,9 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) { boolean isMovementEvent = (event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE || event.getActionMasked() == MotionEvent.ACTION_SCROLL); - if (!isPointerEvent || !isMovementEvent) { + if (isPointerEvent && isMovementEvent) { + // Continue. + } else { return false; } @@ -203,7 +217,8 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) { packet.order(ByteOrder.LITTLE_ENDIAN); // ACTION_HOVER_MOVE always applies to a single pointer only. - addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet); + addPointerForIndex( + event, event.getActionIndex(), pointerChange, 0, IDENTITY_TRANSFORM, packet, context); if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) { throw new AssertionError("Packet position is not on field boundary."); } @@ -211,8 +226,9 @@ public boolean onGenericMotionEvent(@NonNull MotionEvent event) { return true; } - // TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that - // mutates inputs. + /// Calls addPointerForIndex with null for context. + /// + /// Without context the scroll wheel will not mimick android's scroll speed. private void addPointerForIndex( MotionEvent event, int pointerIndex, @@ -220,9 +236,24 @@ private void addPointerForIndex( int pointerData, Matrix transformMatrix, ByteBuffer packet) { + addPointerForIndex( + event, pointerIndex, pointerChange, pointerData, transformMatrix, packet, null); + } + + // TODO: consider creating a PointerPacket class instead of using a procedure that + // mutates inputs. https://github.com/flutter/flutter/issues/132853 + private void addPointerForIndex( + MotionEvent event, + int pointerIndex, + int pointerChange, + int pointerData, + Matrix transformMatrix, + ByteBuffer packet, + Context context) { if (pointerChange == -1) { return; } + final int pointerId = event.getPointerId(pointerIndex); int pointerKind = getPointerDeviceTypeForToolType(event.getToolType(pointerIndex)); // We use this in lieu of using event.getRawX and event.getRawY as we wish to support @@ -238,16 +269,21 @@ private void addPointerForIndex( // Some implementations translate trackpad scrolling into a mouse down-move-up event // sequence with buttons: 0, such as ARC on a Chromebook. See #11420, a legacy // implementation that uses the same condition but converts differently. - ongoingPans.put(event.getPointerId(pointerIndex), viewToScreenCoords); + ongoingPans.put(pointerId, viewToScreenCoords); } } else if (pointerKind == PointerDeviceKind.STYLUS) { + // Returns converted android button state into flutter framework normalized state + // and updates ongoingPans for chromebook trackpad scrolling. + // See + // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/events.dart + // for target button constants. buttons = (event.getButtonState() >> 4) & 0xF; } else { buttons = 0; } int panZoomType = -1; - boolean isTrackpadPan = ongoingPans.containsKey(event.getPointerId(pointerIndex)); + boolean isTrackpadPan = ongoingPans.containsKey(pointerId); if (isTrackpadPan) { panZoomType = getPointerChangeForPanZoom(pointerChange); if (panZoomType == -1) { @@ -278,13 +314,13 @@ private void addPointerForIndex( packet.putLong(pointerKind); // kind } packet.putLong(signalKind); // signal_kind - packet.putLong(event.getPointerId(pointerIndex)); // device + packet.putLong(pointerId); // device packet.putLong(0); // pointer_identifier, will be generated in pointer_data_packet_converter.cc. if (isTrackpadPan) { - float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex)); - packet.putDouble(panStart[0]); - packet.putDouble(panStart[1]); + float[] panStart = ongoingPans.get(pointerId); + packet.putDouble(panStart[0]); // physical_x + packet.putDouble(panStart[1]); // physical_y } else { packet.putDouble(viewToScreenCoords[0]); // physical_x packet.putDouble(viewToScreenCoords[1]); // physical_y @@ -341,16 +377,30 @@ private void addPointerForIndex( packet.putLong(pointerData); // platformData + // See android scrollview for inspiration. + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/ScrollView.java?q=function:onGenericMotionEvent%20filepath:widget%2FScrollView.java&ss=android%2Fplatform%2Fsuperproject%2Fmain if (signalKind == PointerSignalKind.SCROLL) { - packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_HSCROLL)); // scroll_delta_x - packet.putDouble(-event.getAxisValue(MotionEvent.AXIS_VSCROLL)); // scroll_delta_y + double horizontalScaleFactor = DEFAULT_HORIZONTAL_SCROLL_FACTOR; + double verticalScaleFactor = DEFAULT_VERTICAL_SCROLL_FACTOR; + if (context != null) { + horizontalScaleFactor = getHorizontalScrollFactor(context); + verticalScaleFactor = getVerticalScrollFactor(context); + } + // We flip the sign of the scroll value below because it aligns the pixel value with the + // scroll direction in native android. + final double horizontalScrollPixels = + horizontalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerIndex); + final double verticalScrollPixels = + verticalScaleFactor * -event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerIndex); + packet.putDouble(horizontalScrollPixels); // scroll_delta_x + packet.putDouble(verticalScrollPixels); // scroll_delta_y } else { packet.putDouble(0.0); // scroll_delta_x - packet.putDouble(0.0); // scroll_delta_x + packet.putDouble(0.0); // scroll_delta_y } if (isTrackpadPan) { - float[] panStart = ongoingPans.get(event.getPointerId(pointerIndex)); + float[] panStart = ongoingPans.get(pointerId); packet.putDouble(viewToScreenCoords[0] - panStart[0]); packet.putDouble(viewToScreenCoords[1] - panStart[1]); } else { @@ -363,8 +413,46 @@ private void addPointerForIndex( packet.putDouble(0.0); // rotation if (isTrackpadPan && (panZoomType == PointerChange.PAN_ZOOM_END)) { - ongoingPans.remove(event.getPointerId(pointerIndex)); + ongoingPans.remove(pointerId); + } + } + + private float getHorizontalScrollFactor(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return ViewConfiguration.get(context).getScaledHorizontalScrollFactor(); + } else { + // Vertical scroll factor is not a typo. This is what View.java does in android. + return getVerticalScrollFactorPre26(context); + } + } + + private float getVerticalScrollFactor(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return getVerticalScrollFactorAbove26(context); + } else { + return getVerticalScrollFactorPre26(context); + } + } + + @TargetApi(26) + private float getVerticalScrollFactorAbove26(@NonNull Context context) { + return ViewConfiguration.get(context).getScaledVerticalScrollFactor(); + } + + // See + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/view/View.java?q=function:getVerticalScrollFactor%20filepath:android%2Fview%2FView.java&ss=android%2Fplatform%2Fsuperproject%2Fmain + private int getVerticalScrollFactorPre26(@NonNull Context context) { + if (cachedVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + if (!context + .getTheme() + .resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true)) { + return DEFAULT_VERTICAL_SCROLL_FACTOR; + } + cachedVerticalScrollFactor = + (int) outValue.getDimension(context.getResources().getDisplayMetrics()); } + return cachedVerticalScrollFactor; } @PointerChange diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index e248f324a872e..5e4f1d44f4d21 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -952,7 +952,8 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { @Override public boolean onGenericMotionEvent(@NonNull MotionEvent event) { boolean handled = - isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event); + isAttachedToFlutterEngine() + && androidTouchProcessor.onGenericMotionEvent(event, getContext()); return handled ? true : super.onGenericMotionEvent(event); } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 7d15aab7ad89d..46d91eefe6420 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -482,7 +482,8 @@ public boolean onHoverEvent(MotionEvent event) { */ @Override public boolean onGenericMotionEvent(MotionEvent event) { - boolean handled = isAttached() && androidTouchProcessor.onGenericMotionEvent(event); + boolean handled = + isAttached() && androidTouchProcessor.onGenericMotionEvent(event, getContext()); return handled ? true : super.onGenericMotionEvent(event); } diff --git a/shell/platform/android/test/io/flutter/embedding/android/AndroidTouchProcessorTest.java b/shell/platform/android/test/io/flutter/embedding/android/AndroidTouchProcessorTest.java index 09e53be9b0a07..68e1e7f167ff6 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/AndroidTouchProcessorTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/AndroidTouchProcessorTest.java @@ -8,11 +8,16 @@ import static org.mockito.Mockito.when; import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; import android.view.InputDevice; import android.view.MotionEvent; +import android.view.ViewConfiguration; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.renderer.FlutterRenderer; import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,6 +36,12 @@ public class AndroidTouchProcessorTest { AndroidTouchProcessor touchProcessor; @Captor ArgumentCaptor packetCaptor; @Captor ArgumentCaptor packetSizeCaptor; + // Used for mock events in SystemClock.uptimeMillis() time base. + // 2 days in milliseconds + final long eventTimeMilliseconds = 172800000; + final float pressure = 0.8f; + // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/gestures/events.dart + final int enginePrimaryStylusButton = 0x02; @Before public void setUp() { @@ -38,6 +49,10 @@ public void setUp() { touchProcessor = new AndroidTouchProcessor(mockRenderer, false); } + private long readTimeStamp(ByteBuffer buffer) { + return buffer.getLong(1 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + private long readPointerChange(ByteBuffer buffer) { return buffer.getLong(2 * AndroidTouchProcessor.BYTES_PER_FIELD); } @@ -50,6 +65,10 @@ private long readPointerSignalKind(ByteBuffer buffer) { return buffer.getLong(4 * AndroidTouchProcessor.BYTES_PER_FIELD); } + private long readDevice(ByteBuffer buffer) { + return buffer.getLong(5 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + private double readPointerPhysicalX(ByteBuffer buffer) { return buffer.getDouble(7 * AndroidTouchProcessor.BYTES_PER_FIELD); } @@ -58,6 +77,70 @@ private double readPointerPhysicalY(ByteBuffer buffer) { return buffer.getDouble(8 * AndroidTouchProcessor.BYTES_PER_FIELD); } + private long readButtons(ByteBuffer buffer) { + return buffer.getLong(11 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readObscured(ByteBuffer buffer) { + return buffer.getDouble(12 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readSynthesized(ByteBuffer buffer) { + return buffer.getDouble(13 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readPressure(ByteBuffer buffer) { + return buffer.getDouble(14 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readPressureMin(ByteBuffer buffer) { + return buffer.getDouble(15 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readPressureMax(ByteBuffer buffer) { + return buffer.getDouble(16 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readDistance(ByteBuffer buffer) { + return buffer.getDouble(17 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readDistanceMax(ByteBuffer buffer) { + return buffer.getDouble(18 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readSize(ByteBuffer buffer) { + return buffer.getDouble(19 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readRadiusMajor(ByteBuffer buffer) { + return buffer.getDouble(20 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readRadiusMinor(ByteBuffer buffer) { + return buffer.getDouble(21 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readRadiusMin(ByteBuffer buffer) { + return buffer.getDouble(22 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readRadiusMax(ByteBuffer buffer) { + return buffer.getDouble(23 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readStylusTilt(ByteBuffer buffer) { + return buffer.getDouble(25 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readScrollDeltaX(ByteBuffer buffer) { + return buffer.getDouble(27 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readScrollDeltaY(ByteBuffer buffer) { + return buffer.getDouble(28 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + private double readPointerPanX(ByteBuffer buffer) { return buffer.getDouble(29 * AndroidTouchProcessor.BYTES_PER_FIELD); } @@ -66,6 +149,22 @@ private double readPointerPanY(ByteBuffer buffer) { return buffer.getDouble(30 * AndroidTouchProcessor.BYTES_PER_FIELD); } + private double readPointerPanDeltaX(ByteBuffer buffer) { + return buffer.getDouble(31 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readPointerPanDeltaY(ByteBuffer buffer) { + return buffer.getDouble(32 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readScale(ByteBuffer buffer) { + return buffer.getDouble(33 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + + private double readRotation(ByteBuffer buffer) { + return buffer.getDouble(34 * AndroidTouchProcessor.BYTES_PER_FIELD); + } + private class MotionEventMocker { int pointerId; int source; @@ -78,18 +177,43 @@ private class MotionEventMocker { } MotionEvent mockEvent(int action, float x, float y, int buttonState) { + return mockEvent(action, x, y, buttonState, x, y, x, y, x, x, y); + } + + MotionEvent mockEvent( + int action, + float x, + float y, + int buttonState, + float hScroll, + float vScroll, + float axisDistance, + float axisTilt, + float size, + float toolMajor, + float toolMinor) { MotionEvent event = mock(MotionEvent.class); when(event.getDevice()).thenReturn(null); when(event.getSource()).thenReturn(source); + when(event.getEventTime()).thenReturn(eventTimeMilliseconds); when(event.getPointerCount()).thenReturn(1); when(event.getActionMasked()).thenReturn(action); - when(event.getActionIndex()).thenReturn(0); + final int actionIndex = 0; + when(event.getActionIndex()).thenReturn(actionIndex); when(event.getButtonState()).thenReturn(buttonState); - when(event.getPointerId(0)).thenReturn(pointerId); - when(event.getX(0)).thenReturn(x); - when(event.getY(0)).thenReturn(y); - when(event.getToolType(0)).thenReturn(toolType); + when(event.getPointerId(actionIndex)).thenReturn(pointerId); + when(event.getX(actionIndex)).thenReturn(x); + when(event.getY(actionIndex)).thenReturn(y); + when(event.getToolType(actionIndex)).thenReturn(toolType); when(event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)).thenReturn(true); + when(event.getAxisValue(MotionEvent.AXIS_HSCROLL, pointerId)).thenReturn(hScroll); + when(event.getAxisValue(MotionEvent.AXIS_VSCROLL, pointerId)).thenReturn(vScroll); + when(event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerId)).thenReturn(axisDistance); + when(event.getAxisValue(MotionEvent.AXIS_TILT, pointerId)).thenReturn(axisTilt); + when(event.getPressure(actionIndex)).thenReturn(pressure); + when(event.getSize(actionIndex)).thenReturn(size); + when(event.getToolMajor(actionIndex)).thenReturn(toolMajor); + when(event.getToolMinor(actionIndex)).thenReturn(toolMinor); return event; } } @@ -159,6 +283,12 @@ public void trackpadGesture() { assertEquals(0.0, readPointerPhysicalY(packet)); assertEquals(10.0, readPointerPanX(packet)); assertEquals(5.0, readPointerPanY(packet)); + // Always zero. + assertEquals(0.0, readPointerPanDeltaX(packet)); + assertEquals(0.0, readPointerPanDeltaY(packet)); + assertEquals(0.0, readRotation(packet)); + // Always 1. + assertEquals(1.0, readScale(packet)); touchProcessor.onTouchEvent(mocker.mockEvent(MotionEvent.ACTION_UP, 10.0f, 5.0f, 0)); inOrder .verify(mockRenderer) @@ -220,10 +350,317 @@ public void unexpectedMaskedAction() { verify(mockRenderer, never()).dispatchPointerDataPacket(ByteBuffer.allocate(0), 0); } + @Test + @Config(minSdk = Build.VERSION_CODES.O) + public void scrollWheelAbove26() { + // Pointer id must be zero to match actionIndex in mocked event. + final int pointerId = 0; + MotionEventMocker mocker = + new MotionEventMocker( + pointerId, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final float horizontalScrollValue = -1f; + final float verticalScrollValue = .5f; + final Context context = ApplicationProvider.getApplicationContext(); + final double horizontalScaleFactor = + ViewConfiguration.get(context).getScaledHorizontalScrollFactor(); + final double verticalScaleFactor = + ViewConfiguration.get(context).getScaledVerticalScrollFactor(); + // Zero verticalScaleFactor will cause this test to miss bugs. + assertEquals("zero horizontal scale factor", true, horizontalScaleFactor != 0); + assertEquals("zero vertical scale factor", true, verticalScaleFactor != 0); + + final MotionEvent event = + mocker.mockEvent( + MotionEvent.ACTION_SCROLL, + 0.0f, + 0.0f, + 1, + horizontalScrollValue, + verticalScrollValue, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f); + boolean handled = touchProcessor.onGenericMotionEvent(event, context); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + assertEquals(-horizontalScrollValue * horizontalScaleFactor, readScrollDeltaX(packet)); + assertEquals(-verticalScrollValue * verticalScaleFactor, readScrollDeltaY(packet)); + verify(event).getAxisValue(MotionEvent.AXIS_HSCROLL, pointerId); + verify(event).getAxisValue(MotionEvent.AXIS_VSCROLL, pointerId); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + @Config(sdk = {Build.VERSION_CODES.N_MR1}) + public void scrollWheelBelow26() { + // Pointer id must be zero to match actionIndex in mocked event. + final int pointerId = 0; + MotionEventMocker mocker = + new MotionEventMocker( + pointerId, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final float horizontalScrollValue = -1f; + final float verticalScrollValue = .5f; + final Context context = ApplicationProvider.getApplicationContext(); + + final MotionEvent event = + mocker.mockEvent( + MotionEvent.ACTION_SCROLL, + 0.0f, + 0.0f, + 1, + horizontalScrollValue, + verticalScrollValue, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f); + boolean handled = touchProcessor.onGenericMotionEvent(event, context); + assertEquals(true, handled); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + // Magic number from roboletric's theme. + final double magicScrollFactor = 64; + assertEquals(-horizontalScrollValue * magicScrollFactor, readScrollDeltaX(packet)); + assertEquals(-verticalScrollValue * magicScrollFactor, readScrollDeltaY(packet)); + verify(event).getAxisValue(MotionEvent.AXIS_HSCROLL, pointerId); + verify(event).getAxisValue(MotionEvent.AXIS_VSCROLL, pointerId); + + // Trigger default values. + touchProcessor.onGenericMotionEvent(event, null); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + packet = packetCaptor.getValue(); + + assertEquals( + (double) -horizontalScrollValue * AndroidTouchProcessor.DEFAULT_HORIZONTAL_SCROLL_FACTOR, + readScrollDeltaX(packet)); + assertEquals( + (double) -verticalScrollValue * AndroidTouchProcessor.DEFAULT_VERTICAL_SCROLL_FACTOR, + readScrollDeltaY(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void timeStamp() { + final int pointerId = 0; + MotionEventMocker mocker = + new MotionEventMocker( + pointerId, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_SCROLL, 1f, 1f, 1); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + assertEquals(TimeUnit.MILLISECONDS.toMicros(eventTimeMilliseconds), readTimeStamp(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void device() { + final int pointerId = 2; + MotionEventMocker mocker = + new MotionEventMocker( + pointerId, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_SCROLL, 1f, 1f, 1); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + assertEquals(pointerId, readDevice(packet)); + verify(event).getPointerId(0); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void physicalXPhysicalY() { + MotionEventMocker mocker = + new MotionEventMocker(1, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final float x = 10.0f; + final float y = 20.0f; + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_DOWN, x, y, 0); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + assertEquals((double) x, readPointerPhysicalX(packet)); + assertEquals((double) y, readPointerPhysicalY(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void obscured() { + MotionEventMocker mocker = + new MotionEventMocker(1, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_DOWN, 10.0f, 20.0f, 0); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + // Always zero. + assertEquals(0.0, readObscured(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void synthesized() { + MotionEventMocker mocker = + new MotionEventMocker(1, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_DOWN, 10.0f, 20.0f, 0); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + // Always zero. + assertEquals(0.0, readSynthesized(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void pressure() { + MotionEventMocker mocker = + new MotionEventMocker(1, InputDevice.SOURCE_CLASS_POINTER, MotionEvent.TOOL_TYPE_MOUSE); + final MotionEvent event = mocker.mockEvent(MotionEvent.ACTION_DOWN, 10.0f, 20.0f, 0); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + // Always zero. + assertEquals((double) pressure, readPressure(packet)); + // Verify default range with null device. + assertEquals(0.0, readPressureMin(packet)); + assertEquals(1.0, readPressureMax(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void stylusDistance() { + MotionEventMocker mocker = + new MotionEventMocker(0, InputDevice.SOURCE_STYLUS, MotionEvent.TOOL_TYPE_STYLUS); + final float distance = 10.0f; + final float tilt = 20.0f; + final MotionEvent event = + mocker.mockEvent( + MotionEvent.ACTION_DOWN, + 0.0f, + 0.0f, + MotionEvent.BUTTON_STYLUS_PRIMARY, + 0.0f, + 0.0f, + distance, + tilt, + 0.0f, + 0.0f, + 0.0f); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + assertEquals(AndroidTouchProcessor.PointerDeviceKind.STYLUS, readPointerDeviceKind(packet)); + assertEquals((double) distance, readDistance(packet)); + // Always zero. + assertEquals(0.0, readDistanceMax(packet)); + assertEquals((double) tilt, readStylusTilt(packet)); + assertEquals(enginePrimaryStylusButton, readButtons(packet)); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void sizeAndRadius() { + MotionEventMocker mocker = + new MotionEventMocker(0, InputDevice.SOURCE_STYLUS, MotionEvent.TOOL_TYPE_STYLUS); + final float size = 10.0f; + final float radiusMajor = 20.0f; + final float radiusMinor = 30.0f; + final MotionEvent event = + mocker.mockEvent( + MotionEvent.ACTION_DOWN, + 0.0f, + 0.0f, + 0, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + size, + radiusMajor, + radiusMinor); + boolean handled = touchProcessor.onTouchEvent(event); + + InOrder inOrder = inOrder(mockRenderer); + inOrder + .verify(mockRenderer) + .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture()); + ByteBuffer packet = packetCaptor.getValue(); + + verify(event).getSize(0); + verify(event).getToolMajor(0); + verify(event).getToolMinor(0); + + assertEquals((double) size, readSize(packet)); + assertEquals((double) radiusMajor, readRadiusMajor(packet)); + assertEquals((double) radiusMinor, readRadiusMinor(packet)); + // Always zero. + assertEquals(0.0, readRadiusMin(packet)); + assertEquals(0.0, readRadiusMax(packet)); + + inOrder.verifyNoMoreInteractions(); + } + @Test public void unexpectedPointerChange() { // Regression test for https://github.com/flutter/flutter/issues/129765 - MotionEventMocker mocker = new MotionEventMocker(0, InputDevice.SOURCE_MOUSE, MotionEvent.TOOL_TYPE_MOUSE); @@ -252,7 +689,9 @@ public void unexpectedPointerChange() { assertEquals(10.0, readPointerPanX(packet)); assertEquals(5.0, readPointerPanY(packet)); - touchProcessor.onGenericMotionEvent(mocker.mockEvent(MotionEvent.ACTION_SCROLL, 0.0f, 0.0f, 0)); + touchProcessor.onGenericMotionEvent( + mocker.mockEvent(MotionEvent.ACTION_SCROLL, 0.0f, 0.0f, 0), + ApplicationProvider.getApplicationContext()); inOrder .verify(mockRenderer) .dispatchPointerDataPacket(packetCaptor.capture(), packetSizeCaptor.capture());