1/*
2 * Copyright (C) 2012 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
17package com.android.uiautomator.core;
18
19import android.accessibilityservice.AccessibilityService;
20import android.app.UiAutomation;
21import android.app.UiAutomation.AccessibilityEventFilter;
22import android.graphics.Point;
23import android.os.RemoteException;
24import android.os.SystemClock;
25import android.util.Log;
26import android.view.InputDevice;
27import android.view.InputEvent;
28import android.view.KeyCharacterMap;
29import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.MotionEvent.PointerCoords;
32import android.view.MotionEvent.PointerProperties;
33import android.view.accessibility.AccessibilityEvent;
34
35import com.android.internal.util.Predicate;
36
37import java.util.ArrayList;
38import java.util.List;
39import java.util.concurrent.TimeoutException;
40
41/**
42 * The InteractionProvider is responsible for injecting user events such as touch events
43 * (includes swipes) and text key events into the system. To do so, all it needs to know about
44 * are coordinates of the touch events and text for the text input events.
45 * The InteractionController performs no synchronization. It will fire touch and text input events
46 * as fast as it receives them. All idle synchronization is performed prior to querying the
47 * hierarchy. See {@link QueryController}
48 */
49class InteractionController {
50
51    private static final String LOG_TAG = InteractionController.class.getSimpleName();
52
53    private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
54
55    private final KeyCharacterMap mKeyCharacterMap =
56            KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
57
58    private final UiAutomatorBridge mUiAutomatorBridge;
59
60    private static final long REGULAR_CLICK_LENGTH = 100;
61
62    private long mDownTime;
63
64    // Inserted after each motion event injection.
65    private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
66
67    public InteractionController(UiAutomatorBridge bridge) {
68        mUiAutomatorBridge = bridge;
69    }
70
71    /**
72     * Predicate for waiting for any of the events specified in the mask
73     */
74    class WaitForAnyEventPredicate implements AccessibilityEventFilter {
75        int mMask;
76        WaitForAnyEventPredicate(int mask) {
77            mMask = mask;
78        }
79        @Override
80        public boolean accept(AccessibilityEvent t) {
81            // check current event in the list
82            if ((t.getEventType() & mMask) != 0) {
83                return true;
84            }
85
86            // no match yet
87            return false;
88        }
89    }
90
91    /**
92     * Predicate for waiting for all the events specified in the mask and populating
93     * a ctor passed list with matching events. User of this Predicate must recycle
94     * all populated events in the events list.
95     */
96    class EventCollectingPredicate implements AccessibilityEventFilter {
97        int mMask;
98        List<AccessibilityEvent> mEventsList;
99
100        EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
101            mMask = mask;
102            mEventsList = events;
103        }
104
105        @Override
106        public boolean accept(AccessibilityEvent t) {
107            // check current event in the list
108            if ((t.getEventType() & mMask) != 0) {
109                // For the events you need, always store a copy when returning false from
110                // predicates since the original will automatically be recycled after the call.
111                mEventsList.add(AccessibilityEvent.obtain(t));
112            }
113
114            // get more
115            return false;
116        }
117    }
118
119    /**
120     * Predicate for waiting for every event specified in the mask to be matched at least once
121     */
122    class WaitForAllEventPredicate implements AccessibilityEventFilter {
123        int mMask;
124        WaitForAllEventPredicate(int mask) {
125            mMask = mask;
126        }
127
128        @Override
129        public boolean accept(AccessibilityEvent t) {
130            // check current event in the list
131            if ((t.getEventType() & mMask) != 0) {
132                // remove from mask since this condition is satisfied
133                mMask &= ~t.getEventType();
134
135                // Since we're waiting for all events to be matched at least once
136                if (mMask != 0)
137                    return false;
138
139                // all matched
140                return true;
141            }
142
143            // no match yet
144            return false;
145        }
146    }
147
148    /**
149     * Helper used by methods to perform actions and wait for any accessibility events and return
150     * predicated on predefined filter.
151     *
152     * @param command
153     * @param filter
154     * @param timeout
155     * @return
156     */
157    private AccessibilityEvent runAndWaitForEvents(Runnable command,
158            AccessibilityEventFilter filter, long timeout) {
159
160        try {
161            return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter,
162                    timeout);
163        } catch (TimeoutException e) {
164            Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
165            return null;
166        } catch (Exception e) {
167            Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
168            return null;
169        }
170    }
171
172    /**
173     * Send keys and blocks until the first specified accessibility event.
174     *
175     * Most key presses will cause some UI change to occur. If the device is busy, this will
176     * block until the device begins to process the key press at which point the call returns
177     * and normal wait for idle processing may begin. If no events are detected for the
178     * timeout period specified, the call will return anyway with false.
179     *
180     * @param keyCode
181     * @param metaState
182     * @param eventType
183     * @param timeout
184     * @return true if events is received, otherwise false.
185     */
186    public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
187            final int eventType, long timeout) {
188        Runnable command = new Runnable() {
189            @Override
190            public void run() {
191                final long eventTime = SystemClock.uptimeMillis();
192                KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
193                        keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
194                        InputDevice.SOURCE_KEYBOARD);
195                if (injectEventSync(downEvent)) {
196                    KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
197                            keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
198                            InputDevice.SOURCE_KEYBOARD);
199                    injectEventSync(upEvent);
200                }
201            }
202        };
203
204        return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
205                != null;
206    }
207
208    /**
209     * Clicks at coordinates without waiting for device idle. This may be used for operations
210     * that require stressing the target.
211     * @param x
212     * @param y
213     * @return true if the click executed successfully
214     */
215    public boolean clickNoSync(int x, int y) {
216        Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");
217
218        if (touchDown(x, y)) {
219            SystemClock.sleep(REGULAR_CLICK_LENGTH);
220            if (touchUp(x, y))
221                return true;
222        }
223        return false;
224    }
225
226    /**
227     * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
228     * or TYPE_VIEW_SELECTED are received.
229     *
230     * @param x
231     * @param y
232     * @param timeout waiting for event
233     * @return true if events are received, else false if timeout.
234     */
235    public boolean clickAndSync(final int x, final int y, long timeout) {
236
237        String logString = String.format("clickAndSync(%d, %d)", x, y);
238        Log.d(LOG_TAG, logString);
239
240        return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate(
241                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
242                AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
243    }
244
245    /**
246     * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
247     * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
248     * no further waits will be performed and the function returns.
249     * @param x
250     * @param y
251     * @param timeout waiting for event
252     * @return true if both events occurred in the expected order
253     */
254    public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
255        String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y);
256        Log.d(LOG_TAG, logString);
257
258        return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate(
259                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
260                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null;
261    }
262
263    /**
264     * Returns a Runnable for use in {@link #runAndWaitForEvents(Runnable, Predicate, long) to
265     * perform a click.
266     *
267     * @param x coordinate
268     * @param y coordinate
269     * @return Runnable
270     */
271    private Runnable clickRunnable(final int x, final int y) {
272        return new Runnable() {
273            @Override
274            public void run() {
275                if(touchDown(x, y)) {
276                    SystemClock.sleep(REGULAR_CLICK_LENGTH);
277                    touchUp(x, y);
278                }
279            }
280        };
281    }
282
283    /**
284     * Touches down for a long press at the specified coordinates.
285     *
286     * @param x
287     * @param y
288     * @return true if successful.
289     */
290    public boolean longTapNoSync(int x, int y) {
291        if (DEBUG) {
292            Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
293        }
294
295        if (touchDown(x, y)) {
296            SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
297            if(touchUp(x, y)) {
298                return true;
299            }
300        }
301        return false;
302    }
303
304    private boolean touchDown(int x, int y) {
305        if (DEBUG) {
306            Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
307        }
308        mDownTime = SystemClock.uptimeMillis();
309        MotionEvent event = MotionEvent.obtain(
310                mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1);
311        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
312        return injectEventSync(event);
313    }
314
315    private boolean touchUp(int x, int y) {
316        if (DEBUG) {
317            Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
318        }
319        final long eventTime = SystemClock.uptimeMillis();
320        MotionEvent event = MotionEvent.obtain(
321                mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
322        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
323        mDownTime = 0;
324        return injectEventSync(event);
325    }
326
327    private boolean touchMove(int x, int y) {
328        if (DEBUG) {
329            Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
330        }
331        final long eventTime = SystemClock.uptimeMillis();
332        MotionEvent event = MotionEvent.obtain(
333                mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
334        event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
335        return injectEventSync(event);
336    }
337
338    /**
339     * Handle swipes in any direction where the result is a scroll event. This call blocks
340     * until the UI has fired a scroll event or timeout.
341     * @param downX
342     * @param downY
343     * @param upX
344     * @param upY
345     * @param steps
346     * @return true if we are not at the beginning or end of the scrollable view.
347     */
348    public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
349            final int steps) {
350        Log.d(LOG_TAG, "scrollSwipe (" +  downX + ", " + downY + ", " + upX + ", "
351                + upY + ", " + steps +")");
352
353        Runnable command = new Runnable() {
354            @Override
355            public void run() {
356                swipe(downX, downY, upX, upY, steps);
357            }
358        };
359
360        // Collect all accessibility events generated during the swipe command and get the
361        // last event
362        ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
363        runAndWaitForEvents(command,
364                new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
365                Configurator.getInstance().getScrollAcknowledgmentTimeout());
366
367        AccessibilityEvent event = getLastMatchingEvent(events,
368                AccessibilityEvent.TYPE_VIEW_SCROLLED);
369
370        if (event == null) {
371            // end of scroll since no new scroll events received
372            recycleAccessibilityEvents(events);
373            return false;
374        }
375
376        // AdapterViews have indices we can use to check for the beginning.
377        boolean foundEnd = false;
378        if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
379            foundEnd = event.getFromIndex() == 0 ||
380                    (event.getItemCount() - 1) == event.getToIndex();
381            Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
382        } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
383            // Determine if we are scrolling vertically or horizontally.
384            if (downX == upX) {
385                // Vertical
386                foundEnd = event.getScrollY() == 0 ||
387                        event.getScrollY() == event.getMaxScrollY();
388                Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
389            } else if (downY == upY) {
390                // Horizontal
391                foundEnd = event.getScrollX() == 0 ||
392                        event.getScrollX() == event.getMaxScrollX();
393                Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
394            }
395        }
396        recycleAccessibilityEvents(events);
397        return !foundEnd;
398    }
399
400    private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
401        for (int x = events.size(); x > 0; x--) {
402            AccessibilityEvent event = events.get(x - 1);
403            if (event.getEventType() == type)
404                return event;
405        }
406        return null;
407    }
408
409    private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
410        for (AccessibilityEvent event : events)
411            event.recycle();
412        events.clear();
413    }
414
415    /**
416     * Handle swipes in any direction.
417     * @param downX
418     * @param downY
419     * @param upX
420     * @param upY
421     * @param steps
422     * @return true if the swipe executed successfully
423     */
424    public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
425        return swipe(downX, downY, upX, upY, steps, false /*drag*/);
426    }
427
428    /**
429     * Handle swipes/drags in any direction.
430     * @param downX
431     * @param downY
432     * @param upX
433     * @param upY
434     * @param steps
435     * @param drag when true, the swipe becomes a drag swipe
436     * @return true if the swipe executed successfully
437     */
438    public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
439        boolean ret = false;
440        int swipeSteps = steps;
441        double xStep = 0;
442        double yStep = 0;
443
444        // avoid a divide by zero
445        if(swipeSteps == 0)
446            swipeSteps = 1;
447
448        xStep = ((double)(upX - downX)) / swipeSteps;
449        yStep = ((double)(upY - downY)) / swipeSteps;
450
451        // first touch starts exactly at the point requested
452        ret = touchDown(downX, downY);
453        if (drag)
454            SystemClock.sleep(mUiAutomatorBridge.getSystemLongPressTime());
455        for(int i = 1; i < swipeSteps; i++) {
456            ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
457            if(ret == false)
458                break;
459            // set some known constant delay between steps as without it this
460            // become completely dependent on the speed of the system and results
461            // may vary on different devices. This guarantees at minimum we have
462            // a preset delay.
463            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
464        }
465        if (drag)
466            SystemClock.sleep(REGULAR_CLICK_LENGTH);
467        ret &= touchUp(upX, upY);
468        return(ret);
469    }
470
471    /**
472     * Performs a swipe between points in the Point array.
473     * @param segments is Point array containing at least one Point object
474     * @param segmentSteps steps to inject between two Points
475     * @return true on success
476     */
477    public boolean swipe(Point[] segments, int segmentSteps) {
478        boolean ret = false;
479        int swipeSteps = segmentSteps;
480        double xStep = 0;
481        double yStep = 0;
482
483        // avoid a divide by zero
484        if(segmentSteps == 0)
485            segmentSteps = 1;
486
487        // must have some points
488        if(segments.length == 0)
489            return false;
490
491        // first touch starts exactly at the point requested
492        ret = touchDown(segments[0].x, segments[0].y);
493        for(int seg = 0; seg < segments.length; seg++) {
494            if(seg + 1 < segments.length) {
495
496                xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps;
497                yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps;
498
499                for(int i = 1; i < swipeSteps; i++) {
500                    ret &= touchMove(segments[seg].x + (int)(xStep * i),
501                            segments[seg].y + (int)(yStep * i));
502                    if(ret == false)
503                        break;
504                    // set some known constant delay between steps as without it this
505                    // become completely dependent on the speed of the system and results
506                    // may vary on different devices. This guarantees at minimum we have
507                    // a preset delay.
508                    SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
509                }
510            }
511        }
512        ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y);
513        return(ret);
514    }
515
516
517    public boolean sendText(String text) {
518        if (DEBUG) {
519            Log.d(LOG_TAG, "sendText (" + text + ")");
520        }
521
522        KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
523
524        if (events != null) {
525            long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
526            for (KeyEvent event2 : events) {
527                // We have to change the time of an event before injecting it because
528                // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
529                // time stamp and the system rejects too old events. Hence, it is
530                // possible for an event to become stale before it is injected if it
531                // takes too long to inject the preceding ones.
532                KeyEvent event = KeyEvent.changeTimeRepeat(event2,
533                        SystemClock.uptimeMillis(), 0);
534                if (!injectEventSync(event)) {
535                    return false;
536                }
537                SystemClock.sleep(keyDelay);
538            }
539        }
540        return true;
541    }
542
543    public boolean sendKey(int keyCode, int metaState) {
544        if (DEBUG) {
545            Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
546        }
547
548        final long eventTime = SystemClock.uptimeMillis();
549        KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
550                keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
551                InputDevice.SOURCE_KEYBOARD);
552        if (injectEventSync(downEvent)) {
553            KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
554                    keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
555                    InputDevice.SOURCE_KEYBOARD);
556            if(injectEventSync(upEvent)) {
557                return true;
558            }
559        }
560        return false;
561    }
562
563    /**
564     * Rotates right and also freezes rotation in that position by
565     * disabling the sensors. If you want to un-freeze the rotation
566     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
567     * that doing so may cause the screen contents to rotate
568     * depending on the current physical position of the test device.
569     * @throws RemoteException
570     */
571    public void setRotationRight() {
572        mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_270);
573    }
574
575    /**
576     * Rotates left and also freezes rotation in that position by
577     * disabling the sensors. If you want to un-freeze the rotation
578     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
579     * that doing so may cause the screen contents to rotate
580     * depending on the current physical position of the test device.
581     * @throws RemoteException
582     */
583    public void setRotationLeft() {
584        mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_90);
585    }
586
587    /**
588     * Rotates up and also freezes rotation in that position by
589     * disabling the sensors. If you want to un-freeze the rotation
590     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
591     * that doing so may cause the screen contents to rotate
592     * depending on the current physical position of the test device.
593     * @throws RemoteException
594     */
595    public void setRotationNatural() {
596        mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0);
597    }
598
599    /**
600     * Disables the sensors and freezes the device rotation at its
601     * current rotation state.
602     * @throws RemoteException
603     */
604    public void freezeRotation() {
605        mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
606    }
607
608    /**
609     * Re-enables the sensors and un-freezes the device rotation
610     * allowing its contents to rotate with the device physical rotation.
611     * @throws RemoteException
612     */
613    public void unfreezeRotation() {
614        mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_UNFREEZE);
615    }
616
617    /**
618     * This method simply presses the power button if the screen is OFF else
619     * it does nothing if the screen is already ON.
620     * @return true if the device was asleep else false
621     * @throws RemoteException
622     */
623    public boolean wakeDevice() throws RemoteException {
624        if(!isScreenOn()) {
625            sendKey(KeyEvent.KEYCODE_POWER, 0);
626            return true;
627        }
628        return false;
629    }
630
631    /**
632     * This method simply presses the power button if the screen is ON else
633     * it does nothing if the screen is already OFF.
634     * @return true if the device was awake else false
635     * @throws RemoteException
636     */
637    public boolean sleepDevice() throws RemoteException {
638        if(isScreenOn()) {
639            this.sendKey(KeyEvent.KEYCODE_POWER, 0);
640            return true;
641        }
642        return false;
643    }
644
645    /**
646     * Checks the power manager if the screen is ON
647     * @return true if the screen is ON else false
648     * @throws RemoteException
649     */
650    public boolean isScreenOn() throws RemoteException {
651        return mUiAutomatorBridge.isScreenOn();
652    }
653
654    private boolean injectEventSync(InputEvent event) {
655        return mUiAutomatorBridge.injectInputEvent(event, true);
656    }
657
658    private int getPointerAction(int motionEnvent, int index) {
659        return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
660    }
661
662    /**
663     * Performs a multi-touch gesture
664     *
665     * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
666     * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
667     * to specify the touch points along the path of a pointer, the caller is able to specify
668     * complex gestures like circles, irregular shapes etc, where each pointer may take a
669     * different path.
670     *
671     * To create a single point on a pointer's touch path
672     * <code>
673     *       PointerCoords p = new PointerCoords();
674     *       p.x = stepX;
675     *       p.y = stepY;
676     *       p.pressure = 1;
677     *       p.size = 1;
678     * </code>
679     * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
680     *        Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
681     *        path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
682     * @return <code>true</code> if all points on all paths are injected successfully, <code>false
683     *        </code>otherwise
684     * @since API Level 18
685     */
686    public boolean performMultiPointerGesture(PointerCoords[] ... touches) {
687        boolean ret = true;
688        if (touches.length < 2) {
689            throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
690        }
691
692        // Get the pointer with the max steps to inject.
693        int maxSteps = 0;
694        for (int x = 0; x < touches.length; x++)
695            maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps;
696
697        // specify the properties for each pointer as finger touch
698        PointerProperties[] properties = new PointerProperties[touches.length];
699        PointerCoords[] pointerCoords = new PointerCoords[touches.length];
700        for (int x = 0; x < touches.length; x++) {
701            PointerProperties prop = new PointerProperties();
702            prop.id = x;
703            prop.toolType = MotionEvent.TOOL_TYPE_FINGER;
704            properties[x] = prop;
705
706            // for each pointer set the first coordinates for touch down
707            pointerCoords[x] = touches[x][0];
708        }
709
710        // Touch down all pointers
711        long downTime = SystemClock.uptimeMillis();
712        MotionEvent event;
713        event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
714                properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
715        ret &= injectEventSync(event);
716
717        for (int x = 1; x < touches.length; x++) {
718            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
719                    getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
720                    pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
721            ret &= injectEventSync(event);
722        }
723
724        // Move all pointers
725        for (int i = 1; i < maxSteps - 1; i++) {
726            // for each pointer
727            for (int x = 0; x < touches.length; x++) {
728                // check if it has coordinates to move
729                if (touches[x].length > i)
730                    pointerCoords[x] = touches[x][i];
731                else
732                    pointerCoords[x] = touches[x][touches[x].length - 1];
733            }
734
735            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
736                    MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
737                    0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
738
739            ret &= injectEventSync(event);
740            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
741        }
742
743        // For each pointer get the last coordinates
744        for (int x = 0; x < touches.length; x++)
745            pointerCoords[x] = touches[x][touches[x].length - 1];
746
747        // touch up
748        for (int x = 1; x < touches.length; x++) {
749            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
750                    getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
751                    pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
752            ret &= injectEventSync(event);
753        }
754
755        Log.i(LOG_TAG, "x " + pointerCoords[0].x);
756        // first to touch down is last up
757        event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
758                properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
759        ret &= injectEventSync(event);
760        return ret;
761    }
762
763    /**
764     * Simulates a short press on the Recent Apps button.
765     *
766     * @return true if successful, else return false
767     * @since API Level 18
768     */
769    public boolean toggleRecentApps() {
770        return mUiAutomatorBridge.performGlobalAction(
771                AccessibilityService.GLOBAL_ACTION_RECENTS);
772    }
773
774    /**
775     * Opens the notification shade
776     *
777     * @return true if successful, else return false
778     * @since API Level 18
779     */
780    public boolean openNotification() {
781        return mUiAutomatorBridge.performGlobalAction(
782                AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
783    }
784
785    /**
786     * Opens the quick settings shade
787     *
788     * @return true if successful, else return false
789     * @since API Level 18
790     */
791    public boolean openQuickSettings() {
792        return mUiAutomatorBridge.performGlobalAction(
793                AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
794    }
795}
796