| /* |
| * 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 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, 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); |
| } |
| } |
| }; |
| } |
| |
| /** |
| * 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); |
| } |
| } |