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