InteractionController.java

/*
 * Copyright (C) 2012 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 androidx.test.uiautomator;

import android.accessibilityservice.AccessibilityService;
import android.app.Instrumentation;
import android.app.Service;
import android.app.UiAutomation;
import android.app.UiAutomation.AccessibilityEventFilter;
import android.content.Context;
import android.graphics.Point;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;

/**
 * The InteractionProvider is responsible for injecting user events such as touch events
 * (includes swipes) and text key events into the system. To do so, all it needs to know about
 * are coordinates of the touch events and text for the text input events.
 * The InteractionController performs no synchronization. It will fire touch and text input events
 * as fast as it receives them. All idle synchronization is performed prior to querying the
 * hierarchy. See {@link QueryController}
 */
class InteractionController {

    private static final String LOG_TAG = InteractionController.class.getSimpleName();

    private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);

    private final KeyCharacterMap mKeyCharacterMap =
            KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);

    private final Instrumentation mInstrumentation;

    private static final long REGULAR_CLICK_LENGTH = 100;

    private long mDownTime;

    // Inserted after each motion event injection.
    private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;

    public InteractionController(Instrumentation instrumentation) {
        mInstrumentation = instrumentation;
    }

    /**
     * Predicate for waiting for any of the events specified in the mask
     */
    class WaitForAnyEventPredicate implements AccessibilityEventFilter {
        int mMask;
        WaitForAnyEventPredicate(int mask) {
            mMask = mask;
        }
        @Override
        public boolean accept(AccessibilityEvent t) {
            // check current event in the list
            if ((t.getEventType() & mMask) != 0) {
                return true;
            }

            // no match yet
            return false;
        }
    }

    /**
     * Predicate for waiting for all the events specified in the mask and populating
     * a ctor passed list with matching events. User of this predicate must recycle
     * all populated events in the events list.
     */
    class EventCollectingPredicate implements AccessibilityEventFilter {
        int mMask;
        List<AccessibilityEvent> mEventsList;

        EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
            mMask = mask;
            mEventsList = events;
        }

        @Override
        public boolean accept(AccessibilityEvent t) {
            // check current event in the list
            if ((t.getEventType() & mMask) != 0) {
                // For the events you need, always store a copy when returning false from
                // predicates since the original will automatically be recycled after the call.
                mEventsList.add(AccessibilityEvent.obtain(t));
            }

            // get more
            return false;
        }
    }

    /**
     * Predicate for waiting for every event specified in the mask to be matched at least once
     */
    class WaitForAllEventPredicate implements AccessibilityEventFilter {
        int mMask;
        WaitForAllEventPredicate(int mask) {
            mMask = mask;
        }

        @Override
        public boolean accept(AccessibilityEvent t) {
            // check current event in the list
            if ((t.getEventType() & mMask) != 0) {
                // remove from mask since this condition is satisfied
                mMask &= ~t.getEventType();

                // Since we're waiting for all events to be matched at least once
                if (mMask != 0)
                    return false;

                // all matched
                return true;
            }

            // no match yet
            return false;
        }
    }

    /**
     * Helper used by methods to perform actions and wait for any accessibility events and return
     * predicated on predefined filter.
     *
     * @param command
     * @param filter
     * @param timeout
     * @return
     */
    private AccessibilityEvent runAndWaitForEvents(Runnable command,
            AccessibilityEventFilter filter, long timeout) {

        try {
            return getUiAutomation().executeAndWaitForEvent(command, filter, timeout);
        } catch (TimeoutException e) {
            Log.w(LOG_TAG, "runAndwaitForEvents timed out waiting for events");
            return null;
        } catch (Exception e) {
            Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
            return null;
        }
    }

    /**
     * Send keys and blocks until the first specified accessibility event.
     *
     * Most key presses will cause some UI change to occur. If the device is busy, this will
     * block until the device begins to process the key press at which point the call returns
     * and normal wait for idle processing may begin. If no events are detected for the
     * timeout period specified, the call will return anyway with false.
     *
     * @param keyCode
     * @param metaState
     * @param eventType
     * @param timeout
     * @return true if events is received, otherwise false.
     */
    public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
            final int eventType, long timeout) {
        Runnable command = new Runnable() {
            @Override
            public void run() {
                final long eventTime = SystemClock.uptimeMillis();
                KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                        keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
                        InputDevice.SOURCE_KEYBOARD);
                if (injectEventSync(downEvent)) {
                    KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                            keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
                            InputDevice.SOURCE_KEYBOARD);
                    injectEventSync(upEvent);
                }
            }
        };

        return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
                != null;
    }

    /**
     * Clicks at coordinates without waiting for device idle. This may be used for operations
     * that require stressing the target.
     * @param x
     * @param y
     * @return true if the click executed successfully
     */
    public boolean clickNoSync(int x, int y) {
        Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");

        if (touchDown(x, y)) {
            SystemClock.sleep(REGULAR_CLICK_LENGTH);
            if (touchUp(x, y))
                return true;
        }
        return false;
    }

    /**
     * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
     * or TYPE_VIEW_SELECTED are received.
     *
     * @param x
     * @param y
     * @param timeout waiting for event
     * @return true if events are received, else false if timeout.
     */
    public boolean clickAndSync(final int x, final int y, long timeout) {

        String logString = String.format("clickAndSync(%d, %d)", x, y);
        Log.d(LOG_TAG, logString);

        return runAndWaitForEvents(clickRunnable(x, y), new WaitForAnyEventPredicate(
                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
                AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
    }

    /**
     * Clicks at coordinates and waits for for a TYPE_WINDOW_STATE_CHANGED event followed
     * by TYPE_WINDOW_CONTENT_CHANGED. If timeout occurs waiting for TYPE_WINDOW_STATE_CHANGED,
     * no further waits will be performed and the function returns.
     * @param x
     * @param y
     * @param timeout waiting for event
     * @return true if both events occurred in the expected order
     */
    public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
        String logString = String.format("clickAndWaitForNewWindow(%d, %d)", x, y);
        Log.d(LOG_TAG, logString);

        return runAndWaitForEvents(clickRunnable(x, y), new WaitForAllEventPredicate(
                AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED |
                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), timeout) != null;
    }

    /**
     * Returns a Runnable for use in
     * {@link #runAndWaitForEvents(Runnable, AccessibilityEventFilter, long) to perform a click.
     *
     * @param x coordinate
     * @param y coordinate
     * @return Runnable
     */
    private Runnable clickRunnable(final int x, final int y) {
        return new Runnable() {
            @Override
            public void run() {
                if(touchDown(x, y)) {
                    SystemClock.sleep(REGULAR_CLICK_LENGTH);
                    touchUp(x, y);
                }
            }
        };
    }

    /**
     * Returns a Runnable for use in
     * {@link #runAndWaitForEvents(Runnable, AccessibilityEventFilter, long) to perform a long tap.
     *
     * @param x coordinate
     * @param y coordinate
     * @return Runnable
     */
    private Runnable longTapRunnable(final int x, final int y) {
        return new Runnable() {
            @Override
            public void run() {
                if(touchDown(x, y)) {
                    SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
                    touchUp(x, y);
                }
            }
        };
    }

    /**
     * Touches down for a long press at the specified coordinates.
     *
     * @param x
     * @param y
     * @return true if successful.
     */
    public boolean longTapNoSync(int x, int y) {
        if (DEBUG) {
            Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
        }

        if (touchDown(x, y)) {
            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
            if(touchUp(x, y)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Long tap at coordinates and blocks until either accessibility event
     * TYPE_WINDOW_CONTENT_CHANGED or TYPE_VIEW_SELECTED are received.
     *
     * @param x
     * @param y
     * @param timeout waiting for event
     * @return true if events are received, else false if timeout.
     */
    public boolean longTapAndSync(final int x, final int y, long timeout) {

        String logString = String.format("clickAndSync(%d, %d)", x, y);
        Log.d(LOG_TAG, logString);

        return runAndWaitForEvents(longTapRunnable(x, y), new WaitForAnyEventPredicate(
                AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
                AccessibilityEvent.TYPE_VIEW_SELECTED), timeout) != null;
    }

    private boolean touchDown(int x, int y) {
        if (DEBUG) {
            Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
        }
        mDownTime = SystemClock.uptimeMillis();
        MotionEvent event = getMotionEvent(mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y);
        return injectEventSync(event);
    }

    private boolean touchUp(int x, int y) {
        if (DEBUG) {
            Log.d(LOG_TAG, "touchUp (" + x + ", " + y + ")");
        }
        final long eventTime = SystemClock.uptimeMillis();
        MotionEvent event = getMotionEvent(mDownTime, eventTime, MotionEvent.ACTION_UP, x, y);
        mDownTime = 0;
        return injectEventSync(event);
    }

    private boolean touchMove(int x, int y) {
        if (DEBUG) {
            Log.d(LOG_TAG, "touchMove (" + x + ", " + y + ")");
        }
        final long eventTime = SystemClock.uptimeMillis();
        MotionEvent event = getMotionEvent(mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y);
        return injectEventSync(event);
    }

    /**
     * Handle swipes in any direction where the result is a scroll event. This call blocks
     * until the UI has fired a scroll event or timeout.
     * @param downX
     * @param downY
     * @param upX
     * @param upY
     * @param steps
     * @return true if we are not at the beginning or end of the scrollable view.
     */
    public boolean scrollSwipe(final int downX, final int downY, final int upX, final int upY,
            final int steps) {
        Log.d(LOG_TAG, "scrollSwipe (" +  downX + ", " + downY + ", " + upX + ", "
                + upY + ", " + steps +")");

        Runnable command = new Runnable() {
            @Override
            public void run() {
                swipe(downX, downY, upX, upY, steps);
            }
        };

        // Collect all accessibility events generated during the swipe command and get the
        // last event
        ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
        runAndWaitForEvents(command,
                new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
                Configurator.getInstance().getScrollAcknowledgmentTimeout());

        AccessibilityEvent event = getLastMatchingEvent(events,
                AccessibilityEvent.TYPE_VIEW_SCROLLED);

        if (event == null) {
            // end of scroll since no new scroll events received
            recycleAccessibilityEvents(events);
            return false;
        }

        // AdapterViews have indices we can use to check for the beginning.
        boolean foundEnd = false;
        if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
            foundEnd = event.getFromIndex() == 0 ||
                    (event.getItemCount() - 1) == event.getToIndex();
            Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
        } else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
            // Determine if we are scrolling vertically or horizontally.
            if (downX == upX) {
                // Vertical
                foundEnd = event.getScrollY() == 0 ||
                        event.getScrollY() == event.getMaxScrollY();
                Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
            } else if (downY == upY) {
                // Horizontal
                foundEnd = event.getScrollX() == 0 ||
                        event.getScrollX() == event.getMaxScrollX();
                Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
            }
        }
        recycleAccessibilityEvents(events);
        return !foundEnd;
    }

    private AccessibilityEvent getLastMatchingEvent(List<AccessibilityEvent> events, int type) {
        for (int x = events.size(); x > 0; x--) {
            AccessibilityEvent event = events.get(x - 1);
            if (event.getEventType() == type)
                return event;
        }
        return null;
    }

    private void recycleAccessibilityEvents(List<AccessibilityEvent> events) {
        for (AccessibilityEvent event : events)
            event.recycle();
        events.clear();
    }

    /**
     * Handle swipes in any direction.
     * @param downX
     * @param downY
     * @param upX
     * @param upY
     * @param steps
     * @return true if the swipe executed successfully
     */
    public boolean swipe(int downX, int downY, int upX, int upY, int steps) {
        return swipe(downX, downY, upX, upY, steps, false /*drag*/);
    }

    /**
     * Handle swipes/drags in any direction.
     * @param downX
     * @param downY
     * @param upX
     * @param upY
     * @param steps
     * @param drag when true, the swipe becomes a drag swipe
     * @return true if the swipe executed successfully
     */
    public boolean swipe(int downX, int downY, int upX, int upY, int steps, boolean drag) {
        boolean ret = false;
        int swipeSteps = steps;
        double xStep = 0;
        double yStep = 0;

        // avoid a divide by zero
        if(swipeSteps == 0)
            swipeSteps = 1;

        xStep = ((double)(upX - downX)) / swipeSteps;
        yStep = ((double)(upY - downY)) / swipeSteps;

        // first touch starts exactly at the point requested
        ret = touchDown(downX, downY);
        if (drag)
            SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
        for(int i = 1; i < swipeSteps; i++) {
            ret &= touchMove(downX + (int)(xStep * i), downY + (int)(yStep * i));
            if(ret == false)
                break;
            // set some known constant delay between steps as without it this
            // become completely dependent on the speed of the system and results
            // may vary on different devices. This guarantees at minimum we have
            // a preset delay.
            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
        }
        if (drag)
            SystemClock.sleep(REGULAR_CLICK_LENGTH);
        ret &= touchUp(upX, upY);
        return(ret);
    }

    /**
     * Performs a swipe between points in the Point array.
     * @param segments is Point array containing at least one Point object
     * @param segmentSteps steps to inject between two Points
     * @return true on success
     */
    public boolean swipe(Point[] segments, int segmentSteps) {
        boolean ret = false;
        int swipeSteps = segmentSteps;
        double xStep = 0;
        double yStep = 0;

        // avoid a divide by zero
        if(segmentSteps == 0)
            segmentSteps = 1;

        // must have some points
        if(segments.length == 0)
            return false;

        // first touch starts exactly at the point requested
        ret = touchDown(segments[0].x, segments[0].y);
        for(int seg = 0; seg < segments.length; seg++) {
            if(seg + 1 < segments.length) {

                xStep = ((double)(segments[seg+1].x - segments[seg].x)) / segmentSteps;
                yStep = ((double)(segments[seg+1].y - segments[seg].y)) / segmentSteps;

                for(int i = 1; i < swipeSteps; i++) {
                    ret &= touchMove(segments[seg].x + (int)(xStep * i),
                            segments[seg].y + (int)(yStep * i));
                    if(ret == false)
                        break;
                    // set some known constant delay between steps as without it this
                    // become completely dependent on the speed of the system and results
                    // may vary on different devices. This guarantees at minimum we have
                    // a preset delay.
                    SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
                }
            }
        }
        ret &= touchUp(segments[segments.length - 1].x, segments[segments.length -1].y);
        return(ret);
    }


    public boolean sendText(String text) {
        if (DEBUG) {
            Log.d(LOG_TAG, "sendText (" + text + ")");
        }

        KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());

        if (events != null) {
            long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
            for (KeyEvent event2 : events) {
                // We have to change the time of an event before injecting it because
                // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
                // time stamp and the system rejects too old events. Hence, it is
                // possible for an event to become stale before it is injected if it
                // takes too long to inject the preceding ones.
                KeyEvent event = KeyEvent.changeTimeRepeat(event2,
                        SystemClock.uptimeMillis(), 0);
                if (!injectEventSync(event)) {
                    return false;
                }
                SystemClock.sleep(keyDelay);
            }
        }
        return true;
    }

    public boolean sendKey(int keyCode, int metaState) {
        if (DEBUG) {
            Log.d(LOG_TAG, "sendKey (" + keyCode + ", " + metaState + ")");
        }

        final long eventTime = SystemClock.uptimeMillis();
        KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
                keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
                InputDevice.SOURCE_KEYBOARD);
        if (injectEventSync(downEvent)) {
            KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                    keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
                    InputDevice.SOURCE_KEYBOARD);
            if(injectEventSync(upEvent)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Rotates right and also freezes rotation in that position by
     * disabling the sensors. If you want to un-freeze the rotation
     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
     * that doing so may cause the screen contents to rotate
     * depending on the current physical position of the test device.
     * @throws RemoteException
     */
    public void setRotationRight() {
        getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_270);
    }

    /**
     * Rotates left and also freezes rotation in that position by
     * disabling the sensors. If you want to un-freeze the rotation
     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
     * that doing so may cause the screen contents to rotate
     * depending on the current physical position of the test device.
     * @throws RemoteException
     */
    public void setRotationLeft() {
        getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_90);
    }

    /**
     * Rotates up and also freezes rotation in that position by
     * disabling the sensors. If you want to un-freeze the rotation
     * and re-enable the sensors see {@link #unfreezeRotation()}. Note
     * that doing so may cause the screen contents to rotate
     * depending on the current physical position of the test device.
     * @throws RemoteException
     */
    public void setRotationNatural() {
        getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
    }

    /**
     * Disables the sensors and freezes the device rotation at its
     * current rotation state.
     * @throws RemoteException
     */
    public void freezeRotation() {
        getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_CURRENT);
    }

    /**
     * Re-enables the sensors and un-freezes the device rotation
     * allowing its contents to rotate with the device physical rotation.
     * @throws RemoteException
     */
    public void unfreezeRotation() {
        getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
    }

    /**
     * This method simply presses the power button if the screen is OFF else
     * it does nothing if the screen is already ON.
     * @return true if the device was asleep else false
     * @throws RemoteException
     */
    public boolean wakeDevice() throws RemoteException {
        if(!isScreenOn()) {
            sendKey(KeyEvent.KEYCODE_POWER, 0);
            return true;
        }
        return false;
    }

    /**
     * This method simply presses the power button if the screen is ON else
     * it does nothing if the screen is already OFF.
     * @return true if the device was awake else false
     * @throws RemoteException
     */
    public boolean sleepDevice() throws RemoteException {
        if(isScreenOn()) {
            sendKey(KeyEvent.KEYCODE_POWER, 0);
            return true;
        }
        return false;
    }

    /**
     * Checks the power manager if the screen is ON
     * @return true if the screen is ON else false
     */
    public boolean isScreenOn() {
        PowerManager pm = (PowerManager)getContext().getSystemService(Service.POWER_SERVICE);
        return pm.isScreenOn();
    }

    private boolean injectEventSync(InputEvent event) {
        return getUiAutomation().injectInputEvent(event, true);
    }

    private int getPointerAction(int motionEnvent, int index) {
        return motionEnvent + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
    }

    /**
     * Performs a multi-touch gesture
     *
     * Takes a series of touch coordinates for at least 2 pointers. Each pointer must have
     * all of its touch steps defined in an array of {@link PointerCoords}. By having the ability
     * to specify the touch points along the path of a pointer, the caller is able to specify
     * complex gestures like circles, irregular shapes etc, where each pointer may take a
     * different path.
     *
     * To create a single point on a pointer's touch path
     * <code>
     *       PointerCoords p = new PointerCoords();
     *       p.x = stepX;
     *       p.y = stepY;
     *       p.pressure = 1;
     *       p.size = 1;
     * </code>
     * @param touches each array of {@link PointerCoords} constitute a single pointer's touch path.
     *        Multiple {@link PointerCoords} arrays constitute multiple pointers, each with its own
     *        path. Each {@link PointerCoords} in an array constitute a point on a pointer's path.
     * @return <code>true</code> if all points on all paths are injected successfully, <code>false
     *        </code>otherwise
     * @since API Level 18
     */
    public boolean performMultiPointerGesture(PointerCoords[] ... touches) {
        boolean ret = true;
        if (touches.length < 2) {
            throw new IllegalArgumentException("Must provide coordinates for at least 2 pointers");
        }

        // Get the pointer with the max steps to inject.
        int maxSteps = 0;
        for (int x = 0; x < touches.length; x++)
            maxSteps = (maxSteps < touches[x].length) ? touches[x].length : maxSteps;

        // specify the properties for each pointer as finger touch
        PointerProperties[] properties = new PointerProperties[touches.length];
        PointerCoords[] pointerCoords = new PointerCoords[touches.length];
        for (int x = 0; x < touches.length; x++) {
            PointerProperties prop = new PointerProperties();
            prop.id = x;
            prop.toolType = Configurator.getInstance().getToolType();
            properties[x] = prop;

            // for each pointer set the first coordinates for touch down
            pointerCoords[x] = touches[x][0];
        }

        // Touch down all pointers
        long downTime = SystemClock.uptimeMillis();
        MotionEvent event;
        event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 1,
                properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
        ret &= injectEventSync(event);

        for (int x = 1; x < touches.length; x++) {
            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
                    getPointerAction(MotionEvent.ACTION_POINTER_DOWN, x), x + 1, properties,
                    pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
            ret &= injectEventSync(event);
        }

        // Move all pointers
        for (int i = 1; i < maxSteps - 1; i++) {
            // for each pointer
            for (int x = 0; x < touches.length; x++) {
                // check if it has coordinates to move
                if (touches[x].length > i)
                    pointerCoords[x] = touches[x][i];
                else
                    pointerCoords[x] = touches[x][touches[x].length - 1];
            }

            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
                    MotionEvent.ACTION_MOVE, touches.length, properties, pointerCoords, 0, 0, 1, 1,
                    0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);

            ret &= injectEventSync(event);
            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
        }

        // For each pointer get the last coordinates
        for (int x = 0; x < touches.length; x++)
            pointerCoords[x] = touches[x][touches[x].length - 1];

        // touch up
        for (int x = 1; x < touches.length; x++) {
            event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(),
                    getPointerAction(MotionEvent.ACTION_POINTER_UP, x), x + 1, properties,
                    pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
            ret &= injectEventSync(event);
        }

        Log.i(LOG_TAG, "x " + pointerCoords[0].x);
        // first to touch down is last up
        event = MotionEvent.obtain(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 1,
                properties, pointerCoords, 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
        ret &= injectEventSync(event);
        return ret;
    }

    /**
     * Simulates a short press on the Recent Apps button.
     *
     * @return true if successful, else return false
     * @since API Level 18
     */
    public boolean toggleRecentApps() {
        return getUiAutomation().performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
    }

    /**
     * Opens the notification shade
     *
     * @return true if successful, else return false
     * @since API Level 18
     */
    public boolean openNotification() {
        return getUiAutomation().performGlobalAction(AccessibilityService.GLOBAL_ACTION_NOTIFICATIONS);
    }

    /**
     * Opens the quick settings shade
     *
     * @return true if successful, else return false
     * @since API Level 18
     */
    public boolean openQuickSettings() {
        return getUiAutomation().performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
    }

    /** Helper function to obtain a MotionEvent. */
    private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
            float x, float y) {

        PointerProperties properties = new PointerProperties();
        properties.id = 0;
        properties.toolType = Configurator.getInstance().getToolType();

        PointerCoords coords = new PointerCoords();
        coords.pressure = 1;
        coords.size = 1;
        coords.x = x;
        coords.y = y;

        return MotionEvent.obtain(downTime, eventTime, action, 1,
                new PointerProperties[] { properties }, new PointerCoords[] { coords },
                0, 0, 1.0f, 1.0f, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
    }

    UiAutomation getUiAutomation() {
        return UiDevice.getUiAutomation(getInstrumentation());
    }

    Context getContext() {
        return getInstrumentation().getContext();
    }

    Instrumentation getInstrumentation() {
        return mInstrumentation;
    }
}