diff options
Diffstat (limited to 'src/com/android/uiautomator/core/InteractionController.java')
-rw-r--r-- | src/com/android/uiautomator/core/InteractionController.java | 795 |
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); + } +} |