Merge "instrumentation based UI Automator static library"
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
index 8527ec6..63c51e8 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/AccessibilityNodeInfoDumper.java
@@ -105,7 +105,7 @@
serializer.attribute("", "NAF", Boolean.toString(true));
serializer.attribute("", "index", Integer.toString(index));
serializer.attribute("", "text", safeCharSeqToString(node.getText()));
- serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewId()));
+ serializer.attribute("", "resource-id", safeCharSeqToString(node.getViewIdResourceName()));
serializer.attribute("", "class", safeCharSeqToString(node.getClassName()));
serializer.attribute("", "package", safeCharSeqToString(node.getPackageName()));
serializer.attribute("", "content-desc", safeCharSeqToString(node.getContentDescription()));
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
index e8136f4..7deb3a6 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/InteractionController.java
@@ -32,6 +32,8 @@
import com.android.internal.util.Predicate;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.TimeoutException;
/**
@@ -48,7 +50,9 @@
private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
- private static final long DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS = 500;
+ // The events for a scroll typically complete even before touchUp occurs.
+ // This short timeout to make sure we get the very last in cases where the above isn't true.
+ private static final long DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS = 200;
private final KeyCharacterMap mKeyCharacterMap =
KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
@@ -59,6 +63,8 @@
private long mDownTime;
+ private static final long WAIT_FOR_EVENT_TMEOUT = 3 * 1000;
+
// Inserted after each motion event injection.
private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
@@ -67,117 +73,104 @@
}
/**
- * Click at coordinates and blocks until the first specified accessibility event.
- *
- * All clicks will cause some UI change to occur. If the device is busy, this will
- * block until the device begins to process the click at which point the call returns
- * and normal wait for idle processing may begin. If no evens are detected for the
- * timeout period specified, the call will return anyway.
- * @param x
- * @param y
- * @param timeout
- * @param eventType is an {@link AccessibilityEvent} type
- * @return True if busy state is detected else false for timeout waiting for busy state
+ * Predicate for waiting for any of the events specified in the mask
*/
- public boolean clickAndWaitForEvent(final int x, final int y, long timeout,
- final int eventType) {
- return clickAndWaitForEvents(x, y, timeout, false, eventType);
+ class WaitForAnyEventPredicate implements Predicate<AccessibilityEvent> {
+ int mMask;
+ WaitForAnyEventPredicate(int mask) {
+ mMask = mask;
+ }
+ @Override
+ public boolean apply(AccessibilityEvent t) {
+ // check current event in the list
+ if ((t.getEventType() & mMask) != 0) {
+ return true;
+ }
+
+ // no match yet
+ return false;
+ }
}
/**
- * Click at coordinates and blocks until the specified accessibility events. It is possible to
- * set the wait for all events to occur, in no specific order, or to the wait for any.
- *
- * @param x
- * @param y
- * @param timeout
- * @param waitForAll boolean to indicate whether to wait for any or all events
- * @param eventTypes mask
- * @return true if events are received, else false if timeout.
+ * 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.
*/
- public boolean clickAndWaitForEvents(final int x, final int y, long timeout,
- boolean waitForAll, int eventTypes) {
- String logString = String.format("clickAndWaitForEvents(%d, %d, %d, %s, %d)", x, y, timeout,
- Boolean.toString(waitForAll), eventTypes);
- Log.d(LOG_TAG, logString);
+ class EventCollectingPredicate implements Predicate<AccessibilityEvent> {
+ int mMask;
+ List<AccessibilityEvent> mEventsList;
- Runnable command = new Runnable() {
- @Override
- public void run() {
- if(touchDown(x, y)) {
- SystemClock.sleep(REGULAR_CLICK_LENGTH);
- touchUp(x, y);
- }
- }
- };
- return runAndWaitForEvents(command, timeout, waitForAll, eventTypes) != null;
- }
-
- /**
- * Runs a command and waits for a specific accessibility event.
- * @param command is a Runnable to execute before waiting for the event.
- * @param timeout
- * @param eventType
- * @return The AccessibilityEvent if one is received, otherwise null.
- */
- private AccessibilityEvent runAndWaitForEvent(Runnable command, long timeout, int eventType) {
- return runAndWaitForEvents(command, timeout, false, eventType);
- }
-
- /**
- * Runs a command and waits for accessibility events. It is possible to set the wait for all
- * events to occur at least once for each, or wait for any one to occur at least once.
- *
- * @param command
- * @param timeout
- * @param waitForAll boolean to indicate whether to wait for any or all events
- * @param eventTypesMask
- * @return The AccessibilityEvent if one is received, otherwise null.
- */
- private AccessibilityEvent runAndWaitForEvents(Runnable command, long timeout,
- final boolean waitForAll, final int eventTypesMask) {
- if (eventTypesMask == 0)
- throw new IllegalArgumentException("events mask cannot be zero");
-
- class EventPredicate implements Predicate<AccessibilityEvent> {
- int mMask;
- EventPredicate(int mask) {
- mMask = mask;
- }
- @Override
- public boolean apply(AccessibilityEvent t) {
- // check current event in the list
- if ((t.getEventType() & mMask) != 0) {
- if (!waitForAll)
- return true;
-
- // 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;
- }
- // not one of our events
- return false;
- }
+ EventCollectingPredicate(int mask, List<AccessibilityEvent> events) {
+ mMask = mask;
+ mEventsList = events;
}
- AccessibilityEvent event = null;
+ @Override
+ public boolean apply(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 Predicate<AccessibilityEvent> {
+ int mMask;
+ WaitForAllEventPredicate(int mask) {
+ mMask = mask;
+ }
+
+ @Override
+ public boolean apply(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,
+ Predicate<AccessibilityEvent> filter, long timeout) {
+
try {
- event = mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command,
- new EventPredicate(eventTypesMask), timeout);
+ return mUiAutomatorBridge.executeCommandAndWaitForAccessibilityEvent(command, filter,
+ timeout);
} catch (TimeoutException e) {
- Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events: " + eventTypesMask);
+ Log.w(LOG_TAG, "runAndwaitForEvent timedout waiting for events");
return null;
} catch (Exception e) {
Log.e(LOG_TAG, "exception from executeCommandAndWaitForAccessibilityEvent", e);
return null;
}
- return event;
}
/**
@@ -212,7 +205,8 @@
}
};
- return runAndWaitForEvent(command, timeout, eventType) != null;
+ return runAndWaitForEvents(command, new WaitForAnyEventPredicate(eventType), timeout)
+ != null;
}
/**
@@ -222,8 +216,8 @@
* @param y
* @return true if the click executed successfully
*/
- public boolean click(int x, int y) {
- Log.d(LOG_TAG, "click (" + x + ", " + y + ")");
+ public boolean clickNoSync(int x, int y) {
+ Log.d(LOG_TAG, "clickNoSync (" + x + ", " + y + ")");
if (touchDown(x, y)) {
SystemClock.sleep(REGULAR_CLICK_LENGTH);
@@ -234,23 +228,70 @@
}
/**
+ * Click at coordinates and blocks until either accessibility event TYPE_WINDOW_CONTENT_CHANGED
+ * or TYPE_VIEW_SELECTED are received.
+ *
+ * @param x
+ * @param y
+ * @return true if events are received, else false if timeout.
+ */
+ public boolean clickAndSync(final int x, final int y) {
+
+ 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), WAIT_FOR_EVENT_TMEOUT) != 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
* @return true if both events occurred in the expected order
*/
- public boolean clickAndWaitForNewWindow(final int x, final int y, long timeout) {
- return (clickAndWaitForEvents(x, y, timeout, true,
+ public boolean clickAndWaitForNewWindow(final int x, final int y) {
+ 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));
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED), WAIT_FOR_EVENT_TMEOUT) != null;
}
- public boolean longTap(int x, int y) {
+ /**
+ * 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, "longTap (" + x + ", " + y + ")");
+ Log.d(LOG_TAG, "longTapNoSync (" + x + ", " + y + ")");
}
if (touchDown(x, y)) {
@@ -318,34 +359,59 @@
}
};
- AccessibilityEvent event = runAndWaitForEvent(command,
- DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS, AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ // 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),
+ DEFAULT_SCROLL_EVENT_TIMEOUT_MILLIS);
+
+ 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) {
- boolean foundEnd = event.getFromIndex() == 0 ||
+ foundEnd = event.getFromIndex() == 0 ||
(event.getItemCount() - 1) == event.getToIndex();
Log.d(LOG_TAG, "scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
} else if (event.getScrollX() != -1 && event.getScrollY() != -1) {
// Determine if we are scrolling vertically or horizontally.
if (downX == upX) {
// Vertical
- boolean foundEnd = event.getScrollY() == 0 ||
+ foundEnd = event.getScrollY() == 0 ||
event.getScrollY() == event.getMaxScrollY();
Log.d(LOG_TAG, "Vertical scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
} else if (downY == upY) {
// Horizontal
- boolean foundEnd = event.getScrollX() == 0 ||
+ foundEnd = event.getScrollX() == 0 ||
event.getScrollX() == event.getMaxScrollX();
Log.d(LOG_TAG, "Horizontal scrollSwipe reached scroll end: " + foundEnd);
- return !foundEnd;
}
}
- return event != null;
+ 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();
}
/**
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
index 32308ff..2305e3a 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiDevice.java
@@ -379,7 +379,7 @@
if (x >= getDisplayWidth() || y >= getDisplayHeight()) {
return (false);
}
- return getAutomatorBridge().getInteractionController().click(x, y);
+ return getAutomatorBridge().getInteractionController().clickNoSync(x, y);
}
/**
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
index 801765a..2594888 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiObject.java
@@ -22,7 +22,6 @@
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent.PointerCoords;
-import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
/**
@@ -345,9 +344,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().clickAndWaitForEvents(rect.centerX(), rect.centerY(),
- WAIT_FOR_EVENT_TMEOUT, false, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED |
- AccessibilityEvent.TYPE_VIEW_SELECTED);
+ return getInteractionController().clickAndSync(rect.centerX(), rect.centerY());
}
/**
@@ -386,8 +383,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().clickAndWaitForNewWindow(
- rect.centerX(), rect.centerY(), timeout);
+ return getInteractionController().clickAndWaitForNewWindow(rect.centerX(), rect.centerY());
}
/**
@@ -404,7 +400,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().click(rect.left + 5, rect.top + 5);
+ return getInteractionController().clickNoSync(rect.left + 5, rect.top + 5);
}
/**
@@ -421,7 +417,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.right - 5, rect.bottom - 5);
+ return getInteractionController().longTapNoSync(rect.right - 5, rect.bottom - 5);
}
/**
@@ -438,7 +434,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().click(rect.right - 5, rect.bottom - 5);
+ return getInteractionController().clickNoSync(rect.right - 5, rect.bottom - 5);
}
/**
@@ -455,7 +451,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.centerX(), rect.centerY());
+ return getInteractionController().longTapNoSync(rect.centerX(), rect.centerY());
}
/**
@@ -472,7 +468,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- return getInteractionController().longTap(rect.left + 5, rect.top + 5);
+ return getInteractionController().longTapNoSync(rect.left + 5, rect.top + 5);
}
/**
@@ -577,7 +573,7 @@
throw new UiObjectNotFoundException(getSelector().toString());
}
Rect rect = getVisibleBounds(node);
- getInteractionController().longTap(rect.left + 20, rect.centerY());
+ getInteractionController().longTapNoSync(rect.left + 20, rect.centerY());
// check if the edit menu is open
UiObject selectAll = new UiObject(new UiSelector().descriptionContains("Select all"));
if(selectAll.waitForExists(50))
@@ -1029,4 +1025,4 @@
public void multiPointerGesture(PointerCoords[] ...touches) {
getInteractionController().generateMultiPointerGesture(touches);
}
-}
\ No newline at end of file
+}
diff --git a/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java b/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
index 68f37e4..d16aff7 100644
--- a/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
+++ b/uiautomator/library/core-src/com/android/uiautomator/core/UiSelector.java
@@ -772,7 +772,7 @@
}
break;
case UiSelector.SELECTOR_RESOURCE_ID:
- if (node.getViewId() != getString(criterion)) {
+ if (node.getViewIdResourceName() != getString(criterion)) {
return false;
}
break;