• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.accessibility;
18 
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_MOVE;
21 import static android.view.MotionEvent.ACTION_POINTER_DOWN;
22 import static android.view.MotionEvent.ACTION_POINTER_UP;
23 
24 import static com.android.server.testutils.TestUtils.strictMock;
25 
26 import static org.junit.Assert.assertFalse;
27 import static org.junit.Assert.assertTrue;
28 import static org.junit.Assert.fail;
29 import static org.mockito.ArgumentMatchers.eq;
30 import static org.mockito.Matchers.any;
31 import static org.mockito.Matchers.anyInt;
32 import static org.mockito.Mockito.doNothing;
33 import static org.mockito.Mockito.mock;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 
38 import android.animation.ValueAnimator;
39 import android.annotation.NonNull;
40 import android.content.Context;
41 import android.os.Handler;
42 import android.os.Message;
43 import android.util.DebugUtils;
44 import android.view.InputDevice;
45 import android.view.MotionEvent;
46 
47 import androidx.test.InstrumentationRegistry;
48 import androidx.test.runner.AndroidJUnit4;
49 
50 import com.android.server.testutils.OffsettableClock;
51 import com.android.server.testutils.TestHandler;
52 import com.android.server.wm.WindowManagerInternal;
53 
54 import org.junit.After;
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 
59 import java.util.function.IntConsumer;
60 
61 /**
62  * Tests the state transitions of {@link MagnificationGestureHandler}
63  *
64  * Here's a dot graph describing the transitions being tested:
65  * {@code
66  *      digraph {
67  *          IDLE -> SHORTCUT_TRIGGERED [label="a11y\nbtn"]
68  *          SHORTCUT_TRIGGERED -> IDLE [label="a11y\nbtn"]
69  *          IDLE -> DOUBLE_TAP [label="2tap"]
70  *          DOUBLE_TAP -> IDLE [label="timeout"]
71  *          DOUBLE_TAP -> TRIPLE_TAP_AND_HOLD [label="down"]
72  *          SHORTCUT_TRIGGERED -> TRIPLE_TAP_AND_HOLD [label="down"]
73  *          TRIPLE_TAP_AND_HOLD -> ZOOMED [label="up"]
74  *          TRIPLE_TAP_AND_HOLD -> DRAGGING_TMP [label="hold/\nswipe"]
75  *          DRAGGING_TMP -> IDLE [label="release"]
76  *          ZOOMED -> ZOOMED_DOUBLE_TAP [label="2tap"]
77  *          ZOOMED_DOUBLE_TAP -> ZOOMED [label="timeout"]
78  *          ZOOMED_DOUBLE_TAP -> DRAGGING [label="hold"]
79  *          ZOOMED_DOUBLE_TAP -> IDLE [label="tap"]
80  *          DRAGGING -> ZOOMED [label="release"]
81  *          ZOOMED -> IDLE [label="a11y\nbtn"]
82  *          ZOOMED -> PANNING [label="2hold"]
83  *          PANNING -> PANNING_SCALING [label="pinch"]
84  *          PANNING_SCALING -> ZOOMED [label="release"]
85  *          PANNING -> ZOOMED [label="release"]
86  *      }
87  * }
88  */
89 @RunWith(AndroidJUnit4.class)
90 public class MagnificationGestureHandlerTest {
91 
92     public static final int STATE_IDLE = 1;
93     public static final int STATE_ZOOMED = 2;
94     public static final int STATE_2TAPS = 3;
95     public static final int STATE_ZOOMED_2TAPS = 4;
96     public static final int STATE_SHORTCUT_TRIGGERED = 5;
97     public static final int STATE_DRAGGING_TMP = 6;
98     public static final int STATE_DRAGGING = 7;
99     public static final int STATE_PANNING = 8;
100     public static final int STATE_SCALING_AND_PANNING = 9;
101 
102 
103     public static final int FIRST_STATE = STATE_IDLE;
104     public static final int LAST_STATE = STATE_SCALING_AND_PANNING;
105 
106     // Co-prime x and y, to potentially catch x-y-swapped errors
107     public static final float DEFAULT_X = 301;
108     public static final float DEFAULT_Y = 299;
109 
110     private static final int DISPLAY_0 = 0;
111 
112     private Context mContext;
113     MagnificationController mMagnificationController;
114 
115     private OffsettableClock mClock;
116     private MagnificationGestureHandler mMgh;
117     private TestHandler mHandler;
118 
119     private long mLastDownTime = Integer.MIN_VALUE;
120 
121     @Before
setUp()122     public void setUp() {
123         mContext = InstrumentationRegistry.getContext();
124         final MagnificationController.ControllerContext mockController =
125                 mock(MagnificationController.ControllerContext.class);
126         final WindowManagerInternal mockWindowManager = mock(WindowManagerInternal.class);
127         when(mockController.getContext()).thenReturn(mContext);
128         when(mockController.getAms()).thenReturn(mock(AccessibilityManagerService.class));
129         when(mockController.getWindowManager()).thenReturn(mockWindowManager);
130         when(mockController.getHandler()).thenReturn(new Handler(mContext.getMainLooper()));
131         when(mockController.newValueAnimator()).thenReturn(new ValueAnimator());
132         when(mockController.getAnimationDuration()).thenReturn(1000L);
133         when(mockWindowManager.setMagnificationCallbacks(eq(DISPLAY_0), any())).thenReturn(true);
134         mMagnificationController = new MagnificationController(mockController, new Object()) {
135             @Override
136             public boolean magnificationRegionContains(int displayId, float x, float y) {
137                 return true;
138             }
139 
140             @Override
141             void setForceShowMagnifiableBounds(int displayId, boolean show) {}
142         };
143         mMagnificationController.register(DISPLAY_0);
144         mClock = new OffsettableClock.Stopped();
145 
146         boolean detectTripleTap = true;
147         boolean detectShortcutTrigger = true;
148         mMgh = newInstance(detectTripleTap, detectShortcutTrigger);
149     }
150 
151     @After
tearDown()152     public void tearDown() {
153         mMagnificationController.unregister(DISPLAY_0);
154     }
155 
156     @NonNull
newInstance(boolean detectTripleTap, boolean detectShortcutTrigger)157     private MagnificationGestureHandler newInstance(boolean detectTripleTap,
158             boolean detectShortcutTrigger) {
159         MagnificationGestureHandler h = new MagnificationGestureHandler(
160                 mContext, mMagnificationController,
161                 detectTripleTap, detectShortcutTrigger, DISPLAY_0);
162         mHandler = new TestHandler(h.mDetectingState, mClock) {
163             @Override
164             protected String messageToString(Message m) {
165                 return DebugUtils.valueToString(
166                         MagnificationGestureHandler.DetectingState.class, "MESSAGE_", m.what);
167             }
168         };
169         h.mDetectingState.mHandler = mHandler;
170         h.setNext(strictMock(EventStreamTransformation.class));
171         return h;
172     }
173 
174     @Test
testInitialState_isIdle()175     public void testInitialState_isIdle() {
176         assertIn(STATE_IDLE);
177     }
178 
179     /**
180      * Covers paths to get to and back between each state and {@link #STATE_IDLE}
181      * This navigates between states using "canonical" paths, specified in
182      * {@link #goFromStateIdleTo} (for traversing away from {@link #STATE_IDLE}) and
183      * {@link #returnToNormalFrom} (for navigating back to {@link #STATE_IDLE})
184      */
185     @Test
testEachState_isReachableAndRecoverable()186     public void testEachState_isReachableAndRecoverable() {
187         forEachState(state -> {
188             goFromStateIdleTo(state);
189             assertIn(state);
190 
191             returnToNormalFrom(state);
192             try {
193                 assertIn(STATE_IDLE);
194             } catch (AssertionError e) {
195                 throw new AssertionError("Failed while testing state " + stateToString(state), e);
196             }
197         });
198     }
199 
200     @Test
testStates_areMutuallyExclusive()201     public void testStates_areMutuallyExclusive() {
202         forEachState(state1 -> {
203             forEachState(state2 -> {
204                 if (state1 < state2) {
205                     goFromStateIdleTo(state1);
206                     try {
207                         assertIn(state2);
208                         fail("State " + stateToString(state1) + " also implies state "
209                                 + stateToString(state2) + stateDump());
210                     } catch (AssertionError e) {
211                         // expected
212                         returnToNormalFrom(state1);
213                     }
214                 }
215             });
216         });
217     }
218 
219     @Test
testTransitionToDelegatingStateAndClear_preservesShortcutTriggeredState()220     public void testTransitionToDelegatingStateAndClear_preservesShortcutTriggeredState() {
221         mMgh.mDetectingState.transitionToDelegatingStateAndClear();
222         assertFalse(mMgh.mDetectingState.mShortcutTriggered);
223 
224         goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
225         mMgh.mDetectingState.transitionToDelegatingStateAndClear();
226         assertTrue(mMgh.mDetectingState.mShortcutTriggered);
227     }
228 
229     /**
230      * Covers edges of the graph not covered by "canonical" transitions specified in
231      * {@link #goFromStateIdleTo} and {@link #returnToNormalFrom}
232      */
233     @SuppressWarnings("Convert2MethodRef")
234     @Test
testAlternativeTransitions_areWorking()235     public void testAlternativeTransitions_areWorking() {
236         // A11y button followed by a tap&hold turns temporary "viewport dragging" zoom on
237         assertTransition(STATE_SHORTCUT_TRIGGERED, () -> {
238             send(downEvent());
239             fastForward1sec();
240         }, STATE_DRAGGING_TMP);
241 
242         // A11y button followed by a tap turns zoom on
243         assertTransition(STATE_SHORTCUT_TRIGGERED, () -> tap(), STATE_ZOOMED);
244 
245         // A11y button pressed second time negates the 1st press
246         assertTransition(STATE_SHORTCUT_TRIGGERED, () -> triggerShortcut(), STATE_IDLE);
247 
248         // A11y button turns zoom off
249         assertTransition(STATE_ZOOMED, () -> triggerShortcut(), STATE_IDLE);
250 
251 
252         // Double tap times out while zoomed
253         assertTransition(STATE_ZOOMED_2TAPS, () -> {
254             allowEventDelegation();
255             fastForward1sec();
256         }, STATE_ZOOMED);
257 
258         // tap+tap+swipe doesn't get delegated
259         assertTransition(STATE_2TAPS, () -> swipe(), STATE_IDLE);
260 
261         // tap+tap+swipe initiates viewport dragging immediately
262         assertTransition(STATE_2TAPS, () -> swipeAndHold(), STATE_DRAGGING_TMP);
263     }
264 
265     @Test
testNonTransitions_dontChangeState()266     public void testNonTransitions_dontChangeState() {
267         // ACTION_POINTER_DOWN triggers event delegation if not magnifying
268         assertStaysIn(STATE_IDLE, () -> {
269             allowEventDelegation();
270             send(downEvent());
271             send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
272         });
273 
274         // Long tap breaks the triple-tap detection sequence
275         Runnable tapAndLongTap = () -> {
276             allowEventDelegation();
277             tap();
278             longTap();
279         };
280         assertStaysIn(STATE_IDLE, tapAndLongTap);
281         assertStaysIn(STATE_ZOOMED, tapAndLongTap);
282 
283         // Triple tap with delays in between doesn't count
284         Runnable slow3tap = () -> {
285             tap();
286             fastForward1sec();
287             tap();
288             fastForward1sec();
289             tap();
290         };
291         assertStaysIn(STATE_IDLE, slow3tap);
292         assertStaysIn(STATE_ZOOMED, slow3tap);
293     }
294 
295     @Test
testDisablingTripleTap_removesInputLag()296     public void testDisablingTripleTap_removesInputLag() {
297         mMgh = newInstance(/* detect3tap */ false, /* detectShortcut */ true);
298         goFromStateIdleTo(STATE_IDLE);
299         allowEventDelegation();
300         tap();
301         // no fast forward
302         verify(mMgh.getNext(), times(2)).onMotionEvent(any(), any(), anyInt());
303     }
304 
305     @Test
testTripleTapAndHold_zoomsImmediately()306     public void testTripleTapAndHold_zoomsImmediately() {
307         assertZoomsImmediatelyOnSwipeFrom(STATE_2TAPS);
308         assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED);
309     }
310 
311     @Test
testMultiTap_outOfDistanceSlop_shouldInIdle()312     public void testMultiTap_outOfDistanceSlop_shouldInIdle() {
313         // All delay motion events should be sent, if multi-tap with out of distance slop.
314         // STATE_IDLE will check if tapCount() < 2.
315         allowEventDelegation();
316         assertStaysIn(STATE_IDLE, () -> {
317             tap();
318             tap(DEFAULT_X * 2, DEFAULT_Y * 2);
319         });
320         assertStaysIn(STATE_IDLE, () -> {
321             tap();
322             tap(DEFAULT_X * 2, DEFAULT_Y * 2);
323             tap();
324             tap(DEFAULT_X * 2, DEFAULT_Y * 2);
325             tap();
326         });
327     }
328 
assertZoomsImmediatelyOnSwipeFrom(int state)329     private void assertZoomsImmediatelyOnSwipeFrom(int state) {
330         goFromStateIdleTo(state);
331         swipeAndHold();
332         assertIn(STATE_DRAGGING_TMP);
333         returnToNormalFrom(STATE_DRAGGING_TMP);
334     }
335 
assertTransition(int fromState, Runnable transitionAction, int toState)336     private void assertTransition(int fromState, Runnable transitionAction, int toState) {
337         goFromStateIdleTo(fromState);
338         transitionAction.run();
339         assertIn(toState);
340         returnToNormalFrom(toState);
341     }
342 
assertStaysIn(int state, Runnable action)343     private void assertStaysIn(int state, Runnable action) {
344         assertTransition(state, action, state);
345     }
346 
forEachState(IntConsumer action)347     private void forEachState(IntConsumer action) {
348         for (int state = FIRST_STATE; state <= LAST_STATE; state++) {
349             action.accept(state);
350         }
351     }
352 
allowEventDelegation()353     private void allowEventDelegation() {
354         doNothing().when(mMgh.getNext()).onMotionEvent(any(), any(), anyInt());
355     }
356 
fastForward1sec()357     private void fastForward1sec() {
358         fastForward(1000);
359     }
360 
fastForward(int ms)361     private void fastForward(int ms) {
362         mClock.fastForward(ms);
363         mHandler.timeAdvance();
364     }
365 
366     /**
367      * Asserts that {@link #mMgh the handler} is in the given {@code state}
368      */
assertIn(int state)369     private void assertIn(int state) {
370         switch (state) {
371 
372             // Asserts on separate lines for accurate stack traces
373 
374             case STATE_IDLE: {
375                 check(tapCount() < 2, state);
376                 check(!mMgh.mDetectingState.mShortcutTriggered, state);
377                 check(!isZoomed(), state);
378             } break;
379             case STATE_ZOOMED: {
380                 check(isZoomed(), state);
381                 check(tapCount() < 2, state);
382             } break;
383             case STATE_2TAPS: {
384                 check(!isZoomed(), state);
385                 check(tapCount() == 2, state);
386             } break;
387             case STATE_ZOOMED_2TAPS: {
388                 check(isZoomed(), state);
389                 check(tapCount() == 2, state);
390             } break;
391             case STATE_DRAGGING: {
392                 check(isZoomed(), state);
393                 check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
394                         state);
395                 check(mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
396             } break;
397             case STATE_DRAGGING_TMP: {
398                 check(isZoomed(), state);
399                 check(mMgh.mCurrentState == mMgh.mViewportDraggingState,
400                         state);
401                 check(!mMgh.mViewportDraggingState.mZoomedInBeforeDrag, state);
402             } break;
403             case STATE_SHORTCUT_TRIGGERED: {
404                 check(mMgh.mDetectingState.mShortcutTriggered, state);
405                 check(!isZoomed(), state);
406             } break;
407             case STATE_PANNING: {
408                 check(isZoomed(), state);
409                 check(mMgh.mCurrentState == mMgh.mPanningScalingState,
410                         state);
411                 check(!mMgh.mPanningScalingState.mScaling, state);
412             } break;
413             case STATE_SCALING_AND_PANNING: {
414                 check(isZoomed(), state);
415                 check(mMgh.mCurrentState == mMgh.mPanningScalingState,
416                         state);
417                 check(mMgh.mPanningScalingState.mScaling, state);
418             } break;
419             default: throw new IllegalArgumentException("Illegal state: " + state);
420         }
421     }
422 
423     /**
424      * Defines a "canonical" path from {@link #STATE_IDLE} to {@code state}
425      */
426     private void goFromStateIdleTo(int state) {
427         try {
428             switch (state) {
429                 case STATE_IDLE: {
430                     mMgh.clearAndTransitionToStateDetecting();
431                 } break;
432                 case STATE_2TAPS: {
433                     goFromStateIdleTo(STATE_IDLE);
434                     tap();
435                     tap();
436                 } break;
437                 case STATE_ZOOMED: {
438                     if (mMgh.mDetectTripleTap) {
439                         goFromStateIdleTo(STATE_2TAPS);
440                         tap();
441                     } else {
442                         goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED);
443                         tap();
444                     }
445                 } break;
446                 case STATE_ZOOMED_2TAPS: {
447                     goFromStateIdleTo(STATE_ZOOMED);
448                     tap();
449                     tap();
450                 } break;
451                 case STATE_DRAGGING: {
452                     goFromStateIdleTo(STATE_ZOOMED_2TAPS);
453                     send(downEvent());
454                     fastForward1sec();
455                 } break;
456                 case STATE_DRAGGING_TMP: {
457                     goFromStateIdleTo(STATE_2TAPS);
458                     send(downEvent());
459                     fastForward1sec();
460                 } break;
461                 case STATE_SHORTCUT_TRIGGERED: {
462                     goFromStateIdleTo(STATE_IDLE);
463                     triggerShortcut();
464                 } break;
465                 case STATE_PANNING: {
466                     goFromStateIdleTo(STATE_ZOOMED);
467                     send(downEvent());
468                     send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y));
469                 } break;
470                 case STATE_SCALING_AND_PANNING: {
471                     goFromStateIdleTo(STATE_PANNING);
472                     send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 3));
473                     send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4));
474                 } break;
475                 default:
476                     throw new IllegalArgumentException("Illegal state: " + state);
477             }
478         } catch (Throwable t) {
479             throw new RuntimeException("Failed to go to state " + stateToString(state), t);
480         }
481     }
482 
483     /**
484      * Defines a "canonical" path from {@code state} to {@link #STATE_IDLE}
485      */
486     private void returnToNormalFrom(int state) {
487         switch (state) {
488             case STATE_IDLE: {
489                 // no op
490             } break;
491             case STATE_2TAPS: {
492                 allowEventDelegation();
493                 fastForward1sec();
494             } break;
495             case STATE_ZOOMED: {
496                 if (mMgh.mDetectTripleTap) {
497                     tap();
498                     tap();
499                     returnToNormalFrom(STATE_ZOOMED_2TAPS);
500                 } else {
501                     triggerShortcut();
502                 }
503             } break;
504             case STATE_ZOOMED_2TAPS: {
505                 tap();
506             } break;
507             case STATE_DRAGGING: {
508                 send(upEvent());
509                 returnToNormalFrom(STATE_ZOOMED);
510             } break;
511             case STATE_DRAGGING_TMP: {
512                 send(upEvent());
513             } break;
514             case STATE_SHORTCUT_TRIGGERED: {
515                 triggerShortcut();
516             } break;
517             case STATE_PANNING: {
518                 send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y));
519                 send(upEvent());
520                 returnToNormalFrom(STATE_ZOOMED);
521             } break;
522             case STATE_SCALING_AND_PANNING: {
523                 returnToNormalFrom(STATE_PANNING);
524             } break;
525             default: throw new IllegalArgumentException("Illegal state: " + state);
526         }
527     }
528 
529     private void check(boolean condition, int expectedState) {
530         if (!condition) {
531             fail("Expected to be in state " + stateToString(expectedState) + stateDump());
532         }
533     }
534 
535     private boolean isZoomed() {
536         return mMgh.mMagnificationController.isMagnifying(DISPLAY_0);
537     }
538 
539     private int tapCount() {
540         return mMgh.mDetectingState.tapCount();
541     }
542 
543     private static String stateToString(int state) {
544         return DebugUtils.valueToString(MagnificationGestureHandlerTest.class, "STATE_", state);
545     }
546 
547     private void tap() {
548         send(downEvent());
549         send(upEvent());
550     }
551 
552     private void tap(float x, float y) {
553         send(downEvent(x, y));
554         send(upEvent(x, y));
555     }
556 
557     private void swipe() {
558         swipeAndHold();
559         send(upEvent());
560     }
561 
562     private void swipeAndHold() {
563         send(downEvent());
564         send(moveEvent(DEFAULT_X * 2, DEFAULT_Y * 2));
565     }
566 
567     private void longTap() {
568         send(downEvent());
569         fastForward(2000);
570         send(upEvent());
571     }
572 
573     private void triggerShortcut() {
574         mMgh.notifyShortcutTriggered();
575     }
576 
577     private void send(MotionEvent event) {
578         event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
579         try {
580             mMgh.onMotionEvent(event, event, /* policyFlags */ 0);
581         } catch (Throwable t) {
582             throw new RuntimeException("Exception while handling " + event, t);
583         }
584         fastForward(1);
585     }
586 
587     private static MotionEvent fromTouchscreen(MotionEvent ev) {
588         ev.setSource(InputDevice.SOURCE_TOUCHSCREEN);
589         return ev;
590     }
591 
592     private MotionEvent moveEvent(float x, float y) {
593         return fromTouchscreen(
594         	    MotionEvent.obtain(mLastDownTime, mClock.now(), ACTION_MOVE, x, y, 0));
595     }
596 
597     private MotionEvent downEvent() {
598         return downEvent(DEFAULT_X, DEFAULT_Y);
599     }
600 
601     private MotionEvent downEvent(float x, float y) {
602         mLastDownTime = mClock.now();
603         return fromTouchscreen(MotionEvent.obtain(mLastDownTime, mLastDownTime,
604                 ACTION_DOWN, x, y, 0));
605     }
606 
607     private MotionEvent upEvent() {
608         return upEvent(DEFAULT_X, DEFAULT_Y, mLastDownTime);
609     }
610 
611     private MotionEvent upEvent(float x, float y) {
612         return upEvent(x, y, mLastDownTime);
613     }
614 
615     private MotionEvent upEvent(float x, float y, long downTime) {
616         return fromTouchscreen(MotionEvent.obtain(downTime, mClock.now(),
617                 MotionEvent.ACTION_UP, x, y, 0));
618     }
619 
620     private MotionEvent pointerEvent(int action, float x, float y) {
621         MotionEvent.PointerProperties defPointerProperties = new MotionEvent.PointerProperties();
622         defPointerProperties.id = 0;
623         defPointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
624         MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
625         pointerProperties.id = 1;
626         pointerProperties.toolType = MotionEvent.TOOL_TYPE_FINGER;
627 
628         MotionEvent.PointerCoords defPointerCoords = new MotionEvent.PointerCoords();
629         defPointerCoords.x = DEFAULT_X;
630         defPointerCoords.y = DEFAULT_Y;
631         MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
632         pointerCoords.x = x;
633         pointerCoords.y = y;
634 
635         return MotionEvent.obtain(
636             /* downTime */ mClock.now(),
637             /* eventTime */ mClock.now(),
638             /* action */ action,
639             /* pointerCount */ 2,
640             /* pointerProperties */ new MotionEvent.PointerProperties[] {
641                         defPointerProperties, pointerProperties },
642             /* pointerCoords */ new MotionEvent.PointerCoords[] { defPointerCoords, pointerCoords },
643             /* metaState */ 0,
644             /* buttonState */ 0,
645             /* xPrecision */ 1.0f,
646             /* yPrecision */ 1.0f,
647             /* deviceId */ 0,
648             /* edgeFlags */ 0,
649             /* source */ InputDevice.SOURCE_TOUCHSCREEN,
650             /* flags */ 0);
651     }
652 
653     private String stateDump() {
654         return "\nCurrent state dump:\n" + mMgh + "\n" + mHandler.getPendingMessages();
655     }
656 }
657