summaryrefslogtreecommitdiff
path: root/src/com/android/uiautomator/core/InteractionController.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/uiautomator/core/InteractionController.java')
-rw-r--r--src/com/android/uiautomator/core/InteractionController.java795
1 files changed, 795 insertions, 0 deletions
diff --git a/src/com/android/uiautomator/core/InteractionController.java b/src/com/android/uiautomator/core/InteractionController.java
new file mode 100644
index 0000000..73e46f1
--- /dev/null
+++ b/src/com/android/uiautomator/core/InteractionController.java
@@ -0,0 +1,795 @@
+/*
+ * 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 com.android.uiautomator.core;
+
+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 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 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 InteractionController(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 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 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 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, 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);
+ }
+}