package nsl.stg.uiautomator.core;

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

import android.accessibilityservice.AccessibilityService;
import android.app.UiAutomation;
import android.app.UiAutomation.AccessibilityEventFilter;
import android.graphics.Point;
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.accessibility.AccessibilityEvent;

import com.android.internal.util.Predicate;
import com.android.uiautomator.core.Configurator;
import com.android.uiautomator.core.UiAutomatorBridge;

/**
 * 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}
 */
public class MyInteractionController {

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

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

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

	private final UiAutomatorBridge mUiAutomatorBridge;

	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 MyInteractionController(UiAutomatorBridge bridge) {
		mUiAutomatorBridge = bridge;
	}

	/**
	 * 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 fromIndex
				// 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 fromIndex 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 mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter, timeout);
		} catch (TimeoutException e) {
			Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
			return null;
		} catch (Exception e) {
			Log.e(LOG_TAG, "exception fromIndex 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, Predicate, 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);
				}
			}
		};
	}

	/**
	 * 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(mUiAutomatorBridge.getSystemLongPressTime());
			if (touchUp(x, y)) {
				return true;
			}
		}
		return false;
	}

	private boolean touchDown(int x, int y) {
		if (DEBUG) {
			Log.d(LOG_TAG, "touchDown (" + x + ", " + y + ")");
		}
		mDownTime = SystemClock.uptimeMillis();
		MotionEvent event = MotionEvent.obtain(mDownTime, mDownTime, MotionEvent.ACTION_DOWN, x, y, 1);
		event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
		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 = MotionEvent.obtain(mDownTime, eventTime, MotionEvent.ACTION_UP, x, y, 1);
		event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
		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 = MotionEvent.obtain(mDownTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 1);
		event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
		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(mUiAutomatorBridge.getSystemLongPressTime());
		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() {
		mUiAutomatorBridge.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() {
		mUiAutomatorBridge.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() {
		mUiAutomatorBridge.setRotation(UiAutomation.ROTATION_FREEZE_0);
	}

	/**
	 * Disables the sensors and freezes the device rotation at its
	 * current rotation state.
	 * @throws RemoteException
	 */
	public void freezeRotation() {
		mUiAutomatorBridge.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() {
		mUiAutomatorBridge.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()) {
			this.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
	 * @throws RemoteException
	 */
	public boolean isScreenOn() throws RemoteException {
		return mUiAutomatorBridge.isScreenOn();
	}

	private boolean injectEventSync(InputEvent event) {
		return mUiAutomatorBridge.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 = MotionEvent.TOOL_TYPE_FINGER;
			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 mUiAutomatorBridge.performGlobalAction(AccessibilityService.GLOBAL_ACTION_RECENTS);
	}

	/**
	 * Opens the notification shade
	 *
	 * @return true if successful, else return false
	 * @since API Level 18
	 */
	public boolean openNotification() {
		return mUiAutomatorBridge.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 mUiAutomatorBridge.performGlobalAction(AccessibilityService.GLOBAL_ACTION_QUICK_SETTINGS);
	}
}