blob: 1cbee12720b0b4cdc0f343dbf84dfcb7066c5885 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.accessibility;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
import static android.view.MotionEvent.ACTION_POINTER_UP;
import static android.view.MotionEvent.ACTION_UP;
import static com.android.server.testutils.TestUtils.strictMock;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
import android.graphics.PointF;
import android.os.Handler;
import android.os.Message;
import android.util.DebugUtils;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.testutils.OffsettableClock;
import com.android.server.testutils.TestHandler;
import com.android.server.wm.WindowManagerInternal;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntConsumer;
/**
* Tests the state transitions of {@link FullScreenMagnificationGestureHandler}
*
* Here's a dot graph describing the transitions being tested:
* {@code
* digraph {
* IDLE -> SHORTCUT_TRIGGERED [label="a11y\nbtn"]
* SHORTCUT_TRIGGERED -> IDLE [label="a11y\nbtn"]
* IDLE -> DOUBLE_TAP [label="2tap"]
* DOUBLE_TAP -> IDLE [label="timeout"]
* DOUBLE_TAP -> TRIPLE_TAP_AND_HOLD [label="down"]
* SHORTCUT_TRIGGERED -> TRIPLE_TAP_AND_HOLD [label="down"]
* TRIPLE_TAP_AND_HOLD -> ZOOMED [label="up"]
* TRIPLE_TAP_AND_HOLD -> DRAGGING_TMP [label="hold/\nswipe"]
* DRAGGING_TMP -> IDLE [label="release"]
* ZOOMED -> ZOOMED_DOUBLE_TAP [label="2tap"]
* ZOOMED_DOUBLE_TAP -> ZOOMED [label="timeout"]
* ZOOMED_DOUBLE_TAP -> DRAGGING [label="hold"]
* ZOOMED_DOUBLE_TAP -> IDLE [label="tap"]
* DRAGGING -> ZOOMED [label="release"]
* ZOOMED -> IDLE [label="a11y\nbtn"]
* ZOOMED -> PANNING [label="2hold"]
* PANNING -> PANNING_SCALING [label="pinch"]
* PANNING_SCALING -> ZOOMED [label="release"]
* PANNING -> ZOOMED [label="release"]
* }
* }
*/
@RunWith(AndroidJUnit4.class)
public class FullScreenMagnificationGestureHandlerTest {
public static final int STATE_IDLE = 1;
public static final int STATE_ZOOMED = 2;
public static final int STATE_2TAPS = 3;
public static final int STATE_ZOOMED_2TAPS = 4;
public static final int STATE_SHORTCUT_TRIGGERED = 5;
public static final int STATE_DRAGGING_TMP = 6;
public static final int STATE_DRAGGING = 7;
public static final int STATE_PANNING = 8;
public static final int STATE_SCALING_AND_PANNING = 9;
public static final int FIRST_STATE = STATE_IDLE;
public static final int LAST_STATE = STATE_SCALING_AND_PANNING;
// Co-prime x and y, to potentially catch x-y-swapped errors
public static final float DEFAULT_X = 301;
public static final float DEFAULT_Y = 299;
public static final PointF DEFAULT_POINT = new PointF(DEFAULT_X, DEFAULT_Y);
private static final int DISPLAY_0 = 0;
private Context mContext;
MagnificationController mMagnificationController;
private OffsettableClock mClock;
private FullScreenMagnificationGestureHandler mMgh;
private TestHandler mHandler;
private long mLastDownTime = Integer.MIN_VALUE;
@Before
public void setUp() {
mContext = InstrumentationRegistry.getContext();
final MagnificationController.ControllerContext mockController =
mock(MagnificationController.ControllerContext.class);
final WindowManagerInternal mockWindowManager = mock(WindowManagerInternal.class);
when(mockController.getContext()).thenReturn(mContext);
when(mockController.getAms()).thenReturn(mock(AccessibilityManagerService.class));
when(mockController.getWindowManager()).thenReturn(mockWindowManager);
when(mockController.getHandler()).thenReturn(new Handler(mContext.getMainLooper()));
when(mockController.newValueAnimator()).thenReturn(new ValueAnimator());
when(mockController.getAnimationDuration()).thenReturn(1000L);
when(mockWindowManager.setMagnificationCallbacks(eq(DISPLAY_0), any())).thenReturn(true);
mMagnificationController = new MagnificationController(mockController, new Object()) {
@Override
public boolean magnificationRegionContains(int displayId, float x, float y) {
return true;
}
@Override
void setForceShowMagnifiableBounds(int displayId, boolean show) {}
};
mMagnificationController.register(DISPLAY_0);
mClock = new OffsettableClock.Stopped();
boolean detectTripleTap = true;
boolean detectShortcutTrigger = true;
mMgh = newInstance(detectTripleTap, detectShortcutTrigger);
}
@After
public void tearDown() {
mMagnificationController.unregister(DISPLAY_0);
}
@NonNull
private FullScreenMagnificationGestureHandler newInstance(boolean detectTripleTap,
boolean detectShortcutTrigger) {
FullScreenMagnificationGestureHandler h = new FullScreenMagnificationGestureHandler(
mContext, mMagnificationController,
detectTripleTap, detectShortcutTrigger, DISPLAY_0);
mHandler = new TestHandler(h.mDetectingState, mClock) {
@Override
protected String messageToString(Message m) {
return DebugUtils.valueToString(
FullScreenMagnificationGestureHandler.DetectingState.class, "MESSAGE_",
m.what);
}
};
h.mDetectingState.mHandler = mHandler;
h.setNext(strictMock(EventStreamTransformation.class));
return h;
}
@Test
public void testInitialState_isIdle() {
assertIn(STATE_IDLE);
}
/**
* Covers paths to get to and back between each state and {@link #STATE_IDLE}
* This navigates between states using "canonical" paths, specified in
* {@link #goFromStateIdleTo} (for traversing away from {@link #STATE_IDLE}) and
* {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE})
*/
@Test
public void testEachState_isReachableAndRecoverable() {
forEachState(state -> {
goFromStateIdleTo(state);
assertIn(state);
returnToNormalFrom(state);
try {
assertIn(STATE_IDLE);
} catch (AssertionError e) {
throw new AssertionError("Failed while testing state " + stateToString(state), e);
}
});
}
@Test
public void testStates_areMutuallyExclusive() {
forEachState(state1 -> {
forEachState(state2 -> {
if (state1 < state2) {
goFromStateIdleTo(state1);
try {
assertIn(state2);
fail("State " + stateToString(state1) + " also implies state "
+ stateToString(state2) + stateDump());
} catch (AssertionError e) {
// expected
returnToNormalFrom(state1);
}
}
});
});
}
@Test
public void testTransitionToDelegatingStateAndClear_preservesShortcutTriggeredState() {
mMgh.mDetectingState.transitionToDelegatingStateAndClear();
assertFalse(mMgh.mDetectingState.mShortcutTriggered);
goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
mMgh.mDetectingState.transitionToDelegatingStateAndClear();
assertTrue(mMgh.mDetectingState.mShortcutTriggered);
}
/**
* Covers edges of the graph not covered by "canonical" transitions specified in
* {@link #goFromStateIdleTo} and {@link #returnToNormalFrom}
*/
@SuppressWarnings("Convert2MethodRef")
@Test
public void testAlternativeTransitions_areWorking() {
// A11y button followed by a tap&hold turns temporary "viewport dragging" zoom on
assertTransition(STATE_SHORTCUT_TRIGGERED, () -> {
send(downEvent());
fastForward1sec();
}, STATE_DRAGGING_TMP);
// A11y button followed by a tap turns zoom on
assertTransition(STATE_SHORTCUT_TRIGGERED, () -> tap(), STATE_ZOOMED);
// A11y button pressed second time negates the 1st press
assertTransition(STATE_SHORTCUT_TRIGGERED, () -> triggerShortcut(), STATE_IDLE);
// A11y button turns zoom off
assertTransition(STATE_ZOOMED, () -> triggerShortcut(), STATE_IDLE);
// Double tap times out while zoomed
assertTransition(STATE_ZOOMED_2TAPS, () -> {
allowEventDelegation();
fastForward1sec();
}, STATE_ZOOMED);
// tap+tap+swipe doesn't get delegated
assertTransition(STATE_2TAPS, () -> swipe(), STATE_IDLE);
// tap+tap+swipe initiates viewport dragging immediately
assertTransition(STATE_2TAPS, () -> swipeAndHold(), STATE_DRAGGING_TMP);
}
@Test
public void testNonTransitions_dontChangeState() {
// ACTION_POINTER_DOWN triggers event delegation if not magnifying
assertStaysIn(STATE_IDLE, () -> {
allowEventDelegation();
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
});
// Long tap breaks the triple-tap detection sequence
Runnable tapAndLongTap = () -> {
allowEventDelegation();
tap();
longTap();
};
assertStaysIn(STATE_IDLE, tapAndLongTap);
assertStaysIn(STATE_ZOOMED, tapAndLongTap);
// Triple tap with delays in between doesn't count
Runnable slow3tap = () -> {
tap();
fastForward1sec();
tap();
fastForward1sec();
tap();
};
assertStaysIn(STATE_IDLE, slow3tap);
assertStaysIn(STATE_ZOOMED, slow3tap);
}
@Test
public void testDisablingTripleTap_removesInputLag() {
mMgh = newInstance(/* detect3tap */ false, /* detectShortcut */ true);
goFromStateIdleTo(STATE_IDLE);
allowEventDelegation();
tap();
// no fast forward
verify(mMgh.getNext(), times(2)).onMotionEvent(any(), any(), anyInt());
}
@Test
public void testTripleTapAndHold_zoomsImmediately() {
assertZoomsImmediatelyOnSwipeFrom(STATE_2TAPS);
assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED);
}
@Test
public void testMultiTap_outOfDistanceSlop_shouldInIdle() {
// All delay motion events should be sent, if multi-tap with out of distance slop.
// STATE_IDLE will check if tapCount() < 2.
allowEventDelegation();
assertStaysIn(STATE_IDLE, () -> {
tap();
tap(DEFAULT_X * 2, DEFAULT_Y * 2);
});
assertStaysIn(STATE_IDLE, () -> {
tap();
tap(DEFAULT_X * 2, DEFAULT_Y * 2);
tap();
tap(DEFAULT_X * 2, DEFAULT_Y * 2);
tap();
});
}
@Test
public void testTwoFingersOneTap_zoomedState_dispatchMotionEvents() {
goFromStateIdleTo(STATE_ZOOMED);
final EventCaptor eventCaptor = new EventCaptor();
mMgh.setNext(eventCaptor);
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y));
send(upEvent());
assertIn(STATE_ZOOMED);
final List<Integer> expectedActions = new ArrayList();
expectedActions.add(Integer.valueOf(ACTION_DOWN));
expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
expectedActions.add(Integer.valueOf(ACTION_UP));
assertActionsInOrder(eventCaptor.mEvents, expectedActions);
returnToNormalFrom(STATE_ZOOMED);
}
@Test
public void testThreeFingersOneTap_zoomedState_dispatchMotionEvents() {
goFromStateIdleTo(STATE_ZOOMED);
final EventCaptor eventCaptor = new EventCaptor();
mMgh.setNext(eventCaptor);
PointF pointer1 = DEFAULT_POINT;
PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
PointF pointer3 = new PointF(DEFAULT_X * 2, DEFAULT_Y);
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2, pointer3}));
send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
send(pointerEvent(ACTION_POINTER_UP, new PointF[] {pointer1, pointer2, pointer3}));
send(upEvent());
assertIn(STATE_ZOOMED);
final List<Integer> expectedActions = new ArrayList();
expectedActions.add(Integer.valueOf(ACTION_DOWN));
expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
expectedActions.add(Integer.valueOf(ACTION_POINTER_DOWN));
expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
expectedActions.add(Integer.valueOf(ACTION_POINTER_UP));
expectedActions.add(Integer.valueOf(ACTION_UP));
assertActionsInOrder(eventCaptor.mEvents, expectedActions);
returnToNormalFrom(STATE_ZOOMED);
}
@Test
public void testFirstFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
goFromStateIdleTo(STATE_ZOOMED);
PointF pointer1 = DEFAULT_POINT;
PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer1.offset(sWipeMinDistance + 1, 0);
send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
assertIn(STATE_PANNING);
assertIn(STATE_PANNING);
returnToNormalFrom(STATE_PANNING);
}
@Test
public void testSecondFingerSwipe_TwoPinterDownAndZoomedState_panningState() {
goFromStateIdleTo(STATE_ZOOMED);
PointF pointer1 = DEFAULT_POINT;
PointF pointer2 = new PointF(DEFAULT_X * 1.5f, DEFAULT_Y);
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, new PointF[] {pointer1, pointer2}));
//The minimum movement to transit to panningState.
final float sWipeMinDistance = ViewConfiguration.get(mContext).getScaledTouchSlop();
pointer2.offset(sWipeMinDistance + 1, 0);
send(pointerEvent(ACTION_MOVE, new PointF[] {pointer1, pointer2}));
assertIn(STATE_PANNING);
assertIn(STATE_PANNING);
returnToNormalFrom(STATE_PANNING);
}
private void assertActionsInOrder(List<MotionEvent> actualEvents,
List<Integer> expectedActions) {
assertTrue(actualEvents.size() == expectedActions.size());
final int size = actualEvents.size();
for (int i = 0; i < size; i++) {
final int expectedAction = expectedActions.get(i);
final int actualAction = actualEvents.get(i).getActionMasked();
assertTrue(String.format(
"%dth action %s is not matched, actual events : %s, ", i,
MotionEvent.actionToString(expectedAction), actualEvents),
actualAction == expectedAction);
}
}
private void assertZoomsImmediatelyOnSwipeFrom(int state) {
goFromStateIdleTo(state);
swipeAndHold();
assertIn(STATE_DRAGGING_TMP);
returnToNormalFrom(STATE_DRAGGING_TMP);
}
private void assertTransition(int fromState, Runnable transitionAction, int toState) {
goFromStateIdleTo(fromState);
transitionAction.run();
assertIn(toState);
returnToNormalFrom(toState);
}
private void assertStaysIn(int state, Runnable action) {
assertTransition(state, action, state);
}
private void forEachState(IntConsumer action) {
for (int state = FIRST_STATE; state <= LAST_STATE; state++) {
action.accept(state);
}
}
private void allowEventDelegation() {
doNothing().when(mMgh.getNext()).onMotionEvent(any(), any(), anyInt());
}
private void fastForward1sec() {
fastForward(1000);
}
private void fastForward(int ms) {
mClock.fastForward(ms);
mHandler.timeAdvance();
}
/**
* Asserts that {@link #mMgh the handler} is in the given {@code state}
*/
private void assertIn(int state) {
switch (state) {
// Asserts on separate lines for accurate stack traces
case STATE_IDLE: {
check(tapCount() < 2, state);
check(!mMgh.mDetectingState.mShortcutTriggered, state);
check(!isZoomed(), state);
} break;
case STATE_ZOOMED: {
check(isZoomed(), state);
check(tapCount() < 2, state);
} break;
case STATE_2TAPS: {
check(!isZoomed(), state);
check(tapCount() == 2, state);
} break;
case STATE_ZOOMED_2TAPS: {
check(isZoomed(), state);
check(tapCount() == 2, state);
} break;
case STATE_DRAGGING: {
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
state);
check(mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
} break;
case STATE_DRAGGING_TMP: {
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
state);
check(!mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
} break;
case STATE_SHORTCUT_TRIGGERED: {
check(mMgh.mDetectingState.mShortcutTriggered, state);
check(!isZoomed(), state);
} break;
case STATE_PANNING: {
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mPanningScalingState,
state);
check(!mMgh.mPanningScalingState.mScaling, state);
} break;
case STATE_SCALING_AND_PANNING: {
check(isZoomed(), state);
check(mMgh.mCurrentState == mMgh.mPanningScalingState,
state);
check(mMgh.mPanningScalingState.mScaling, state);
} break;
default: throw new IllegalArgumentException("Illegal state: " + state);
}
}
/**
* Defines a "canonical" path from {@link #STATE_IDLE} to {@code state}
*/
private void goFromStateIdleTo(int state) {
try {
switch (state) {
case STATE_IDLE: {
mMgh.clearAndTransitionToStateDetecting();
} break;
case STATE_2TAPS: {
goFromStateIdleTo(STATE_IDLE);
tap();
tap();
} break;
case STATE_ZOOMED: {
if (mMgh.mDetectTripleTap) {
goFromStateIdleTo(STATE_2TAPS);
tap();
} else {
goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
tap();
}
} break;
case STATE_ZOOMED_2TAPS: {
goFromStateIdleTo(STATE_ZOOMED);
tap();
tap();
} break;
case STATE_DRAGGING: {
goFromStateIdleTo(STATE_ZOOMED_2TAPS);
send(downEvent());
fastForward1sec();
} break;
case STATE_DRAGGING_TMP: {
goFromStateIdleTo(STATE_2TAPS);
send(downEvent());
fastForward1sec();
} break;
case STATE_SHORTCUT_TRIGGERED: {
goFromStateIdleTo(STATE_IDLE);
triggerShortcut();
} break;
case STATE_PANNING: {
goFromStateIdleTo(STATE_ZOOMED);
send(downEvent());
send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
fastForward(ViewConfiguration.getTapTimeout());
} break;
case STATE_SCALING_AND_PANNING: {
goFromStateIdleTo(STATE_PANNING);
send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 3));
send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4));
} break;
default:
throw new IllegalArgumentException("Illegal state: " + state);
}
} catch (Throwable t) {
throw new RuntimeException("Failed to go to state " + stateToString(state), t);
}
}
/**
* Defines a "canonical" path from {@code state} to {@link #STATE_IDLE}
*/
private void returnToNormalFrom(int state) {
switch (state) {
case STATE_IDLE: {
// no op
} break;
case STATE_2TAPS: {
allowEventDelegation();
fastForward1sec();
} break;
case STATE_ZOOMED: {
if (mMgh.mDetectTripleTap) {
tap();
tap();
returnToNormalFrom(STATE_ZOOMED_2TAPS);
} else {
triggerShortcut();
}
} break;
case STATE_ZOOMED_2TAPS: {
tap();
} break;
case STATE_DRAGGING: {
send(upEvent());
returnToNormalFrom(STATE_ZOOMED);
} break;
case STATE_DRAGGING_TMP: {
send(upEvent());
} break;
case STATE_SHORTCUT_TRIGGERED: {
triggerShortcut();
} break;
case STATE_PANNING: {
send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y));
send(upEvent());
returnToNormalFrom(STATE_ZOOMED);
} break;
case STATE_SCALING_AND_PANNING: {
returnToNormalFrom(STATE_PANNING);
} break;
default: throw new IllegalArgumentException("Illegal state: " + state);
}
}
private void check(boolean condition, int expectedState) {
if (!condition) {
fail("Expected to be in state " + stateToString(expectedState) + stateDump());
}
}
private boolean isZoomed() {
return mMgh.mMagnificationController.isMagnifying(DISPLAY_0);
}
private int tapCount() {
return mMgh.mDetectingState.tapCount();
}
private static String stateToString(int state) {
return DebugUtils.valueToString(FullScreenMagnificationGestureHandlerTest.class, "STATE_",
state);
}
private void tap() {
send(downEvent());
send(upEvent());
}
private void tap(float x, float y) {
send(downEvent(x, y));
send(upEvent(x, y));
}
private void swipe() {
swipeAndHold();
send(upEvent());
}
private void swipeAndHold() {
send(downEvent());
send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));
}
private void longTap() {
send(downEvent());
fastForward(2000);
send(upEvent());
}
private void triggerShortcut() {
mMgh.notifyShortcutTriggered();
}
private void send(MotionEvent event) {
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
try {
mMgh.onMotionEvent(event, event, /* policyFlags */ 0);
} catch (Throwable t) {
throw new RuntimeException("Exception while handling " + event, t);
}
fastForward(1);
}
private static MotionEvent fromTouchscreen(MotionEvent ev) {
ev.setSource(InputDevice.SOURCE_TOUCHSCREEN);
return ev;
}
private MotionEvent moveEvent(float x, float y) {
return fromTouchscreen(
MotionEvent.obtain(mLastDownTime, mClock.now(), ACTION_MOVE, x, y, 0));
}
private MotionEvent downEvent() {
return downEvent(DEFAULT_X, DEFAULT_Y);
}
private MotionEvent downEvent(float x, float y) {
mLastDownTime = mClock.now();
return fromTouchscreen(MotionEvent.obtain(mLastDownTime, mLastDownTime,
ACTION_DOWN, x, y, 0));
}
private MotionEvent upEvent() {
return upEvent(DEFAULT_X, DEFAULT_Y, mLastDownTime);
}
private MotionEvent upEvent(float x, float y) {
return upEvent(x, y, mLastDownTime);
}
private MotionEvent upEvent(float x, float y, long downTime) {
return fromTouchscreen(MotionEvent.obtain(downTime, mClock.now(),
MotionEvent.ACTION_UP, x, y, 0));
}
private MotionEvent pointerEvent(int action, float x, float y) {
return pointerEvent(action, new PointF[] {DEFAULT_POINT, new PointF(x, y)});
}
private MotionEvent pointerEvent(int action, PointF[] pointersPosition) {
final MotionEvent.PointerProperties[] PointerPropertiesArray =
new MotionEvent.PointerProperties[pointersPosition.length];
for (int i = 0; i < pointersPosition.length; i++) {
MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
pointerProperties.id = i;
pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
PointerPropertiesArray[i] = pointerProperties;
}
final MotionEvent.PointerCoords[] pointerCoordsArray =
new MotionEvent.PointerCoords[pointersPosition.length];
for (int i = 0; i < pointersPosition.length; i++) {
MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
pointerCoords.x = pointersPosition[i].x;
pointerCoords.y = pointersPosition[i].y;
pointerCoordsArray[i] = pointerCoords;
}
return MotionEvent.obtain(
/* downTime */ mClock.now(),
/* eventTime */ mClock.now(),
/* action */ action,
/* pointerCount */ pointersPosition.length,
/* pointerProperties */ PointerPropertiesArray,
/* pointerCoords */ pointerCoordsArray,
/* metaState */ 0,
/* buttonState */ 0,
/* xPrecision */ 1.0f,
/* yPrecision */ 1.0f,
/* deviceId */ 0,
/* edgeFlags */ 0,
/* source */ InputDevice.SOURCE_TOUCHSCREEN,
/* flags */ 0);
}
private String stateDump() {
return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages();
}
private class EventCaptor implements EventStreamTransformation {
List<MotionEvent> mEvents = new ArrayList<>();
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
mEvents.add(event.copy());
}
@Override
public void setNext(EventStreamTransformation next) {
}
@Override
public EventStreamTransformation getNext() {
return null;
}
}
}